Global States With Context

Global States With Context

·

5 min read

React does not provide a global states solution. So normally we use state lift to share state between multiple components. Or we choose some global state management tool such as Redux, MobX. React provides a feature called context to share data globally. It is actually not fully designed for global states. In this article, let see how to build a global state manually with context, what's the problem of it and how to solve the problem.

Global states

Using context is pretty simple. It consists of 3 parts:

  1. Define a context
  2. Use a provider to provide value for the context
  3. Consume the value in the context
import { createContext, useContext } from "react";

// 1. Define a context
const ThemeContext = createContext("light");

// 3. Consume the value in the context
const Component = () => {
  const theme = useContext(ThemeContext);
  return <div>Hello {theme}</div>;
};

// 2. Use a provider to provide value for the context
const App = () => {
  return (
    <ThemeContext.Provider value="dark">
      <Component />
    </ThemeContext.Provider>
  );
};

There may be more than one providers and providers can be nested.

const App = () => {
  return (
    <ThemeContext.Provider value="dark">
      <ThemeContext.Provider value="blue">
        <Component />
      </ThemeContext.Provider>
      <Component />
    </ThemeContext.Provider>
  );
};

Using Context only won't solve the problem. The power emerges when we combine context with state.

Take a look the code below. We define a context, then provide the context with states values [theme, setTheme]. Then 2 components C1 and C2 consumes the context values. Only component C2 use the setTheme function but when it is called, both C1 and C2 are rerendered to get the updated theme state.

const ThemeContext = createContext();

const ThemeProvider = ({ children }) => {
  // provide context with state and setState values
  const [theme, setTheme] = useState("light");
  return (
    <ThemeContext.Provider value={[theme, setTheme]}>
      {children}
    </ThemeContext.Provider>
  );
};

const C1 = () => {
  const [theme, setTheme] = useContext(ThemeContext);
  return <div>Hello {theme}</div>;
};

const C2 = () => {
  const [theme, setTheme] = useContext(ThemeContext);
  return (
    <div>
      <p>Hello {theme}</p>
      <button
        onClick={() =>
          setTheme((theme) => (theme === "light" ? "dark" : "light"))
        }
      >toggleTheme</button>
    </div>
  );
};

const App = () => {
  return (
    <ThemeProvider>
      <>
        <C1 />
        <C2 />
      </>
    </ThemeProvider>
  );
};

That't it. That's how we create global states with context. But it's not done. Now let's see what is the problem.

Over rerender

Suppose we store a state userInfo { name: "yaox023", age: 10, } in a context. Then we have two component Name and Age. The Name component will only read/write state of name and the Age component will only read/write state of age. So normally when we change the state of name, we expect no rerender of the Age component.

But that is not the case. Actually, when the state in the context changes, all the components consume the state will be rerendered. That's what I called over rerender. Below code show the example.

const UserContext = createContext();

const UserProvider = ({ children }) => {
  const [userInfo, setUserInfo] = useState({
    name: "yaox023",
    age: 10,
  });
  return (
    <UserContext.Provider value={[userInfo, setUserInfo]}>
      {children}
    </UserContext.Provider>
  );
};

const Name = () => {
  console.log("render Name");
  const [userInfo, setUserInfo] = useContext(UserContext);
  return (
    <div>
      <p>I'm {userInfo.name}</p>
      <button
        onClick={() =>
          setUserInfo((userInfo) => {
            return {
              ...userInfo,
              name: userInfo.name === "yaox023" ? "nobody" : "yaox023",
            };
          })
        }
      >
        toggleName
      </button>
    </div>
  );
};

const Age = () => {
  console.log("render Age");
  const [userInfo, setUserInfo] = useContext(UserContext);
  return (
    <div>
      <p>I'm {userInfo.age}</p>
      <button
        onClick={() =>
          setUserInfo((userInfo) => {
            return {
              ...userInfo,
              age: userInfo.age === 10 ? 20 : 10,
            };
          })
        }
      >
        toggleName
      </button>
    </div>
  );
};

const Sex = () => {
  console.log("render Sex");
  return <div>I am male</div>;
};

const App = () => {
  return (
    <UserProvider>
      <>
        <Name />
        <Age />
        <Sex />
      </>
    </UserProvider>
  );
};

So we need a smarter global states. We need the one only triggers component rerender when it is related.

Multiple contexts

The key to prevent rerender is to split states into multiple contexts. Each context will only trigger its consumers. So when we have multiple contexts, over rerender will be gone.

const NameContext = createContext();
const AgeContext = createContext();

const NameProvider = ({ children }) => {
  return (
    <NameContext.Provider value={useState("yaox023")}>
      {children}
    </NameContext.Provider>
  );
};
const AgeProvider = ({ children }) => {
  return (
    <AgeContext.Provider value={useState(20)}>{children}</AgeContext.Provider>
  );
};

const Name = () => {
  console.log("render Name");
  const [name, setName] = useContext(NameContext);
  return (
    <div>
      <p>I'm {name}</p>
      <button
        onClick={() =>
          setName((name) => (name === "yaox023" ? "nobody" : "yaox023"))
        }
      >
        toggleName
      </button>
    </div>
  );
};
const Age = () => {
  console.log("render Age");
  const [age, setAge] = useContext(AgeContext);
  return (
    <div>
      <p>I'm {age}</p>
      <button onClick={() => setAge((age) => (age === 20 ? 10 : 20))}>
        toggleAge
      </button>
    </div>
  );
};

const App = () => {
  return (
    <NameProvider>
      <AgeProvider>
        <>
          <Name />
          <Age />
        </>
      </AgeProvider>
    </NameProvider>
  );
};

Now the rerender problem is solved. We can create contexts as many as want. But if we do this, take a look at the App component, it will be ugly for nested providers.

Nested Providers

This problem can be solved programmatically. We can put all providers into an array and use reduce function and React.createElement to create the component tree.

const providers = [NameProvider, AgeProvider];

const App = () => {
  return providers.reduceRight(
    (children, Component) => React.createElement(Component, null, children),
    <>
      <Name />
      <Age />
    </>
  );
};

That's it. Hope you enjoyed it. Thanks for reading.