Keeping Components Pure
Some JavaScript functions are pure. Pure functions only perform a calculation and nothing more. By strictly only writing your components as pure functions, you can avoid an entire class of baffling bugs and unpredictable behavior as your codebase grows. To get these benefits, though, there are a few rules you must follow.
AprenderĂĄs
- What purity is and how it helps you avoid bugs
- How to keep components pure by keeping changes out of the render phase
- How to use Strict Mode to find mistakes in your components
Purity: Components as formulas
In computer science (and especially the world of functional programming), a pure function is a function with the following characteristics:
- It minds its own business. It does not change any objects or variables that existed before it was called.
- Same inputs, same output. Given the same inputs, a pure function should always return the same result.
You might already be familiar with one example of pure functions: formulas in math.
Consider this math formula: y = 2x.
If x = 2 then y = 4. Always.
If x = 3 then y = 6. Always.
If x = 3, y wonât sometimes be 9 or â1 or 2.5 depending on the time of day or the state of the stock market.
If y = 2x and x = 3, y will always be 6.
If we made this into a JavaScript function, it would look like this:
function double(number) {
return 2 * number;
}
In the above example, double
is a pure function. If you pass it 3
, it will return 6
. Always.
React is designed around this concept. React assumes that every component you write is a pure function. This means that React components you write must always return the same JSX given the same inputs:
function Recipe({ drinkers }) { return ( <ol> <li>Boil {drinkers} cups of milk.</li> <li>Add {2 * drinkers} spoons of masala spices.</li> <li>Remove from heat, and add {drinkers} spoons of tea.</li> </ol> ); } export default function App() { return ( <section> <h1>Spiced Chai Recipe</h1> <h2>For one</h2> <Recipe drinkers={1} /> <h2>For a gathering</h2> <Recipe drinkers={4} /> </section> ); }
When you pass drinkers={1}
to Recipe
, it will return JSX containing 1 cups of milk
. Always.
If you pass drinkers={4}
, it will return JSX containing 4 cups of milk
. Always.
Just like a math formula.
You could think of your components as recipes: if you follow them and donât introduce new ingredients during the cooking process, you will get the same dish every time. That âdishâ is the JSX that the component serves to React to render.
Side Effects: (un)intended consequences
Reactâs rendering process must always be pure. Components should only return their JSX, and not change any objects or variables that existed before renderingâthat would make them impure!
Here is a component that breaks this rule:
let guest = 0; function Cup() { // Bad: changing a preexisting variable! guest = guest + 1; return <h2>Tea cup for guest #{guest}</h2>; } export default function TeaSet() { return ( <> <Cup /> <Cup /> <Cup /> </> ) }
This component is reading and writing a guest
variable declared outside of it. This means that calling this component multiple times will produce different JSX! And whatâs more, if other components read guest
, they will produce different JSX, too, depending on when they were rendered! Thatâs not predictable.
Going back to our formula y = 2x, now even if x = 2, we cannot trust that y = 4. Our tests could fail, our users would be baffled, planes would fall out of the skyâyou can see how this would lead to confusing bugs!
You can fix this component by passing guest
as a prop instead:
function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>; } export default function TeaSet() { return ( <> <Cup guest={1} /> <Cup guest={2} /> <Cup guest={3} /> </> ); }
Now your component is pure, as the JSX it returns only depends on the guest
prop.
In general, you should not expect your components to be rendered in any particular order. It doesnât matter if you call y = 2x before or after y = 5x: both formulas will resolve independently of each other. In the same way, each component should only âthink for itselfâ, and not attempt to coordinate with or depend upon others during rendering. Rendering is like a school exam: each component should calculate JSX on their own!
Deep Dive
Detecting impure calculations with StrictMode
Detecting impure calculations with StrictMode
Although you might not have used them all yet, in React there are three kinds of inputs that you can read while rendering: props, state, and context. You should always treat these inputs as read-only.
When you want to change something in response to user input, you should set state instead of writing to a variable. You should never change preexisting variables or objects while your component is rendering.
React offers a âStrict Modeâ in which it calls each componentâs function twice during development. By calling the component functions twice, Strict Mode helps find components that break these rules.
Notice how the original example displayed âGuest #2â, âGuest #4â, and âGuest #6â instead of âGuest #1â, âGuest #2â, and âGuest #3â. The original function was impure, so calling it twice broke it. But the fixed pure version works even if the function is called twice every time. Pure functions only calculate, so calling them twice wonât change anythingâjust like calling double(2)
twice doesnât change whatâs returned, and solving y = 2x twice doesnât change what y is. Same inputs, same outputs. Always.
Strict Mode has no effect in production, so it wonât slow down the app for your users. To opt into Strict Mode, you can wrap your root component into <React.StrictMode>
. Some frameworks do this by default.
Local mutation: Your componentâs little secret
In the above example, the problem was that the component changed a preexisting variable while rendering. This is often called a âmutationâ to make it sound a bit scarier. Pure functions donât mutate variables outside of the functionâs scope or objects that were created before the callâthat makes them impure!
However, itâs completely fine to change variables and objects that youâve just created while rendering. In this example, you create an []
array, assign it to a cups
variable, and then push
a dozen cups into it:
function Cup({ guest }) { return <h2>Tea cup for guest #{guest}</h2>; } export default function TeaGathering() { let cups = []; for (let i = 1; i <= 12; i++) { cups.push(<Cup key={i} guest={i} />); } return cups; }
If the cups
variable or the []
array were created outside the TeaGathering
function, this would be a huge problem! You would be changing a preexisting object by pushing items into that array.
However, itâs fine because youâve created them during the same render, inside TeaGathering
. No code outside of TeaGathering
will ever know that this happened. This is called âlocal mutationââitâs like your componentâs little secret.
Where you can cause side effects
While functional programming relies heavily on purity, at some point, somewhere, something has to change. Thatâs kind of the point of programming! These changesâupdating the screen, starting an animation, changing the dataâare called side effects. Theyâre things that happen âon the sideâ, not during rendering.
In React, side effects usually belong inside event handlers. Event handlers are functions that React runs when you perform some actionâfor example, when you click a button. Even though event handlers are defined inside your component, they donât run during rendering! So event handlers donât need to be pure.
If youâve exhausted all other options and canât find the right event handler for your side effect, you can still attach it to your returned JSX with a useEffect
call in your component. This tells React to execute it later, after rendering, when side effects are allowed. However, this approach should be your last resort.
When possible, try to express your logic with rendering alone. Youâll be surprised how far this can take you!
Deep Dive
Why does React care about purity?
Why does React care about purity?
Writing pure functions takes some habit and discipline. But it also unlocks marvelous opportunities:
- Your components could run in a different environmentâfor example, on the server! Since they return the same result for the same inputs, one component can serve many user requests.
- You can improve performance by skipping rendering components whose inputs have not changed. This is safe because pure functions always return the same results, so they are safe to cache.
- If some data changes in the middle of rendering a deep component tree, React can restart rendering without wasting time to finish the outdated render. Purity makes it safe to stop calculating at any time.
Every new React feature weâre building takes advantage of purity. From data fetching to animations to performance, keeping components pure unlocks the power of the React paradigm.
RecapitulaciĂłn
- A component must be pure, meaning:
- It minds its own business. It should not change any objects or variables that existed before rendering.
- Same inputs, same output. Given the same inputs, a component should always return the same JSX.
- Rendering can happen at any time, so components should not depend on each othersâ rendering sequence.
- You should not mutate any of the inputs that your components use for rendering. That includes props, state, and context. To update the screen, âsetâ state instead of mutating preexisting objects.
- Strive to express your componentâs logic in the JSX you return. When you need to âchange thingsâ, youâll usually want to do it in an event handler. As a last resort, you can
useEffect
. - Writing pure functions takes a bit of practice, but it unlocks the power of Reactâs paradigm.
DesafĂo 1 de 3: Fix a broken clock
This component tries to set the <h1>
âs CSS class to "night"
during the time from midnight to six hours in the morning, and "day"
at all other times. However, it doesnât work. Can you fix this component?
You can verify whether your solution works by temporarily changing the computerâs timezone. When the current time is between midnight and six in the morning, the clock should have inverted colors!
export default function Clock({ time }) { let hours = time.getHours(); if (hours >= 0 && hours <= 6) { document.getElementById('time').className = 'night'; } else { document.getElementById('time').className = 'day'; } return ( <h1 id="time"> {time.toLocaleTimeString()} </h1> ); }