react-slice-context
v1.2.0
Published
A a lightweight, performant, proxy-based state management library for React without any external dependencies.
Downloads
23
Maintainers
Readme
React Slice Context 🍕
react-slice-context
is a lightweight, performant, proxy-based state management library for React, built on the concept of slices and leveraging the power of React hooks. It provides a simple and flexible way to manage state in your React applications.
Table of Contents
- Features
- Demo and Examples
- Requirements
- Installation
- Getting Started
- API
- Optimization
- Dispatch
- Plugins
- Update Context Value Outside of Components
- Get Context Value Outside of Components
- Common Mistakes
Features
No <Provider>
Hassle
Say goodbye to the <Provider>
wrapper! Simply initialize the context, and it's ready to be used from any part of your application!
Update States in A Breeze
No more action creators, actions, and reducers just to update a simple state. With React Slice Context, updating states is as straightforward as declaring functions that directly mutate the context state without worrying about reactivity. Less boilerplate, more productivity!
Effortless Nested State Updates
Forget about duplicating layers of objects just to modify a single property. Thanks to the proxy-based implementation, updating objects and arrays becomes a breeze. Plus, there's no need for Immer in React Slice Context!
No Context Loss
In contrast to the native React context, the context value in React Slice Context can be set up at the same level as your main application. This flexibility enables the context value to be accessed across multiple renderers within a single application!
Demo and Examples
Requirements
To use this library, make sure your react
and react-dom
versions are both 16.8.4 or later, as hooks were introduced in React 16.8.
Installation
Install react-slice-context
in your project using any package manager of your choice; for example:
npm install react-slice-context
Getting Started
Import the
createSliceContext
function fromreact-slice-context
:import { createSliceContext } from 'react-slice-context'
Create a slice context with an initial state and a dispatcher:
import { createSliceContext } from 'react-slice-context' const pizzaContext = createSliceContext({ state: () => { // Return your initial state here. return { price: 10, flavor: 'Pepperoni', } }, dispatch: (pizza) => { // Define the functions to update the state of this slice context here. // You can mutate the state directly without any concerns! return { incrementPrice: () => { pizza.price++ }, setFlavor: (flavor: string) => { pizza.flavor = flavor }, } }, })
Export the
useContext
anddispatch
from the slice context. It is recommended to use destructing assignment syntax to rename them so that it's more convenient when you have multiple contexts.// Either export and rename them after declaration. const pizzaContext = createSliceContext({ ... }) export const { useContext: usePizzaContext, dispatch: pizzaDispatch } = pizzaContext // ...or do it all at once. export const { useContext: usePizzaContext, dispatch: pizzaDispatch, } = createSliceContext({ ... })
Use the exported
usePizzaContext
(formerlyuseContext
) andpizzaDispatch
(formerlydispatch
) to access the state and dispatcher within your components:import { usePizzaContext, pizzaDispatch } from './pizza-context' const MyComponent = () => { // This will cause the component to re-render whenever there's a change in `pizzaContext`. const pizza = usePizzaContext() const { incrementPrice } = pizzaDispatch return ( <div> <h2>Pizza price: {pizza.price}</h2> <h2>Pizza flavor: {pizza.flavor}</h2> <button onClick={incrementPrice}>Increment Price</button> </div> ) }
API
createSliceContext(options)
Creates a slice context with the specified options.
Options
| Property | Type | Required | Description | Default Value | | ---------- | -------- | :------: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | |
state
| function | ✅ | A function that returns the initial state for the slice context. | | |dispatch
| function | ✅ | A function that returns the dispatcher (a set of dispatch functions) for the slice context. The functions declared in the dispatcher are the only ones allowed to change the context state. For more information, please refer to the Dispatch section below. | | |plugins
| Array | No | An array of plugins that enables you to inject custom hooks into the context's lifecycle. Please refer to the Plugins section below. |undefined
|Return Value
It returns a slice context with the following properties:
| Property | Type | Description | | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | |
useContext(selector)
| function |useContext(selector)
is a hook to retrieve the state of the associated context; it can only be used within the body of a React functiona component. By default, when the optionalselector
argument is not provided,useContext()
returns the entire state, which will cause the component to re-render whenever there's a change in the context. If your component only care about specific context properties, using something likeconst state = useMyContext()
may not deliver optimal performance. Check out the Optimization section below for further insights. | |dispatch
| object | The dispatcher for the slice context. It works both inside and outside of components. See the Dispatch section below for more information. | |getState()
| function | Returns a read-only state in the slice context. This is useful for getting context state outside of components. |
Optimization
By default, useContext()
returns the entire state, which will cause the component to re-render whenever there's a change in the context. Consider the pizzaContext
as shown below:
const { useContext: usePizzaContext } = createSliceContext({
state: () => ({
price: 10,
flavor: 'Pepperoni',
}),
})
In the following component, only the price
from pizzaContext
is relevant, but the change of pizzaContext.flavor
will still cause this component to re-render:
const MyComponent = () => {
// Bad
const pizza = usePizzaContext()
// `pizza.flavor` is not being used anywhere in this component.
// However, the change of `pizza.flavor` will still cause this
// component to re-render!
return <div>Pizza price: {pizza.price}</div>
}
To address this and optimize performance, we can utilize the optional selector
argument in useContext(selector)
. For example:
const MyComponent = () => {
// Good!
const pizzaPrice = usePizzaContext((state) => state.price)
// `pizza.flavor` doesn't affect this component anymore.
return <div>Pizza price: {pizza.price}</div>
}
Think of useContext(selector)
as useState()
with built-in awareness of when to update itself. The selector
function receives the current context value and expects a return value. If the price
in pizzaContext
changes, the pizzaPrice
here will update, leading to a re-render of this component.
Noted that selector
executes whenever there's a change in the context value. This means if the return value of selector
is a new non-primitve value (e.g., an object or array), the component will still re-render whenever there's a change in the context value, even if related values haven't changed. For example:
// Context
const { useContext: usePizzaContext } = createSliceContext({
state: () => ({
price: 10,
flavor: 'Pepperoni',
frozen: true,
}),
})
// Component
const MyComponent = () => {
// Bad
const priceAndFlavor = usePizzaContext((state) => ({
price: state.price,
flavor: state.flavor,
}))
// `pizza.frozen` is not being used anywhere in this component.
// However, the change of `pizza.frozen` will still cause this
// component to re-render!
return <div>...</div>
}
To mitigate this, separate priceAndFlavor
into two distinct usePizzaContext(selector)
calls:
// Context
const { useContext: usePizzaContext } = createSliceContext({
state: () => ({
price: 10,
flavor: 'Pepperoni',
frozen: true,
}),
})
// Component
const MyComponent = () => {
// Good!
const price = usePizzaContext((state) => state.price)
const flavor = usePizzaContext((state) => state.flavor)
// `pizza.frozen` doesn't affect this component anymore.
return <div>...</div>
}
This ensures that changes in pizzaContext.frozen
do not cause unnecessary re-renders.
Dispatch
The dispatch
object returned by createSliceContext(options)
is a set of dispatch functions. Only the functions declared in the dispatcher are permitted to modify the context state.
States Outside of Dispatch
Are Read-Only
The values returned by useContext(selector)
and getState()
are read-only. Attempting to update the context value without using the corresponding dispatch functions will trigger a warning in the console, and no changes will be applied to the context. Trying to execute code similar to the following example will result in a warning:
const MyComponent = () => {
const pizza = usePizzaContext()
const raisePrice = () => {
// Invalid: this will generate a warning in the console,
// and `pizza.price` will remain unchanged.
pizza.price += 5
}
return <div>...</div>
}
Asynchronous Dispatch
Asynchronous dispatch functions are supported in React Slice Context. If your dispatch function involves any asynchronous operations, such as calling an API, make sure to use the async
keyword to ensure that state are updated correctly within an asynchronous function. For example:
const context = createSliceContext({
// ...
dispatch: (state) => {
// The `async` here is necessary!
loadData: async () => {
state.loading = true
state.data = await callAPI()
state.loading = false
}
},
})
Plugins
A plugin serves as an optional extension to the slice context, enabling you to inject custom hooks into the context's lifecycle. The plugin interface encompasses the following hooks (all hooks are optional!):
| Name | Description |
| -------------------- | ----------------------------------------------------------------------------------------- |
| onStateInit(state)
| Called when the context state is initialized. The provided state
is read-only. |
| onChange(state)
| Called whenever there's a change in the context state. The provided state
is read-only. |
This feature is particularly useful when you need to persist the state somewhere upon a state change, such as in localStorage
or a database. For example:
const myContext = createSliceContext({
state: () => ({ ... }),
dispatch: () => ({ ... })
plugins: [
{
onChange: (state) => {
localStorage.setItem('SOME_KEY', JSON.stringify(state))
}
},
// ...other plugins
]
})
You can have multiple plugins within a slice context, and the hooks in these plugins are invoked in the order they are arranged within the plugins
array.
Update Context Value Outside of Components
To update context value outside of components, you can use the functions declared in the dispatcher, just as you would when updating the context value inside components. For example:
// Context
const { dispatch: authDispatch } = createSliceContext({
state: () => ({
token: undefined,
}),
dispatch: (auth) => {
setToken: (token: string) => {
auth.token = token
}
},
})
// In some other non-component files
import { authDispatch } from './auth-context'
authDispatch.setToken('...')
Get Context Value Outside of Components
To get context value outside of components, you can simply utilize the getState()
function provided by createSliceContext(options)
. For example:
// Context
const { getState: getAuthState } = createSliceContext({
state: () => ({
token: undefined,
}),
})
// In some other non-component files
import { getAuthState } from './auth-context'
axios.interceptors.request.use((request) => {
const { token } = getAuthState()
request.headers.Authorization = `Bearer ${token}`
})
It's important to note that the value returned by getState()
is read-only. As mentioned earlier, only functions declared in the dispatcher are permitted to modify the context state.
Common Mistakes
Please be aware that, due to the nature of JavaScript, primitive types won't behave as expected when used with destructuring assignment or when assigned to another variable. For example:
// Context
const { useContext: usePizzaContext } = createSliceContext({
state: () => ({
price: 10,
}),
})
// Component
const MyComponent = () => {
// Incorrect: `price` will not be reactive.
const { price } = usePizzaContext()
// Incorrect: `price` will not be reactive.
const { price } = usePizzaContext((state) => state)
// Incorrect: `price` will not be reactive.
const price = usePizzaContext().price
// Correct!
const price = usePizzaContext((state) => state.price)
return <div>...</div>
}