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-slice-context

v1.2.0

Published

A a lightweight, performant, proxy-based state management library for React without any external dependencies.

Downloads

23

Readme

React Slice Context 🍕

npm bundle size npm GitHub last commit (branch)

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

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

  1. Import the createSliceContext function from react-slice-context:

    import { createSliceContext } from 'react-slice-context'
  2. 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
          },
        }
      },
    })
  3. Export the useContext and dispatch 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({
      ...
    })
  4. Use the exported usePizzaContext (formerly useContext) and pizzaDispatch (formerly dispatch) 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

  1. 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 optional selector 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 like const 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>
}