View interactive documentation on the official website.
fluidstate-react
provides React bindings for the fluidstate
fine-grained reactivity library. It allows you to build highly performant React applications where components update automatically and efficiently in response to state changes, without manual subscriptions or monolithic state updates.
This library offers hooks and Higher-Order Components (HOCs) to seamlessly integrate fluidstate
's reactive state into your React components.
- Fine-grained reactivity: Components re-render only when the specific data they use changes.
-
useReactive
hook: A simple and powerful hook to create reactive views from your state. -
withReactive
HOC: A Higher-Order Component to make any functional component reactive. -
createReactiveSetup
: A powerful pattern for creating encapsulated, reusable, and testable state management modules with Providers and consumer hooks/HOCs. -
Seamless integration: Works with the entire
fluidstate
ecosystem.
You'll need react
, fluidstate
, a fluidstate
reactive layer like fluidstate-mobx
, and fluidstate-react
.
npm install react fluidstate fluidstate-mobx fluidstate-react
# or
yarn add react fluidstate fluidstate-mobx fluidstate-react
fluidstate
is designed as a layer on top of a core reactive engine. Before using fluidstate
or fluidstate-react
, you must provide this reactive layer. This is typically done once at the entry point of your application.
Here's how to set it up using fluidstate-mobx
:
import { createRoot } from "react-dom/client";
import { provideReactiveLayer } from "fluidstate";
import { getReactiveLayer } from "fluidstate-mobx";
import { App } from "./app";
// 1. Get the reactive layer from the provider (e.g., fluidstate-mobx)
const reactiveLayer = getReactiveLayer();
// 2. Provide it to fluidstate. This enables all fluidstate features.
provideReactiveLayer(reactiveLayer);
// 3. Render your application
const root = createRoot(document.getElementById("app")!);
root.render(<App />);
With this setup complete, you can now use fluidstate-react
's APIs in your components.
The two primary ways to make your components reactive are the useReactive
hook and the withReactive
HOC.
The useReactive
hook is the most common and flexible way to consume reactive state in your functional components. You provide a function that reads from your reactive state, and the hook ensures your component re-renders whenever any of the accessed state properties change.
Here's an example of a simple counter:
import { createReactive } from "fluidstate";
export type CounterStore = {
count: number;
increment: () => void;
decrement: () => void;
};
// Create a reactive store.
// For more details on `createReactive`, see the fluidstate documentation.
export const counterStore = createReactive<CounterStore>({
count: 0,
increment() {
counterStore.count++;
},
decrement() {
counterStore.count--;
},
});
import { useReactive } from "fluidstate-react";
import { counterStore } from "./counter-store";
export const Counter = () => {
// The component will re-render only when `counterStore.count` changes.
const count = useReactive(() => counterStore.count);
return (
<div>
<h1>Counter: {count}</h1>
<button onClick={counterStore.increment}>Increment</button>
<button onClick={counterStore.decrement}>Decrement</button>
</div>
);
};
In this example, the Counter
component subscribes only to counterStore.count
. If other properties were added to counterStore
and changed, this component would not re-render unnecessarily.
The withReactive
HOC provides an alternative way to make a component reactive. It wraps your component and automatically re-renders it when any reactive state accessed during its render cycle changes.
Here is the same counter example, but implemented with withReactive
:
import { withReactive } from "fluidstate-react";
import { counterStore } from "./counter-store";
// Wrap the component with withReactive to make it reactive.
// Note: The component may receive reactive state via props or closure.
// For this example, we'll access the global `counterStore`.
const CounterComponent = withReactive(() => {
return (
<div>
<h1>Counter: {counterStore.count}</h1>
<button onClick={counterStore.increment}>Increment</button>
<button onClick={counterStore.decrement}>Decrement</button>
</div>
);
});
withReactive
is useful for wrapping components that might not use hooks, or for situations where you prefer the HOC pattern.
For larger applications, managing global state can become complex. createReactiveSetup
helps by allowing you to create encapsulated state modules. It generates a set of tools — a Provider, a consumer hook, and an HOC — for a specific slice of state. This promotes better organization, testability, and reusability.
One of the features is the automatic side effects cleanup. Any reactions (such as data fetching or logging) returned from your state creation function in a reactions
array will be automatically stopped when it unmounts, preventing resource leaks.
- Define State: You define the shape of your state and a factory function to create it.
-
Create Setup: You call
createReactiveSetup
with your factory function. -
Provide State: You use the generated
ReactiveProvider
to wrap a part of your component tree. -
Consume State: Descendant components can access the state using the generated
useReactiveState
hook orwithReactiveState
HOC.
Let's build a module for managing user profile data.
1. Create the reactive setup
import {
createReactive,
Reaction,
createReaction,
cloneInert,
} from "fluidstate";
import { createReactiveSetup } from "fluidstate-react";
// This type is used to pass initial data to our state creator.
export type UserProfileSetupProps = {
initialName: string;
};
export type UserProfileData = {
name: string;
email: null | string;
};
export type UserProfileActions = {
updateName: (newName: string) => void;
setEmail: (email: string) => void;
};
export type UserProfileState = {
data: UserProfileData;
actions: UserProfileActions;
reactions: Reaction[];
};
// Generate the provider, hook, and HOC and rename them to be more specific
export const {
ReactiveProvider: UserProfileProvider,
useReactiveState: useUserProfileState,
withReactiveState: withUserProfileState,
MockProvider: MockUserProfileProvider,
} = createReactiveSetup((props: UserProfileSetupProps): UserProfileState => {
const data = createReactive<UserProfileData>({
name: props.initialName,
email: null,
});
const actions = createReactive<UserProfileActions>({
updateName(newName: string) {
data.name = newName;
},
setEmail(email: string) {
data.email = email;
},
});
// An example of a reaction that will be automatically cleaned up (stopped)
// when the provider unmounts.
const syncProfile = createReaction(() => {
fetch(`api/syncProfile`, {
method: "POST",
body: JSON.stringify(cloneInert(data)),
});
});
return {
data,
actions,
reactions: [syncProfile],
};
});
2. Provide the state
Wrap your application or a feature area with the generated UserProfileProvider
.
import { UserProfileProvider } from "./user-profile-setup";
import { UserProfileEditor } from "./user-profile-editor";
import { UserProfileDisplay } from "./user-profile-display";
export const App = () => {
return (
<UserProfileProvider setupProps={{ initialName: "John Doe" }}>
<div>
<h1>User Management</h1>
<UserProfileEditor />
<hr />
<UserProfileDisplay />
</div>
</UserProfileProvider>
);
};
3. Consume the state
Components can now consume the state using either the hook or the HOC.
Using the useReactiveState
hook:
import { useUserProfileState } from "./user-profile-setup";
import type { ChangeEvent } from "react";
export const UserProfileEditor = () => {
// Select the specific data and actions needed.
// The component will only re-render if `state.data.name` or `state.data.email` changes.
const { name, email, updateName, setEmail } = useUserProfileState(
(state) => ({
name: state.data.name,
email: state.data.email,
updateName: state.actions.updateName,
setEmail: state.actions.setEmail,
})
);
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => {
updateName(e.target.value);
};
const handleEmailChange = (e: ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
return (
<div>
<h2>Edit Profile</h2>
<div>
<label>Name: </label>
<input type="text" value={name} onChange={handleNameChange} />
</div>
<div>
<label>Email: </label>
<input type="email" value={email ?? ""} onChange={handleEmailChange} />
</div>
</div>
);
};
Using the withReactiveState
HOC:
import { withUserProfileState } from "./user-profile-setup";
export const UserProfileDisplay = withUserProfileState(({ state }) => {
// The HOC will re-render this component whenever any property on `state` read here changes.
return (
<div>
<h2>Current Profile</h2>
<p>
<strong>Name:</strong> {state.data.name}
</p>
<p>
<strong>Email:</strong> {state.data.email ?? "Not set"}
</p>
</div>
);
});
The createReactiveSetup
pattern is ideal for building scalable applications by promoting clear separation of concerns between state logic and UI components. The generated MockProvider
is also very useful for testing components in isolation.
A React hook that subscribes a component to reactive state changes.
-
getState: () => T
: A function that reads one or more properties from reactive state and returns a value. The component will re-render when any of the reactive dependencies accessed within this function change. -
dependencyArray?: ReadonlyArray<unknown>
: Optional. An array of dependencies, similar touseMemo
oruseEffect
. If provided, thegetState
function will be called again when a value in this array changes.
A Higher-Order Component that makes a functional component reactive.
-
Component
: The React functional component to wrap. The wrapped component will automatically re-render whenever any reactive state it accesses during render changes.
A factory function that creates a set of utilities for managing an encapsulated slice of state.
-
createState: (props: SetupProps) => State
: A function that takessetupProps
and returns the reactive state object. If the returned object contains areactions
property (an array ofReaction
instances fromfluidstate
), they will be automatically stopped when theReactiveProvider
unmounts.
It returns a ReactiveSetup
object with the following properties:
A React Provider component. You must wrap the part of your application that needs access to this state within this provider. It accepts a setupProps
prop, which is passed to your createState
function to initialize the state.
A React hook to consume the state within a descendant component of the ReactiveProvider
. It takes a selector function (state: State) => T
and returns the selected value. The component re-renders when the properties accessed inside the selector function change.
A HOC that injects the entire state object as a state
prop into the wrapped component. The component must be a descendant of the ReactiveProvider
.
A React Provider component intended for testing or tools like Storybook. It allows you to provide a partial or complete mock state object directly via its value
prop, bypassing the createState
function.
The original createState
function you provided is returned for convenience, which can be useful for testing the state logic itself.
The React Context object used by the Provider.
createReactiveSetup
also enables several utility types for better type inference:
-
StateType<T>
: Extracts theState
type from aReactiveProvider
type. -
SetupPropsType<T>
: Extracts theSetupProps
type from aReactiveProvider
type. -
CreateStateType<T>
: Extracts the type of thecreateState
function from aReactiveProvider
type.