npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

react-context-slices

v9.3.2

Published

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

Downloads

1,579

Readme

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

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.

Dedicated website (recommended)

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.

Table of Contents

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

Installation

npm i react-context-slices

How to use it (javascript)

// 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>
  );
}

Get initial state from storage (React Context slices)

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;

Define middleware (React Context slices)

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;

Pass options to the redux store

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());
      }),
  },
});

How to use it (typescript)

// 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;

How to use it in micro-frontend solutions

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.

Things you can do

// 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;

A note on why "initialArg" nomenclature (React Context slices)

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.

A note on testing

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.

API Reference

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.

getHookAndProviderFromSlices (default import)

(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 slices, AsyncStorage, and reduxStoreOptions. The slices key is an object wich its keys are the slices names and its values, the defintion of the slices.

export const { useSlice, Provider } = getHookAndProviderFromSlices({
  slices: {
    count: { initialArg: 0 },
  },
});

defineSlice (used in typescript)

<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 }),
  },
});

ReduxMiddleware (typescript type)

(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

{
  reduxStoreOptions: {
    middleware: (getDefaultMiddleware) =>
      getDefaultMiddleware().concat(((store) => (next) => (action) => {
        //...
      }) as ReduxMiddleware);
  }
}

Next are described other entities encountered when using this library:

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 getHookAndProviderFromSlices. It can contain three optional keys: slices, AsyncStorage, and reduxStoreOptions.

{
  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: initialArg, init, reducer, isGetInitialStateFromStorage, and middleware; and for a Redux slice: reducers, and initialState. The keys for a React Context slice are all optional. What makes a slice to be a Redux slice is the presence of the reducers key. If it's not present, then it is a React Context slice.

{} // React Context slice

initialArg

K | T;

It's the argument passed to the init function to compute the initial state. If no init function is present in the definition of the slice, then it becomes the initial state. Used in React Context slices.

{
  initialArg: 0;
}

init

(initialArg: K) => T;

It's the function used to compute initial state of the slice. It takes initialArg as an argument. If no present then initialArg it's the initial state. Used in React Context slices.

{
  init: () => 0;
}

reducer

(state: T, action: any) => T;

If a reducer is supplied in the definition of a React Context slice, then the useSlice, when used with this slice, will return a dispatch function as a second value in the array. If it is not defined, then the useSlice hook will return, for this slice, a setter function as a second value in the array.

{
  reducer: (state) => !state;
}

isGetInitialStateFromStorage

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;
}

middleware

((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);
    },
  ];
}

reducers

{
  [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 reducers object passed to the createSlice from Redux Toolkit (check the documentation there).

{
  initialState: 0,
  reducers: {
    increment: (state) => state + 1,
    decrement: (state) => state - 1
  }
}

initialState

T extends undefined ? never : T;

Used for Redux slices. It's the initial state for the slice. Cannot be undefined (make it null instead).

{
  initialState: [],
  reducers: {
    add: (state, {payload}) => {
      state.push(payload);
    }
  }
}

useSlice

(<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 getHookAndProviderFromSlices. When used, you must pass the name of the slice you want to fetch or use. It will return, in the case of a React Context slice, an array where the first element is the state value of the slice and the second a dispatch or setter function, depending on if a reducer was defined or not for the slice. In the case of Redux slices, you can pass a selector as a second parameter to the call of the useSlice hook. It will return an array where the first element is the state value for the slice (with the selector applied, if any), the second element is the dispatch function, and the third element is the actions object (action creators) for the slice.

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]);

Provider

({ children }: React.PropsWithChildren) => JSX.Element;

It's the provider returned by the call to getHookAndProviderFromSlices. It must be used up in the tree, in order for the hook useSlice to work.

root.render(
  <Provider>
    <App />
  </Provider>
);

License

Licensed under the ISC License, Copyright © 2022-present Roger Gomez Castells.

See LICENSE for more information.