@everystate/react 1.0.6

React adapter for @everystate/core. Five hooks and a provider - that's the entire API. ~50 lines, zero dependencies beyond React and the core store.

See it in action Complete React + EveryState example - a full task manager with every file shown inline. Great for sharing with React devs.

Install

npm install @everystate/react @everystate/core react

Peer dependencies: @everystate/core >=1.0.5 and react >=18.0.0.

Quick Start

import { createEveryState } from '@everystate/core';
import { EventStateProvider, usePath, useIntent } from '@everystate/react';

// Store lives outside React
const store = createEveryState({
  state: { count: 0 },
});

// Business logic lives outside React
store.subscribe('intent.increment', () => {
  store.set('state.count', store.get('state.count') + 1);
});

function Counter() {
  const count = usePath('state.count');
  const increment = useIntent('intent.increment');
  return <button onClick={() => increment(true)}>Count: {count}</button>;
}

function App() {
  return (
    <EventStateProvider store={store}>
      <Counter />
    </EventStateProvider>
  );
}

Architecture

The key insight: React is a rendering engine, not the application architecture.

This means you can test all business logic without rendering a single component, share state across multiple React trees (or non-React UIs), and swap the view layer without touching state code.

EventStateProvider

<EventStateProvider store={store}> children </EventStateProvider>

Makes a store available to descendant hooks via React Context. The store is created outside React - the provider doesn't own, create, or destroy it.

import { createEveryState } from '@everystate/core';

// store.js - created once, outside any component
export const store = createEveryState({ state: { tasks: [] } });

// App.jsx
import { EventStateProvider } from '@everystate/react';
import { store } from './store.js';

export default function App() {
  return (
    <EventStateProvider store={store}>
      <Header />
      <TaskList />
    </EventStateProvider>
  );
}

Why not just import the store directly? You can. The provider makes testing easier (inject a mock store) and makes the dependency explicit.

useStore()

useStore() → store

Returns the EveryState store from context. Throws if called outside a provider.

const store = useStore();
const value = store.get('some.path');

You rarely need this - usePath and useIntent cover most cases.

usePath(path)

usePath(path: string) → any

Subscribe to a dot-path. Re-renders the component only when the value at that path changes. Uses React 18's useSyncExternalStore for concurrent-mode safety.

function Header() {
  const count = usePath('state.taskCount') || 0;
  return <span>{count} task{count === 1 ? '' : 's'}</span>;
}

function TaskList() {
  const items = usePath('derived.tasks.filtered') || [];
  return <ul>{items.map(t => <li key={t.id}>{t.text}</li>)}</ul>;
}

This is the "read" side. No props, no selectors, no context consumers - just a path.

useIntent(path)

useIntent(path: string) → (value: any) → any

Returns a stable, memoized function that publishes a value to a path. Safe to pass as a prop without causing re-renders.

function TaskInput() {
  const [text, setText] = useState('');
  const add = useIntent('intent.addTask');

  return <input
    value={text}
    onKeyDown={(e) => {
      if (e.key === 'Enter') { add(text); setText(''); }
    }}
  />;
}

This is the "write" side. The component publishes intent. A subscriber handles the logic.

useWildcard(path)

useWildcard(wildcardPath: string) → any

Subscribe to a wildcard path. Re-renders when any child of that path changes. Returns the parent object.

function UserCard() {
  const user = useWildcard('state.user.*');
  // Re-renders when state.user.name, state.user.email, etc. change
  return <div>{user?.name} ({user?.email})</div>;
}

useAsync(path)

useAsync(path: string) → { data, status, error, execute, cancel }

Async data fetching with automatic status tracking.

function UserList() {
  const { data, status, error, execute, cancel } = useAsync('users');

  useEffect(() => {
    execute((signal) =>
      fetch('/api/users', { signal }).then(r => r.json())
    );
  }, [execute]);

  if (status === 'loading') return <Spinner />;
  if (error) return <p>Error: {error}</p>;
  return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

Calling execute again auto-aborts the previous in-flight request. No race conditions, no stale data.

Three Namespaces

NamespacePurposeHook
state.*Authoritative application stateusePath
derived.*Computed projections (pure functions of state)usePath
intent.*Write-only signals from the UIuseIntent

Business Logic Outside Components

// store.js - no React imports
store.subscribe('intent.addTask', (text) => {
  const t = String(text || '').trim();
  if (!t) return;
  const tasks = store.get('state.tasks') || [];
  const next = [...tasks, { id: genId(), text: t, completed: false }];
  store.set('state.tasks', next);
  store.set('state.taskCount', next.length);
});

Derived State

function recomputeDerived() {
  const tasks = store.get('state.tasks') || [];
  const filter = store.get('state.filter');
  store.set('derived.tasks.filtered', filterTasks(tasks, filter));
}

store.subscribe('state.tasks', recomputeDerived);
store.subscribe('state.filter', recomputeDerived);
recomputeDerived();

Components read from derived.* - they never compute:

const items = usePath('derived.tasks.filtered');

Race Condition Handling

function SearchResults() {
  const { data, execute } = useAsync('search');
  const query = usePath('state.searchQuery');

  useEffect(() => {
    if (!query) return;
    execute((signal) =>
      fetch(`/api/search?q=${query}`, { signal }).then(r => r.json())
    );
  }, [query, execute]);

  return <ul>{data?.map(r => <li key={r.id}>{r.title}</li>)}</ul>;
}

No cancelled boolean. No cleanup function. No stale closures. The store owns the abort lifecycle.

Testing Without React

import { store } from './store.js';

test('adding a task increments taskCount', () => {
  const before = store.get('state.taskCount');
  store.set('intent.addTask', 'test task');
  expect(store.get('state.taskCount')).toBe(before + 1);
});

test('filter shows only active tasks', () => {
  store.set('intent.addTask', 'task 1');
  store.set('intent.addTask', 'task 2');
  store.set('intent.toggleTask', store.get('state.tasks')[0].id);
  store.set('intent.changeFilter', 'active');
  expect(store.get('derived.tasks.filtered')).toHaveLength(1);
});