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
useReactive
hook,withReactive
HOC, or the hyper-performantuseSignals
hook. 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, 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
useSignals
hook: 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
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.
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/signal
signal 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 astate
prop.StateContext
: The Preact Context object used by the Provider. Pass this touseSignals
to connect to the provided state.MockProvider
: A provider for testing that accepts avalue
prop with mock state.createState
: The originalcreateState
function, 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.useReactive
withReactive
createReactiveSetup
fluidstate-preact/signals
: TheuseSignals
hook that uses@preact/signals
.useSignals