Small RecipesFor Disaster

Snippets and personal opinions.

Take it with a pinch of salt.

This is to be considered in a "use client" or a single page application use case. I will talk about context in react server component another day. Also this article isn't updated regarding the changes brought by React 19. The principles and practices mentioned here remain largely unaffected. That said, certain technical specifics might require reconsideration under React 19, which I'll address at a later date.

Introduction

With all the advanced tools at our disposal for managing state in React, I often find myself returning to context for certain types of challenges. Let’s dive into the situations where context is not merely a fallback, but actually the best tool for the job.

Simplifying Component Communication

When creating library components that are agnostic of specific projects or business use cases—designed for broad reusability across different projects without tying down to external libraries—React Context shines. Consider components from libraries like Radix, which exemplify the asChild ref pattern, compound components approach, and remain both unstyled and opinion-free.

Example: Radix Popover API

import * as Popover from '@radix-ui/react-popover';

export default () => (
  <Popover.Root>
    <Popover.Trigger />
    <Popover.Anchor />
    <Popover.Portal>
      <Popover.Content>
        <Popover.Close />
        <Popover.Arrow />
      </Popover.Content>
    </Popover.Portal>
  </Popover.Root>
);

This setup showcases how the compound component approach divides the API into manageable sections, allowing users to engage with only the parts they need. For instance, if there’s no need to anchor the popover or display an arrow indicating origin, those components can be omitted. This flexibility lets users customize parts without a bloated API and avoids prop drilling.

The key to making this work lies in the Root component, which essentially acts as a context provider, ensuring all child components have access to shared data without manual prop threading. Context can also be stacked, meaning you can nest popovers with the nearest parent provider taking precedence, allowing for example the creation of multi-level dropdowns. Moreover, context abides by the component lifecycle, resetting automatically when the component unmounts—handy for clean state management upon navigation changes.

Using Portals with Context

A quick aside on structure flexibility—using Portal, children components can render outside their parent in the DOM tree but still retain access to the parent's context. This is particularly useful for UI elements like modals or tooltips that need to break out of their DOM hierarchy while still interacting with shared context data.

Maintaining Design and Tool Flexibility

A critical advantage of using context in your library components is the ability to remain agnostic about the design choices and tools used by developers who adopt your library. This flexibility is paramount for creating components that are versatile and adaptable across a wide range of projects and development environments. Let's see from Radix the context menu example which really show how really you keep the freedom to customize the component the way you want it:

const ContextMenuDemo = () => {
  const [bookmarksChecked, setBookmarksChecked] = React.useState(true);
  const [urlsChecked, setUrlsChecked] = React.useState(false);
  const [person, setPerson] = React.useState('pedro');

  return (
    <ContextMenu.Root>
      <ContextMenu.Trigger className="ContextMenuTrigger">Right-click here.</ContextMenu.Trigger>
      <ContextMenu.Portal>
        <ContextMenu.Content className="ContextMenuContent" sideOffset={5} align="end">
          <ContextMenu.Item className="ContextMenuItem">
            Back <div className="RightSlot">⌘+[</div>
          </ContextMenu.Item>
          <ContextMenu.Item className="ContextMenuItem" disabled>
            Forward <div className="RightSlot">⌘+]</div>
          </ContextMenu.Item>
          <ContextMenu.Item className="ContextMenuItem">
            Reload <div className="RightSlot">⌘+R</div>
          </ContextMenu.Item>
          <ContextMenu.Sub>
            <ContextMenu.SubTrigger className="ContextMenuSubTrigger">
              More Tools
              <div className="RightSlot">
                <ChevronRightIcon />
              </div>
            </ContextMenu.SubTrigger>
            <ContextMenu.Portal>
              <ContextMenu.SubContent
                className="ContextMenuSubContent"
                sideOffset={2}
                alignOffset={-5}
              >
                <ContextMenu.Item className="ContextMenuItem">
                  Save Page As… <div className="RightSlot">⌘+S</div>
                </ContextMenu.Item>
                <ContextMenu.Item className="ContextMenuItem">Create Shortcut…</ContextMenu.Item>
                <ContextMenu.Item className="ContextMenuItem">Name Window…</ContextMenu.Item>
                <ContextMenu.Separator className="ContextMenuSeparator" />
                <ContextMenu.Item className="ContextMenuItem">Developer Tools</ContextMenu.Item>
              </ContextMenu.SubContent>
            </ContextMenu.Portal>
          </ContextMenu.Sub>

          <ContextMenu.Separator className="ContextMenuSeparator" />

          <ContextMenu.CheckboxItem
            className="ContextMenuCheckboxItem"
            checked={bookmarksChecked}
            onCheckedChange={setBookmarksChecked}
          >
            <ContextMenu.ItemIndicator className="ContextMenuItemIndicator">
              <CheckIcon />
            </ContextMenu.ItemIndicator>
            Show Bookmarks <div className="RightSlot">⌘+B</div>
          </ContextMenu.CheckboxItem>
          <ContextMenu.CheckboxItem
            className="ContextMenuCheckboxItem"
            checked={urlsChecked}
            onCheckedChange={setUrlsChecked}
          >
            <ContextMenu.ItemIndicator className="ContextMenuItemIndicator">
              <CheckIcon />
            </ContextMenu.ItemIndicator>
            Show Full URLs
          </ContextMenu.CheckboxItem>

          <ContextMenu.Separator className="ContextMenuSeparator" />

          <ContextMenu.Label className="ContextMenuLabel">People</ContextMenu.Label>
          <ContextMenu.RadioGroup value={person} onValueChange={setPerson}>
            <ContextMenu.RadioItem className="ContextMenuRadioItem" value="pedro">
              <ContextMenu.ItemIndicator className="ContextMenuItemIndicator">
                <DotFilledIcon />
              </ContextMenu.ItemIndicator>
              Pedro Duarte
            </ContextMenu.RadioItem>
            <ContextMenu.RadioItem className="ContextMenuRadioItem" value="colm">
              <ContextMenu.ItemIndicator className="ContextMenuItemIndicator">
                <DotFilledIcon />
              </ContextMenu.ItemIndicator>
              Colm Tuite
            </ContextMenu.RadioItem>
          </ContextMenu.RadioGroup>
        </ContextMenu.Content>
      </ContextMenu.Portal>
    </ContextMenu.Root>
  );
};

