Files
effect-fc/packages/docs/docs/getting-started.md
T
Julien Valverdé c941e5970a
Lint / lint (push) Successful in 16s
Docs
2026-06-08 22:19:58 +02:00

11 KiB

sidebar_position, title
sidebar_position title
1 Getting Started

Getting Started

effect-fc lets React components be written as Effect programs. Inside a component body you can yield services, run Effects, subscribe to Effect-powered state, and still export a normal React function component at the edge of your app.

This guide starts with the smallest useful setup:

  1. Install effect-fc with its peer dependencies.
  2. Create a React runtime from an Effect Layer.
  3. Wrap your React app with ReactRuntime.Provider.
  4. Write a component with Component.make.
  5. Convert it to a React component with Component.withRuntime.

Install

Install effect-fc alongside effect and React 19.2 or newer:

npm install effect-fc effect react react-dom

If your project uses TypeScript, also install React's type packages:

npm install --save-dev @types/react @types/react-dom

Create A Runtime

An Effect-FC app needs an Effect runtime. Build one from the services your UI needs, then share it with React through ReactRuntime.Provider.

For an empty app, Layer.empty is enough:

import { Layer } from "effect"
import { ReactRuntime } from "effect-fc"

export const runtime = ReactRuntime.make(Layer.empty)

As your app grows, add services to the layer:

import { FetchHttpClient } from "@effect/platform"
import { Layer } from "effect"
import { ReactRuntime } from "effect-fc"

const AppLive = Layer.empty.pipe(
  Layer.provideMerge(FetchHttpClient.layer),
)

export const runtime = ReactRuntime.make(AppLive)

Provide The Runtime

At the React root, wrap your app with ReactRuntime.Provider:

import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { ReactRuntime } from "effect-fc"
import { App } from "./App"
import { runtime } from "./runtime"

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <ReactRuntime.Provider runtime={runtime}>
      <App />
    </ReactRuntime.Provider>
  </StrictMode>,
)

ReactRuntime.Provider also works with routers. Keep it above your router provider so route components can be converted with the same runtime context.

Write Your First Component

Use Component.make when you want automatic tracing spans, or Component.makeUntraced when you only want the component behavior. This creates an Effect-FC component, not a plain React component yet.

import { Effect } from "effect"
import { Component } from "effect-fc"

export const HelloEffect = Component.make("HelloEffect")(function* (props: {
  readonly name: string
}) {
  const message = yield* Effect.succeed(`Hello, ${props.name}`)

  return <h1>{message}</h1>
})

At this point HelloEffect can yield Effects, services, and scoped lifecycle work, but React cannot render it directly.

Apply A Runtime

Use Component.withRuntime at the boundary where an Effect-FC component needs to become a regular React function component.

import { Component } from "effect-fc"
import { HelloEffect } from "./HelloEffect"
import { runtime } from "./runtime"

export const Hello = HelloEffect.pipe(
  Component.withRuntime(runtime.context),
)

Hello is now a normal React component:

import { Hello } from "./Hello"

export function App() {
  return <Hello name="Effect" />
}

Use Effect-FC Components Together

Inside an Effect-FC component, other Effect-FC components are available through their .use effect. Yield .use to get a React component for the current runtime context, then render it like JSX.

import { Component } from "effect-fc"
import { HelloEffect } from "./HelloEffect"

export const GreetingCardEffect = Component.make("GreetingCard")(
  function* () {
    const Hello = yield* HelloEffect.use

    return (
      <section>
        <Hello name="Effect" />
        <p>This component is still running inside Effect-FC.</p>
      </section>
    )
  },
)

Use Component.withRuntime only when you leave the Effect-FC tree and need a normal React component again:

import { Component } from "effect-fc"
import { GreetingCardEffect } from "./GreetingCardEffect"
import { runtime } from "./runtime"

export const GreetingCard = GreetingCardEffect.pipe(
  Component.withRuntime(runtime.context),
)

Component Lifecycle

Every Effect-FC component instance exposes an Effect Scope. Effects that need Scope.Scope can use that component scope to register finalizers, fork scoped work, or acquire scoped resources.

The component scope is created when React mounts the component and closes when React unmounts it. When the scope closes, Effect runs the finalizers registered inside that scope.

import { Console, Effect } from "effect"
import { Component } from "effect-fc"

export const MountedMessageEffect = Component.make("MountedMessage")(
  function* () {
    yield* Component.useOnMount(() =>
      Effect.gen(function* () {
        yield* Console.log("MountedMessage mounted")
        yield* Effect.addFinalizer(() =>
          Console.log("MountedMessage unmounted"),
        )
      }),
    )

    return <p>This component owns an Effect scope.</p>
  },
)

