9.3.2 • Published 2 months ago

react-context-slices v9.3.2

Weekly downloads
-
License
ISC
Repository
github
Last release
2 months ago

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

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.

9.3.2

2 months ago

9.3.1

3 months ago

9.3.0

3 months ago

9.2.5

3 months ago

9.2.4

8 months ago

9.2.3

8 months ago

9.2.2

8 months ago

9.2.1

8 months ago

9.2.0

8 months ago

9.0.9

10 months ago

9.0.8

10 months ago

9.0.7

10 months ago

9.0.6

10 months ago

9.0.5

10 months ago

9.0.4

10 months ago

9.0.3

11 months ago

8.0.9

11 months ago

2.2.1

12 months ago

8.0.8

11 months ago

2.2.0

12 months ago

2.2.3

12 months ago

2.2.2

12 months ago

8.0.5

11 months ago

8.0.4

11 months ago

2.2.4

12 months ago

8.0.7

11 months ago

8.0.6

11 months ago

7.1.3

11 months ago

7.1.2

11 months ago

7.1.1

11 months ago

7.1.0

11 months ago

7.2.6

11 months ago

7.2.5

11 months ago

7.2.4

11 months ago

3.0.2

12 months ago

7.2.3

11 months ago

3.0.1

12 months ago

7.2.9

11 months ago

7.2.8

11 months ago

7.2.7

11 months ago

9.0.13

10 months ago

9.0.12

10 months ago

9.0.14

10 months ago

3.0.0

12 months ago

9.0.11

10 months ago

9.0.10

10 months ago

4.0.0

12 months ago

7.2.10

11 months ago

9.1.1

10 months ago

9.1.0

10 months ago

8.0.10

11 months ago

8.0.11

11 months ago

5.0.1

11 months ago

5.0.0

11 months ago

9.1.2

10 months ago

6.0.1

11 months ago

6.0.0

11 months ago

7.0.0

11 months ago

7.2.2

11 months ago

7.2.1

11 months ago

7.2.0

11 months ago

8.0.1

11 months ago

8.0.0

11 months ago

8.0.3

11 months ago

8.0.2

11 months ago

4.1.0

12 months ago

4.1.2

11 months ago

4.1.1

11 months ago

9.0.2

11 months ago

9.0.1

11 months ago

9.0.0

11 months ago

2.1.2

1 year ago

2.1.1

1 year ago

2.1.4

1 year ago

2.1.3

1 year ago

2.1.6

1 year ago

2.1.5

1 year ago

2.1.8

1 year ago

2.1.7

1 year ago

2.1.0

1 year ago

2.1.9

1 year ago

2.1.10

1 year ago

2.1.11

1 year ago

2.0.9

2 years ago

2.0.8

2 years ago

2.0.7

2 years ago

2.0.6

2 years ago

2.0.5

2 years ago

2.0.4

2 years ago

2.0.3

2 years ago

2.0.2

2 years ago

2.0.1

2 years ago

2.0.0

2 years ago

1.0.7

2 years ago

1.0.6

2 years ago

1.0.5

2 years ago

1.0.4

2 years ago

1.0.3

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago