Add Effect v4 support, Fast Refresh tooling, and revamped docs #56
@@ -1,56 +1,155 @@
|
|||||||
# Effect FC Next
|
<p align="center">
|
||||||
|
<a href="https://thila.dev/effect-view">
|
||||||
|
<img src="../docs/static/img/logo.svg" width="104" height="104" alt="Effect View logo" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[Effect-TS](https://effect.website/) integration for React 19.2+ that allows you to write function components using Effect generators.
|
<h1 align="center">Effect View</h1>
|
||||||
|
|
||||||
This library is in early development. While it is (almost) feature complete and mostly usable, expect bugs and quirks. Things are still being ironed out, so ideas and criticisms are more than welcome.
|
<p align="center">
|
||||||
|
Write React components as typed Effect programs.
|
||||||
|
</p>
|
||||||
|
|
||||||
Documentation is currently being written. In the meantime, you can take a look at the `packages/example` directory.
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/package/effect-view"><img src="https://img.shields.io/npm/v/effect-view?color=08777b" alt="npm version" /></a>
|
||||||
|
<a href="https://thila.dev/effect-view"><img src="https://img.shields.io/badge/docs-Effect_View-16a3a1" alt="Effect View documentation" /></a>
|
||||||
|
<a href="https://github.com/Thiladev/effect-view/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/effect-view?color=e99526" alt="MIT license" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Peer dependencies
|
<p align="center">
|
||||||
- `effect` 3.19+
|
<strong><a href="https://thila.dev/effect-view">Read the documentation →</a></strong>
|
||||||
- `react` & `@types/react` 19.2+
|
</p>
|
||||||
|
|
||||||
## Known issues
|
Effect View brings Effect's typed services, resource safety, concurrency, and
|
||||||
- React Refresh doesn't work for Effect FC's yet. Page reload is required to view changes. Regular React components are unaffected.
|
data modeling into React 19 without replacing React's component model. Yield
|
||||||
|
Effects and services from a component body, let scopes follow the React
|
||||||
|
lifecycle, and return ordinary JSX.
|
||||||
|
|
||||||
## What writing components looks like
|
> **Effect v4 beta:** Effect View is built for the Effect v4 beta release. If
|
||||||
```typescript
|
> your application uses Effect v3, use the legacy
|
||||||
export class TodosView extends Component.make("TodosView")(function*() {
|
> [`effect-fc` package](https://www.npmjs.com/package/effect-fc) instead.
|
||||||
const state = yield* TodosState
|
|
||||||
const [todos] = yield* Component.useSubscribables([state.subscriptionRef])
|
|
||||||
|
|
||||||
yield* Component.useOnMount(() => Effect.andThen(
|
```bash
|
||||||
Console.log("Todos mounted"),
|
npm install effect-view effect@beta react
|
||||||
Effect.addFinalizer(() => Console.log("Todos unmounted")),
|
```
|
||||||
))
|
|
||||||
|
|
||||||
const Todo = yield* TodoView.use
|
Effect View does not depend on `react-dom`. Install the renderer used by your
|
||||||
|
application—for example, `react-dom` for the web:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install react-dom
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use Effect View with React Native or any other React renderer instead.
|
||||||
|
|
||||||
|
## What it looks like
|
||||||
|
|
||||||
|
An Effect View component is an Effect program that produces JSX. It can create
|
||||||
|
scoped state, access services, and use React-facing hooks without leaving the
|
||||||
|
generator model:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Effect, SubscriptionRef } from "effect"
|
||||||
|
import { Component, Lens } from "effect-view"
|
||||||
|
import { runtime } from "./runtime"
|
||||||
|
|
||||||
|
const CounterValueView = Component.make("CounterValue")(
|
||||||
|
function* ({ count }: { readonly count: Lens.Lens<number> }) {
|
||||||
|
yield* Component.useOnMount(() =>
|
||||||
|
Effect.addFinalizer(() =>
|
||||||
|
Effect.log("CounterValue unmounted"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const [value, setValue] = yield* Lens.useState(count)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<button onClick={() => setValue((n) => n + 1)}>
|
||||||
<Heading align="center">Todos</Heading>
|
Count: {value}
|
||||||
|
</button>
|
||||||
<Flex direction="column" align="stretch" gap="2" mt="2">
|
|
||||||
<Todo _tag="new" />
|
|
||||||
|
|
||||||
{Chunk.map(todos, todo =>
|
|
||||||
<Todo key={todo.id} _tag="edit" id={todo.id} />
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Container>
|
|
||||||
)
|
)
|
||||||
}) {}
|
},
|
||||||
|
|
||||||
const Index = Component.make("IndexView")(function*() {
|
|
||||||
const context = yield* Component.useLayer(TodosState.Default)
|
|
||||||
const Todos = yield* Effect.provide(TodosView.use, context)
|
|
||||||
|
|
||||||
return <Todos />
|
|
||||||
}).pipe(
|
|
||||||
Component.withContext(runtime.context)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
const CounterView = Component.make("Counter")(function* () {
|
||||||
component: Index
|
const count = yield* Component.useOnMount(() =>
|
||||||
|
Effect.map(
|
||||||
|
SubscriptionRef.make(0),
|
||||||
|
Lens.fromSubscriptionRef,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
const CounterValue = yield* CounterValueView.use
|
||||||
|
|
||||||
|
return <CounterValue count={count} />
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const Counter = CounterView.pipe(
|
||||||
|
Component.withContext(runtime.context),
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`CounterView` composes another Effect View component by yielding its `.use`
|
||||||
|
Effect, then renders the resulting component as ordinary JSX. The child can
|
||||||
|
access the current Effect context and receives its own component scope.
|
||||||
|
|
||||||
|
`Effect.addFinalizer` uses the scope already provided to the component body, so
|
||||||
|
the finalizer above runs when `CounterValue` unmounts.
|
||||||
|
|
||||||
|
At the outer boundary, `Counter` is still a normal React component and its Effect requirements remain
|
||||||
|
typed until they reach the runtime.
|
||||||
|
|
||||||
|
[Build the runtime and render your first component →](https://thila.dev/effect-view/docs/getting-started)
|
||||||
|
|
||||||
|
## Why Effect View
|
||||||
|
|
||||||
|
| | Capability | What it gives you |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Effect components** | `Component.make` | Yield Effects and typed services directly, then return JSX. |
|
||||||
|
| **Managed lifecycles** | `Scope.Scope` | Finalizers, fibers, subscriptions, and resources close with their component or dependency lifecycle. |
|
||||||
|
| **One application runtime** | `ReactRuntime` | Build your Layer once and make its services available throughout the UI. |
|
||||||
|
| **Reactive state** | `Lens` and `View` | Focus large state models into small writable values and subscribe only where React needs them. |
|
||||||
|
| **Server state** | `Query` and `Mutation` | TanStack Query-style caching, invalidation, staleness, and mutations with typed Effects underneath. |
|
||||||
|
| **Schema-driven forms** | `MutationForm` and `LensForm` | Keep input-friendly encoded values while application code receives validated, decoded types. |
|
||||||
|
| **Async rendering** | `Async` and `Memoized` | Integrate asynchronous Effects with Suspense while retaining Effect errors and cancellation. |
|
||||||
|
|
||||||
|
## Designed for both ecosystems
|
||||||
|
|
||||||
|
Effect View does not hide Effect behind callbacks or recreate React around a
|
||||||
|
new rendering model. React still owns rendering, JSX, hooks, Suspense, and
|
||||||
|
events. Effect owns services, errors, resource safety, concurrency, streams,
|
||||||
|
and schemas. Effect View provides the lifecycle-aware boundary between them.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Getting started](https://thila.dev/effect-view/docs/getting-started)
|
||||||
|
- [State management](https://thila.dev/effect-view/docs/state-management)
|
||||||
|
- [Queries](https://thila.dev/effect-view/docs/query)
|
||||||
|
- [Mutations](https://thila.dev/effect-view/docs/mutation)
|
||||||
|
- [Forms](https://thila.dev/effect-view/docs/forms)
|
||||||
|
|
||||||
|
The complete documentation is available at
|
||||||
|
**[thila.dev/effect-view](https://thila.dev/effect-view)**.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- React 19.2 or newer
|
||||||
|
- **Effect v4 beta** (`effect@beta`)
|
||||||
|
- TypeScript and `@types/react` for TypeScript projects
|
||||||
|
|
||||||
|
Effect View is renderer-independent and does not require `react-dom`.
|
||||||
|
|
||||||
|
## Project status
|
||||||
|
|
||||||
|
Effect View is currently beta software. The main APIs are available, but
|
||||||
|
breaking changes and rough edges are still possible before a stable release.
|
||||||
|
React Fast Refresh does not yet recognize Effect View component definitions, so
|
||||||
|
changes to them currently require a page reload; ordinary React components are
|
||||||
|
unaffected (currently being worked on).
|
||||||
|
|
||||||
|
Issues, ideas, and contributions are welcome on
|
||||||
|
[GitHub](https://github.com/Thiladev/effect-view).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](https://github.com/Thiladev/effect-view/blob/main/LICENSE)
|
||||||
|
|||||||
Reference in New Issue
Block a user