Global State in React

Global State in React

The plan here is to start with a component that has some state, then migrate to maintaining that state in a way that any component in our application can access.

Local State

Let's start by creating a login button that maintains its own state. The useState hook should do the trick. When the button is clicked, the "isLoggedIn" boolean value is toggled.

// LoginButton.jsx
import React, { useState } from 'react';

const LoginButton = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <button
      type="button"
      style={{
        padding: '1rem',
        minWidth: '150px',
        backgroundColor: isLoggedIn
          ? 'lightsteelblue'
          : 'limegreen',
        fontSize: '24px',
      }}
      onClick={() => setIsLoggedIn(!isLoggedIn)}
    >
      {isLoggedIn ? 'Logout' : 'Login'}
    </button>
  );
};

export default LoginButton;

Don't forget to import LoginButton into App.

// App.jsx
import React from 'react';
import LoginButton from './LoginButton';

const App = () => (
  <LoginButton />
);

export default App;

Passing Props

Our button works, but what if more than one component needs to know if the user is logged in? We'd better move our useState hook to the parent component, then pass the variable and function to the child components as needed.

// App.jsx
import React, { useState } from 'react';
import LoginButton from './LoginButton';
import SuperSecretComponent from './SuperSecretComponent';

const App = () => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <>
      <LoginButton 
        isLoggedIn={isLoggedIn} 
        setIsLoggedIn={setIsLoggedIn} 
      />
      <SuperSecretComponent isLoggedIn={isLoggedIn} />
    </>
  );
};

export default App;
// LoginButton.jsx
import React from 'react';
import PropTypes from 'prop-types';

const LoginButton = ({
  isLoggedIn,
  setIsLoggedIn,
}) => (
  <button
    type="button"
    style={{
      padding: '1rem',
      minWidth: '150px',
      backgroundColor: isLoggedIn
        ? 'lightsteelblue'
        : 'limegreen',
      fontSize: '24px',
    }}
    onClick={() => setIsLoggedIn(!isLoggedIn)}
  >
    {isLoggedIn ? 'Logout' : 'Login'}
  </button>
);

LoginButton.propTypes = {
  isLoggedIn: PropTypes.bool.isRequired,
  setIsLoggedIn: PropTypes.func.isRequired,
};

export default LoginButton;
// SuperSecretComponent.jsx
import React from 'react';
import PropTypes from 'prop-types';

const SuperSecretComponent = ({ isLoggedIn }) => (
  <div
    style={{
      marginTop: '10px',
      fontSize: '18px',
      fontWeight: 'bold',
    }}
  >
    {isLoggedIn ? 'Secret Data: Hamburger' : 'Access Denied'}
  </div>
);

SuperSecretComponent.propTypes = {
  isLoggedIn: PropTypes.bool.isRequired,
};

export default SuperSecretComponent;

App Context

That worked pretty well too, but what if we need to pass isLogged in to several components at different nesting levels? That's where Context comes in. Rather than passing isLoggedIn through many layers of components, we'll have an object that is accessible to any component as needed. I'm not going to create any more components, you'll have to use your imagination for that.

In AppContext.jsx we create:

  1. our context, called AppContext
  2. a hook, useAppContext, to retrieve context in our components
  3. and a context provider called AppContextProvider, which provides context to any component that is nested within it.
// AppContext.jsx
import React, {
  createContext,
  useContext,
  useState,
} from 'react';
import Proptypes from 'prop-types';

const AppContext = createContext();

export const useAppContext = () => {
  const context = useContext(AppContext);

  if (!context) {
    throw Error(
      'useAppContext must be used in AppContextProvider',
    );
  }

  return context;
};

export const AppContextProvider = ({ children }) => {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <AppContext.Provider value={{ isLoggedIn, setIsLoggedIn }}>
      {children}
    </AppContext.Provider>
  );
};

AppContextProvider.propTypes = {
  children: Proptypes.node.isRequired,
};

Wrap any components that need context with AppContextProvider.

// App.jsx
import React from 'react';
import LoginButton from './LoginButton';
import SuperSecretComponent from './SuperSecretComponent';
import { AppContextProvider } from './AppContext';

const App = () => (
  <AppContextProvider>
    <LoginButton />
    <SuperSecretComponent />
  </AppContextProvider>
);

export default App;

useAppContext() will provide our state and update function.

// LoginButton.jsx
import React from 'react';
import { useAppContext } from './AppContext';

const LoginButton = () => {
  const { isLoggedIn, setIsLoggedIn } = useAppContext();

  return (
    <button
      type="button"
      style={{
        padding: '1rem',
        minWidth: '150px',
        backgroundColor: isLoggedIn
          ? 'lightsteelblue'
          : 'limegreen',
        fontSize: '24px',
      }}
      onClick={() => setIsLoggedIn(!isLoggedIn)}
    >
      {isLoggedIn ? 'Logout' : 'Login'}
    </button>
  );
};

export default LoginButton;

Here we only need isLoggedIn.

// SuperSecretComponent.jsx
import React from 'react';
import { useAppContext } from './AppContext';

const SuperSecretComponent = () => {
  const { isLoggedIn } = useAppContext();

  return (
    <div
      style={{
        marginTop: '10px',
        fontSize: '18px',
        fontWeight: 'bold',
      }}
    >
      {isLoggedIn ? 'Secret Data: Hamburger' : 'Access Denied'}
    </div>
  );
};

export default SuperSecretComponent;

Conclusion

You now have a way to pass global state to any component.