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

v2.0.3

Published

Optimized React context

Downloads

165

Readme

react-atomic-context npm version

When we use React Context, we often face the problem that when the Provider's value changes (e.g., when a component below it modifies a property in the value), all components that use that context are re-rendered, even if they do not depend on the changed property value. This can lead to unnecessary re-renders and negatively impact the performance of our application.

For this issue, this library provides a simple solution.

You can use this library to individually read and write each property in React context without worrying about triggering a full re-render of all related components under the context.

Install:

npm i react-atomic-context

yarn add react-atomic-context

pnpm install react-atomic-context

Only 3kB size.

Example:

import React from 'react'
import { createAtomicContext, useAtomicContext } from 'react-atomic-context'

const AppContext = createAtomicContext({
  foo: 'foo',
  bar: 'bar',
})

const Foo = React.memo(function Foo() {
  const { foo, setFoo, setBar } = useAtomicContext(AppContext)
  console.log('foo rendered')
  return (
    <div>
      <p> this is foo: {foo} </p>
      <button
        onClick={() => {
          setFoo(`foo${Math.random().toString().slice(0, 5)}`)
        }}
      >
        change foo
      </button>
      <button
        onClick={() => {
          setBar(`bar${Math.random().toString().slice(0, 5)}`)
        }}
      >
        change bar
      </button>
      <hr />
      <Bar />
    </div>
  )
})

const Bar = React.memo(function Bar() {
  const { bar, setBar } = useAtomicContext(AppContext)
  console.log('bar rendered')
  return (
    <div>
      <p> this is bar: {bar} </p>
      <button
        onClick={() => {
          setBar(`bar${Math.random().toString().slice(0, 5)}`)
        }}
      >
        change bar
      </button>
    </div>
  )
})

export default function App() {
  const initState = React.useMemo(() => {
    return {
      foo: 'foo',
      bar: 'bar',
    }
  }, [])
  return (
    <AppContext.Provider
      value={initState}
      onChange={({ key, value, oldValue }) => {
        console.log(`${key} changed from ${oldValue} to ${value}`)
      }}
    >
      <Foo />
    </AppContext.Provider>
  )
}

Open in CodeSandbox

Overall, the usage of react-atomic-context is similar to regular React context. First, you create a context. Then, you use its Provider to wrap the components you want to render. Within the component, you can read and modify the context value using the provided use hook.

