Kyle Shevlin

Software Engineer
May 05, 2022
0 strokes bestowed

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.


Finished reading?

Here are a few options for what to do next.

Like
Liked the post? Click the beard up to 50 times to show it
Share
Sharing this post on Twitter & elsewhere is a great way to help me out

Related Posts:
Renderless ComponentsHow to Use React Context Effectively
Tags
ReactJavaScript

Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a software engineer who specializes in JavaScript, React and front end web development.

Let's talk some more about JavaScript, React, and software engineering.

I write a newsletter to share my thoughts and the projects I'm working on. I would love for you to join the conversation. You can unsubscribe at any time.

Data Structures and Algorithms Logo
Data Structures and Algorithms

Check out my courses!

Liked the post? You might like my courses, too. Click the button to view this course or go to Courses for more information.
I would like give thanks to those who have contributed fixes and updates to this blog. If you see something that needs some love, you can join them. This blog is open sourced at https://github.com/kyleshevlin/blog
alexloudenjacobwsmithbryndymentJacobMGEvanseclectic-codingjhsukgcreativeerikvorhesHaroenvmarktnoonandependabotmarcuslyonsbrentmclarkfederico-fiorinimedayzDoNormalFanchGadjonoahmateenbrandonpittman
©2022 Kyle Shevlin. All Rights Reserved.