React / foldkit-react

Foldkit inside React

ts
import { FoldkitComponent } from 'foldkit-react'

const Counter = FoldkitComponent.define({
  // Runs once per runtime. Props here are initial configuration only.
  make: (container, props: CounterProps) => makeCounter(container, props.initialCount),
  // Re-sent whenever the selected value changes (Object.is).
  inbound: { stepChanged: props => props.step },
  // Called with the latest props, so a new onCountChange needs no resubscribe.
  outbound: { countChanged: (count, props) => props.onCountChange?.(count) },
})

// <Counter initialCount={10} step={2} onCountChange={setCount} />

makeCounter returns a program from Runtime.makeElement with ports. Port names and value types are checked against it: bindings send and receive each Port's Encoded side, the same values EmbedHandle.ports uses.

The component renders a <div> (it accepts className and style) and embeds the program in a child element React never renders. On unmount it disposes the runtime, stopping its Subscriptions, Managed Resources, and in-flight Commands.

  • Strict Mode mounts, disposes, and mounts again; only the second runtime keeps running.
  • Initial values arrive in order. Inbound values sent before the program's Subscriptions start are buffered, and outbound values emitted by init Commands are delivered after the listeners are attached.
  • Invalid inbound values fail the Port's Schema, are logged, and never reach the app. The same invalid value is not re-sent on every render.

Custom equality and restarts

ts
FoldkitComponent.define({
  make: (container, props: DashboardProps) => makeDashboard(container, props.userId),
  inbound: { filtersChanged: { select: props => props.filters, equals: shallowEqual } },
  // A different user is a different program, not a new Port value.
  restartKey: props => props.userId,
})

When restartKey changes (compared with Object.is), the runtime is disposed and a fresh one is made with the current props, which also receives every inbound value.

useFoldkitElement

The hook under FoldkitComponent, for components that need the Port handles themselves:

ts
const { ref, handle } = useFoldkitElement({
  make: container => makeCounter(container, 0),
})
useEffect(() => {
  handle?.ports.stepChanged.send(step)
}, [handle, step])
return createElement('div', { ref })

handle is undefined before the first effect and after unmount. onEmbed runs synchronously after embed, the place to subscribe to an outbound Port without missing emissions from init. Give the ref'd element no React children.

Limits

  • No Foldkit children inside a React island: pass React children as props.children, and place Foldkit content beside the island.
  • Foldkit Commands are not React Suspense. An outer <Suspense> does not wait for an embedded program; the program shows its own loading states from its Model.
  • A server render emits an empty <foldkit-react-host>; the React component renders on the client.
  • The event mapper's argument types come from the last overload of an overloaded callback prop.