Skip to content

React Integration

SynState provides a companion package synstate-react-hooks for seamless React integration.

Terminal window
npm add synstate-react-hooks

createState from synstate-react-hooks returns a React hook instead of a raw Observable:

import type * as React from 'react';
import { createState } from 'synstate-react-hooks';
const [useUserState, setUserState] = createState({
name: '',
email: '',
});
const UserProfile = (): React.JSX.Element => {
const user = useUserState();
return (
<div>
<p>{`Name: ${user.name}`}</p>
<button
onClick={() => {
setUserState({
name: 'Alice',
email: 'alice@example.com',
});
}}
>
{'Set User'}
</button>
</div>
);
};

This is equivalent to the following code without synstate-react-hooks:

import * as React from 'react';
import { createState } from 'synstate';
const [userState, setUserState] = createState({
name: '',
email: '',
});
const UserProfile = (): React.JSX.Element => {
const user = React.useSyncExternalStore(
(onStoreChange: () => void) => {
const { unsubscribe } = userState.subscribe(onStoreChange);
return unsubscribe;
},
() => userState.getSnapshot().value,
);
return (
<div>
<p>{`Name: ${user.name}`}</p>
<button
onClick={() => {
setUserState({
name: 'Alice',
email: 'alice@example.com',
});
}}
>
{'Set User'}
</button>
</div>
);
};

synstate-react-hooks’s createState also returns a 3-element tuple, just like the core package. The third element includes the same utilities (updateState, resetState, getSnapshot, initialState) described in createState in Depth, plus state — the underlying Observable:

import type * as React from 'react';
import { createState } from 'synstate-react-hooks';
// The third element provides additional utilities and the underlying Observable.
const [
useCount,
setCount,
{
state,
updateState: updateCount,
resetState: resetCount,
getSnapshot: getCountSnapshot,
},
] = createState(0);
const increment = (): void => {
updateCount((n) => n + 1);
};
const Counter = (): React.JSX.Element => {
const count = useCount();
return (
<div>
<p>{`Count: ${count}`}</p>
<button onClick={increment}>{'Increment'}</button>
<button onClick={resetCount}>{'Reset'}</button>
</div>
);
};
// `state` is the same InitializedObservable<number> that the core
// synstate package's createState returns as its first element.
// You can use it with pipe, combine, subscribe, etc.
state.subscribe((value) => {
console.log('count changed:', value);
});
// Read the current value synchronously (outside of React rendering)
console.log('current count:', getCountSnapshot());
PropertyTypeDescription
stateInitializedObservable<S>The same Observable that the core synstate package’s createState returns as its first element. Use it with pipe, combine, subscribe, etc.
updateState(updateFn: (prev: S) => S) => SUpdate the state using a function of the previous value (see createState in Depth)
resetState() => SReset the state to its initial value
getSnapshot() => SRead the current value synchronously without subscribing
initialStateSThe initial value passed to createState

Subscribing to Derived Observables: useObservableValue

Section titled “Subscribing to Derived Observables: useObservableValue”

The state property from the third element is a regular Observable that you can transform with pipe, map, combine, etc. To subscribe to a derived Observable in a React component, use useObservableValue:

import type * as React from 'react';
import { map } from 'synstate';
import { createState, useObservableValue } from 'synstate-react-hooks';
const [useCount, , { state: count$ }] = createState(0);
// Derive a new Observable using pipe + map
const doubled$ = count$.pipe(map((n) => n * 2));
const message$ = count$.pipe(
map((n) => (n === 0 ? 'Click to start' : `Count is ${n}`)),
);
const CountDisplay = (): React.JSX.Element => {
const count = useCount(); // Equivalent to using useObservableValue(count$)
// Subscribe to derived Observables with useObservableValue
const doubled = useObservableValue(doubled$);
const message = useObservableValue(message$);
return (
<div>
<p>{`Count: ${count}, Doubled: ${doubled}`}</p>
<p>{message}</p>
</div>
);
};

useObservableValue works with any Observable — whether it comes from createState, pipe, combine, or any other SynState operator. It subscribes to the Observable and returns the current value as React state, re-rendering the component when the value changes.

Fallback for Observables Without an Initial Value

Section titled “Fallback for Observables Without an Initial Value”

useObservableValue is overloaded so the return type tracks whether the source has an initial value:

SignatureReturn type
useObservableValue<A>(obs: InitializedObservable<A>)A
useObservableValue<A>(obs: Observable<A>)A | undefined
useObservableValue<A, B = A>(obs: Observable<A>, initialValue: B)A | B

The second argument is a fallback used while the source has not produced a value yet (it is applied via Optional.unwrapOr):

import type * as React from 'react';
import { type Observable } from 'synstate';
import { useObservableValue } from 'synstate-react-hooks';
// Observable<string> that may not have emitted yet.
declare const userName$: Observable<string>;
const Greeting = (): React.JSX.Element => {
const userName = useObservableValue(userName$, 'Guest'); // string
return <p>{`Hello, ${userName}`}</p>;
};

Observables created from createState (or any pipe chain that preserves the initial value) are InitializedObservable<T> and never need a fallback.

If you’re using React 16.8–17 (without useSyncExternalStore), install synstate-react-hooks-compat:

Terminal window
npm add synstate-react-hooks-compat

This package provides the same API as synstate-react-hooks but uses useState + useEffect internally. Just change the import:

import { createState } from 'synstate-react-hooks-compat';
const [useUserState, setUserState] = createState({
name: '',
email: '',
});

All hooks (createState, createReducer, createBooleanState, useObservableValue, useObservableEffect) work identically. When you upgrade to React 18+, switch the import to synstate-react-hooks — no other code changes are needed.

You can also use the core synstate package directly with useState and useEffect:

import * as React from 'react';
import { createState } from 'synstate';
// Global state (outside component)
const [userState, setUserState, { getSnapshot }] = createState({
name: '',
email: '',
});
const UserProfile = (): React.JSX.Element => {
const [user, setUser] = React.useState(getSnapshot());
React.useEffect(() => {
const subscription = userState.subscribe(setUser);
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div>
<p>{`Name: ${user.name}`}</p>
<button
onClick={() => {
setUserState({
name: 'Alice',
email: 'alice@example.com',
});
}}
>
{'Set User'}
</button>
</div>
);
};