DEV Community

roggc
roggc

Posted on • Edited on

The ultimate solution to global state management in React

react-context-slices offers a zero-boilerplate solution to global state management in React by seamlessly integrating both Redux and React Context.

react-context-slices is the ultimate solution to global state management in React

Installation

npm i react-context-slices
Enter fullscreen mode Exit fullscreen mode

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)
  },
});
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode

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
  }
});
Enter fullscreen mode Exit fullscreen mode

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
  }
});
Enter fullscreen mode Exit fullscreen mode

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)
  },
});
Enter fullscreen mode Exit fullscreen mode
// 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;
Enter fullscreen mode Exit fullscreen mode

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)