fluidstate-preact
v1.0.0
Published
Library for using fine-grained reactivity state management library fluidstate in Preact
Maintainers
Readme
fluidstate-preact
View interactive documentation on the official website.:
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.
A Library with Two Roles
fluidstate-preact is designed to serve two distinct but complementary purposes:
Standalone Reactive Layer: It offers a reactive layer for
fluidstatebuilt on top of@preact/signals-core. This part of the library can be used independently of Preact, enabling you to leverage the performance of Preact Signals in anyfluidstate-powered JavaScript application.Preact Bindings: It 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. - Standalone Reactive Layer: A
fluidstatereactive layer powered by@preact/signals-corethat can be used in any environment, even without Preact. 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 and fluidstate-preact. Depending on which features you use, you may also need to install preact, @preact/signals-core, and @preact/signals as peer dependencies.
# Core library
npm install fluidstate fluidstate-preact
# or
yarn add fluidstate fluidstate-preactOptional Peer Dependencies:
- To use the Preact bindings (
useReactive,withReactive,createReactiveSetup): installpreact. - To use the
useSignalshook: installpreactand@preact/signals. - To use the standalone reactive layer: install
@preact/signals-core.
Example: To use everything, you would install:
npm install preact @preact/signals @preact/signals-core
# or
yarn add preact @preact/signals @preact/signals-corePart 1: Standalone Reactive Layer (via @preact/signals)
fluidstate-preact includes a reactive layer that bridges fluidstate with @preact/signals-core. This allows you to use fluidstate's ergonomic API while leveraging the highly efficient Preact Signals engine under the hood.
This layer can be used in any project, even one that doesn't use Preact for its UI, making it a powerful choice for any fluidstate-based application.
Setup
To enable fluidstate, you must provide it with a reactive layer. This is a one-time setup at your application's entry point.
// file: main.ts
import {
provideReactiveLayer,
createReactive,
createReaction,
runAction,
} from "fluidstate";
// Note the import path for the reactive layer
import { getReactiveLayer } from "fluidstate-preact/reactive-layer";
// 1. Get the reactive layer from fluidstate-preact
const reactiveLayer = getReactiveLayer();
// 2. Provide it to fluidstate
provideReactiveLayer(reactiveLayer);
// 3. You can now use the fluidstate API throughout your application
const counter = createReactive({ value: 0 });
createReaction(() => {
console.log(`Counter value: ${counter.value}`);
});
// LOGS: Counter value: 0
runAction(() => {
counter.value++;
});
// LOGS: Counter value: 1After this setup, you can use the fluidstate package for all your state management tasks. For more details on createReactive, createReaction, and other features, please refer to the main fluidstate documentation.
Part 2: Preact Bindings
This part of the library is focused on integrating your fluidstate reactive state into Preact components.
One-Time Setup
Before using any Preact bindings, you must configure fluidstate with a reactive layer, as shown in Part 1. You can use any fluidstate reactive layer (e.g., fluidstate-alien, fluidstate-mobx or fluidstate-preact/reactive-layer). The Preact bindings are agnostic to the underlying engine.
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.
Example Store:
// file: counter-store.ts
import { createReactive } from "fluidstate";
export type CounterStore = {
count: number;
increment: () => void;
decrement: () => void;
};
export const counterStore = createReactive<CounterStore>({
count: 0,
increment() {
counterStore.count++;
},
decrement() {
counterStore.count--;
},
});Component using useReactive:
// file: counter.tsx
import { useReactive } from "fluidstate-preact";
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>
);
};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.
// file: counter-hoc.tsx
import { withReactive } from "fluidstate-preact";
import { counterStore } from "./counter-store";
export const CounterComponent = 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.
// file: counter-signals.tsx
import { useSignals } from "fluidstate-preact/signals";
import { counterStore } from "./counter-store";
export const Counter = () => {
// `get` is a function that creates a Preact signal for a given piece of state.
const get = useSignals();
return (
<div>
{/* The component itself only renders once. Only the text nodes update. */}
<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, createReactiveSetup helps create encapsulated, testable, and reusable state modules. It generates a Provider, consumer hooks/HOCs, and a Context object for a specific slice of state.
Any reactions (e.g., for side effects) returned in a reactions array will be automatically cleaned up when the Provider unmounts.
Example: User Profile Module
1. Create the reactive setup
// file: user-profile-setup.ts
import {
createReactive,
Reaction,
createReaction,
cloneInert,
} from "fluidstate";
import { createReactiveSetup } from "fluidstate-preact";
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 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: 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;
},
});
const syncProfile = createReaction(() => {
fetch(`api/syncProfile`, {
method: "POST",
body: JSON.stringify(cloneInert(data)),
});
});
return { data, actions, reactions: [syncProfile] };
});2. Provide the state
// file: app.tsx
import { UserProfileProvider } from "./user-profile-setup";
import { UserProfileEditor } from "./user-profile-editor";
import { UserProfileDisplay } from "./user-profile-display";
export const App = () => (
<UserProfileProvider setupProps={{ initialName: "John Doe" }}>
<div>
<h1>User Management</h1>
<UserProfileEditor />
<hr />
<UserProfileDisplay />
</div>
</UserProfileProvider>
);3. Consume the state
You can now consume state using the hook, HOC, or useSignals.
Using the useReactiveState hook:
// file: user-profile-editor.tsx
import { useUserProfileState } from "./user-profile-setup";
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,
})
);
// ... component logic
};Using useSignals for maximum performance:
// file: user-profile-display.tsx
import { useSignals } from "fluidstate-preact/signals";
import { UserProfileContext } from "./user-profile-setup";
export const UserProfileDisplay = () => {
// Pass the context to `useSignals` to connect to the provider state.
const get = useSignals(UserProfileContext);
return (
<div>
<h2>Current Profile</h2>
<p>
<strong>Name:</strong> {get((state) => state.data.name)}
</p>
<p>
<strong>Email:</strong> {get((state) => state.data.email ?? "Not set")}
</p>
</div>
);
};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
fluidstate-preact/reactive-layer: The standalone reactive layer backed by@preact/signals-core.getReactiveLayer
