May 05, 2022

Updating State with a Component

Not with a State Updater Function
edit

Have you ever wanted to create a component like Head from next/head or Helmet from react-helmet that would update some state by using the children of a component? It’s simpler than you might think.

A while back, I was working with a design that had an h1 heading in the middle of the header. Like this:

Logo
H1 Content here
Small Nav
Page Content

It’s a bit of an odd design, but it’s what they wanted. The challenge of the design is that the heading element has to live within the <header> of the app, but the component code we’ll be writing is for the area labeled Page Content. There’s a literal border (pun intended) between the content we’re creating and the place we need to update the heading.

Now, one way to solve this is to use a Header component with a prop for the heading, or potentially with children so you could compose Header with the correct page title. Perhaps like this:

function Header({ children }) {
  return (
    <header
      style={{
        display: 'flex',
        justifyContent: 'space-between',
        // In order to center the heading regardless of other items in the
        // header, we'll need to absolutely position it
        position: 'relative',
      }}
    >
      <Logo />
      <h1
        style={{
          position: 'absolute',
          left: '50%',
          transform: 'translateX(-50%)',
        }}
      >
        {children}
      </h1>
      <Nav />
    </header>
  )
}

// Then used like so:
function SomePage({ children }) {
  return (
    <div>
      <Header>This page's title</Header>
      <main>{children}</main>
    </div>
  )
}

The downside of this approach is that we would have to use Header in every single page in the application. That’s not ideal. It would be preferable to have Header in a layout component higher up in the component hierarchy, used only a few times. It would be pretty easy to forget some important piece of the app layout on a page this way.

While this might be the most common approach to solving the problem, there are alternatives. The next approach to try would be to use React Context and a custom hook. I cover this in my article How to Use React Context Effectively, so check it out when you have a chance.

In this approach, we’d setup a context and export a hook we can use to update that context. Let’s do that briefly, since it will be useful for the next part of this post as well:

// Create the context
const HeadingContext = React.createContext()

// Create our provider
function HeadingProvider({ children }) {
  const [heading, setHeading] = React.useState('')

  return (
    <HeadingContext.Provider value={{ heading, setHeading }}>
      {children}
    </HeadingContext.Provider>
  )
}

// Custom hook for consuming the context
const useHeadingContext = () => React.useContext(HeadingContext)

// Hook specifically for updating the heading
// Name provides a little more context where it gets used
const useUpdateHeading = value => {
  const { setHeading } = useHeadingContext()
  setHeading(value)
}

// Now our header consumes the `heading` value (let's assume that
// the `HeadingProvider` is higher in the tree)
function Header() {
  const { heading } = useHeadingContext()

  return (
    <header>
      <Logo />
      <h1>{heading}</h1>
      <Nav />
    </header>
  )
}

// And then we would update that heading like so
function SomePage({ children }) {
  useUpdateHeading("This page's title")

  return (
    <div>
      <main>{children}</main>
    </div>
  )
}

This is reasonable. I certainly wouldn’t stop anyone from doing this approach, but it feels off to me personally. I experience a bit of a disconnect here. This hook feels like an unrelated function, used arbitrarily and doesn’t connect to the rest of the code in the component in a cohesive way. Admittedly, how I instinctively respond to some code is not a strong argument for anything, so as I said before, I wouldn’t stop my team from doing this approach, but is there an alternative?

There is. We can use that same context and a component to create a composable state updater. All we need to do is make use of children. Here’s one way of doing that.

const ACCEPTABLE_TYPES = ['string', 'number']

function Heading({ children }) {
  const { setHeading } = useHeadingContext()

  React.useEffect(() => {
    if (ACCEPTABLE_TYPES.includes(typeof children)) {
      setHeading(children)
    }
  }, [children, setHeading])

  return null
}

// Now we can use that component _anywhere_ to update the heading
function SomePage({ children }) {
  return (
    <main>
      <Heading>This page's title</Heading>
      {children}
    </main>
  )
}

Now, we use a renderless component and an effect to keep the page title synced up. We’ve also created a component that conveys some semantics to someone reading the code. In the case of the design, Heading really is the heading for the page, it’s just at a distance to us in the DOM. Using this semantic component let’s our code feel cohesive without negatively impacting the actual layout of the app.

You can experiment with other ways to utilize children to achieve this effect. Just be sure to sanitize children in some way. You may want to look into the React.Children API if you do. Otherwise, enjoy adding this little trick to your coding repertoire.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Need help with your software problems?

My team and I are ready to help you. Hire Agathist to build your next great project or to improve one of your existing ones.

Get in touch
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Agathist
Good software by good people.
Visit https://agath.ist to learn more
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.
Logo for Array.reduce()
Array.reduce()
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.