Before using react-context-slices maybe you want to consider using jotai-wrapper, a super simple and tiny library around jotai that makes using jotai even simpler. The migration from react-context-slices to jotai-wrapper it's fairly simple. In react-context-slices you define a slices.js
file, so in jotai-wrapper you define an atoms.js
file. react-context-slices default exports getHookAndProviderFromSlices
function, so jotai-wrapper default exports getAPIFromAtoms
function. The reason for migration is a high usage of memory by react-context-slices when using React Context slices.
react-context-slices
offers a unique solution to global state management in React by seamlessly integrating both Redux and React Context with zero-boilerplate.
Define your slices using the getHookAndProviderFromSlices
function provided by the library. This gives you the useSlice
hook and a provider component.
Use the useSlice
hook in your components to get the value of the slice state, a setter or dispatch function, and an actions object (for Redux slices).
What differentiates a Redux slice from a React Context slice is the presence of the reducers
key in its definition (if present, it's a Redux slice; otherwise it's a React Context slice).
React Context slices can initialize state from storage (local for web and async for React Native) and use middleware for action workflow customization in a per-slice basis.
Use react-context-slices
to manage global state in React with zero-boilerplate either by defining Redux slices or React Context slices.
If you want to read this documentation it's fine. But there is a dedicated website for the library. It is recommended you take a look at it instead.
Installation
How to use it (javascript)
Get initial state from storage (React Context slices)
Define middleware (React Context slices)
Pass options to the Redux store
How to use it (typescript)
How to use it in micro-frontend solutions
Things you can do
A note on why "initialArg" nomenclature (React Context slices)
A note on testing
API Reference
License
npm i react-context-slices
// slices.js
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
count1: {
// Redux slice
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
},
count2: {
// React Context slice
initialArg: 0,
},
count3: {
// React Context slice
initialArg: 0,
reducer: (state, { type }) => {
switch (type) {
case "increment":
return state + 1;
default:
return state;
}
},
},
todos: {
// Redux slice
initialState: [],
reducers: {
add: (state, { payload }) => {
state.push(payload);
},
},
},
// rest of slices (either Redux or React Context slices)
},
});
// app.jsx
import { useSlice } from "./slices";
const App = () => {
const [count1, reduxDispatch, { increment }] = useSlice("count1");
const [count2, setCount2] = useSlice("count2");
const [count3, dispatchCount3] = useSlice("count3");
const [todos, , { add }] = useSlice("todos");
const [firstTodo] = useSlice("todos", (state) => state[0]);
return (
<>
<div>
<button onClick={() => reduxDispatch(increment())}>+</button>
{count1}
</div>
<div>
<button onClick={() => setCount2((c) => c + 1)}>+</button>
{count2}
</div>
<div>
<button onClick={() => dispatchCount3({ type: "increment" })}>+</button>
{count3}
</div>
<div>
<button onClick={() => reduxDispatch(add("use react-context-slices"))}>
add
</button>
{todos.map((t, i) => (
<div key={i}>{t}</div>
))}
</div>
<div>{firstTodo}</div>
</>
);
};
export default App;
// index.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "./slices";
import App from "./app";
const container = document.getElementById("root");
if (container !== null) {
createRoot(container).render(
<StrictMode>
<Provider>
<App />
</Provider>
</StrictMode>
);
}
For React Context slices only, in case you want to get initial value of a slice from local storage, you do:
// slices.js
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
counter: { initialArg: 0, isGetInitialStateFromStorage: true }, // React Context slice
// rest of slices (either Redux or React Context slices)
},
});
and then in your component you do:
// app.jsx
import { useSlice } from "./slices";
import { useEffect } from "react";
const App = () => {
const [count, setCount] = useSlice("counter");
// this persist the value to local storage
useEffect(() => {
localStorage.setItem("counter", JSON.stringify(count));
}, [count]);
return (
<>
<button onClick={() => setCount((c) => c + 1)}>+</button>
{count}
</>
);
};
export default App;
For React Native you do the same but pass also AsyncStorage
to the configuration object accepted by getHookAndProviderFromSlices
:
// slices.js
import getHookAndProviderFromSlices from "react-context-slices";
import AsyncStorage from "@react-native-async-storage/async-storage";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
counter: { initialArg: 0, isGetInitialStateFromStorage: true }, // React Context slice
// rest of slices (either Redux or React Context slices)
},
AsyncStorage, // <-- set AsyncStorage key to AsyncStorage for React Native
});
and in your component you must do (for React Native):
// app.jsx
import React, { useEffect, useRef } from "react";
import { useSlice } from "./slices";
import { Button, Text, View } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
const App = () => {
const isInitialMountRef = useRef(true);
const [count, setCount] = useSlice("counter");
useEffect(() => {
(async () => {
!isInitialMountRef.current &&
(await AsyncStorage.setItem("counter", JSON.stringify(count)));
})();
}, [count]);
useEffect(() => {
isInitialMountRef.current = false;
}, []);
return (
<View>
<Button title="+" onPress={() => setCount((c) => c + 1)} />
<Text>{count}</Text>
</View>
);
};
export default App;
For React Context slices only, you can also pass middleware (without access to the state). You must specify it in the definition of a slice:
// slices.js
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
todos: {
// React Context slice
initialArg: [],
reducer: (state, action) => {
switch (action.type) {
case "FETCH_TODOS_REQUEST":
return state;
case "FETCH_TODOS_SUCCESS":
return action.payload;
case "FETCH_TODOS_FAILURE":
return state;
default:
return state;
}
},
middleware: [
() => (next) => (action) => {
// <-- logger middleware (first middleware applied)
console.log("dispathing action:", action);
next(action);
},
(dispatch) => (next) => (action) => {
// <-- async middleware (second middleware applied)
if (typeof action === "function") {
return action(dispatch);
}
next(action);
},
],
},
// rest of slices (either Redux or React Context slices)
},
});
Then you can write your action creator like:
const fetchTodos = () => async (dispatch) => {
dispatch({ type: "FETCH_TODOS_REQUEST" });
try {
const response = await fetch("https://api.example.com/todos");
const todos = await response.json();
dispatch({ type: "FETCH_TODOS_SUCCESS", payload: todos });
} catch (error) {
dispatch({ type: "FETCH_TODOS_FAILURE", payload: error.message });
}
};
and then call it in your component with:
// todos.jsx
import { useSlice } from "./slices";
import { useEffect } from "react";
const Todos = () => {
const [todos, dispatchTodos] = useSlice("todos");
useEffect(() => {
dispatchTodos(fetchTodos());
}, [dispatchTodos]);
return {todos.map(/* ... */)};
};
export default Todos;
You can also pass options to the Redux store (parameters of the configureStore
function from Redux Toolkit, except reducer
; check the documentation in Redux Toolkit):
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
count1: {
// Redux slice
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
},
// rest of slices (either Redux or React Context slices)
},
reduxStoreOptions: {
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat((store) => (next) => (action) => {
console.log("dispatching action:", action);
next(action);
console.log("next state:", store.getState());
}),
},
});
// slices.ts
"use client";
import getHookAndProviderFromSlices, {
defineSlice,
} from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
count1: defineSlice<number>({
// Redux slice
initialState: 0,
reducers: {
increment: (state) => state + 1,
},
}),
count2: defineSlice<number>({
// React Context slice
initialArg: 0,
}),
count3: defineSlice<number>({
// React Context slice
initialArg: 0,
reducer: (state, { type }) => {
switch (type) {
case "increment":
return state + 1;
default:
return state;
}
},
}),
todos: defineSlice<string[]>({
// Redux slice
initialState: [],
reducers: {
add: (state, { payload }) => {
state.push(payload);
},
},
}),
// rest of slices (either Redux or React Context slices)
},
});
Then in your component:
// app.tsx
import { useSlice } from "./slices";
const App = () => {
const [count1, reduxDispatch, { increment }] = useSlice<number>("count1");
const [count2, setCount2] = useSlice<number>("count2");
const [count3, dispatchCount3] = useSlice<number>("count3");
const [todos, , { add }] = useSlice<string[]>("todos");
const [firstTodo] = useSlice<string[], string>("todos", (state) => state[0]);
return (
<>
<div>
<button onClick={() => reduxDispatch(increment())}>+</button>
{count1}
</div>
<div>
<button onClick={() => setCount2((c) => c + 1)}>+</button>
{count2}
</div>
<div>
<button onClick={() => dispatchCount3({ type: "increment" })}>+</button>
{count3}
</div>
<div>
<button onClick={() => reduxDispatch(add("use react-context-slices"))}>
add
</button>
{todos.map((t, i) => (
<div key={i}>{t}</div>
))}
</div>
<div>{firstTodo}</div>
</>
);
};
export default App;
react-context-slices
is well-suited for micro-frontend solutions. It
enables seamless sharing of state among micro-frontend projects and
provides support for local shared state within each individual
micro-frontend. With react-context-slices, both shared states can
coexist harmoniously within each micro-frontend project. Check out
this
step-by-step article on how to implement it effectively.
// slices.js
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
count: {}, // <-- intialArg === undefined (React Context slice)
// rest of slices (either Redux or React Context slices)
},
});
// slices.js
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
isLightTheme: { initialArg: true, reducer: (state) => !state }, // <-- reducer without action (React Context slice)
// rest of slices (either Redux or React Context slices)
},
});
// slices.js
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
greeting: { initialArg: "hello", reducer: () => "bye" }, // <-- reducer without state and action (React Context slice)
// rest of slices (either Redux or React Context slices)
},
});
// slices.js
"use client";
import getHookAndProviderFromSlices from "react-context-slices";
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
greeting: { init: () => "hello" }, // <-- pass an 'init' function without an 'initialArg' (React Context slice)
// rest of slices (either Redux or React Context slices)
},
});
// app.jsx
import { useSlice } from "./slices";
const App = () => {
const [foo, setFoo] = useSlice(""); // 'foo' and 'setFoo' will be undefined. If you pass an empty string or a slice name that has not been defined (doesn't exist), it returns undefined for both 'value' and 'setValue'
return null;
};
export default App;
To define a slice you must pass an object which its possible keys (all optional) are initialArg
, init
, reducer
, isGetInitialStateFromStorage
and middleware
. The first three of them are exactly the same as the defined in the React docs about useReducer
hook. Check there the info to know what they do. The isGetInitialStateFromStorage
its name is not isGetInitialArgFromStorage
because in this case the init
function will not be applied (in the case that a value from local storage has been recovered) even when supplied in the definition of the slice because what we save in the local storage it's the state value and not initialArg
, so when we recover it we do not must apply the init
function and use directly this value as initial state.
If you want to write unit tests while using the library, you must exclude react-context-slices
from transformIgnorePatterns
in jest
configuration file:
// jest.config.js
module.exports = {
transformIgnorePatterns: ["/node_modules/(?!(react-context-slices)/)"],
// rest of configuration settings
};
On React Native you should also exclude react-native
from the list of transformIgnorePatterns
:
// jest.config.js
module.exports = {
transformIgnorePatterns: [
"/node_modules/(?!(react-context-slices|@react-native|react-native)/)",
],
// rest of configuration settings
};
Essentially what this tells is to not parse the node_modules
folder except for react-context-slices
. This is so because react-context-slices
has import
statements in it, and need to be parsed by tsc
or babel
when using jest
.
The library exports two functions: getHookAndProviderFromSlices
and defineSlice
. The first one is the main one and it's a default export. The second one it's only used in typescript.
Name | Type | Description | Example |
---|---|---|---|
|
(config: {
slices?: {
[slice: string]: Slice<any, any>;
};
AsyncStorage?: any;
reduxStoreOptions?: {
middleware:
| ((getDefaultMiddleware: any) => MiddlewareArray)
| MiddlewareArray;
};
}) => {
useSlice: (<T, K = T>(
slice: string,
selector: (state: T) => K
) => [K, ReduxDispatch<AnyAction>, { [x: string]: any }]) &
(<T, K = T>(
slice: string
) => [
K,
SetValue<T> & Dispatch & ReduxDispatch<AnyAction>,
{ [x: string]: any }
]);
Provider: ContextProviderType;
} |
It is the main (and default) function exported by the library. You pass a config object with optional keys |
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
count: { initialArg: 0 },
},
}); |
|
<T, K = T>(slice: Slice<T, K>) => Slice<T, K>; |
This function enforces rules for types in the definition of a slice object. It's a generic function. |
export const { useSlice, Provider } = getHookAndProviderFromSlices({
slices: {
count: defineSlice<number>({ initialArg: 0 }),
},
}); |
|
(store: {
getState: () => any;
dispatch: ReduxDispatch<AnyAction>;
}) => (next: ReduxDispatch<AnyAction>) => (action: AnyAction) => void; |
Is the type against to make an assertion in typescript when defining middleware in |
{
reduxStoreOptions: {
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(((store) => (next) => (action) => {
//...
}) as ReduxMiddleware);
}
} |
Next are described other entities encountered when using this library:
Name | Type | Description | Example |
---|---|---|---|
config object |
{
slices?: {
[slice: string]: Slice<any, any>;
};
AsyncStorage?: any;
reduxStoreOptions?: {
middleware?:
| ((getDefaultMiddleware: any) => MiddlewareArray)
| MiddlewareArray;
devTools?: any;
preloadedState?: any;
enhancers?: any;
};
} |
It's the object passed to |
{
slices: {
//...
}
} |
slices object |
{
[name:string]: Slice<T, K>;
} |
The slices object is an object which its keys are the name of the slices and its values are the slices themselves. |
{
count: {initialArg: 0}, // Context slice
todos: { // Redux slice
initialState: [],
reducers: {
add: (state, {payload}) => {
state.push(payload);
}
}
}
} |
a slice object |
{
initialArg?: K | T;
init?: (initialArg: K) => T;
reducer?: (state: T, action: any) => T;
isGetInitialStateFromStorage?: boolean;
middleware?: ((dispatch: Dispatch) => (next: Dispatch) => (action: any) => any)[];
} | {
initialState: NonUndefined<T>;
reducers: {
[x: string]: {
(state: T, action: any): void | T;
};
};
} |
A slice object is an object which its possible keys are for a React Context slice: |
{} // React Context slice |
|
K | T; |
It's the argument passed to the |
{
initialArg: 0;
} |
|
(initialArg: K) => T; |
It's the function used to compute initial state of the slice. It takes |
{
init: () => 0;
} |
|
(state: T, action: any) => T; |
If a reducer is supplied in the definition of a React Context slice, then the |
{
reducer: (state) => !state;
} |
|
boolean; |
Indicates whether the initial state for the slice will be recovered from local storage (web) or Async Storage (React Native). Used in React Context slices. |
{
isGetInitialStateFromStorage: true;
} |
|
((dispatch: Dispatch) => (next: Dispatch) => (action: any) => any)[] |
It's an array where the middleware for the dispatch function is passed. The first middleware applied will be the first on the array, the second the next, etc, ending with the dispatch function itself. The middleware does not have access to the state value of the slice. Used in React Context slices. |
{
middleware: [
() => (next) => (action) => {
console.log("I am a middleware");
next(action);
},
(dispatch) => (next) => (action) => {
if (typeof action === "function") {
return action(dispatch);
}
next(action);
},
];
} |
|
{
[x: string]: {
(state: T, action: any): void | T;
};
} |
When this key is present in the definition of a slice object, then the slice it's a Redux slice. Otherwise it's a React Context slice. It's the |
{
initialState: 0,
reducers: {
increment: (state) => state + 1,
decrement: (state) => state - 1
}
} |
|
T extends undefined ? never : T; |
Used for Redux slices. It's the initial state for the slice. Cannot be |
{
initialState: [],
reducers: {
add: (state, {payload}) => {
state.push(payload);
}
}
} |
|
(<T, K = T>(
slice: string,
selector: (state: T) => K
) => [K, ReduxDispatch<AnyAction>, { [x: string]: any }]) &
(<T>(
slice: string
) => [
T,
SetValue<T> & Dispatch & ReduxDispatch<AnyAction>,
{ [x: string]: any }
]); |
It's the hook returned by the call to |
const [count, setCount] = useSlice("count");
const [count2, dispatchCount2] = useSlice("count2");
const [count3, dispatchCount3, { increment, decrement }] = useSlice("count3");
const [todos, dispatchTodos, { add }] = useSlice("todos");
const [firstTodo] = useSlice("todos", (state) => state[0]); |
|
({ children }: React.PropsWithChildren) => JSX.Element; |
It's the provider returned by the call to |
root.render(
<Provider>
<App />
</Provider>
); |
Licensed under the ISC License, Copyright © 2022-present Roger Gomez Castells.
See LICENSE for more information.