By focusing on minimizing dependencies and not prescribing design or architectural choices, you ensure that your library remains a flexible and appealing choice for developers, regardless of the complexities or specifics of their projects. By using something stackable and using a compound approach, you ensure your solution provide enough composibility for satisfying complex requirements. Importantly, specific decisions related to state management or design styles are left to the developers, giving them complete control over the integration and behavior of the components within their projects.

Clear UI region dependency on an actor

Creating a cohesive user interface for a specific subject, such as a musical artist, can be challenging when the data is spread across multiple sections like biography, discography, and upcoming concerts. Splitting these into separate components keeps the interface organized but introduces complexities in managing shared data.

image

Rather than relying on mutable stores or specifying the same query options several times for a tool like react-query, using context can streamline the process. By encapsulating the artist data in a single context, each component — whether it's displaying biography details or concert dates—can access the needed information without redundant fetches or complex state management.

Using context in this scenario is particularly apt because it aligns with the logical structure of the information: all components relate to a single notion, the artist. This not only simplifies development by reducing the need for repeated data access logic but also ensures that the entire UI can react dynamically to changes in the artist's data.

Also, you can setup your context is a way to get rid of the uncertainty on your data. You can check at the top level of your tree that the data has been fetched/provided and then don't think about it anymore. Something akin to the useSuspenseQuery pattern but without the requirements of a queryKey.

Snippet for creating a strict context

So how do you get rid of uncertainties from your data when using your context? By simply asserting that your data isn't undefined when retrieving it from your context! By throwing early, you remind the developer that he needs to use the context within the related provider and you put the pressure of the uncertainty on the provider alone.

Here an example of an utility which does that for you:

import React from 'react'

interface CreateStrictContextOptions<T> {
  errorMessage?: string
  defaultValues?: Partial<T>
  name: string
}

export function createStrictContext<T>(options: CreateStrictContextOptions<T>) {
  const Context = React.createContext<T>(undefined!)

  Context.displayName = options.name

  function useContext(): T {
    const context = React.useContext(Context)
    if (!context) {
      throw new Error(
        `[${options.name}] ${
          options.errorMessage ?? 'Context Provider is missing.'
        }`,
      )
    }

    return { ...options.defaultValues, ...context }
  }

  return [Context.Provider, useContext] as const
}

// Would be used like so:
const [ArtistProvider, useArtistContext] = createStrictContext<Artist>({
  name: 'Artist',
})

Now, when you call useArtistContext you will be sure that you will get one. And you have to specify the way you get the artist only once at the provider level.

References

Here a few sources that led me to write this post:

Many thanks to them for writing them in the first place.