react-context-slices offers a zero-boilerplate solution to global state management in React by seamlessly integrating both Redux and React Context.
Installation
npm i react-context-slices
How to use it (javascript)
react-context-slices seamlessly integrates the best of both worlds with zero-boilerplate. You define either React Context or Redux slices, and the function getHookAndProviderFromSlices
will get you a hook, useSlice
, and a provider.
What differentiates a slice of being a Redux slice from being a React Context slice is the presence of the key reducers
in its definition. If the key is present, then it will be a Redux slice. Otherwise it will be a React Context slice.
// slices.js
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;
The useSlice
hook is used to fetch the slices, either the Redux slices or React Context slices. For Redux slices you can pass an optional selector as a second parameter to the function. As a first parameter, you always pass the name of the slice.
For React Context slices it returns a tuple where the first element is the value of the state for the slice and the second element is either a setter or dispatch function, depending on if you defined a reducer for the slice (key reducer
, not reducers
).
For Redux slices, the hook returns an array where the first element is the state value selected, the second is a dispatch function, and the third is an object containing the actions for the slice.
Other features
react-context-slices allows us to retrieve the initial state of a React Context slice from local storage (in the case of the web) or from async storage (in the case of React Native).
// slices.js
import getHookAndProviderFromSlices from "react-context-slices";
export const {useSlice, Provider} = getHookAndProviderFromSlices({
slices: {
count: {initialArg: 0, isGetInitialStateFromStorage: true}, // React Context slice
// rest of slices
}
});
Then we need to persist the state value of the slice every time it changes.
It also allows the definition of middleware for a given React Context slice to intercept and customise actions workflow. This way, we can make API calls, logging, or other side effects. Middleware does not have access to state value (in the case of React Context slices).
// slices.js
import getHookAndProviderFromSlices from "react-context-slices";
export const {useSlice, Provider} = getHookAndProviderFromSlices({
slices:{
todos: { // React Context slice
initialArg: [],
reducer: (state, action) => {
// ...
},
middleware: [
() => next => action => { // first middleware applied
console.log('dispatching action:', action);
next(action);
},
(dispatch) => next => action => { // second middleware applied
if(typeof action === 'function'){
return action(dispatch);
}
next(action);
},
],
},
// rest of slices
}
});
Typescript
react-context-slices has also support for typescript.
// slices.ts
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)
},
});
// 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;
Summary
react-context-slices seamlessly integrates the best of both worlds, Redux and React Context, in a zero-boilerplate solution.
Use react-context-slices for global state management in React and React Native.
Top comments (0)