React Integration
SynState provides a companion package synstate-react-hooks for seamless React integration.
Installation
Section titled “Installation”npm add synstate-react-hooksBasic Usage
Section titled “Basic Usage”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> );};Additional Utilities: Third Element
Section titled “Additional Utilities: Third Element”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());| Property | Type | Description |
|---|---|---|
state | InitializedObservable<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) => S | Update the state using a function of the previous value (see createState in Depth) |
resetState | () => S | Reset the state to its initial value |
getSnapshot | () => S | Read the current value synchronously without subscribing |
initialState | S | The 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 + mapconst 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:
| Signature | Return 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.
React v17 or Earlier
Section titled “React v17 or Earlier”If you’re using React 16.8–17 (without useSyncExternalStore), install synstate-react-hooks-compat:
npm add synstate-react-hooks-compatThis 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.
Manual alternative (no extra package)
Section titled “Manual alternative (no extra package)”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> );};