Finalize the Effect View rename and refresh docs, examples, and tooling #59
@@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
title: Async
|
||||||
|
---
|
||||||
|
|
||||||
|
# Async
|
||||||
|
|
||||||
|
Effect View components run synchronously by default. Apply `Async.async` when a
|
||||||
|
component must wait for an asynchronous Effect before it can return JSX. The
|
||||||
|
component suspends while the Effect runs and accepts React Suspense props such
|
||||||
|
as `fallback`.
|
||||||
|
|
||||||
|
```tsx title="src/UserCard.tsx"
|
||||||
|
import { Effect } from "effect"
|
||||||
|
import { Async, Component } from "effect-view"
|
||||||
|
import { loadUser } from "./api"
|
||||||
|
|
||||||
|
export const UserCard = Component.make("UserCard")(
|
||||||
|
function* ({ userId }: { readonly userId: string }) {
|
||||||
|
const user = yield* Component.useOnChange(
|
||||||
|
() => loadUser(userId),
|
||||||
|
[userId],
|
||||||
|
)
|
||||||
|
|
||||||
|
return <article>{user.name}</article>
|
||||||
|
},
|
||||||
|
).pipe(Async.async)
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass a fallback when rendering the component:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
const User = yield* UserCard.use
|
||||||
|
|
||||||
|
return <User userId="123" fallback={<p>Loading user...</p>} />
|
||||||
|
```
|
||||||
|
|
||||||
|
Or configure a default fallback once:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export const UserCard = Component.make("UserCard")(
|
||||||
|
function* ({ userId }: { readonly userId: string }) {
|
||||||
|
const user = yield* Component.useOnChange(
|
||||||
|
() => loadUser(userId),
|
||||||
|
[userId],
|
||||||
|
)
|
||||||
|
|
||||||
|
return <article>{user.name}</article>
|
||||||
|
},
|
||||||
|
).pipe(
|
||||||
|
Async.async,
|
||||||
|
Async.withOptions({ defaultFallback: <p>Loading user...</p> }),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hook ordering
|
||||||
|
|
||||||
|
**Important:** Do not use React hooks or Effect View hook helpers after an
|
||||||
|
asynchronous computation in the component body. After the computation suspends,
|
||||||
|
the generator continuation runs outside React's synchronous render phase.
|
||||||
|
|
||||||
|
Place every hook before the first operation that may suspend:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
const UserCard = Component.make("UserCard")(
|
||||||
|
function* ({ userId }: { readonly userId: string }) {
|
||||||
|
// Hooks and hook helpers go before asynchronous work.
|
||||||
|
const [showDetails, setShowDetails] = useState(false)
|
||||||
|
|
||||||
|
// This may suspend, so no hooks may be used after it.
|
||||||
|
const user = yield* Component.useOnChange(
|
||||||
|
() => loadUser(userId),
|
||||||
|
[userId],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article>
|
||||||
|
<button onClick={() => setShowDetails((value) => !value)}>
|
||||||
|
{user.name}
|
||||||
|
</button>
|
||||||
|
{showDetails && <p>{user.bio}</p>}
|
||||||
|
</article>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
).pipe(Async.async)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memoization
|
||||||
|
|
||||||
|
An async computation starts whenever its component renders. Apply
|
||||||
|
`Memoized.memoized` after `Async.async` to prevent an unrelated parent render
|
||||||
|
from restarting an async child whose props have not changed:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Async, Component, Memoized } from "effect-view"
|
||||||
|
|
||||||
|
export const UserCard = Component.make("UserCard")(
|
||||||
|
function* ({ userId }: { readonly userId: string }) {
|
||||||
|
const user = yield* Component.useOnChange(
|
||||||
|
() => loadUser(userId),
|
||||||
|
[userId],
|
||||||
|
)
|
||||||
|
|
||||||
|
return <article>{user.name}</article>
|
||||||
|
},
|
||||||
|
).pipe(
|
||||||
|
Async.async,
|
||||||
|
Memoized.memoized,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Async components compare props with `Object.is` by default and ignore the
|
||||||
|
`fallback` prop during that comparison. A changed `userId` renders the child
|
||||||
|
again, closes the previous dependency scope, and runs `loadUser` for the new
|
||||||
|
value.
|
||||||
|
|
||||||
|
If props contain immutable objects or arrays that may be recreated with the same
|
||||||
|
content, use `Equal.asEquivalence()` for full structural equality:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Equal } from "effect"
|
||||||
|
import { Async, Component, Memoized } from "effect-view"
|
||||||
|
|
||||||
|
export const UserCard = Component.make("UserCard")(
|
||||||
|
function* (props: { readonly user: { readonly id: string } }) {
|
||||||
|
const details = yield* Component.useOnChange(
|
||||||
|
() => loadUser(props.user.id),
|
||||||
|
[props.user.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
return <article>{details.name}</article>
|
||||||
|
},
|
||||||
|
).pipe(
|
||||||
|
Async.async,
|
||||||
|
Memoized.memoized,
|
||||||
|
Memoized.withOptions({
|
||||||
|
propsEquivalence: Equal.asEquivalence(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Supplying `propsEquivalence` replaces the async component's default comparison,
|
||||||
|
so `fallback` is also included in this structural comparison.
|
||||||
@@ -199,51 +199,25 @@ to provide local services to a subtree.
|
|||||||
|
|
||||||
## Synchronous and asynchronous components
|
## Synchronous and asynchronous components
|
||||||
|
|
||||||
A regular Effect View component runs its body during React render. That Effect
|
A regular Effect View component runs its body during React render. Effects
|
||||||
must complete synchronously: yielding a service, reading synchronous state, or
|
executed directly by that body—including setup passed to `useOnMount`,
|
||||||
creating a scoped object is fine; sleeping, fetching, or awaiting a promise is
|
`useOnChange`, or `useLayer`—must complete synchronously every time they run.
|
||||||
not.
|
Yielding services and reading synchronous state is fine; sleeping, fetching, or
|
||||||
|
awaiting a promise is not.
|
||||||
|
|
||||||
Use `Async.async` when render genuinely depends on an asynchronous Effect. The
|
Choose the integration that matches the asynchronous work:
|
||||||
component then suspends and accepts React Suspense props such as `fallback`:
|
|
||||||
|
|
||||||
```tsx title="src/UserView.tsx"
|
- Make the component asynchronous with [`Async.async`](./async) when it must
|
||||||
import { Async, Component, Memoized } from "effect-view"
|
wait for a one-off asynchronous Effect before producing JSX. The component
|
||||||
import { loadUser } from "./api"
|
suspends while it waits.
|
||||||
|
- Use [Query](./query) for server reads that need caching, sharing, refresh, or
|
||||||
export const UserView = Component.make("UserView")(
|
invalidation.
|
||||||
function* (props: { readonly userId: string }) {
|
- Use [Mutation](./mutation) for user-triggered writes with observable pending
|
||||||
const user = yield* Component.useOnChange(
|
and error state.
|
||||||
() => loadUser(props.userId),
|
- Use `useRunPromise` or `useCallbackPromise` for asynchronous event work that
|
||||||
[props.userId],
|
does not need Mutation state.
|
||||||
)
|
- Use a post-commit hook with a scoped fiber for subscriptions or background
|
||||||
|
work tied to the component lifecycle.
|
||||||
return <h2>{user.name}</h2>
|
|
||||||
},
|
|
||||||
).pipe(
|
|
||||||
Async.async,
|
|
||||||
Async.withOptions({ defaultFallback: <p>Loading user...</p> }),
|
|
||||||
Memoized.memoized,
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Use it from an Effect View parent in the usual way:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
const User = yield* UserView.use
|
|
||||||
|
|
||||||
return <User userId={selectedUserId} />
|
|
||||||
```
|
|
||||||
|
|
||||||
`Memoized.memoized` prevents an unrelated parent re-render from restarting an
|
|
||||||
async child whose props have not changed. A changed `userId` creates a new
|
|
||||||
dependency scope, cleans up the previous one, and runs `loadUser` again.
|
|
||||||
|
|
||||||
An async component's rejected Effect is handled by the nearest React error
|
|
||||||
boundary. Suspense handles waiting, not failures.
|
|
||||||
|
|
||||||
For server data that should be cached, refreshed, and shared, prefer the
|
|
||||||
[Query module](./query) over a raw async component.
|
|
||||||
|
|
||||||
## Use Effect services
|
## Use Effect services
|
||||||
|
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ subscribe to changes, and write updates back to the underlying source. A Lens
|
|||||||
can point at a whole state object or focus on one nested field inside it.
|
can point at a whole state object or focus on one nested field inside it.
|
||||||
|
|
||||||
`effect-view` re-exports the `Lens` and `View` modules from
|
`effect-view` re-exports the `Lens` and `View` modules from
|
||||||
[`effect-lens`](https://github.com/Thiladev/effect-lens) for convenience. The
|
[`effect-lens`](https://www.npmjs.com/package/effect-lens/v/beta) for convenience. The
|
||||||
core data model and transformation APIs belong to `effect-lens`, so check the
|
core data model and transformation APIs belong to `effect-lens`, so check the
|
||||||
[`effect-lens` documentation](https://github.com/Thiladev/effect-lens/tree/master/packages/effect-lens) for the full Lens/View API.
|
[`effect-lens` documentation](https://www.npmjs.com/package/effect-lens/v/beta) for the full Lens/View API.
|
||||||
|
|
||||||
The usual pattern is to create state with Effect primitives such as
|
The usual pattern is to create state with Effect primitives such as
|
||||||
`SubscriptionRef`, turn that primitive into a Lens with a matching constructor
|
`SubscriptionRef`, turn that primitive into a Lens with a matching constructor
|
||||||
@@ -271,4 +271,4 @@ Updating the focused `name` Lens through `Lens.useState` updates the parent
|
|||||||
|
|
||||||
For focusing into nested state, deriving lenses, custom write behavior, and the
|
For focusing into nested state, deriving lenses, custom write behavior, and the
|
||||||
complete API, refer to the
|
complete API, refer to the
|
||||||
[`effect-lens` documentation](https://github.com/Thiladev/effect-lens/tree/master/packages/effect-lens).
|
[`effect-lens` documentation](https://www.npmjs.com/package/effect-lens/v/beta).
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const sidebars: SidebarsConfig = {
|
|||||||
docsSidebar: [
|
docsSidebar: [
|
||||||
"getting-started",
|
"getting-started",
|
||||||
"state-management",
|
"state-management",
|
||||||
|
"async",
|
||||||
"query",
|
"query",
|
||||||
"mutation",
|
"mutation",
|
||||||
"forms",
|
"forms",
|
||||||
|
|||||||
@@ -72,9 +72,15 @@
|
|||||||
letter-spacing: -0.075em;
|
letter-spacing: -0.075em;
|
||||||
line-height: 0.92;
|
line-height: 0.92;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero h1 span {
|
.hero h1 .unbreakable {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 .heroAccent {
|
||||||
background: linear-gradient(105deg, var(--home-teal) 5%, #1a9793 48%, var(--home-amber));
|
background: linear-gradient(105deg, var(--home-teal) 5%, #1a9793 48%, var(--home-amber));
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export default function Home(): ReactNode {
|
|||||||
Effect View for React 19
|
Effect View for React 19
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
<h1>
|
||||||
React components,
|
React <span className={styles.unbreakable}>components,</span>
|
||||||
<span> powered by Effect.</span>
|
<span className={styles.heroAccent}> powered by Effect.</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className={styles.lede}>
|
<p className={styles.lede}>
|
||||||
Bring typed services, scoped resources, reactive state, server
|
Bring typed services, scoped resources, reactive state, server
|
||||||
|
|||||||
Reference in New Issue
Block a user