API:

  • createAtomicContext

    • This method is used to create a context, similar to React.createContext, but it requires an object as the initial value. eg:

      import { createAtomicContext } from 'react-atomic-context'
      // The initial value is required and can only be one object
      const AppContext = createAtomicContext({
        foo: 'foo',
        bar: 'bar',
      })
      export { AppContext }
    • The created context provides a Provider component, which wraps the components to be rendered. It must be provided with a prop named value, whose value is typically similar to the initial value and contains properties consistent with the initial value(can not contain any extra properties). Changing value of value prop won't take effect. eg:

      const App = () => {
        // Right way
        const initValue = React.useMemo(() => {
          return {
            foo: 'foooo',
            bar: 'barrr',
          }
        }, [])
        // Wrong! "initValue" can not change, the provider does not care about the overall change of the initial value
        const initValue = {
          foo: 'foooo',
          bar: 'barrr',
        }
        // Wrong! Because "baz" is not in AppContext initial value.
        const initValue = React.useMemo(() => {
          return {
            foo: 'foooo',
            bar: 'barrr',
            baz: 'bazzz', // baz is invalid here.
          }
        }, [])
        return (
          <AppContext.Provider value={initValue}>
            <YourComponent />
          </AppContext.Provider>
        )
      }
    • The Provider component also provides an additional onChange prop that accepts a function, which is called whenever any property changes. eg:

      const App = () => {
        const initValue = React.useMemo(() => {
          return {
            foo: 'foooo',
            bar: 'barrr',
          }
        }, [])
        const handleChange = React.useCallback(({ key, value, oldValue }, methods) => {
          console.log(`${key} changed from ${oldValue} to ${value}`)
          console.log('getters and setters', methods)
        }, [])
        return (
          <AppContext.Provider value={initValue} onChange={handleChange}>
            <YourComponent />
          </AppContext.Provider>
        )
      }
    • There is no "Consumer" component available. The lib only supports accessing the context through calling useAtomicContext.

  • useAtomicContext

    • This method is used to retrieve the current value from the context (provided by the nearest Provider's value prop). The value must be accessed using deconstructed syntax, for example: const { foo } = useAtomicContext(context). eg:

      import { useAtomicContext } from 'react-atomic-context'
      
      const MyComponent = () => {
        // Right way
        const { foo, bar } = useAtomicContext(AppContext)
        if (foo) {
          return <div>{foo}</div>
        }
        return <div>{bar}</div>
        // Wrong!
        const value = useAtomicContext(AppContext)
        if (value.foo) {
          return <div>{value.foo}</div>
        }
        return <div>{value.bar}</div>
      }
    • For each property, there are two additional methods provided. Such as, for the property foo, both getFoo and setFoo methods are provided. The setFoo method is the only way to update the value of the foo property, while getFoo is used solely to obtain the latest value of the foo property(in most case, just use foo instead of calling getFoo). eg:

      const MyComponent = () => {
        const { foo, setFoo, getFoo } = useAtomicContext(AppContext)
        return (
          <div
            onClick={() => {
              setFoo(newValue)
            }}
          >
            {foo}
          </div>
        )
      }
    • Accessor methods(getters and setters) are reference-stable and will not change, so you can confidently ignore them in dependency arrays. eg:

      const MyComponent = () => {
        const { bar, setFoo, getFoo } = useAtomicContext(AppContext)
        React.useEffect(() => {
          if (getFoo() !== bar) {
            setFoo(bar)
          }
        }, [bar]) // It is safe to omit getFoo and setFoo here.
        return <div>...</div>
      }
    • Specifically, this hook will return a method named get that returns the whole current value of the context (the returned value is read-only, like snapshot of state, for debugging purposes only). eg:

      const MyComponent = () => {
        const { get } = useAtomicContext(AppContext)
      
        return (
          <div
            onClick={() => {
              console.log('current value of context is', get())
            }}
          >
            inspect current value
          </div>
        )
      }

TypeScript support

import type {
  AtomicContextGettersType,
  AtomicContextSettersType,
  AtomicContextMethodsType,
  ProviderOnChangeType,
} from 'react-atomic-context'

const initValue = {
  foo: 'foo',
  bar: 123,
  baz: false,
}

const context = createAtomicContext(initValue)

/**
 * Getters = {
 *  getFoo: () => string
 *  getBar: () => number
 *  getBaz: () => boolean
 * }
 */
type Getters = AtomicContextGettersType<typeof initValue>

/**
 * Setters = {
 *  setFoo: (newValue: string) => void
 *  setBar: (newValue: number) => void
 *  setBaz: (newValue: boolean) => void
 * }
 */
type Setters = AtomicContextSettersType<typeof initValue>

/**
 * Setters = {
 *  setFoo: (newValue: string) => void
 *  setBaz: (newValue: boolean) => void
 * }
 */
type Setters = AtomicContextSettersType<typeof initValue, 'foo' | 'baz'>

/**
 * Methods = {
 *  setFoo: (newValue: string) => void
 *  setBar: (newValue: number) => void
 *  setBaz: (newValue: boolean) => void
 *  getFoo: () => string
 *  getBar: () => number
 *  getBaz: () => boolean
 * }
 */
type Methods = AtomicContextMethodsType<typeof initValue>

/**
 * type of "onChange" callback type passed to Provider.
 *
 * OnChange = (
 *  changeInfo:
 *   | { key: 'foo', value: string, oldValue: string }
 *   | { key: 'bar', value: number, oldValue: number }
 *   | { key: 'baz', value: boolean, oldValue: boolean }
 *  methods: {
 *    getFoo: () => string,
 *    setFoo: (v: string) => void,
 *    getBar: () => number,
 *    setBar: (v: number) => void,
 *    getBaz: () => boolean,
 *    setBaz: (v: boolean) => void,
 *    get: () => { foo: string; bar: number; baz: boolean }
 *  }
 * ) => void
 */
type OnChange = ProviderOnChangeType<typeof initValue>

Q&A

  1. Why provide getFoo when we can directly access the value of foo?

    In most cases, there is no need to use getFoo. Accessing foo directly through destructuring informs React that the current component's rendering depends on the value of foo. Thus, when foo changes, the current component will be re-rendered. However, there are situations where we only want to retrieve the value of foo without caring about its changes. In such cases, you should use the getFoo method.

  2. Why do I have to keep the Provider's value prop reference-stable?

    To ensure that the context of data changes has a stable single way, that is, by calling the setter method corresponding to the property. This also explains why the value passed to the provider cannot contain additional properties relative to the initial value when creating the context.

  3. Why must context data be accessed in a deconstructed manner?

    Virtually every access to a context property is preceded by a call to useContext, so the property access order must be kept steady and comply with react hook requirements. And it's always a good programming practice to declare the properties and methods to be accessed in a deconstructed way at the beginning, which is clearer and easier to maintain your component.

  4. Why my component still get rerendered?

    Important point but easy to ignore is that do not forget to use React.memo to wrap your component. By the way, There are a fews reasons leading to react component rerender. Such as calling setState, using normal react context, changing the key of the component, etc.

Thank you very much and hope to raise any questions.