1.0.1 • Published 1 year ago

@okenneth/reactive-state v1.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
1 year ago

ReactiveState

A lightweight, reactive state management library for React with TypeScript support.

Features

  • Type Safety: Fully typed API using TypeScript.
  • Lightweight: Minimal bundle size, avoiding unnecessary dependencies.
  • Intuitive API: Simple, hook-based API that feels native to React.
  • Fine-Grained Reactivity: Components only re-render when the specific part of the state they depend on changes.
  • Flexible: Support for both atomic and global store patterns.
  • Optimized Performance: Efficient state updates, avoiding unnecessary renders.
  • Persistable State: Support for localStorage or custom storage solutions.
  • Asynchronous Actions: Built-in support for async operations.
  • Selectors & Derivations: Computed state and optimized subscriptions.
  • Middleware Support: Logging, async processing, validation, and more.
  • SSR Compatible: Works with server-side rendering frameworks like Next.js.

Installation

npm install @okenneth/reactive-state
# or
yarn add @okenneth/reactive-state
# or
pnpm add @okenneth/reactive-state

Basic Usage

Creating a Store

import { createStore } from '@okenneth/reactive-state';

// Define your state type
interface CounterState {
  count: number;
  incrementBy: number;
}

// Create a store with initial state
const counterStore = createStore<CounterState>({
  initialState: {
    count: 0,
    incrementBy: 1
  }
});

// Use the store directly
counterStore.setState(state => ({
  ...state,
  count: state.count + state.incrementBy
}));

console.log(counterStore.getState()); // { count: 1, incrementBy: 1 }

Using Stores in Components

import React from 'react';
import { useStore, useSelector } from '@okenneth/reactive-state';

function Counter() {
  // Subscribe to the entire store
  const state = useStore(counterStore);
  
  const increment = () => {
    counterStore.setState(state => ({
      ...state,
      count: state.count + state.incrementBy
    }), 'INCREMENT');
  };
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment by {state.incrementBy}</button>
    </div>
  );
}

function CountDisplay() {
  // Subscribe to only the count part of the state
  const count = useSelector(
    counterStore,
    state => state.count
  );
  
  // This component only re-renders when count changes
  return <p>Current count: {count}</p>;
}

Using the Combined State and Setter Hook

import React from 'react';
import { useStoreState } from '@okenneth/reactive-state';

function CounterWithHook() {
  // Get both state and setState in one hook
  const [state, setState] = useStoreState(counterStore);
  
  const increment = () => {
    setState(state => ({
      ...state,
      count: state.count + state.incrementBy
    }), 'INCREMENT');
  };
  
  return (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={increment}>Increment by {state.incrementBy}</button>
    </div>
  );
}

Async Actions

import { AsyncAction } from '@okenneth/reactive-state';

// Define an async action
const fetchUserData: AsyncAction<UserState> = async (
  state,
  setState,
  getState
) => {
  // Show loading state
  setState(state => ({ ...state, loading: true }), 'FETCH_USER_START');
  
  try {
    // Fetch data from API
    const response = await fetch('/api/user');
    const userData = await response.json();
    
    // Update state with the result
    setState(state => ({
      ...state,
      user: userData,
      loading: false
    }), 'FETCH_USER_SUCCESS');
    
    return userData;
  } catch (error) {
    // Handle error
    setState(state => ({
      ...state,
      error: error.message,
      loading: false
    }), 'FETCH_USER_ERROR');
    
    throw error;
  }
};

// Dispatch the action
userStore.dispatch(fetchUserData).then(user => {
  console.log('User data loaded:', user);
}).catch(error => {
  console.error('Failed to load user data:', error);
});

Creating Derived State

import { createDerivedStore } from '@okenneth/reactive-state';

// Create a derived store that depends on another store
const doubledCountStore = createDerivedStore(
  [counterStore],
  (counterState) => ({
    doubledCount: counterState.count * 2,
    isEven: (counterState.count * 2) % 2 === 0
  })
);

// Use in a component with useStore hook
function DoubledCounter() {
  const { doubledCount, isEven } = useStore(doubledCountStore);
  
  return (
    <div>
      <p>Doubled count: {doubledCount}</p>
      <p>Is even: {isEven ? 'Yes' : 'No'}</p>
    </div>
  );
}

Using Middleware

import {
  createStore,
  createLoggerMiddleware,
  createPerformanceMiddleware
} from '@okenneth/reactive-state';

// Create a store with middleware
const storeWithMiddleware = createStore({
  initialState: { count: 0 },
  middleware: [
    createLoggerMiddleware({ name: 'CounterStore' }),
    createPerformanceMiddleware({ warnIfExceeds: 5 })
  ]
});