Use Component.useOnMount for work that should run once for the component instance. Use Component.useOnChange when the scoped work should be recreated when dependencies change.

const UserPanelEffect = Component.make("UserPanel")(
  function* (props: { readonly userId: string }) {
    const user = yield* Component.useOnChange(
      () => fetchUser(props.userId),
      [props.userId],
    )

    return <p>{user.name}</p>
  },
)

In this example, changing userId closes the previous dependency scope before creating a new one. Unlike useOnMount, useOnChange does not expose the component's root scope directly. It creates and provides its own scope for that dependency window. Some other Effect-FC hooks follow the same pattern when they need a lifecycle that is narrower than the whole component instance.

Useful Effect-FC Hooks

The most common Effect-FC hooks are:

  • Component.useOnMount: run an Effect once for the component instance and return its value.
const initialData = yield* Component.useOnMount(() => loadInitialData)
  • Component.useOnChange: run an Effect again when dependencies change and return its latest value.
const user = yield* Component.useOnChange(() => fetchUser(id), [id])
  • Component.useReactEffect: Effect-powered React.useEffect with scoped finalizers.
yield* Component.useReactEffect(() => subscribe(id), [id])
  • Component.useReactLayoutEffect: Effect-powered React.useLayoutEffect.
yield* Component.useReactLayoutEffect(() => measure(ref), [])
  • Component.useRunSync and Component.useRunPromise: run Effects from React event handlers.
const runPromise = yield* Component.useRunPromise()

return (
  <button onClick={() => void runPromise(saveUser)}>
    Save
  </button>
)
  • Component.useCallbackSync and Component.useCallbackPromise: create stable React callbacks.
const save = yield* Component.useCallbackPromise(
  (user: User) => saveUser(user),
  [],
)

return <button onClick={() => void save(user)}>Save</button>
  • Component.useContextFromLayer: create a React runtime context from an Effect Layer.
yield* Component.useContextFromLayer(UserLive)

Use React Normally

An Effect-FC component is still a React function component. Anything you can do in a regular React component can also be done in an Effect-FC component, including React hooks, refs, event handlers, context, and JSX composition.

import { Component } from "effect-fc"
import * as React from "react"

const CounterEffect = Component.make("Counter")(function* () {
  const [count, setCount] = React.useState(0)
  const buttonRef = React.useRef<HTMLButtonElement>(null)

  return (
    <button ref={buttonRef} onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
})

Use regular React hooks for local UI concerns, and reach for Effect-FC helpers when the component needs Effect services, scopes, resources, or Effect-powered callbacks.

Use Services

Components can yield Effect services directly. Define services with Effect, provide them in your runtime layer, then consume them from the component body.

import { Effect } from "effect"

export class GreetingService extends Effect.Service<GreetingService>()(
  "GreetingService",
  {
    succeed: {
      greet: (name: string) => `Welcome, ${name}`,
    },
  },
) {}

Provide the service in your runtime:

import { Layer } from "effect"
import { ReactRuntime } from "effect-fc"
import { GreetingService } from "./services"

const AppLive = Layer.empty.pipe(
  Layer.provideMerge(GreetingService.Default),
)

export const runtime = ReactRuntime.make(AppLive)

Then read it inside a component:

import { Component } from "effect-fc"
import { runtime } from "./runtime"
import { GreetingService } from "./services"

const GreetingEffect = Component.make("Greeting")(function* (props: {
  readonly name: string
}) {
  const greeting = yield* GreetingService

  return <p>{greeting.greet(props.name)}</p>
})

export const Greeting = GreetingEffect.pipe(
  Component.withRuntime(runtime.context),
)

Mount And Cleanup Effects

Use Component.useOnMount for scoped work that should start when the component mounts and finalize when it unmounts.

import { Console, Effect } from "effect"
import { Component } from "effect-fc"

const Mounted = Component.make("Mounted")(function* () {
  yield* Component.useOnMount(() =>
    Effect.gen(function* () {
      yield* Console.log("Mounted")
      yield* Effect.addFinalizer(() => Console.log("Unmounted"))
    }),
  )

  return <p>Open the console, then unmount me.</p>
})

Finalizers are tied to the component scope, so this is the right place for subscriptions, resources, and other lifecycle-bound Effects.

Where To Go Next

Once the runtime and component boundary are in place, the rest of the library builds on the same idea:

  • Subscribable.useAll reads Effect subscribables and rerenders when they change.
  • Lens connects React state and Effect SubscriptionRef values.
  • Query and Mutation model async data and user-triggered operations.
  • Form, SubmittableForm, and SynchronizedForm help build Effect-backed forms.

The important pattern is small and repeatable: write Effect-FC components inside the runtime, then use Component.withRuntime at React boundaries.