Elements are the fundamentals building blocks of React UI. It was the first we started with when you started learning React in this workshop series. And React has some smarts under the hood we can take advantage of when we’re rendering elements.
What we’re going to be learning about in this exercise can be summed up as:
If you give React the same element you gave it on the last render, it wont bother re-rendering that element. – @kentcdodds
Here’s a simple example:
function Message({ greeting }) {console.log('rendering greeting', greeting)return <div>{greeting}</div>}function Counter() {const [count, setCount] = useState(0)const increment = () => setCount((c) => c + 1)return (<div><button onClick={increment}>The count is {count}</button><Message greeting="Hello!" /></div>)}
With this set up, we’ll get a log every time the counter is incremented. Meaning
the <Message />
component is rerendered every time its parent rerenders
(which is to be expected). But this is unnecessary since the greeting component
won’t ever change what it’s rendering.
What if I refactored things a little bit. For example:
function Message({ greeting }) {console.log('rendering greeting', greeting)return <div>{greeting}</div>}const message = <Message greeting="Hello!" />function Counter() {const [count, setCount] = useState(0)const increment = () => setCount((c) => c + 1)return (<div><button onClick={increment}>The count is {count}</button>{message}</div>)}
In this situation, the <Message />
component only renders once and won’t
rerender whenever the count changes. This is because we’re giving React the same
element every time. How does this work?
When React is given the same element it was given on the last render, it won’t bother rerendering that element again because that would be pointless. It just keeps the same element and moves on.
This is a simple, but powerful optimization that can help you avoid unnecessary rerenders in your application. It’s not always possible to do this, but you’d be surprised how simply restructuring your components can make this possible more and more often.
The optimization we’re going to be learning about in this exercise is explained with examples in One simple trick to optimize React re-renders. Feel free to check that out for a more in depth explanation for how this works.
👨💼 We’re going to be building a simple user interface and in the process we want to limit unnecessary renders as much as possible (not because it’s necessary for this specific application, but because it’s a useful exercise for you to learn how to do for when it is necessary).
Right now, when you click the counter button, both the App
and Footer
components rerender.
But the Footer
component doesn’t change so that render is unnecessary. So
please take advantage of React’s element optimization by creating a Footer
element outside the App
so you can render that inline in the App
component.
Make sure to check the before/after using the React DevTools so you know that your work is making a difference.
🧝♂️ I’ve enhanced our app a bit to add some buttons that will control the color
of our footer. This means we can’t just render the component outside the App
component since we need to pass the color
as a prop. Feel free
to
👨💼 Yep, to make it work, Kellie had to render the <Footer />
in <Main />
which means the <Footer />
gets rerendered every time we click the count
button even if the color doesn’t change.
So your job is to restructure things so the Footer
only re-renders when the
color
is changed.
Make sure to pull up the React DevTools to be certain incrementing the count doesn’t trigger a rerender of the footer.
👨💼 We now have a counter inside the App
component and every time that count is
incremented, we trigger a rerender of the Footer
component! Have we lost the
ability to take advantage of React’s element optimization? No!
Instead of accepting the color
via props, we can place it in a context provider
that the Footer
can consume. And with that, the footer
no longer accepts any
dynamic props so we can move it outside the App
component again, which means
the only way it can be rerendered is if the context changes (which is exactly
what we want).
When you’re done, make certain that the Footer
only rerenders when the color
changes and not when the counters are incremented.
👨💼 Now we want our users to be able to type their name and have that show up in
the footer. But we don’t want to use context for the name like we do with the
color just yet. We just want to be able to pass the name
as a regular prop.
But we still want to make sure that the Footer
only rerenders when it should
and no more. So you’re going to need to combine useMemo
with this React
element optimization.
Remember that useMemo
allows you to get the same value back if the
dependencies haven’t changed. Well, you can create a React element that depends
on the name
and render that:
const someElement = useMemo(() => <MyComponent myProp={myProp} />, [myProp])
Give that a shot! (Double-check your work with the React DevTools).
🦉 Memoizing elements is such a common idea that React has a built-in optimizer
called memo
which allows you to memoize an entire component based on its
props:
import { memo } from 'react'const MyComponent = memo(function MyComponent(props) {/* only rerenders if props change */})
This effectively turns the props
object into a dependency array a la useMemo
and applys wherever the component is rendered.
🧝♂️
Footer
out of useMemo
👨💼 You’re going to use memo
to get the Footer
back into a position where
it only renders when the name
or color
change. Good luck!
import { createContext, memo, use, useState } from 'react'import * as ReactDOM from 'react-dom/client'const ColorContext = createContext<string | null>(null)function useColor() {const color = use(ColorContext)if (!color) throw new Error('ColorContext not found')return color}const Footer = memo(function FooterImpl({ name }: { name: string }) {const color = useColor()return (<footer style={{ color }}>I am the ({color}) footer, {name || 'Unnamed'}</footer>)})function Main({ footer }: { footer: React.ReactNode }) {const [count, setCount] = useState(0)const increment = () => setCount((c) => c + 1)return (<div><button onClick={increment}>The count is {count}</button>{footer}</div>)}function App() {const [appCount, setAppCount] = useState(0)const [color, setColor] = useState('black')const [name, setName] = useState('Kody')return (<ColorContext value={color}><div><div><p>Set the footer color:</p><div style={{ display: 'flex', gap: 4 }}><button onClick={() => setColor('black')}>Black</button><button onClick={() => setColor('blue')}>Blue</button><button onClick={() => setColor('green')}>Green</button></div></div><div><p>Set the footer name:</p><label>Name:<inputvalue={name}onChange={(e) => setName(e.currentTarget.value)}/></label></div><button onClick={() => setAppCount((c) => c + 1)}>The app count is {appCount}</button><Main footer={<Footer name={name} />} /></div></ColorContext>)}const rootEl = document.createElement('div')document.body.append(rootEl)ReactDOM.createRoot(rootEl).render(<App />)
Quick Links
Legal Stuff
Social Media