Namespace Store for React is a simple and powerful state management library designed to solve issues with nested contexts in React applications. Inspired by Radix UI's scopeContext and createContext wrapper, it leverages the Proxy API to effectively handle both global and local state.
- Lightweight: Minimal overhead added to your application.
- Namespaced State Management: Isolate state within specific contexts or scopes.
- Flexible Store Options: Supports both global and local stores.
- TypeScript Support: Fully typed for better developer experience.
- Inspired by Radix UI: Leverages concepts from Radix UI's
scopeContext
for modular component development.
https://react.dev/reference/react/useContext#passing-data-deeply-into-the-tree
When using React's Context API, the following issues can arise:
- The entire tree wrapped by the context gets re-rendered.
- Only the nearest parent context is accessible, making it difficult to manage when multiple contexts are nested.
Inspired by the concept of "scope" introduced by the Radix UI library (Radix Context), I developed this library to address these challenges.
- Provides a more flexible and scalable approach to state management.
- Reduces unnecessary re-renders.
- Simplifies implementation for deeply nested components.
The main purpose of this library is not so much for global state management (although it can be used that way too!) but rather for managing state in a flexible, simple, and efficient manner when developing component-based applications, much like its name 'namespace' suggests.
It is recommended for use in the following cases:
- Developing design system components (I use it with radix-ui)
- Large and complex components such as features, widgets, and pages in the FSD architecture, or organisms in the atomic design pattern
- Recursive structures like nested tabs, especially when there is a need to manipulate them mutually
- When external components need to manipulate the state within a context
npm install @lodado/namespace-core @lodado/react-namespace
# or
yarn add @lodado/namespace-core @lodado/react-namespace
...etc
(TO-DO)
First, you need to create a store that will hold your application's state. This store should extend the NamespaceStore
provided by the library.
import { NamespaceStore } from '@lodado/namespace-core';
interface AppState {
user: {
name: string;
email: string;
};
theme: 'light' | 'dark';
}
class AppStore extends NamespaceStore<AppState> {
constructor(initialState: AppState) {
super(initialState);
}
// Define actions to modify the state
setUser(user: AppState['user']) {
this.state.user = user;
}
toggleTheme() {
this.state.theme = this.state.theme === 'light' ? 'dark' : 'light';
}
}
// Create a global store instance
const globalStore = new AppStore({
user: {
name: 'John Doe',
email: '<[email protected]>',
},
theme: 'light',
});
Use the createNamespaceContext
function to create a context for your namespace store.
import { createNamespaceContext } from "@lodado/react-namespace";
const {
Provider: AppProvider,
useNamespaceStores,
useNamespaceAction,
} = createNamespaceContext<AppState, AppStore>({
globalStore, // global store
// localStore, // store per provider
});
Wrap your application or specific components with the AppProvider
to provide the store context.
import React from 'react';
import { AppProvider } from './path-to-your-provider';
function App() {
return (
<AppProvider>
<YourComponent />
</AppProvider>
);
}
export default App;
Use the useNamespaceStores
and useNamespaceAction
hooks to access the state and actions within your components.
import React from 'react';
import { useNamespaceStores, useNamespaceAction } from './path-to-your-provider';
function YourComponent() {
const { user } = useNamespaceStores((state) => {
return { user: state.user }
}});
const { setUser, toggleTheme } = useNamespaceAction();
return (
<div>
<h1>Welcome, {user.name}</h1>
<button onClick={() => toggleTheme()}>
Switch to {state.theme === 'light' ? 'dark' : 'light'} mode
</button>
</div>
);
}
export default YourComponent;
or you can just use useNamespaceStores
import React from 'react';
import { useNamespaceStores, useNamespaceAction } from './path-to-your-provider';
function YourComponent() {
const { user, setUser, toggleTheme } = useNamespaceStores((state) => {
return { user: state.user }
}})
return (
<div>
<h1>Welcome, {state.user.name}</h1>
<button onClick={() => toggleTheme()}>
Switch to {state.theme === 'light' ? 'dark' : 'light'} mode
</button>
</div>
);
}
export default YourComponent;
The concept of "scope" overcomes React Context's limitations, such as the challenges of managing nested contexts and the overhead of re-rendering entire trees.
Below is an example that demonstrates how to create and use scoped state with @lodado/react-namespace
.
import { NamespaceStore } from '@lodado/namespace-core';
// Counter store for managing count
class Counter extends NamespaceStore<{ count: number }> {
constructor(initialCount = 0) {
super({ count: initialCount });
}
increment() {
this.state.count += 1;
}
decrement() {
this.state.count -= 1;
}
}
// Text store for managing text
class Text extends NamespaceStore<{ text: string }> {
constructor() {
super({ text: 'test' });
}
updateText() {
this.state.text = 'updated';
}
}
Scopes allow you to isolate state for different contexts. In this example, a Dialog
scope and an AlertDialog
scope are created.
import { createNamespaceScope, Scope } from '@lodado/react-namespace';
// Create a Dialog scope
const [createDialogContext, createDialogScope] = createNamespaceScope('Dialog');
const { Provider: DialogProvider, useNamespaceStores: useDialogNamespaceStore } = createDialogContext('Dialog', {
localStore: () => new Counter(),
});
// Create an AlertDialog scope, extending Dialog scope
const [createAlertDialogProvider, createAlertDialogScope] = createNamespaceScope('AlertDialog', [createDialogScope]);
const { Provider: AlertDialogProvider, useNamespaceStores: useAlertDialogNamespaceStore } = createAlertDialogProvider('AlertDialogContext', {
localStore: () => new Text(),
});
Using useNamespaceStores
, you can access state from specific scopes.
const DialogContent = ({ scope, scope2 }: { scope: Scope<any>; scope2: Scope<any> }) => {
const { count } = useDialogNamespaceStore((state) => ({ count: state.count }), scope);
const { text } = useAlertDialogNamespaceStore((state) => ({ text: state.text }), scope);
const { increment } = useDialogNamespaceStore(() => ({}), scope2);
return (
<div>
<button onClick={increment}>Click!</button>
<div>
Content: {count} - {text}
</div>
</div>
);
};
You can nest providers with different scopes to isolate and manage state efficiently.
export const ScopeExample = () => {
const scope1 = createAlertDialogScope()({});
const scope2 = createAlertDialogScope()({});
return (
<AlertDialogProvider scope={scope1.__scopeAlertDialog}>
<AlertDialogProvider scope={scope2.__scopeAlertDialog}>
<DialogProvider scope={scope2.__scopeAlertDialog}>
<DialogProvider scope={scope1.__scopeAlertDialog}>
<DialogContent scope={scope1.__scopeAlertDialog} scope2={scope2.__scopeAlertDialog} />
<DialogContent scope={scope2.__scopeAlertDialog} scope2={scope1.__scopeAlertDialog} />
</DialogProvider>
</DialogProvider>
</AlertDialogProvider>
</AlertDialogProvider>
);
};
This example highlights how scoped state allows you to create isolated, modular, and reusable contexts for state management, particularly in scenarios with nested or complex components.
Creates a namespace context for managing state and actions within a specific namespace.
function createNamespaceContext<
State extends Record<string | symbol, any>,
StoreType extends NamespaceStore<State>,
>(options: StoreOption<State, StoreType>): {
readonly Context: React.Context<StoreType | undefined>;
readonly Provider: React.FC<{
overwriteStore?: (() => StoreType) | StoreType;
children: React.ReactNode;
}>;
readonly store: StoreType | undefined;
readonly useNamespaceStores: () => { state: State };
readonly useNamespaceAction: () => StoreType; // context functions
readonly useNamespaceContext: () => StoreType // "context"
};
options
: An object containing the following properties:globalStore
: An optional global store instance or a function that returns one.localStore
: An optional local store instance or a function that returns one.
An object containing:
Context
: The React context for the namespace.Provider
: A provider component to wrap your application or components.store
: The global store instance.useNamespaceStores
: A hook to access the state.useNamespaceAction
: A hook to access the actions.
import { NamespaceStore } from '@lodado/namespace-core'
import { createNamespaceContext } from '@lodado/react-namespace'
class Counter extends NamespaceStore<{ count: number; text: string }> {
constructor() {
super({ count: 0, text: 'teest' })
}
increment() {
this.state.count += 1
}
decrement() {
this.state.count -= 1
}
updateText() {
this.state.text = 'updated'
}
}
let cnt = 0
const { Provider: ExampleProvider, useNamespaceStores } = createNamespaceContext({
globalStore: new Counter(),
})
Creates a namespace scope for managing state and actions within a specific namespace.
function createNamespaceScope(
scopeName: string,
createContextScopeDeps: CreateScope[] = [],
): readonly [
typeof createScopeContext,
ReturnType<typeof composeContextScopes>,
];
scopeName
: The name of the namespace scope.createContextScopeDeps
: An array of dependencies for creating the context scope.
A tuple containing:
createScopeContext
: Function to create a context for a namespace store.composeContextScopes
: Function to compose multiple context scopes.
Composes multiple context scopes into a single scope.
function composeContextScopes(...scopes: CreateScope[]): CreateScope;
scopes
: A list ofCreateScope
functions to compose.
createScope
: A function that creates a composed scope.
https://github.com/lodado/react-namespace/blob/main/apps/docs/stories/scope/table/App.tsx
https://github.com/lodado/react-namespace/tree/main/apps/docs/stories/scope/tictactoe
You can also provide a local store specific to a component or a subtree.
const localStore = new AppStore({
user: {
name: 'Jane Doe',
email: '<[email protected]>',
},
theme: 'dark',
});
// In your component
<AppProvider overwriteStore={localStore}>
<YourComponent />
</AppProvider>;
If you're building a library of components and want to avoid conflicts, you can create a namespaced scope.
import { createNamespaceScope } from "@lodado/react-namespace";
const [createScopeContext, createScope] = createNamespaceScope('MyComponent');
const {
Provider: MyComponentProvider,
useNamespaceStores,
} = createScopeContext<AppStore>('MyComponent', { globalStore });
// Use in your component
<MyComponentProvider>
<YourComponent />
</MyComponentProvider>;
Compose multiple scopes to create isolated contexts.
const [createScopeContextA, createScopeA] = createNamespaceScope('ScopeA');
const [createScopeContextB, createScopeB] = createNamespaceScope('ScopeB');
const composedScope = composeContextScopes(createScopeA, createScopeB);
// Now you can use the composed scope in your providers and hooks
const [createScopeContext, createScope] = createNamespaceScope('ComposedScope', [composedScope]);
const {
Provider: ComposedProvider,
useNamespaceStores,
useNamespaceAction,
} = createScopeContext<AppStore>('ComposedScope', { globalStore });
// Use in your component
<ComposedProvider>
<YourComponent />
</ComposedProvider>;
you can reset states with reset function in useNamespaceAction or useNamespaceStores
const TestComponent = () => {
const { count } = useNamespaceStores((state) => ({ count: state.count }))
const { increment, decrement, reset } = useNamespaceAction()
return (
<div>
<button type="button" data-testid="reset-button" onClick={reset}>
Reset
</button>
<button type="button" data-testid="increment-button" onClick={increment}>
Increment
</button>
<button type="button" data-testid="decrement-button" onClick={decrement}>
Decrement
</button>
</div>
)
}
you can use literally "context" class instance via useNamespaceContext
const TestComponent = () => {
const context = useNamespaceContext()
...
}
MIT License
This library simplifies state management in React applications by providing a structured and scalable way to handle state across different components and scopes. Whether you're building a small application or a large-scale project, this namespace store can help you maintain clean and maintainable code.
Feel free to contribute to the project by submitting issues or pull requests on the GitHub repository.