Updating State with a Component
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:
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.