DEV Community

Cover image for Manage global state with React hooks.
Charles Stover
Charles Stover

Posted on • Updated on • Originally published at charles-stover.Medium

Manage global state with React hooks.

Since the announcement of experimental Hooks in React 16.7, they have taken the React community by storm.

Unfortunately, the same way class components only manage local state, the built-in useState React hook only manages local state in functional components. Global state management is still left to higher-order components and community-contributed endeavors.

The reactn package, while also targeting class components, offers React hooks for accessing and managing global state in functional components. The ReactN package intends to integrate global state into React as if it were native functionality. In contrast to libraries like MobX and Redux, which are state-first solutions to state management, ReactN aims to be a React-first solution to global state management.

To read more about or contribute to the ReactN project, the GitHub repository is welcoming to the community. To install ReactN, use npm install reactn or yarn add reactn.

A useState Overview 🏁

Analogous to the built-in React hook useState, the useGlobal hook of ReactN behaves as similar as possible, with a few key differences. To clearly identify these differences, I’ll first provide useState’s behavior.

The useState function takes a default value and returns a 2-item array, where the first item is the state value and the second item is a function that updates that state value.

const [ value, setValue ] = useState(DEFAULT_VALUE);
Enter fullscreen mode Exit fullscreen mode
import { useState } from 'react';

