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-performant useSignals 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>
  );
};
 

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

  1. Define State: You define the shape of your state and a factory function to create it.
  2. Create Setup: You call createReactiveSetup with your factory function.
  3. Provide State: You use the generated ReactiveProvider to wrap a part of your component tree.
  4. Consume State: Descendant components can access the state using the generated useReactiveState hook or withReactiveState 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 from createReactiveSetup) 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 accepts setupProps.
  • useReactiveState: A hook to consume the state.
  • withReactiveState: A HOC to inject the state via a state prop.
  • StateContext: The Preact Context object used by the Provider. Pass this to useSignals to connect to the provided state.
  • MockProvider: A provider for testing that accepts a value prop with mock state.
  • createState: The original createState 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: The useSignals hook that uses @preact/signals.

    • useSignals