18.5.2022 |

React Composition - an Alternative to React Context

Since React 16.8 and the introduction of React hooks, many developers use React context to avoid props drilling.

What Is Prop Drilling?

React uses components to build UI elements. These components build a component tree. If a component in a deeply nested part of the tree needs data from higher up, we'll need to pass this data down as props. React components use props to communicate with each other.

prop drilling (image from the React docs)

Developers want to avoid prop drilling to minimize unnecessary re-renders of components.
If a component in a higher position in the tree changes its data and hands this data down as props, all intermediate components also re-render.

What About React Context?

React Context is a way to solve the problem of prop drilling. With context you can "teleport" your data to the components that need them, and pass intermediate children.

react context

Context is React's dependency injection mechanism.

Example Code:

const ThemeContext = React.createContext();

function ThemeProvider(props) {
  const [theme, setTheme] = React.useState("dark");
  const value = [theme, setTheme];
  return <ThemeContext.Provider value={value} {...props} />;
}

function ThemeDisplay() {
  const [theme] = React.useContext(ThemeContext);
  return <div>{`The current theme is ${theme}`}</div>;
}

function Theme() {
  const [, setTheme] = React.useContext(ThemeContext);
  const changeTheme = () => setTheme("light");
  return <button onClick={changeTheme}>Change theme to light</button>;
}

function App() {
  return (
    <div>
      <ThemeProvider>
        <ThemeDisplay />
        <Theme />
      </ThemeProvider>
    </div>
  );
}

The Case Against Context

Using React Context has become very popular, but even the official React documentation warns about overusing this pattern.

From Mark Erikson, Redux maintainer:

[...] all components that are subscribed to that context will be forced to re-render, even if they only care about part of the data. This may lead to performances issues, depending on the size of the state value, how many components are subscribed to that data, and how often they re-render.

React core team architect Sebastian Markbage explained in 2018:

My personal summary is that new context is ready to be used for low frequency unlikely updates (like locale/theme). It's also good to use it in the same way as old context was used. I.e. for static values and then propagate updates through subscriptions. It's not ready to be used as a replacement for all Flux-like state propagation.

React Composition

You can pass JSX as children and thus use composition to achieve a similar effect as using the Context API.

While the following example won't help with re-rendering all children components, it helps with re-using components. For example, the Dashboard Content component (which doesn't need the user) can be re-used elsewhere for a different Dashboard. It doesn't need to be wrapped in a context provider and doesn't have an implicit dependency.

Example:

export default function App() {
  const [user, setUser] = React.useState(null);

  return (
    <div>
      {user ? (
        <AuthenticatedApp>
          <Dashboard>
            <Logout onLogout={() => setUser(null)} />
            <DashboardNav />
            <DashboardContent>
              <UserInfo user={user} />
            </DashboardContent>
          </Dashboard>
        </AuthenticatedApp>
      ) : (
        <UnauthenticatedApp onLogin={() => setUser({ name: 'Bob' })} />
      )}
    </div>
  );
}

function Logout({ onLogout }) {
  return <button onClick={onLogout}>Logout</button>;
}

function AuthenticatedApp({ children }) {
  return (
    <React.Fragment>
      <h1>My app</h1>
      {children}
    </React.Fragment>
  );
}

function DashboardNav() {
  return (
    <React.Fragment>
      <h2>Dashboard Nav</h2>
    </React.Fragment>
  );
}

function Dashboard({ children }) {
  return (
    <React.Fragment>
      <h3>Dashboard</h3>
      {children}
    </React.Fragment>
  );
}

function DashboardContent({ children }) {
  return (
    <React.Fragment>
      <h3>Dashboard</h3>
      {children}
    </React.Fragment>
  );
}

function UserInfo({ user }) {
  return (
    <ul>
      <li>Username: {user.name} </li>
    </ul>
  );
}

function UnauthenticatedApp({ onLogin }) {
  return (
    <React.Fragment>
      <h1>UnauthenticatedApp</h1>
      <button onClick={onLogin}>Login</button>
    </React.Fragment>
  );
}

In comparison, the need for a context provider can lead to problems if you try to render your component outside of the provider.
Developers often solve this problem by wrapping the context provider globally around the application.
As stated above, this practice can lead to performance problems.

Using composition is a viable alternative to React context for avoiding prop drilling.

Links

Sophia
Zur Übersicht

Mehr vom DevSquad...

Tim Tilch

Angular Router Animations

Adrian Görisch

brew autoupdate