const MyComponent = () => {
  const [ avatar, setAvatar ] = useState('anonymous.png');
  return (
    <img
      alt="Avatar"
      onClick={() => {
        const newAvatar = prompt("Enter your avatar URL:");
        setAvatar(newAvatar);
      }}
      src={avatar}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

In the above example, MyComponent renders an image anonymous.png (because that is the default value of the state). When you click the image, you are prompted for a new avatar URL. The functional component’s state updates with this new URL, and it re-renders (due to state change), displaying the image you entered instead.

This works great if you want to track the avatar only in this component. But what if you have multiple components that display the user’s avatar? Or multiple instances of this same component? Each instance of MyComponent will have its own instance of state, meaning each instance of MyComponent can have a different state. In many cases like these, developers opt for a global state instead — insuring that all components are in sync with each other. If one component updates the user’s avatar, then all other components displaying the user’s avatar need to update too.

Global State Differences 🆚

An important distinction when dealing with global state is how nonsensical it is to have a default value when instantiating the state. If every component that relied on the user’s avatar had to have a default value, then the value isn’t really global: The components would not be in sync with each other, because each one would have its own, different value. You can give them each the same default value, but at that point you are not using DRY code. Every time you want to change the default value, you have to go through the effort of changing it on every component. Not only is that a huge annoyance, but it opens you up for error when one of the components coincidentally is forgotten about during the change.

Because of this, global state is typically instantiated outside of the components that use it. If the global state is given a value up front, then the components don’t need to provide a default value in case one does not already exist — it does already exist.

Instantiating the Global State 🌞

With ReactN, you can instantiate the global state with the setGlobal helper function. Just provide the state object, and you’re done.

import { setGlobal } from 'reactn';

setGlobal({
  avatar: 'anonymous.png'
});
Enter fullscreen mode Exit fullscreen mode

It is recommended that this occur before ReactDOM.render, because you typically want the state to exist before any components attempt to mount.

Using the Global State 🌎

As mentioned, using the global state is meant to be as straightforward as using the local state. It is a React hook, prefixed with use, placed at the top of your functional component, that returns a 2-item array where the first item is the state value and the second item is a function that updates the state value. Since the default value is instantiated elsewhere, you do not pass the default value as a parameter to the global state hook; instead, it receives the property name of the global state that you want to access. The global state is an object of many different values that you may want to manage throughout your entire application, not a single value. In the instantiation example, we created an avatar property, so we will access it here.

import { useGlobal } from 'reactn';

const MyComponent = () => {
  const [ avatar, setAvatar ] = useGlobal('avatar');
  return (
    <img
      alt="Avatar"
      onClick={() => {
        const newAvatar = prompt("Enter your avatar URL:");
        setAvatar(newAvatar);
      }}
      src={avatar}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

That’s it. We changed useState to useGlobal and we replaced the default state value with the property we wanted to access. Whenever the global property avatar is updated by any component, all components using useGlobal('avatar') will re-render with the new value.

Can I access the entire global state? 👪

Yes! If you do not provide a property to useGlobal, it will return the entire global state for you to use as you please.

const MyComponent = () => {
  const [ global, setGlobal ] = useGlobal();
  return (
    <img
      alt="Avatar"
      onClick={() => {
        const newAvatar = prompt("Enter your avatar URL:");
        setGlobal({
          avatar: newAvatar
        });
      }}
      src={global.avatar}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

The same as when you provide a specific property, your component will only re-render if you access a property, not any time the global state is updated. This may be useful if you want to conditionally subscribe to certain properties. Your component will only re-render if it accesses global.property instead of every time global.property updates.

const MyComponent = () => {
  const [ global, setGlobal ] = useGlobal();
  if (global.x) {
    return global.x;
  }
  return global.y;
};
Enter fullscreen mode Exit fullscreen mode

In the above example, if global.x is truthy, your component will only re-render when the x property of the global state updates, not when the y property of the global state updates. This is because the y property of the global state does not impact the render of your component at all!

If the x property is falsey, your component will update whenever either x or y update. This is because both x and y changes will impact the render of your component.

The “magic” here is simply that your component re-renders when there is a change in a global state property that your component has accessed. Above, if x is truthy, the y property is never accessed. The component returns before ever getting the chance. If x is falsey, the y property is accessed.

If you were to useGlobal('x') and useGlobal('y'), you would be accessing both the x and y properties — even if you were to ignore y. As a result, your component would update when the unused y property is changed.

What about Reducers? 🤔

React 16.7 introduced a beautiful hook alongside useState known as useReducer. The useReducer hook allows you to pass a reducer function and initial state. It returns the state and a dispatch function. Unlike setState returned by useState, the dispatch function passes your arguments along to the reducer function.

Here is the reducer demonstrated by the React documentation:

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case 'reset':
      return initialState;
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
  }
}

function Counter() {
  const [ state, dispatch ] = useReducer(
    reducer,
    {count: initialCount}
  );
  const reset = () => dispatch({ type: 'reset' });
  const increment = () => dispatch({ type: 'increment' });
  const decrement = () => dispatch({ type: 'decrement' });
  return (
    <>
      Count: {state.count}
      <button onClick={reset}>Reset</button>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the above example, useReducer returns the state, which defaults to { count: 0 }, and a function that passes your parameters on to the reducer. The reducer takes the current state and your parameters to determine what the new state should be. Since actions such as { type: 'increment' } depend on the current state, the reducer returns the current state plus one.

ReactN uses useDispatch to handle reducers. The above example using global state would look like this:

import { useDispatch } from 'reactn';

setGlobal({ count: 0 });

function reducer(count, action) {
  switch (action.type) {
    case 'reset':
      return 0;
    case 'increment':
      return count + 1;
    case 'decrement':
      return count - 1;
  }
}

function Counter() {

  // Subscribe to changes to count, because our view depends on it.
  const [ count ] = useGlobal('count');

  // Dispatch changes to count using our reducer function.
  const dispatch = useDispatch(reducer, 'count');

  const reset = () => dispatch({ type: 'reset' });
  const increment = () => dispatch({ type: 'increment' });
  const decrement = () => dispatch({ type: 'decrement' });
  return (
    <>
      Count: {count}
      <button onClick={reset}>Reset</button>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Summary 📝

Similarities:

  • useGlobal and useDispatch are React hooks.
  • useGlobal returns a value and a function for changing that value.
  • useDispatch supports reducers.
  • useDispatch returns a function for changing the current state.

Differences:

  • useGlobal takes an property name instead of an initial value.
  • useGlobal can return the entire global state by not providing a parameter.
  • useDispatch takes a property name in addition to a reducer.
  • useDispatch does not (currently) return the value of the property, so as not to subscribe to it.
    • If your Component only updates the value, it does not need to re-render when that value changes. It may not display that value at all.

To install ReactN, use npm install reactn or yarn add reactn.

Conclusion 🔚

Community feedback and pull requests for improving the useGlobal and useDispatch React hooks, as well as the plethora of other global state features of the ReactN package, are appreciated on the GitHub repository.

If you liked this article, feel free to give it a heart or unicorn. It’s quick, it’s easy, and it’s free! If you have any questions or relevant great advice, please leave them in the comments below.

To read more of my columns, you may follow me on LinkedIn, Medium, and Twitter, or check out my portfolio on CharlesStover.com.

Top comments (1)

Collapse
 
lexiebkm profile image
Alexander B.K.

I have tried your package this morning. For simple global state in my app, it worked. It means, that, at least now, I don't need to learn Redux which will have take long time to get started. Only when I have time, I will probably learn Redux.
I see in the GitHub there are 11 contributors to this package so that I can feel secure to use it. Besides, there are some other people who has written articles on this package, mentioning positive opinions, and even recommend it as an alternative to Redux, etc.
Thank you for this package, as its simplicity makes me feel dealing with global state is as easy/straightforward as I found when I coded in desktop non-web using Visual Basic 6/VB.Net :):)