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

@workflow-ts/react

v0.1.2

Published

React bindings for workflow-ts

Readme

@workflow-ts/react

React hooks for workflow-ts.

Installation

pnpm add @workflow-ts/react @workflow-ts/core

Recommended Architecture

Use workflow-ts as a rendering subscription + mapping system:

  1. Subscribe to workflow rendering with useWorkflow.
  2. Map that rendering tree to React components.
import { useWorkflow } from '@workflow-ts/react';

function AppScreen({ userId }: { userId: string }) {
  const rendering = useWorkflow(appWorkflow, { userId });
  return <AppRenderer rendering={rendering} />;
}

This keeps workflow logic inside workflows and keeps React focused on rendering.

Performance

  • Preferred setup: React Compiler enabled in the consuming app.
  • With React Compiler, manual React.memo is usually unnecessary.
  • Keep props/rendering references stable to minimize work.
  • Keep workflow props small and immutable (prefer flat scalar values like ids/flags/strings).
  • Avoid passing large nested object graphs as hook props; pass only minimal derived inputs.

Props Contract

useWorkflow and useWorkflowWithState expose a TypeScript AllowedProp contract for hook props. At runtime, unsupported values are validated and rejected only in development environments (React Native __DEV__, NODE_ENV !== 'production', or bundler dev flags).

Allowed values:

  • primitives (string, number, boolean, bigint, symbol, null, undefined)
  • functions
  • arrays
  • plain objects (Object.prototype or null prototype)
  • Date, Map, Set
  • ArrayBuffer, DataView, typed arrays

Rejected values:

  • class instances
  • branded built-ins outside the allowlist (URL, Error, RegExp, etc.)
  • Promise, WeakMap, WeakSet

Hooks

useWorkflow(workflow, props, onOutput?, options?)

Subscribe to a workflow's rendering. Re-renders component when workflow state changes.

import { useWorkflow } from '@workflow-ts/react';
import { type Workflow } from '@workflow-ts/core';

const counterWorkflow: Workflow<void, State, never, Rendering> = {
  // ... workflow definition
};

function Counter() {
  const { count, onIncrement, onDecrement } = useWorkflow(
    counterWorkflow,
    undefined, // props
  );

  return (
    <div>
      <span>{count}</span>
      <button onClick={onIncrement}>+</button>
      <button onClick={onDecrement}>-</button>
    </div>
  );
}

Parameters:

  • workflow - The workflow definition
  • props - Props to pass to the workflow (must satisfy the plain-only contract above)
  • onOutput? - Optional callback for workflow outputs
  • options? - Optional hook options

Options:

  • resetOnWorkflowChange?: boolean - Recreate runtime when workflow identity changes (opt-in). Defaults to false. To hard-reset in React, consider using a component key.
  • outputHandlers?: { [K in O extends { type: string } ? O['type'] : never]?: (output: Extract<O, { type: K }>) => void } - Typed per-output handlers for discriminated union outputs.
  • Hooks are compatible with React StrictMode development replays.
  • Disposed runtimes in @workflow-ts/core still throw when used (strict disposal contract).
  • lifecycle?: 'always-on' | 'pause-when-backgrounded' - Runtime lifecycle mode. Defaults to 'always-on'.
  • isActive?: boolean - Active state used with 'pause-when-backgrounded'. Defaults to true.
  • In pause mode, explicit isActive: true -> false transitions dispose runtime immediately.

Returns: The current rendering (type R from workflow)

useWorkflowWithState(workflow, options)

Like useWorkflow, but also exposes runtime controls.

import { useWorkflowWithState } from '@workflow-ts/react';

function SearchComponent() {
  const { rendering, state, updateProps, snapshot } = useWorkflowWithState(searchWorkflow, {
    props: { query: '' },
    onOutput: (output) => console.log('Output:', output),
  });

  return (
    <div>
      <input value={state.query} onChange={(e) => updateProps({ query: e.target.value })} />
      <ul>
        {rendering.results.map((r) => (
          <li key={r.id}>{r.name}</li>
        ))}
      </ul>
    </div>
  );
}

Options:

  • props: P - Initial props (must satisfy the plain-only contract above)
  • onOutput?: (output: O) => void - Output callback
  • outputHandlers?: { [K in O extends { type: string } ? O['type'] : never]?: (output: Extract<O, { type: K }>) => void } - Typed per-output handlers for discriminated union outputs.
  • resetOnWorkflowChange?: boolean - Recreate runtime when workflow identity changes (opt-in). Defaults to false.
  • StrictMode development replay is supported without changing core runtime disposal semantics.
  • lifecycle?: 'always-on' | 'pause-when-backgrounded' - Runtime lifecycle mode. Defaults to 'always-on'.
  • isActive?: boolean - Active state used with 'pause-when-backgrounded'. Defaults to true.
  • Inactive controls are safe: updateProps no-ops and snapshot returns last-known value (or undefined).

