Pure functions only perform a calculation and nothing more. It makes your code easier to understand, debug, and allows React to automatically optimize your components and Hooks correctly. 순수 함수는 계산만 수행하고 그 이상은 하지 않습니다. 순수 함수는 코드를 더 쉽게 이해할 수 있고, 디버깅하기 쉬우며, React가 컴포넌트와 Hook을 자동으로 올바르게 최적화할 수 있게 해줍니다.
- Why does purity matter? 왜 순수성이 중요한가요?
- Components and Hooks must be idempotent
- Side effects must run outside of render
- Props and state are immutable
- Return values and arguments to Hooks are immutable
- Values are immutable after being passed to JSX
Why does purity matter? 왜 순수성이 중요한가요?
One of the key concepts that makes React, React is purity. A pure component or hook is one that is: React를 만드는 핵심 개념 중 하나는 순수성 입니다. 순수한 컴포넌트 또는 Hook은 다음을 의미합니다:
-
Idempotent – You always get the same result every time you run it with the same inputs – props, state, context for component inputs; and arguments for hook inputs. 멱등성 - props, state, context, hook의 인수 등의 동일한 입력에 대해 항상 동일한 결과를 도출합니다.
-
Has no side effects in render – Code with side effects should run separately from rendering. For example as an event handler – where the user interacts with the UI and causes it to update; or as an Effect – which runs after render. 렌더링에 부작용이 없음 - 부작용이 있는 코드는 렌더링과 별도로 실행해야 합니다. 예를 들어, 사용자가 UI와 상호작용하여 UI가 업데이트되도록 하는 이벤트 핸들러 또는 렌더링 후에 실행되는 Effect와 같이 말이죠.
-
Does not mutate non-local values: Components and Hooks should never modify values that aren’t created locally in render. 비로컬 값을 변경하지 않습니다: 컴포넌트와 Hook은 렌더링에서 절대로 로컬로 생성되지 않은 값을 수정하지 않아야 합니다.
When render is kept pure, React can understand how to prioritize which updates are most important for the user to see first. This is made possible because of render purity: since components don’t have side effects in render, React can pause rendering components that aren’t as important to update, and only come back to them later when it’s needed. 렌더링이 순수하게 유지되면 React는 사용자에게 가장 중요한 업데이트의 우선순위를 정하는 방법을 이해할 수 있습니다. 이는 렌더링 순수성 덕분에 가능합니다. 컴포넌트에는 렌더링에 부작용이 없으므로, React는 업데이트가 중요하지 않은 컴포넌트의 렌더링을 일시 중지하고, 이후 필요할 때에 다시 돌아올 수 있습니다.
Concretely, this means that rendering logic can be run multiple times in a way that allows React to give your user a pleasant user experience. However, if your component has an untracked side effect – like modifying the value of a global variable during render – when React runs your rendering code again, your side effects will be triggered in a way that won’t match what you want. This often leads to unexpected bugs that can degrade how your users experience your app. You can see an example of this in the Keeping Components Pure page. 구체적으로, 이는 React가 사용자에게 쾌적한 사용자 경험을 제공할 수 있는 방식으로 렌더링 로직을 여러 번 실행할 수 있다는 것을 의미합니다. 그러나 컴포넌트에 추적되지 않은 부작용(예: 렌더링 중에 전역 변수 값 수정)이 있는 경우 React가 렌더링 코드를 다시 실행할 때 사용자가 원하는 것과 일치하지 않는 부작용이 발생될 수 있습니다. 이로 인해 사용자가 앱을 경험하는 방식이 저하될 수 있는 예기치 않은 버그가 발생하는 경우가 많습니다. 이에 대한 예시는 컴포넌트 순수성 유지 페이지에서 확인할 수 있습니다.
How does React run your code? React는 코드를 어떻게 실행하나요?
React is declarative: you tell React what to render, and React will figure out how best to display it to your user. To do this, React has a few phases where it runs your code. You don’t need to know about all of these phases to use React well. But at a high level, you should know about what code runs in render, and what runs outside of it. React는 선언적입니다. 사용자가 React에 무엇을 렌더링할지 알려주면 React가 사용자에게 가장 잘 표시할 수 있는 방법을 알아냅니다. 이를 위해 React에는 코드를 실행하는 몇 가지 단계가 있습니다. React를 잘 사용하기 위해 이 모든 단계를 알 필요는 없습니다. 하지만 높은 수준, 즉 render에서 실행되는 코드와 그 밖에서 실행되는 코드에 대해서는 알아야 합니다.
Rendering refers to calculating what the next version of your UI should look like. After rendering, Effects are flushed (meaning they are run until there are no more left) and may update the calculation if the Effects have impacts on layout. React takes this new calculation and compares it to the calculation used to create the previous version of your UI, then commits just the minimum changes needed to the DOM (what your user actually sees) to catch it up to the latest version. 렌더링은 UI의 다음 버전이 어떻게 보일지 계산하는 것을 말합니다. 렌더링 후, Effect는 flush(더 이상 남지 않을 때까지 실행됨을 의미)되며, 효과가 레이아웃에 영향을 미치는 경우 계산을 업데이트할 수 있습니다. React는 이 새로운 계산을 가져와서 이전 버전의 UI를 만드는 데 사용된 계산과 비교한 다음, DOM에 필요한 최소한의 변경 사항(사용자가 실제로 보는 것)만 커밋하여 최신 버전으로 따라잡습니다.
Deep Dive | 심층 탐구
One quick heuristic to tell if code runs during render is to examine where it is: if it’s written at the top level like in the example below, there’s a good chance it runs during render.
function Dropdown() {
const selectedItems = new Set(); // created during render
// ...
}
Event handlers and Effects don’t run in render:
function Dropdown() {
const selectedItems = new Set();
const onSelect = (item) => {
// this code is in an event handler, so it's only run when the user triggers this
selectedItems.add(item);
}
}
function Dropdown() {
const selectedItems = new Set();
useEffect(() => {
// this code is inside of an Effect, so it only runs after rendering
logForAnalytics(selectedItems);
}, [selectedItems]);
}
Components and Hooks must be idempotent
Components must always return the same output with respect to their inputs – props, state, and context. This is known as idempotency. Idempotency is a term popularized in functional programming. It refers to the idea that you always get the same result every time you run that piece of code with the same inputs.
This means that all code that runs during render must also be idempotent in order for this rule to hold. For example, this line of code is not idempotent (and therefore, neither is the component):
function Clock() {
const time = new Date(); // 🔴 Bad: always returns a different result!
return <span>{time.toLocaleString()}</span>
}
new Date()
is not idempotent as it always returns the current date and changes its result every time it’s called. When you render the above component, the time displayed on the screen will stay stuck on the time that the component was rendered. Similarly, functions like Math.random()
also aren’t idempotent, because they return different results every time they’re called, even when the inputs are the same.
This doesn’t mean you shouldn’t use non-idempotent functions like new Date()
at all – you should just avoid using them during render. In this case, we can synchronize the latest date to this component using an Effect:
import { useState, useEffect } from 'react'; function useTime() { // 1. Keep track of the current date's state. `useState` receives an initializer function as its // initial state. It only runs once when the hook is called, so only the current date at the // time the hook is called is set first. const [time, setTime] = useState(() => new Date()); useEffect(() => { // 2. Update the current date every second using `setInterval`. const id = setInterval(() => { setTime(new Date()); // ✅ Good: non-idempotent code no longer runs in render }, 1000); // 3. Return a cleanup function so we don't leak the `setInterval` timer. return () => clearInterval(id); }, []); return time; } export default function Clock() { const time = useTime(); return <span>{time.toLocaleString()}</span>; }
By wrapping the non-idempotent new Date()
call in an Effect, it moves that calculation outside of rendering.
If you don’t need to synchronize some external state with React, you can also consider using an event handler if it only needs to be updated in response to a user interaction.
Side effects must run outside of render
Side effects should not run in render, as React can render components multiple times to create the best possible user experience.
While render must be kept pure, side effects are necessary at some point in order for your app to do anything interesting, like showing something on the screen! The key point of this rule is that side effects should not run in render, as React can render components multiple times. In most cases, you’ll use event handlers to handle side effects. Using an event handler explicitly tells React that this code doesn’t need to run during render, keeping render pure. If you’ve exhausted all options – and only as a last resort – you can also handle side effects using useEffect
.
When is it okay to have mutation?
Local mutation
One common example of a side effect is mutation, which in JavaScript refers to changing the value of a non-primitive value. In general, while mutation is not idiomatic in React, local mutation is absolutely fine:
function FriendList({ friends }) {
const items = []; // ✅ Good: locally created
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // ✅ Good: local mutation is okay
}
return <section>{items}</section>;
}
There is no need to contort your code to avoid local mutation. Array.map
could also be used here for brevity, but there is nothing wrong with creating a local array and then pushing items into it during render.
Even though it looks like we are mutating items
, the key point to note is that this code only does so locally – the mutation isn’t “remembered” when the component is rendered again. In other words, items
only stays around as long as the component does. Because items
is always recreated every time <FriendList />
is rendered, the component will always return the same result.
On the other hand, if items
was created outside of the component, it holds on to its previous values and remembers changes:
const items = []; // 🔴 Bad: created outside of the component
function FriendList({ friends }) {
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // 🔴 Bad: mutates a value created outside of render
}
return <section>{items}</section>;
}
When <FriendList />
runs again, we will continue appending friends
to items
every time that component is run, leading to multiple duplicated results. This version of <FriendList />
has observable side effects during render and breaks the rule.
Lazy initialization
Lazy initialization is also fine despite not being fully “pure”:
function ExpenseForm() {
SuperCalculator.initializeIfNotReady(); // ✅ Good: if it doesn't affect other components
// Continue rendering...
}
Changing the DOM
Side effects that are directly visible to the user are not allowed in the render logic of React components. In other words, merely calling a component function shouldn’t by itself produce a change on the screen.
function ProductDetailPage({ product }) {
document.window.title = product.title; // 🔴 Bad: Changes the DOM
}
One way to achieve the desired result of updating window.title
outside of render is to synchronize the component with window
.
As long as calling a component multiple times is safe and doesn’t affect the rendering of other components, React doesn’t care if it’s 100% pure in the strict functional programming sense of the word. It is more important that components must be idempotent.
Props and state are immutable
A component’s props and state are immutable snapshots. Never mutate them directly. Instead, pass new props down, and use the setter function from useState
.
You can think of the props and state values as snapshots that are updated after rendering. For this reason, you don’t modify the props or state variables directly: instead you pass new props, or use the setter function provided to you to tell React that state needs to update the next time the component is rendered.
Don’t mutate Props
Props are immutable because if you mutate them, the application will produce inconsistent output, which can be hard to debug since it may or may not work depending on the circumstance.
function Post({ item }) {
item.url = new Url(item.url, base); // 🔴 Bad: never mutate props directly
return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
const url = new Url(item.url, base); // ✅ Good: make a copy instead
return <Link url={url}>{item.title}</Link>;
}
Don’t mutate State
useState
returns the state variable and a setter to update that state.
const [stateVariable, setter] = useState(0);
Rather than updating the state variable in-place, we need to update it using the setter function that is returned by useState
. Changing values on the state variable doesn’t cause the component to update, leaving your users with an outdated UI. Using the setter function informs React that the state has changed, and that we need to queue a re-render to update the UI.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
count = count + 1; // 🔴 Bad: never mutate state directly
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // ✅ Good: use the setter function returned by useState
}
return (
<button onClick={handleClick}>
You pressed me {count} times
</button>
);
}
Return values and arguments to Hooks are immutable
Once values are passed to a hook, you should not modify them. Like props in JSX, values become immutable when passed to a hook.
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
if (icon.enabled) {
icon.className = computeStyle(icon, theme); // 🔴 Bad: never mutate hook arguments directly
}
return icon;
}
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // ✅ Good: make a copy instead
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}
One important principle in React is local reasoning: the ability to understand what a component or hook does by looking at its code in isolation. Hooks should be treated like “black boxes” when they are called. For example, a custom hook might have used its arguments as dependencies to memoize values inside it:
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
return useMemo(() => {
const newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]);
}
If you were to mutate the Hooks arguments, the custom hook’s memoization will become incorrect, so it’s important to avoid doing that.
style = useIconStyle(icon); // `style` is memoized based on `icon`
icon.enabled = false; // Bad: 🔴 never mutate hook arguments directly
style = useIconStyle(icon); // previously memoized result is returned
style = useIconStyle(icon); // `style` is memoized based on `icon`
icon = { ...icon, enabled: false }; // Good: ✅ make a copy instead
style = useIconStyle(icon); // new value of `style` is calculated
Similarly, it’s important to not modify the return values of Hooks, as they may have been memoized.
Values are immutable after being passed to JSX
Don’t mutate values after they’ve been used in JSX. Move the mutation before the JSX is created.
When you use JSX in an expression, React may eagerly evaluate the JSX before the component finishes rendering. This means that mutating values after they’ve been passed to JSX can lead to outdated UIs, as React won’t know to update the component’s output.
function Page({ colour }) {
const styles = { colour, size: "large" };
const header = <Header styles={styles} />;
styles.size = "small"; // 🔴 Bad: styles was already used in the JSX above
const footer = <Footer styles={styles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}
function Page({ colour }) {
const headerStyles = { colour, size: "large" };
const header = <Header styles={headerStyles} />;
const footerStyles = { colour, size: "small" }; // ✅ Good: we created a new value
const footer = <Footer styles={footerStyles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}