fluidstate-preact
fluidstate-preact provides Preact bindings for the fluidstate fine-grained reactivity library. It allows you to build highly performant Preact applications where components update automatically and efficiently in response to state changes, without manual subscriptions or monolithic state updates.
This library provides a rich set of tools: hooks, Higher-Order Components (HOCs), and signal-based utilities—to seamlessly integrate fluidstate's reactive state into your Preact components, offering multiple patterns for different use cases.
Features
- Fine-grained reactivity: Components re-render only when the specific data they use changes.
- Multiple Integration Patterns: Choose from the
useReactivehook,withReactiveHOC, or the hyper-performantuseSignalshook. 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
fluidstateecosystem, regardless of the underlying reactive layer.
Installation
You will need fluidstate, a fluidstate reactive layer like fluidstate-mobx and fluidstate-preact.
npm install fluidstate fluidstate-mobx fluidstate-preact
# or
yarn add fluidstate fluidstate-mobx fluidstate-preact
Optional Peer Dependencies:
- To use the
useSignalshook: install@preact/signals.
Example: To use everything, you would install:
npm install fluidstate fluidstate-mobx fluidstate-preact @preact/signals
# or
yarn add fluidstate fluidstate-mobx fluidstate-preact @preact/signals
Getting Started: One-Time Setup
fluidstate is designed as a layer on top of a core reactive engine. Before using fluidstate or fluidstate-preact, 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 as the reactive layer:
import { render } from "preact"; 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 render(<App />, document.getElementById("app"));
With this setup complete, you can now use fluidstate-preact's APIs in your components.
Three Ways to Consume State
fluidstate-preact offers three primary ways to make your components reactive, each with different trade-offs.
1. useReactive Hook
The useReactive hook is a flexible way to consume reactive state. You provide a function that reads from your 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 { useReactive } from "fluidstate-preact"; import { createReactive } from "fluidstate"; export const counterStore = createReactive({ count: 0, increment() { counterStore.count++; }, decrement() { counterStore.count--; }, }); export const App = () => { // 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> ); };
2. withReactive HOC
The withReactive HOC provides an alternative, wrapping your component to make it automatically re-render when any reactive state it accesses changes.
import { createReactive } from "fluidstate"; import { withReactive } from "fluidstate-preact"; export const counterStore = createReactive({ count: 0, increment() { counterStore.count++; }, decrement() { counterStore.count--; }, }); export const App = withReactive(() => { return ( <div> <h1>Counter: {counterStore.count}</h1> <button onClick={counterStore.increment}>Increment</button> <button onClick={counterStore.decrement}>Decrement</button> </div> ); });
3. useSignals Hook
The useSignals hook is the most performant option. It bridges fluidstate state with @preact/signals, allowing you to update parts of your UI without re-rendering the entire component. This requires @preact/signals to be installed.
You can import useSignals from fluidstate-preact/signals.
import { createReactive } from "fluidstate"; import { useSignals } from "fluidstate-preact/signals"; export const counterStore = createReactive({ count: 0, increment() { counterStore.count++; }, decrement() { counterStore.count--; }, }); let i = 0; export const App = () => { // `get` is a function that creates a Preact signal for a given piece of state. const get = useSignals(); // The component itself only renders once. Only the text nodes update. console.log("App render", ++i); return ( <div> <h1>Counter: {get(() => counterStore.count)}</h1> <button onClick={counterStore.increment}>Increment</button> <button onClick={counterStore.decrement}>Decrement</button> </div> ); };
Recommended Usage: createReactiveSetup
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.
The Pattern
- Define State: You define the shape of your state and a factory function to create it.
- Create Setup: You call
createReactiveSetupwith your factory function. - Provide State: You use the generated
ReactiveProviderto wrap a part of your component tree. - Consume State: Descendant components can access the state using the generated
useReactiveStatehook orwithReactiveStateHOC.
Example: User Profile Module
import { createReactive, Reaction, createReaction, cloneInert, } from "fluidstate"; import { useSignals } from "fluidstate-preact/signals"; import { createReactiveSetup } from "fluidstate-preact"; // --- 1. Create the reactive setup --- // Generate and rename the setup utilities for clarity. export const { ReactiveProvider: UserProfileProvider, useReactiveState: useUserProfileState, withReactiveState: withUserProfileState, MockProvider: MockUserProfileProvider, StateContext: UserProfileContext, // Export the context for use with `useSignals` } = createReactiveSetup((props) => { const data = createReactive({ name: props.initialName, email: null, }); const actions = createReactive({ updateName(newName) { data.name = newName; }, setEmail(email) { data.email = email; }, }); // An example of a reaction that will be automatically cleaned up (stopped) // when the provider unmounts. const syncProfile = createReaction(() => { console.log(`Sending request to "api/syncProfile"`, { method: "POST", body: JSON.stringify(cloneInert(data)), }); }); return { data, actions, reactions: [syncProfile] }; }); // --- 2. Provide the state --- export const App = () => ( <UserProfileProvider setupProps={{ initialName: "John Doe" }}> <div> <h1>User Management</h1> <UserProfileEditor /> <hr /> <div> <h2>Current Profile</h2> <UserNameDisplay /> <UserEmailDisplay /> </div> </div> </UserProfileProvider> ); // --- 3. Consume the state --- // Using `useReactiveState` hook export const UserProfileEditor = () => { 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) => { updateName(e.target.value); }; const handleEmailChange = (e) => { setEmail(e.target.value); }; return ( <div> <h2>Edit Profile</h2> <div> <label>Name: </label> <input type="text" value={name} onInput={handleNameChange} /> </div> <div> <label>Email: </label> <input type="email" value={email ?? ""} onInput={handleEmailChange} /> </div> </div> ); }; // Using preact signals via `useSignals` hook export const UserNameDisplay = () => { // Passing the context to `useSignals` to connect to the provider state. const get = useSignals(UserProfileContext); return ( <p> <strong>Name:</strong> {get((state) => state.data.name)} </p> ); }; // Using `withReactiveState` Higher-Order Component export const UserEmailDisplay = withUserProfileState(({ state }) => { return ( <p> <strong>Email:</strong> {state.data.email ?? "Not set"} </p> ); });
API Reference
useReactive<T>(getState: () => T, dependencyArray?: ReadonlyArray<unknown>): T
A Preact hook that subscribes a component to reactive state changes, causing a re-render when dependencies change.
withReactive<P>(Component: FunctionComponent<P>): FunctionComponent<P>
A HOC that makes a functional component reactive. It will re-render whenever any reactive state it accesses changes.
useSignals<CV>(Context?: Context<CV>): (fn: (value: CV) => T) => Signal<T>
A hook that returns a function to create Preact signals from fluidstate data.
Context: Optional. A Preact Context object (like one fromcreateReactiveSetup) to source the state from. If omitted, it works with the state passed from the component.- The returned function
get(fn)takes a selector and returns a memoized@preact/signalsignal for the selected value.
createReactiveSetup<SetupProps, State>(createState)
A factory that creates a set of utilities for encapsulated state management.
It returns a ReactiveSetup object with:
ReactiveProvider: A Provider component that acceptssetupProps.useReactiveState: A hook to consume the state.withReactiveState: A HOC to inject the state via astateprop.StateContext: The Preact Context object used by the Provider. Pass this touseSignalsto connect to the provided state.MockProvider: A provider for testing that accepts avalueprop with mock state.createState: The originalcreateStatefunction, which can be useful for testing the state logic itself.
Package Entry Points
To keep the library modular and dependencies optional, features are exposed via different entry points:
fluidstate-preact: Core Preact bindings.useReactivewithReactivecreateReactiveSetup
fluidstate-preact/signals: TheuseSignalshook that uses@preact/signals.useSignals