Returns:

  • rendering: R - Current rendering
  • state: S - Current state (for debugging)
  • props: P - Current props
  • updateProps: (props: P) => void - Update props
  • snapshot: () => string | undefined - Get state snapshot

React Native lifecycle example

Use AppState to pause runtime activity while the app is backgrounded:

import { AppState } from 'react-native';
import { useEffect, useState } from 'react';
import { useWorkflow } from '@workflow-ts/react';

function Screen() {
  const [isActive, setIsActive] = useState(AppState.currentState === 'active');

  useEffect(() => {
    const subscription = AppState.addEventListener('change', (nextState) => {
      setIsActive(nextState === 'active');
    });
    return () => subscription.remove();
  }, []);

  const rendering = useWorkflow(workflow, props, undefined, {
    lifecycle: 'pause-when-backgrounded',
    isActive,
  });

  return <Content rendering={rendering} />;
}

Example: Async Data Fetching

import { useEffect } from 'react';
import { useWorkflow } from '@workflow-ts/react';
import { type Workflow, createWorker } from '@workflow-ts/core';

type State =
  | { type: 'idle' }
  | { type: 'loading' }
  | { type: 'success'; users: User[] }
  | { type: 'error'; message: string };

interface Rendering {
  isLoading: boolean;
  users: User[];
  error: string | null;
  load: () => void;
}

const loadUsersWorker = createWorker('load-users', async (signal) => {
  const res = await fetch('/api/users', { signal });
  return res.json();
});

const usersWorkflow: Workflow<void, State, never, Rendering> = {
  initialState: () => ({ type: 'idle' }),

  render: (_props, state, ctx) => {
    if (state.type === 'loading') {
      ctx.runWorker(loadUsersWorker, 'load', (users) => () => ({
        state: { type: 'success', users },
      }));
    }

    return {
      isLoading: state.type === 'loading',
      users: state.type === 'success' ? state.users : [],
      error: state.type === 'error' ? state.message : null,
      load: () => ctx.actionSink.send(() => ({ state: { type: 'loading' } })),
    };
  },
};

function UserList() {
  const { isLoading, users, error, load } = useWorkflow(usersWorkflow, undefined);

  useEffect(() => {
    load();
  }, []);

  if (isLoading) return <Spinner />;
  if (error) return <Error message={error} />;

  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Example: Props-Driven Workflow

import { useState } from 'react';
import { useWorkflow } from '@workflow-ts/react';
import { type Workflow } from '@workflow-ts/core';

// Workflow that derives state from props
const searchWorkflow: Workflow<{ query: string }, State, never, Rendering> = {
  initialState: (props) => ({ query: props.query, results: [] }),

  render: (props, state, ctx) => {
    // Update state when props change
    if (props.query !== state.query) {
      ctx.actionSink.send((s) => ({ state: { ...s, query: props.query } }));
    }

    return {
      query: state.query,
      results: state.results,
    };
  },
};

function Search() {
  const [input, setInput] = useState('');
  const { results } = useWorkflow(searchWorkflow, { query: input });

  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <ul>
        {results.map((r) => (
          <li key={r.id}>{r.name}</li>
        ))}
      </ul>
    </div>
  );
}

Testing Components

import { render, screen, fireEvent } from '@testing-library/react';
import { useWorkflow } from '@workflow-ts/react';

test('counter increments', () => {
  render(<Counter />);

  expect(screen.getByText('0')).toBeInTheDocument();

  fireEvent.click(screen.getByText('+'));

  expect(screen.getByText('1')).toBeInTheDocument();
});

TypeScript Tips

Extract Types

// Define types separately for reuse
interface CounterState {
  count: number;
}

interface CounterRendering {
  count: number;
  onIncrement: () => void;
  onDecrement: () => void;
}

type CounterOutput = { type: 'reachedZero' } | { type: 'reachedTen' };

const counterWorkflow: Workflow<void, CounterState, CounterOutput, CounterRendering> = {
  // ...
};

// Use in component
function Counter() {
  const rendering: CounterRendering = useWorkflow(counterWorkflow, undefined);
  // ...
}

Generic Components

interface WorkflowProps<P, R> {
  workflow: Workflow<P, any, any, R>;
  props: P;
}

function WorkflowComponent<P, R>({ workflow, props }: WorkflowProps<P, R>) {
  const rendering = useWorkflow(workflow, props);
  // ...
}

License

MIT