diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 92b8dd8..7171a1c 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -18,6 +18,7 @@ module.exports = {
plugins: ['react'],
rules: {
'@typescript-eslint/semi': 'off',
+ '@typescript-eslint/strict-boolean-expressions': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
},
settings: {
diff --git a/README.md b/README.md
index dcbdebc..9e4c10f 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,442 @@
-**Dantian - React State Management Reimagined**
+# Dantian - Event-Based State Management
[![npm version](https://badge.fury.io/js/dantian.svg)](https://badge.fury.io/js/dantian)
-Dantian is an event-based state management library for React applications that delivers pinpoint performance and effortless integration with forms. Say goodbye to unnecessary rerenders and complex state comparisons – Dantian ensures that components only update when they subscribe to specific state changes.
+Dantian is an event-based state management library designed specifically for React applications. By embracing an event-driven architecture, Dantian offers a fresh approach to managing application state, providing developers with flexibility, efficiency, and simplicity.
-**The Problem**
+```
+import { useStoreValue } from './store';
+
+function ProfileForm() {
+ const [name, setName] = useStoreValue('profile.name');
+ const [city, setCity] = useStoreValue('profile.city');
+
+ return (
+
+ );
+}
+```
+
+## Why Dantian?
+
+```mermaid
+flowchart LR
+ subgraph Initialization
+ InitState["Initial State"] --> CreateStore["createEventStore() Dantian Event Store"]
+ end
+
+ subgraph Hydration
+ HydrationSource["Hydration Source (API, localStorage, etc.)"] --> |@@HYDRATED| CreateStore
+ end
+
+ subgraph PropertyUpdate
+ PublishEvent["publish(type, payload) Publish Event"] --> |type| CreateStore
+ CreateStore --> |type| SubscribedComponents["Subscribed Components"]
+ SubscribedComponents --> StateUpdate["Component State Update"]
+ StateUpdate --> UI["UI Update"]
+ end
+
+```
+
+### Efficient Updates
+
+Dantian leverages events as triggers for state updates, ensuring that only components subscribed to relevant events are re-rendered. This approach minimizes unnecessary re-renders and improves overall performance, especially in applications with complex state interactions.
+
+### Flexibility
+
+With Dantian, developers have complete control over how state updates are handled. There's no need to adhere to predefined structures or patterns imposed by a library. Instead, developers can define their own logic and workflows tailored to their specific needs, empowering them to build React applications that are both powerful and flexible.
+
+### Simplicity
+
+By embracing a declarative and functional approach, Dantian simplifies state management logic. Developers can focus on defining pure functions and utilities to handle state updates, leading to cleaner, more maintainable code. With Dantian, state management becomes intuitive and straightforward, even for complex applications.
-Traditional state management solutions in React often lead to performance issues, especially in applications with frequent updates or large forms. Unnecessary rerenders can slow down the user experience.
+## Key Features
-**Our Solution**
+- **Event-Based Architecture**: Dantian relies on events to trigger state updates, providing a clear and efficient mechanism for managing application state.
+- **Fine-Grained Control**: Developers can subscribe to specific events and define custom logic for handling state updates, giving them precise control over how their applications behave.
+- **Optimized Performance**: By minimizing unnecessary re-renders and focusing on efficient updates, Dantian ensures optimal performance even in large-scale applications with complex state interactions.
+- **Simplified Development**: With its intuitive API and focus on simplicity, Dantian makes state management easy and accessible for developers of all skill levels, reducing the learning curve and speeding up development time.
-Dantian's event-driven approach focuses on precision. State updates are triggered by events, and only components subscribed to those events rerender. This provides granular control and optimizes performance where it matters most.
+## The React Landscape
-**Features**
+In the realm of React state management, various solutions like Zustand, Redux, and even React's built-in `useState` and `useReducer` hooks have become popular choices. However, they each come with their own set of trade-offs and considerations.
-- **useState-like hooks:** Familiar interface for ease of use.
-- **Custom events:** Power and flexibility for complex scenarios.
-- **Asynchronous state updates:** Handle async patterns gracefully.
-- **Optimized form integration:** Tackle large forms without performance compromises.
+### Zustand and Redux
-**Installation**
+Zustand and Redux are excellent choices for managing global state in React applications. They offer centralized stores, actions, and reducers, making it easier to manage complex state interactions. However, they can sometimes introduce unnecessary boilerplate and complexity, especially for smaller applications or simple state management needs.
+
+### useState and useReducer
+
+On the other end of the spectrum, React's built-in `useState` and `useReducer` hooks provide a lightweight and straightforward way to manage local component state. They are perfect for managing state within individual components or handling simple UI interactions. However, they lack built-in mechanisms for managing global state or handling complex state interactions across multiple components.
+
+## The Downside of Traditional Approaches
+
+While these solutions have their merits, they often suffer from one common issue: excessive re-renders. Whether it's due to shallow comparisons, unnecessary updates, or complex state management logic, the result is the same: decreased performance and potential UI glitches.
+
+## The Promise of Event Sourcing
+
+Event-based state management offers a compelling alternative. By decoupling state updates from the underlying logic and relying solely on events to trigger updates, it provides a more predictable and efficient way to manage state.
+
+### Benefits of Event Sourcing
+
+- **Efficient Updates:** Events provide a clear trigger for state updates, ensuring that only the components subscribed to relevant events are re-rendered. This minimizes unnecessary re-renders and improves overall performance.
+- **Flexibility:** With event sourcing, developers have complete control over how state updates are handled. There's no need to adhere to predefined structures or patterns imposed by a library. Instead, developers can define their own logic and workflows tailored to their specific needs.
+- **Simplicity:** By embracing a more declarative and functional approach, event-based state management simplifies state management logic. Developers can focus on defining pure functions and utilities to handle state updates, leading to cleaner and more maintainable code.
+
+## Conclusion
+
+In a landscape filled with state management solutions, event-based state management offers a refreshing alternative. By prioritizing efficiency, flexibility, and simplicity, it empowers developers to build React applications that are performant, scalable, and easy to maintain.
+
+---
+
+This readme aims to shed light on the benefits of event-based state management in React applications, highlighting its advantages over traditional approaches and offering insights into its potential impact on application development. By embracing event sourcing, developers can unlock new possibilities and overcome the limitations of existing state management solutions.
+
+## Installation
```bash
npm install dantian
```
-**Basic usage**
+## Basic example
+
+```
+// store.ts
+import { createEventStore } from 'dantian';
-```TypeScript
-import { useEventState } from 'dantian';
+export const {
+ state$,
+ useStoreValue,
+ useHydrateStore,
+ useIsHydrated,
+ getPropertyObservable,
+ globalEventStore$,
+ publish,
+ systemEvents$,
+} = createEventStore({
+ count: 0,
+ profile: { name: '', city: '' },
+ data: null,
+ isLoading: false
+});
-function MyComponent() {
- const [count, setCount] = useEventState('counter', 0);
+```
+
+### Basic counter
+
+```
+// counter.tsx
+import { useStoreValue, publish } from './store';
- const increment = () => setCount(count + 1);
+function Counter() {
+ const [count, setCount] = useStoreValue('count');
return (
Count: {count}
-
+
+
);
}
+
```
-**Getting Started**
+### Profile form
+
+```
+import { useStoreValue, publish } from './store';
+
+function ProfileForm() {
+ const [name, setName] = useStoreValue('profile.name');
+ const [city, setCity] = useStoreValue('profile.city');
+
+ return (
+
+ );
+}
+```
-**Why Dantian?**
+### Hydration at store creation
-Performance by design: Avoid unnecessary rerenders, especially in complex forms.
-Developer-friendly: Intuitive API and seamless form integration.
-**License**
+```
+import { createEventStore } from 'dantian';
+
+export const {
+ useStoreValue,
+ useHydrateStore,
+ useIsHydrated,
+ publish,
+ systemEvents$,
+} = createEventStore({
+ // Initial state ...
+}, {
+ hydrator: async () => {
+ // Check if we have saved state in localStorage
+ const storedState = localStorage.getItem('dantianState');
+ if (storedState) {
+ return JSON.parse(storedState);
+ }
+
+ // If no localStorage state, perhaps load defaults or fetch from API:
+ // return await fetch('/api/initial-state').then(res => res.json());
+ },
+ persist: (state) => {
+ try {
+ localStorage.setItem('dantianState', JSON.stringify(state));
+ } catch (error) {
+ // Handle potential errors during state saving
+ console.error('Failed to persist state', error);
+ }
+ }
+});
+
+```
+
+```
+import { useStoreValue, useIsHydrated } from './store';
+
+function App() {
+ const isHydrated = useIsHydrated();
+
+ // ... other components
+
+ return (
+
+ {isHydrated ? (
+ {/* Render your main application content here */}
+ ) : (
+
+ );
+}
+
+```
+
+### Hydration and SSR
+
+SSR offers performance and SEO benefits, but careful state management is crucial. Global singleton-based state libraries can introduce unintended side effects when rendering an application on the server:
+
+- Shared State Across Requests: A single store instance shared across multiple user requests can lead to state contamination, with data from one user potentially bleeding into another's rendered output.
+- Hydration Mismatches: Client-side hydration assumes it begins with the same global state the server rendered. Singleton-based stores can make this challenging.
+
+#### Dantian's Approach
+
+Dantian addresses these challenges through its approach to hydration and event-based state updates:
+
+1. Hydration as Initialization: In Dantian, hydration is a method for initializing store values on the client. Updates are still driven by targeted events, preserving state isolation across user sessions.
+
+2. Request-Specific Stores (If Needed): For SSR frameworks requiring independent stores per request, Dantian can be adapted:
+
+- Create a new store instance within each server-side rendering request.
+- Pass the initial state from this store to the client for hydration.
+
+#### Example Code Snippet (Illustrative)
+
+```
+// Simplified SSR scenario
+import { createEventStore } from 'dantian';
+
+function renderAppToString(request) {
+ // 1. Create a fresh store instance for each request
+ const { useStoreValue } = createEventStore({
+ ...requestSpecificInitialState
+ });
+
+ // ... Use useStoreValue within components during SSR ...
+
+ // 3. Extract the store's state for client-side hydration
+ const clientHydrationState = /* Get current store state */;
+
+ return `
+ ... HTML markup ...
+
+ `;
+}
+
+```
+
+## RxJS
+
+```
+import { useEffect } from 'react';
+import { state$ } from './store';
+
+function usePersistState(storageKey = 'dantianState') {
+ useEffect(() => {
+ const subscription = state$.subscribe((state) => {
+ try {
+ localStorage.setItem(storageKey, JSON.stringify(state));
+ } catch (error) {
+ console.error('Failed to save state', error);
+ }
+ });
+
+ return () => subscription.unsubscribe();
+ }, [storageKey]);
+}
+
+```
+
+### useStoreValue
+
+The `useStoreValue` hook is a fundamental part of the Dantian library, providing a powerful mechanism for accessing and updating state within React components. What sets `useStoreValue` apart is its integration with RxJS, a reactive programming library that enables efficient event handling and state management.
+
+### Leveraging RxJS Observables
+
+At its core, the `useStoreValue` hook utilizes RxJS observables to subscribe to changes in the underlying state. When you initially access a property using `useStoreValue`, it creates a subscription to the `state$` observable, which represents the current state of the application.
+
+```javascript
+const [count, setCount] = useStoreValue('count');
+```
+
+This initial subscription ensures that your component receives the latest value of the specified property and automatically updates whenever the state changes.
+
+#### Event-Based Updates
+
+In addition to the initial subscription to the state observable, useStoreValue also subscribes to the global event bus (globalEventStore$). This event bus emits events whenever state updates occur, allowing components to react to specific changes efficiently.
+
+```
+useEffect(() => {
+ const subscription = getPropertyObservable(type).subscribe({
+ next: (value) => {
+ setValue(value);
+ },
+ });
+ return () => {
+ subscription.unsubscribe();
+ };
+}, []);
+
+```
+
+By combining the initial value subscription with event-based updates, useStoreValue achieves a balance between performance and flexibility. Components receive immediate updates from the local state, ensuring smooth user interactions, while also staying in sync with the global event store to handle changes from other parts of the application.
+
+#### Caching
+
+The `useStoreValue` hook offers control over local state caching, allowing you to fine-tune component updates for optimal performance.
+
+```
+import { useStoreValue } from './store';
+
+// Access a property with default caching behavior
+const [count, setCount] = useStoreValue('count');
+
+// Access a property and disable caching
+const [profileName, setProfileName] = useStoreValue('profile.name', { disableCache: true });
+
+```
+
+By default, useStoreValue maintains local state, ensuring immediate updates without waiting for a roundtrip through the global store. When the store is updated, the value is propagated back to the hook, synchronizing the local state.
+
+This approach is generally effective and particularly powerful with controlled inputs, where there's no discernible difference from having a useState next to the input field. However, in certain edge cases where there are high-frequency updates originating from multiple sources, visual glitches may occur.
+
+Imagine two events being fired almost simultaneously with different values. Although this scenario is typically indicative of a code smell, the result may be that the user sees one value immediately overwritten by another value. In such cases, disabling the local cache will yield a smoother outcome.
+
+## License
MIT
+
+## API Overview
+
+- `createEventStore(initialState: T, options?: {...})`
+
+ - Creates a new Dantian event store.
+ - Parameters:
+ - `initialState`: The initial state object of your store.
+ - `options`: An optional object containing configuration settings:
+ - `debug`: Enables debug logging (default: `false`).
+ - `hydrator`: An asynchronous function returning a promise resolving to the hydration data.
+ - `persist`: An asynchronous function to handle state persistence.
+
+- `publish, TPayload extends GetValueType>(type: TType, payload: TPayload)`
+
+ - Publishes an event to update the store.
+ - Parameters:
+ - `type`: The property path within the store to be updated (e.g., 'count', 'profile.name').
+ - `payload`: The new value for the specified property.
+
+- `getPropertyObservable>(eventType: K)`
+
+ - Returns an RxJS Observable that emits values whenever the specified property in the store is updated.
+
+- `getHydrationObservable$()`
+
+ - Returns an RxJS Observable that emits values when the store is hydrated.
+
+- `state$`
+
+ - An RxJS BehaviorSubject that holds the current state of the store.
+
+- `useStoreValue>(type: K, options?: {...})`
+
+ - React hook to access and update a specific property within the store.
+ - Parameters:
+ - `type`: The property path within the store.
+ - `options`: Optional object with a `disableCache` property to control local caching behavior.
+ - Returns: An array containing:
+ - The current value of the property.
+ - A function to update the property.
+
+- `useHydrateStore()`
+
+ - React hook to trigger store hydration.
+ - Returns: A function to initialize the store with hydrated data.
+
+- `useIsHydrated()`
+
+ - React hook to determine if the store is hydrated.
+ - Returns: A boolean indicating hydration status.
+
+- `systemEvents$`
+
+ - An RxJS Observable that emits Dantian system events (e.g., '@@INIT', '@@HYDRATED').
diff --git a/lib/event-store.ts b/lib/event-store.ts
index 333c58f..ede42e3 100644
--- a/lib/event-store.ts
+++ b/lib/event-store.ts
@@ -101,10 +101,16 @@ export function createEventStore(
const useStoreValue = >(
type: K,
+ options?: { disableCache?: boolean },
): [GetValueType, (payload: GetValueType) => void] => {
+ const disableCache = options?.disableCache ?? false;
+
const defaultValue: GetValueType = get(type, state$.getValue());
+
const [value, setValue] = useState>(defaultValue);
+
const handleUpdate = useCallback((payload: GetValueType) => {
+ if (!disableCache) setValue(payload);
publish(type, payload);
}, []);
@@ -133,6 +139,7 @@ export function createEventStore(
return [value, handleUpdate];
};
+
const useHydrateStore = () => {
return useCallback((payload: T) => {
globalEventStore$.next({ type: '@@HYDRATED', payload });
diff --git a/package.json b/package.json
index aec1a77..a7d0c23 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,10 @@
"module": "./dist/dantian.js",
"types": "./dist/index.d.ts",
"files": [
- "dist"
+ "dist",
+ "LICENSE",
+ "README.md",
+ "CHANGELOG.md"
],
"scripts": {
"dev": "vite",
diff --git a/stories/event-store/state.ts b/stories/event-store/state.ts
index a24b455..de11ea3 100644
--- a/stories/event-store/state.ts
+++ b/stories/event-store/state.ts
@@ -1,4 +1,4 @@
-interface State {
+export interface State {
user: {
name: string;
address: {
diff --git a/stories/event-store/store.ts b/stories/event-store/store.ts
index 56b86e7..aeb2b79 100644
--- a/stories/event-store/store.ts
+++ b/stories/event-store/store.ts
@@ -1,28 +1,37 @@
-import { wuji } from '../../lib';
+import { createEventStore } from '../../lib';
+import { State } from './state';
-export const { state$, useStoreValue, useHydrateStore, useIsHydrated } =
- wuji(
- {
- user: { address: { city: 'n/a', street: 'n/a' }, name: 'n/a' },
- },
- {
- hydrator: () => {
- return new Promise((resolve) => {
- setTimeout(
- () =>
- resolve({
- user: {
- address: {
- city: 'Aubonne',
- street: 'Chemin du Mont-Blanc 16',
- },
- name: 'Julius',
+export const {
+ state$,
+ useStoreValue,
+ useHydrateStore,
+ useIsHydrated,
+ getPropertyObservable,
+ globalEventStore$,
+ publish,
+ systemEvents$,
+} = createEventStore(
+ {
+ user: { address: { city: 'n/a', street: 'n/a' }, name: 'n/a' },
+ },
+ {
+ hydrator: () => {
+ return new Promise((resolve) => {
+ setTimeout(
+ () =>
+ resolve({
+ user: {
+ address: {
+ city: 'Aubonne',
+ street: 'Chemin du Mont-Blanc 16',
},
- }),
- 3000,
- );
- });
- },
- debug: true,
+ name: 'Julius',
+ },
+ }),
+ 3000,
+ );
+ });
},
- );
+ debug: true,
+ },
+);