Persistable State

import { createStore } from '@okenneth/reactive-state';

// Create a store with persistence enabled
const persistedStore = createStore({
  initialState: { preferences: { theme: 'light' } },
  persist: {
    key: 'app-preferences',
    storage: localStorage // Optional, defaults to localStorage
  }
});

Undo/Redo Functionality

import React from 'react';
import { createStore, createUndoRedoMiddleware } from '@okenneth/reactive-state';

const initialState = { text: '' };

// Create the undo/redo middleware
const { middleware, controls } = createUndoRedoMiddleware();

// Create a store with the middleware
const textStore = createStore({
  initialState,
  middleware: [middleware]
});

function TextEditor() {
  const [state, setState] = useStoreState(textStore);
  
  const handleChange = (e) => {
    setState({ text: e.target.value }, 'UPDATE_TEXT');
  };
  
  const handleUndo = () => {
    controls.undo(newState => textStore.setState(newState));
  };
  
  const handleRedo = () => {
    controls.redo(newState => textStore.setState(newState));
  };
  
  return (
    <div>
      <textarea
        value={state.text}
        onChange={handleChange}
      />
      <button
        onClick={handleUndo}
        disabled={!controls.canUndo()}
      >
        Undo
      </button>
      <button
        onClick={handleRedo}
        disabled={!controls.canRedo()}
      >
        Redo
      </button>
    </div>
  );
}

Advanced Usage

Creating a Store Hook Factory

import { createStoreHook } from '@okenneth/reactive-state';

// Create a store and a custom hook for it
const { store: todoStore, useStore: useTodoStore } = createStoreHook({
  initialState: {
    todos: [],
    filter: 'all'
  }
});

// Use in components
function TodoList() {
  const [state, setState] = useTodoStore();
  
  // Now you can use the state and setState directly
  // without having to import the store separately
}

Custom Middleware

import { Middleware } from '@okenneth/reactive-state';

// Create a custom middleware
const analyticsMiddleware: Middleware<any> = (nextState, prevState, action) => {
  if (action) {
    // Send action to analytics service
    sendToAnalytics({
      action,
      timestamp: new Date().toISOString()
    });
  }
  
  return nextState;
};

// Use the middleware
const storeWithAnalytics = createStore({
  initialState: { /* ... */ },
  middleware: [analyticsMiddleware]
});

SSR Support

ReactiveState works seamlessly with server-side rendering:

// On the server
const serverStore = createStore({ initialState: { /* ... */ } });

// Render with initial data
const html = renderToString(<App store={serverStore} />);

// Serialize state for client hydration
const initialData = serverStore.getState();

// On the client
const clientStore = createStore({ 
  initialState: window.__INITIAL_DATA__ || { /* fallback */ } 
});

// Hydrate the application
hydrate(<App store={clientStore} />, document.getElementById('root'));

Performance Optimization

ReactiveState is designed for optimal performance:

  1. Fine-grained reactivity: Components only re-render when their specific dependencies change.
  2. Shallow equality checks: Prevents unnecessary renders when state references change but values don't.
  3. Memoized selectors: The useSelector hook only triggers updates when the selected state changes.
  4. Batch updates: Multiple state changes within the same event loop are batched to avoid cascading renders.

API Reference

Core

createStore<T>(options: CreateStoreOptions<T>): Store<T>

Creates a new store with the provided options.

createDerivedStore<D, T>(dependencies, deriveFn): Store<T>

Creates a derived store that depends on other stores.

Hooks

useStore<T>(store: Store<T>): T

Subscribes to a store and returns its current state.

useSelector<T, R>(store: Store<T>, selector: (state: T) => R, equalityFn?): R

Subscribes to a specific part of a store's state.

useSetState<T>(store: Store<T>): (updater, action?) => void

Returns a memoized setState function for the store.

useStoreState<T>(store: Store<T>): [T, (updater, action?) => void]

Returns both the state and setState function.

useSelectorState<T, R>(store, selector, equalityFn?): [R, (updater, action?) => void]

Returns both the selected state and setState function.

Middleware

createLoggerMiddleware(options?): Middleware<T>

Creates middleware that logs state changes.

createPerformanceMiddleware(options?): Middleware<T>

Creates middleware that tracks update performance.

createThrottleMiddleware(options?): Middleware<T>

Creates middleware that throttles updates.

createValidationMiddleware(validator): Middleware<T>

Creates middleware that validates state changes.

createUndoRedoMiddleware(options?): { middleware, controls }

Creates middleware and controls for undo/redo functionality.

License

MIT