⚛️ Solving Boilerplate with the Storeflow Hook
Why Another State Library?
If you've ever dealt with complex global or nested state in React, you know the routine was: Define an action, write a reducer, select the data... it's all tedious (even modern state manager like zustand / jotai). This boilerplate hits especially hard when you just want to update a single nested property like user.profile.name.
In this article, we're going to introduce Storeflow, a Zustand or Jotai alternative, it's a custom store hook built on React's useSyncExternalStore hook. Storeflow is designed to eliminate this boilerplate by automatically generating "Magic Setters" for every property—even nested ones—while providing powerful features like Middleware and Cross-Store Dependencies.
🚀 The Power of Storeflow!
Storeflow usees the native React hook philosophy to deliver a zero-boilerplate state solution. If you're ready to see how simple state management can be, check out the package and give the repository a star!
| Resource | Command/Link |
|---|---|
| Install via NPM | npm install storeflow |
| Install via PNPM | pnpm add storeflow |
| Source Code | GitHub Repository 👈 Give it a ⭐️ |
| Example | https://storeflow-example.netlify.app/ |
| Example Source Code | Source code |
Basic usage
import { store } from 'storeflow'
// store/use-post-store.ts
export const usePostStore = store({
title: 'Initial title',
content: 'Initial content'
meta: {
updatedAt: new Date()
}
})
// components/component.ts
import { usePostStore } from 'store/use-post-store';
function Component() {
const { title, $title, $meta_updatedAt } = usePostStore();
return (
<div className="flex items-center gap-2">
<button onClick={() => $title('Hello world')}>{title}</button>
<button onClick={() => $meta_updatedAt(new Date()}>Update Meta</button>
</div>
);
}
Step 1: The Core - useSyncExternalStore
The core of Storeflow is the classic external store pattern, leveraging the power of the useSyncExternalStore hook (thanks to that). This powerful, native React Hook allows us to manage state outside of React components, trigger updates globally, and ensure only components subscribed to those specific changes are re-rendered.
The basic store structure defines the getters and the subscription mechanism required by the React runtime:
function store<T extends object>(initialState: T) {
let state = initialState;
let listeners = new Set<() => void>();
const getState = () => state;
const subscribe = (onStoreChange: () => void) => {
listeners.add(onStoreChange);
return () => listeners.delete(onStoreChange);
};
// The actual Storeflow hook exposed to components
const useStore = () => {
// This hook ensures component updates whenever listeners are notified
const currentState = useSyncExternalStore(subscribe, getState);
// ... magic setters will be injected here ...
return currentState;
};
// ... chaining methods (.effect, .middleware, .depends) ...
return useStore;
}
Step 2: The Magic - Dynamic Nested Setters
This is the key Storeflow feature where the boilerplate disappears. We want to be able to call setters like $title() or $meta_updatedAt() without manually writing them.
To achieve this, we need to solve a critical issue: calling React hooks (useCallback) inside a dynamic loop is a violation of the Rules of Hooks.
The Hooks Fix
We fix this by ensuring the number of setter paths is static and known before the component renders (based on the initial state shape). This allows us to safely inline the useCallback loop within the useStore custom hook:
// Calculated ONCE outside the hook/component
const flattened = flattenObject(initialState);
const setterPaths = Object.keys(flattened);
// Inside the useStore hook:
const useStore = (): StoreHookResult<T> => {
const currentState = useSyncExternalStore(subscribe, getState);
const hookSetters = {} as DynamicSetters<T>;
// FIX: The loop iterates over a static list (setterPaths) and is defined
// consistently within the hook body, satisfying React's rules.
for (const path of setterPaths) {
const setterName = `$${path.replace(/\\./g, '_')}`;
const setterFn = (valueOrUpdater: any) => {
// Logic to handle value or updater function (v => v + 1)
// ...
_setPathValue(path, valueToSet); // Central update function
};
// Hooks are called in a static, predictable order
(hookSetters as any)[setterName] = useCallback(setterFn, [path]);
}
return { ...currentState, ...hookSetters };
};
This ensures React can track the hooks consistently, resolving the Rules of Hooks violation while delivering highly dynamic setter functions.
Step 3: Advanced Control - Side-Effects and Previous State
Storeflow allows you to chain configuration methods to your store definition for enhanced capabilities.
1. The .effect() Chaining Hook
The .effect() method registers a function that runs after every state change. This is the perfect place for asynchronous operations, data saving, or logging. It receives the currentState and a special prev getter for auditing.
// Define the effect when creating the store:
const usePostStore = store({ title: 'Initial Title' })
.effect(async ([state], prev) => {
// Use dot notation to check any previous state value
const prevTitle = prev('title');
console.log('%c--- Post Store Effect Triggered ---', 'color: blue;');
if (prevTitle !== state.title) {
console.warn(`Title changed from "${prevTitle}" to "${state.title}"`);
// Example: Save to an API...
// await fetch('/api/save-post', { method: 'POST', body: JSON.stringify(state) });
}
});
2. Middleware for Pre-Update Validation
Middleware intercepts the update before the state is committed. This is your chance to validate, transform, or even cancel the update chain entirely.
const useStoreWithMiddleware = store({ })
.middleware([
// Middleware 1: Validation and Cancellation
(currentState, incomingUpdate, next) => {
const hasDataUpdate = Object.keys(incomingUpdate).some(path => path !== 'locked');
if (currentState.locked && hasDataUpdate) {
console.error('Update CANCELLED: Store is locked.');
return next(false); // next(false) cancels the entire update
}
next();
},
// Middleware 2: Transformation and Audit
(currentState, incomingUpdate, next) => {
// Transform incoming 'data' to uppercase
if ('data' in incomingUpdate && typeof incomingUpdate.data \=== 'string') {
next({ data: incomingUpdate.data.toUpperCase() });
} else {
next(); // Pass update map unchanged
}
}
]);
Step 4: State Orchestration - Dependencies
The .depends() method allows a store's effect to subscribe to and react to changes in other Storeflow stores. This creates an explicit and clean data flow across your application.
When you define dependencies, the .effect() function receives the dependent stores' results (state + Magic Setters) as extra arguments in a tuple.
// 1. Define Store A
const usePostStore = store({ content: '...' });
// 2. Define Store B, which depends on A
const useAnotherOneStore = store({ count: 0 })
.depends([usePostStore]) // Register the dependency
.effect(([ownState, postStoreResult]) => {
const { count } = ownState;
// Access the Magic Setter of the dependent store\!
const { $content } = postStoreResult;
if (count === 5) {
console.log("Count reached 5! Updating Post Content directly.");
// Use the dependent store's setter to update its state
$content(`Content update triggered by dependency store: Count is ${count}.`);
}
});
Conclusion
Storeflow demonstrates how to leverage powerful native React APIs like useSyncExternalStore and useCallback to build a robust, high-performance state solution that is perfectly aligned with modern React best practices. By focusing on zero boilerplate through dynamic setters and explicit data flow through chaining, you can write cleaner, more maintainable code without relying on bulky external libraries.
Give it a try in your next project! If you found this pattern valuable and plan to use Storeflow, please give the GitHub repository a star 🌟, it helps tremendously with visibility and future development!
Top comments (1)
What patterns do you find most difficult to implement cleanly in standard React state? Let me know in the comments!