Files
effect-view/packages/docs/docs/async.md
T
ThilawynandJulien Valverdé 4b712de788
Publish / publish (push) Successful in 3m23s
Lint / lint (push) Successful in 48s
Finalize the Effect View rename and refresh docs, examples, and tooling (#59)
## Summary

- Rename `effect-fc-next` to `effect-view` and update package metadata, internal symbols, imports, and repository references.
- Promote the Effect 4 example to `packages/example` and move the legacy Effect 3 example to `packages/effect-fc-example`.
- Add `effect-view` to the npm publishing workflow.
- Update the Vite Fast Refresh plugin for `effect-view` and rename `effectViewPlugin()` to `effectView()`.
- Upgrade Effect, React, TypeScript, build tooling, and related dependencies.
- Refresh the documentation and homepage.

## Documentation

- Add a dedicated Async guide covering:
  - Suspense fallbacks
  - Hook ordering
  - Memoization
  - Structural prop equality
- Clarify synchronous and asynchronous component behavior.
- Document runner scope and service requirements.
- Explain stable construction of Query, Mutation, and Form instances.
- Expand focused Lens and Form examples, including curried focus helpers.
- Document the optional `@effect/platform-browser` dependency for window-focus query refresh.
- Improve the Forms guide structure and examples.
- Fix responsive homepage headline wrapping and update homepage copy.

## Notable API changes

- Package rename:

  ```diff
  - effect-fc-next
  + effect-view

---------

Co-authored-by: Julien Valverdé <julien.valverde@mailo.com>
Reviewed-on: #59
2026-07-27 04:11:42 +02:00

3.8 KiB

sidebar_position, title
sidebar_position title
2 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.

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:

const User = yield* UserCard.use

return <User userId="123" fallback={<p>Loading user...</p>} />

Or configure a default fallback once:

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:

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:

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:

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.