A lightweight, zero-dependency, type-safe state management library for React.
redux-lite
offers a modern, simple, and highly performant state management solution, designed to provide an excellent developer experience with TypeScript. Unit testing your components is now unimaginably easy.
- 🚀 Zero-Dependency: Extremely lightweight with no third-party runtime dependencies (only
react
as a peer dependency). - ⚡️ High Performance: Avoids unnecessary re-renders by design through smart value comparisons.
- ✨ Simple & Intuitive API: A minimal API that is easy to learn and use.
- 🔒 Fully Type-Safe: End-to-end type safety, from store definition to dispatchers, with excellent autocompletion.
- ✅ Unbelievably Easy Testing: A flexible provider makes mocking state for unit tests trivial.
- 🐞 DevTools Ready: Optional, zero-cost integration with Redux DevTools for a great debugging experience.
- 🔌 Middleware Support: Extend functionality with custom middlewares, similar to Redux.
npm install @oldbig/redux-lite
# or
yarn add @oldbig/redux-lite
# or
pnpm add @oldbig/redux-lite
Create a storeDefinition
object. This single object is the source of truth for your entire state structure and types.
// store.ts
import { initiate, optional } from '@oldbig/redux-lite';
export const STORE_DEFINITION = {
user: {
name: 'Jhon' as string | null,
age: 30,
},
// Use `optional` for state slices that might not exist
task: optional({
id: 1,
title: 'Finish redux-lite',
}),
counter: 0,
};
export const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION);
In your main application file, wrap your component tree with the ReduxLiteProvider
.
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ReduxLiteProvider } from './store';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ReduxLiteProvider>
<App />
</ReduxLiteProvider>
</React.StrictMode>,
);
Use the useReduxLiteStore
hook to access state slices and their corresponding dispatchers. The hook returns a flattened object containing all state properties and type-safe dispatcher functions.
// MyComponent.tsx
import { useReduxLiteStore } from './store';
const MyComponent = () => {
// Destructure state and dispatchers
const {
user,
counter,
dispatchUser,
dispatchPartialUser,
dispatchCounter
} = useReduxLiteStore();
return (
<div>
<h2>User: {user.name}</h2>
<p>Counter: {counter}</p>
{/* Full update */}
<button onClick={() => dispatchUser({ name: 'Ken', age: 31 })}>
Set User
</button>
{/* Partial update */}
<button onClick={() => dispatchPartialUser({ age: 35 })}>
Update Age
</button>
{/* Functional update with access to the full store */}
<button onClick={() => dispatchPartialUser((currentUser, store) => ({ age: currentUser.age + store.counter }))}>
Increment Age by Counter
</button>
</div>
);
};
The sole entry point for the library.
storeDefinition
: An object that defines the shape and initial values of your store.options
(optional): An object for additional configuration.devTools
(optional):boolean | { name: string }
- Enable or configure Redux DevTools.middlewares
(optional):Middleware[]
- An array of middlewares to apply.
- Returns: An object containing
{ ReduxLiteProvider, useReduxLiteStore, useSelector }
.
The hook returns a flattened object containing all state slices and dispatchers.
Dispatchers
For each slice of state (e.g., user
), two dispatchers are generated:
dispatchUser(payload)
: For full updates.dispatchPartialUser(payload)
: For partial updates.
The payload
can be a value or a function. If it's a function, it receives the previous state of that slice as the first argument, and the entire store state as the second argument: (prevState, fullStore) => newState
.
A helper function to mark a state slice as optional. The state property will be typed as T | undefined
.
initialValue
(optional): The initial value of the property. If not provided, the state will beundefined
.
A hook for selecting and subscribing to a part of the state, with performance optimizations. It is similar to the useSelector
hook in react-redux
.
selector
:(store: TStore) => TSelected
- A function that takes the entire store state and returns the selected value.equalityFn
(optional):(a: TSelected, b: TSelected) => boolean
- A function to compare the selected value. Defaults toisEqual
(a deep equality check). If the selector function returns the same result as the previous call (determined by this equality function),useSelector
will return the previous result, which can help prevent unnecessary re-renders in the component that uses it. In most cases, you don't need to provide this parameter. It's only necessary if the value returned by theselector
contains function fields.
When to use useSelector
?
While useReduxLiteStore
is convenient for accessing both state and dispatchers, useSelector
is highly recommended for performance-critical components that only need to read a small piece of state. It helps prevent unnecessary re-renders when other parts of the store change.
Example:
import { useSelector } from './store';
const UserName = () => {
// This component will only re-render when `user.name` changes.
const userName = useSelector(store => store.user.name);
return <div>{userName}</div>
}
const UserAge = () => {
// This component will only re-render when `user.age` changes.
const userAge = useSelector(store => store.user.age);
return <div>{userAge}</div>
}
redux-lite
is designed for high performance. The internal reducer uses smart value comparison to prevent state updates and re-renders when data has not changed.
In a benchmark test that simulates a real-world scenario by calling a dispatch function repeatedly, redux-lite
was able to perform:
- 10,000 Counter Updates in approximately 16.43 milliseconds (0.0016ms per update)
- 1,000 Array Push Operations in approximately 3.9 milliseconds (0.0040ms per operation)
- 10,000 Object Property Updates in approximately 15.48 milliseconds (0.0015ms per update)
- 10,000 Partial Object Updates in approximately 15.15 milliseconds (0.0015ms per update)
- 1,000 Deeply Nested Updates in approximately 3.42 milliseconds (0.0034ms per update)
This demonstrates its exceptional speed even when including React's rendering lifecycle.
Feature | Redux (with Redux Toolkit) | redux-lite |
---|---|---|
Boilerplate | Requires createSlice , configureStore , actions, reducers. |
Almost zero. Define one object, get everything you need. |
API Surface | Larger API with multiple concepts (slices, thunks, selectors). | Minimal. initiate , optional , and the returned hook. |
Type Safety | Good, but can require manual typing for thunks and selectors. | End-to-end. Types are automatically inferred for everything. |
Performance | Highly performant, but relies on memoized selectors (reselect ). |
Built-in. Automatically prevents updates if values are deeply equal. |
Dependencies | @reduxjs/toolkit and react-redux . |
None. Only react as a peer dependency. |
Simplicity | Steeper learning curve. | Extremely simple. If you know React hooks, you know redux-lite . |
Testing Your Components
redux-lite
makes testing components that use the store incredibly simple. The ReduxLiteProvider
accepts an initStore
prop, which allows you to provide a deep partial state to override the default initial state for your tests.
This means you don't need to dispatch actions to set up your desired test state. You can directly render your component with the exact state it needs.
Here's how you can easily mock state for your components:
import { render } from '@testing-library/react';
import { initiate } from '@oldbig/redux-lite';
import React from 'react';
// Assume this is your initial store configuration
const STORE_DEFINITION = {
user: { name: 'Guest', age: 0, profile: { theme: 'dark' } },
isAuthenticated: false,
};
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION);
// --- Your Component ---
const UserProfile: React.FC = () => {
const { user } = useReduxLiteStore();
return <div>Welcome, {user.name} (Theme: {user.profile.theme})</div>;
};
// --- Your Test ---
it('should display the authenticated user name with overridden profile', () => {
const { getByText } = render(
<ReduxLiteProvider initStore={{ user: { name: 'Alice', profile: { theme: 'light' } }, isAuthenticated: true }}>
<UserProfile />
</ReduxLiteProvider>
);
// The component renders with the exact state you provided
expect(getByText('Welcome, Alice (Theme: light)')).toBeInTheDocument();
});
it('should shallow merge user slice and replace nested objects', () => {
const { getByText } = render(
<ReduxLiteProvider initStore={{ user: { name: 'Bob' } }}>
<UserProfile />
</ReduxLiteProvider>
);
// user.name is overridden, user.age remains default, user.profile is untouched
expect(getByText('Welcome, Bob (Theme: dark)')).toBeInTheDocument();
});
You can easily test your components in different states without any complex setup or mocking.
DevTools Integration
redux-lite
offers optional integration with the Redux DevTools Extension for a first-class debugging experience, including action tracking and time-travel debugging.
This feature is disabled by default and has zero performance cost when not in use.
How to Enable
To enable the integration, pass the devTools
option to the initiate
function.
// Enable with default options
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION, {
devTools: true
});
// Or provide a name for your store instance
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION, {
devTools: { name: 'MyAppStore' }
});
Installation
- Install the Redux DevTools Extension for your browser:
- Enable the feature in your code as shown above.
- Open your browser's developer tools and find the "Redux" tab.
Middleware
redux-lite
supports a middleware API that is almost identical to Redux's, allowing you to extend the store's capabilities for logging, handling async actions, and more.
How to Use Middleware
Pass an array of middlewares in the options
object when calling initiate
.
import { initiate, Middleware } from '@oldbig/redux-lite';
const logger: Middleware<any> = (api) => (next) => (action) => {
console.log('dispatching', action);
const result = next(action);
console.log('next state', api.getState());
return result;
};
const { ReduxLiteProvider, useReduxLiteStore } = initiate(STORE_DEFINITION, {
middlewares: [logger]
});
Writing Custom Middleware
A middleware is a higher-order function with the following signature:
type Middleware<S> = (api: MiddlewareAPI<S>) => (next: (action: Action<S>) => Action<S>) => (action: Action<S>) => Action<S>;
api
: An object with two methods:getState()
: Returns the current state.dispatch(action)
: Dispatches an action. This will send the action to the start of the middleware chain.
next
: A function that passes the action to the next middleware in the chain. You must callnext(action)
at some point for the action to eventually reach the reducer.action
: The action being dispatched.
Important Middleware Best Practices
- Avoid Infinite Loops: Calling
api.dispatch(action)
within a middleware sends the action back to the beginning of the middleware chain. To prevent infinite loops, always placeapi.dispatch
calls within appropriate conditional blocks:
const conditionalDispatchMiddleware: Middleware<any> = (api) => (next) => (action) => {
// BAD - This will cause an infinite loop
// api.dispatch({ type: 'someAction', payload: 'data', isPartial: false });
// GOOD - Place dispatch in a conditional block
if (action.type === 'user_login') {
api.dispatch({ type: 'notifications_show', payload: 'Welcome!', isPartial: false });
}
return next(action);
};
-
Error Handling: Wrap middleware logic in try-catch blocks to prevent one faulty middleware from breaking the entire chain.
-
Performance: Minimize heavy computations in middlewares as they run synchronously and can block the UI thread.
- Todo List App - A complete todo list application demonstrating core features
- Performance Test - Performance benchmarks demonstrating the efficiency of redux-lite
If you find redux-lite
helpful and would like to support its development, please consider:
- Giving a ⭐️ on GitHub
- Buying me a coffee
Your support is greatly appreciated!
This project is licensed under the MIT License.