npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

foldkit-react

v0.4.1

Published

Run React components inside a Foldkit view and Foldkit programs inside React, with Messages and Ports as the only boundary.

Readme

foldkit-react

Use React components inside a Foldkit view, and Foldkit programs inside a React app. Each side keeps its own renderer and its own state; they meet at a single DOM element, and only Messages and Ports cross it.

React inside Foldkit                     Foldkit inside React

Model ─▶ view ─▶ ReactComponent.view     <Counter step={2} onCount={…} />
                   │ props (a DOM prop)       │ props ─▶ inbound Ports
                   ▼                          ▼
          <foldkit-react-host>          <div> (React owns)
                   │ React root               │ Runtime.embed
                   ▼                          ▼
             React component              Foldkit program
                   │ event                    │ outbound Ports
                   ▼                          ▼
         Message ─▶ update                callback props

Who owns what

  • React inside Foldkit. The Foldkit Model owns every value it passes as a prop. React owns the component's internal state (useState, focus, a library's selection), which survives Foldkit renders. A React event never changes the Model directly: you map it to a Message and update decides.
  • Foldkit inside React. The Foldkit program owns its Model. React props reach it only through the program's inbound Ports, and it reports back only through outbound Ports. There is no access to its Model from React.

Neither renderer ever touches the other's DOM subtree, so there is no second reconciler and no shared ownership to reason about.

Install

pnpm add foldkit-react foldkit effect react react-dom

React 19 is required. examples/react runs both directions, plus codegen, as a pinned transcript.

React inside Foldkit

import { Schema } from 'effect'
import type { Html, HtmlBuilder } from 'foldkit/html'
import { defineMessageUnion } from 'foldkit/message'
import { ReactComponent } from 'foldkit-react'
import { createElement } from 'react'

const Button = (props: { readonly label: string; readonly onPress?: () => void }) =>
  createElement('button', { onClick: props.onPress }, props.label)
const ReactButton = ReactComponent.define(Button, { events: ['onPress'] })
const Model = Schema.Struct({ count: Schema.Number })
type Model = typeof Model.Type
const Message = defineMessageUnion({ Incremented: {} })
type Message = typeof Message.Type

const update = (model: Model, _message: Message) => ({
  model: { count: model.count + 1 },
})
const view = (model: Model, h: HtmlBuilder<Message>): Html =>
  ReactButton.view({
    props: { label: `Count: ${model.count}` },
    messages: { onPress: () => Message.Incremented() },
  }, h)

Use this view and update in a Foldkit program initialized with { count: 0 }. The button is a real React component; its callback becomes Incremented, and the next Model supplies its next label. Replace Button with a library widget using the same event mapping. Non-event function props remain ordinary props.

What happens:

  • define does no rendering. It records the component and which props are events. Events must be callbacks returning void; a callback that returns a value (shouldClose: () => boolean) is a normal prop.
  • view returns ordinary Foldkit Html: a <foldkit-react-host> element whose input DOM property carries the props. Foldkit diffs that property like any other, so a new Model re-renders the same React root. It does not remount.
  • messages maps each event's arguments to a Message. The Message reaches update through Foldkit's normal event path, so inside a Submodel it is lifted by toParentMessage like a click would be. An event without a mapper is not passed to the component.
  • The Message type comes from h, so a mapper returning the wrong Message is a type error.

Suspense and errors

Each island is its own React root, so it has its own Suspense boundary: lazy, use(promise), and libraries that suspend all work, showing suspenseFallback (default: nothing) meanwhile.

The following integration fragment assumes your Editor, Spinner, and EditorCrashed Message; define ReactEditor with ReactComponent.define(Editor).

ReactEditor.view(
  {
    props: { document: model.document },
    suspenseFallback: createElement(Spinner),
    errorBoundary: {
      fallback: () => createElement('p', null, 'The editor crashed.'),
      toMessage: error => EditorCrashed({ reason: String(error) }),
    },
  },
  h,
)

Without errorBoundary, a render error is uncaught at the island's React root, which React handles as in any app: it removes that root's tree and reports the error. With it, the island shows fallback and, if given, toMessage dispatches a Message so the Model can record the crash. The fallback stays until the island is removed.

Waiting on Model data: readAsyncData

Loading belongs to Foldkit: a Command fetches, update stores an AsyncData in the Model. An island can suspend on that value instead of branching on its tag:

const Profile = ({ user }: { readonly user: AsyncData.AsyncData<User, string> }) => {
  const { data, isRefreshing } = readAsyncData(user)
  return createElement('p', null, isRefreshing ? `${data.name} (refreshing)` : data.name)
}

ReactProfile.view(
  {
    props: { user: model.user },
    suspenseFallback: createElement(Spinner),
    errorBoundary: {
      fallback: error => (error instanceof AsyncDataFailure ? `Not found: ${error.error}` : 'Crashed'),
    },
  },
  h,
)
Idle | Loading       -> suspends: the island's suspenseFallback
Failure(error)       -> throws AsyncDataFailure: the island's errorBoundary
Success(data)        -> { data }
Refreshing(data)     -> { data, isRefreshing: true }        no suspension
Stale(error, data)   -> { data, maybeStaleError: Some(error) }

readAsyncData starts nothing and caches nothing. The suspended component retries when the next value reaches it as props or React state, which is how island props and outbound Port values arrive; a value read from anywhere else would never retry.

The host element

hostAttributes go on <foldkit-react-host>, which Foldkit owns: keys for keyed lists, classes, ARIA labels. The host is display: inline by default, as every unknown element is; style it if the component needs a block.

ReactCounter.view({ props: { id: row.id }, hostAttributes: [h.Key(row.id), h.Class('row')] }, h)

With Foldkit DevTools time travel, jumping to a past Model re-renders the same React root with that Model's props; React's internal state is not rewound, since Foldkit does not own it. Island events while paused reach no Model, like any other handler in the replayed view, and resuming restores the live props.

An island removed by Foldkit unmounts its React tree, running effect cleanups. A keyed move keeps it mounted, because the host defers unmounting until after the move completes.

Foldkit inside React

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

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:

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.