}),
+)
+```
+
+## 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 (
+
+
+ {showDetails &&
{user.bio}
}
+
+ )
+ },
+).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 {user.name}
+ },
+).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 {details.name}
+ },
+).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.
diff --git a/packages/docs/docs/getting-started.md b/packages/docs/docs/getting-started.md
index 393e36b..dbc8c2d 100644
--- a/packages/docs/docs/getting-started.md
+++ b/packages/docs/docs/getting-started.md
@@ -199,51 +199,25 @@ to provide local services to a subtree.
## Synchronous and asynchronous components
-A regular Effect View component runs its body during React render. That Effect
-must complete synchronously: yielding a service, reading synchronous state, or
-creating a scoped object is fine; sleeping, fetching, or awaiting a promise is
-not.
+A regular Effect View component runs its body during React render. Effects
+executed directly by that body—including setup passed to `useOnMount`,
+`useOnChange`, or `useLayer`—must complete synchronously every time they run.
+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
-component then suspends and accepts React Suspense props such as `fallback`:
+Choose the integration that matches the asynchronous work:
-```tsx title="src/UserView.tsx"
-import { Async, Component, Memoized } from "effect-view"
-import { loadUser } from "./api"
-
-export const UserView = Component.make("UserView")(
- function* (props: { readonly userId: string }) {
- const user = yield* Component.useOnChange(
- () => loadUser(props.userId),
- [props.userId],
- )
-
- return
}),
- Memoized.memoized,
-)
-```
-
-Use it from an Effect View parent in the usual way:
-
-```tsx
-const User = yield* UserView.use
-
-return
-```
-
-`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.
+- Make the component asynchronous with [`Async.async`](./async) when it must
+ wait for a one-off asynchronous Effect before producing JSX. The component
+ suspends while it waits.
+- Use [Query](./query) for server reads that need caching, sharing, refresh, or
+ invalidation.
+- Use [Mutation](./mutation) for user-triggered writes with observable pending
+ and error state.
+- Use `useRunPromise` or `useCallbackPromise` for asynchronous event work that
+ does not need Mutation state.
+- Use a post-commit hook with a scoped fiber for subscriptions or background
+ work tied to the component lifecycle.
## Use Effect services
diff --git a/packages/docs/docs/state-management.md b/packages/docs/docs/state-management.md
index 19285e8..9d4366d 100644
--- a/packages/docs/docs/state-management.md
+++ b/packages/docs/docs/state-management.md
@@ -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.
`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
-[`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
`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
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).
diff --git a/packages/docs/sidebars.ts b/packages/docs/sidebars.ts
index 7f3da56..b316607 100644
--- a/packages/docs/sidebars.ts
+++ b/packages/docs/sidebars.ts
@@ -6,6 +6,7 @@ const sidebars: SidebarsConfig = {
docsSidebar: [
"getting-started",
"state-management",
+ "async",
"query",
"mutation",
"forms",
diff --git a/packages/docs/src/pages/index.module.css b/packages/docs/src/pages/index.module.css
index 042c345..aee6612 100644
--- a/packages/docs/src/pages/index.module.css
+++ b/packages/docs/src/pages/index.module.css
@@ -72,9 +72,15 @@
letter-spacing: -0.075em;
line-height: 0.92;
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-clip: text;
color: transparent;
diff --git a/packages/docs/src/pages/index.tsx b/packages/docs/src/pages/index.tsx
index 7119772..8afa80c 100644
--- a/packages/docs/src/pages/index.tsx
+++ b/packages/docs/src/pages/index.tsx
@@ -100,8 +100,8 @@ export default function Home(): ReactNode {
Effect View for React 19
- React components,
- powered by Effect.
+ React components,
+ powered by Effect.
Bring typed services, scoped resources, reactive state, server