✓ useReducer
- https://react.dev/reference/react/useReducer
- https://ko.react.dev/reference/react/useReducer
- React Hook that lets you add a reducer to your component.
컴포넌트에 리듀서를 추가하기 위한 React Hook
import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialArg, init?);
- Call useReducer at the top level of your component to manage its state with a reducer.
리듀서로 state를 관리하기 위해 컴포넌트 top level에서 호출할 것
- Parameters
1) reducer: The reducer function that specifies how the state gets updated. It must be pure, should take the state and action as arguments, and should return the next state. State and action can be of any types.
state가 어떻게 업데이트될지 기술하는 리듀서 함수
반드시 state와 action을 파라미터로 가지면서, 다음 state를 반환하는 순수 함수여야 함
state와 action은 어떤 타입이던지 가능
2) initialArg: The value from which the initial state is calculated. It can be a value of any type. How the initial state is calculated from it depends on the next init argument.
초기 state를 계산할 때 사용하는 값, 어떤 타입이던지 가능
3) optional init: The initializer function that should return the initial state. If it’s not specified, the initial state is set to initialArg. Otherwise, the initial state is set to the result of calling init(initialArg).
초기 state를 반환하는 초기화 함수(옵션)
init이 없을 경우 두 번째 인자 initialArg가 초기 state가 되고, 있는 경우 init 함수를 호출한 결과가 초기 state로 세팅
- Return
1) The current state. During the first render, it’s set to init(initialArg) or initialArg (if there’s no init).
현재 state를 리턴. 첫 번째 렌더링인 경우 initialArg나 init 값
2) The dispatch function that lets you update the state to a different value and trigger a re-render.
state를 업데이트 하고 리렌더링을 트리거하는 디스패치 함수
- Caveats
1) useReducer is a Hook, so you can only call it at the top level of your component or your own Hooks. You can’t call it inside loops or conditions. If you need that, extract a new component and move the state into it.
2) In Strict Mode, React will call your reducer and initializer twice in order to help you find accidental impurities. This is development-only behavior and does not affect production. If your reducer and initializer are pure (as they should be), this should not affect your logic. The result from one of the calls is ignored.
- dispatch function
The dispatch function returned by useReducer lets you update the state to a different value and trigger a re-render. You need to pass the action as the only argument to the dispatch function:
useReducer가 반환하는 디스패치 함수는 state를 다른 값으로 업데이트하고, 리렌더링을 트리거 함
반드시 action을 함수의 파라미터로 전달해야 함
const [state, dispatch] = useReducer(reducer, { age: 42 });
function handleClick() {
dispatch({ type: 'incremented_age' });
// ...
React will set the next state to the result of calling the reducer function you’ve provided with the current state and the action you’ve passed to dispatch.
리액트는 현재 state와 디스패치된 action을 인자로 하는 리듀서 함수를 실행한 결과로 다음 state를 세팅
- Parameters
action: The action performed by the user. It can be a value of any type. By convention, an action is usually an object with a type property identifying it and, optionally, other properties with additional information.
사용자가 행하는 action. 어떤 타입의 값이라도 가능.
action은 주로 어떤것을 서술하는 타입 속성을 가진 객체(컨벤션), 옵셔널하게 부가적인 정보를 나타내는 속성을 담기도 함.
- Returns
dispatch functions do not have a return value.
- Caveats
1) The dispatch function only updates the state variable for the next render. If you read the state variable after calling the dispatch function, you will still get the old value that was on the screen before your call.
디스패치 함는 다음 렌더링을 위한 state variable만 업데이트 함
디스패치 함수를 호출하고 state를 바로 읽으면, 디스패치 함수를 호출하기 전 old state를 가져옴
2) If the new value you provide is identical to the current state, as determined by an Object.is comparison, React will skip re-rendering the component and its children. This is an optimization. React may still need to call your component before ignoring the result, but it shouldn’t affect your code.
Object.is로 얕은 비교, 객체의 2 depth까지 비교해서 state를 업데이트 하는게 아님(렌더링 스킵)
3) React batches state updates. It updates the screen after all the event handlers have run and have called their set functions. This prevents multiple re-renders during a single event. In the rare case that you need to force React to update the screen earlier, for example to access the DOM, you can use flushSync.
리액트는 state를 업데이트 하는 것을 배치 형태로 실행. # State as Snapshot
하나의 이벤트 동안 여러번 리렌더링되는 것을 막기 위해, 모든 이벤트 핸들러가 실행되고 set 함수까지 완료한 후에 화면을 업데이트함
반면, DOM에 접근하기 위해 fulshSync(즉시 DOM 업데이트)를 사용하는 경우 React는 화면을 그전에 업데이트하게 됨
- Usage
import { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
return {
name: state.name,
age: state.age + 1
};
}
case 'changed_name': {
return {
name: action.nextName,
age: state.age
};
}
}
throw Error('Unknown action: ' + action.type);
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, { age: 42 });
return (
<>
<button onClick={() => {
dispatch({ type: 'incremented_age' })
}}>
Increment age
</button>
<p>Hello! You are {state.age}.</p>
</>
);
}
React will pass the current state and the action to your reducer function. Your reducer will calculate and return the next state. React will store that next state, render your component with it, and update the UI.
리액트는 리듀서 함수에 현재 state와 action을 전달
-> 리듀서는 함수 실행 후 다음 state 반환
-> 리액트는 다음 state를 저장하고
-> 다음 state로 컴포넌트를 렌더링
-> 그리고 UI 업데이트
useReducer is very similar to useState, but it lets you move the state update logic from event handlers into a single function outside of your component. Read more about choosing between useState and useReducer.
useState랑 매우 유사하지만, state 업데이트 로직을 이벤트 핸들러에서 꺼내 컴포넌트 밖의 하나의 리듀서 함수 안에 기술.
The action type names are local to your component. Each action describes a single interaction, even if that leads to multiple changes in data. The shape of the state is arbitrary, but usually it’ll be an object or an array.
각 action은 그게 아무리 데이터의 여러 변화를 이끌어도, 하나의 행위만을 서술해야 함
- Pitfall
// State is read-only. Don’t modify any objects or arrays in state:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Don't mutate an object in state like this:
state.age = state.age + 1;
return state;
}
// Instead, always return new objects from your reducer:
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// ✅ Instead, return a new object
return {
...state,
age: state.age + 1
};
}
- Writing concise update logic with Immer
If updating arrays and objects without mutation feels tedious, you can use a library like Immer to reduce repetitive code. Immer lets you write concise code as if you were mutating objects, but under the hood it performs immutable updates:
mutation 없이 객체나 배열을 수정하는게 걸리고, 반복되는 코드를 줄이고 싶다면 Immer 라이브러리 사용
Immer 라이브러리를 사용하면 객체를 변경(mutation)하는 것 처럼 느낄지라도, 변경하지 않는(immutable) 방향으로 업데이트 됨
` Immer를 사용하지 않은 코드
import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false }
];
` Immer를 사용한 코드
import { useImmerReducer } from 'use-immer';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
function tasksReducer(draft, action) {
switch (action.type) {
case 'added': {
draft.push({
id: action.id,
text: action.text,
done: false
});
break;
}
case 'changed': {
const index = draft.findIndex(t =>
t.id === action.task.id
);
draft[index] = action.task;
break;
}
case 'deleted': {
return draft.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
export default function TaskApp() {
const [tasks, dispatch] = useImmerReducer(
tasksReducer,
initialTasks
);
function handleAddTask(text) {
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}
function handleChangeTask(task) {
dispatch({
type: 'changed',
task: task
});
}
function handleDeleteTask(taskId) {
dispatch({
type: 'deleted',
id: taskId
});
}
return (
<>
<h1>Prague itinerary</h1>
<AddTask
onAddTask={handleAddTask}
/>
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
</>
);
}
let nextId = 3;
const initialTasks = [
{ id: 0, text: 'Visit Kafka Museum', done: true },
{ id: 1, text: 'Watch a puppet show', done: false },
{ id: 2, text: 'Lennon Wall pic', done: false },
];
- Avoiding recreating the initial state
초기 state를 다시 생성하지 않는 법
React saves the initial state once and ignores it on the next renders.
리액트는 initial state를 한번 저장하고, 그 이후 렌더링부터는 무시함
` 초기화 함수를 전달해서 재생성을 막는 법
function createInitialState(username) {
// ...
}
function TodoList({ username }) {
// Although the result of createInitialState(username) is only used for the initial render,
// you’re still calling this function on every render.
// const [state, dispatch] = useReducer(reducer, createInitialState(username));
// To solve this, you may pass it as an initializer function to useReducer as the third argument instead:
// If your initializer doesn’t need any information to compute the initial state,
// you may pass null as the second argument to useReducer.
const [state, dispatch] = useReducer(reducer, username, createInitialState);
// ...
}
- Trouble shooting
1) I’ve dispatched an action, but logging gives me the old state value.
디스패치 함수 뒤에 호출한 state가 old한 값인 경우
function handleClick() {
console.log(state.age); // 42
dispatch({ type: 'incremented_age' }); // Request a re-render with 43
console.log(state.age); // Still 42!
setTimeout(() => {
console.log(state.age); // Also 42!
}, 5000);
}
This is because states behaves like a snapshot. Updating state requests another render with the new state value, but does not affect the state JavaScript variable in your already-running event handler.
If you need to guess the next state value, you can calculate it manually by calling the reducer yourself:
State는 스냅샷같이 행동하기 때문에 발생
State 업데이트는 새로운 state 값으로 또 다른 렌더링을 요청함
현재 자바스크립트 변수로서의 state는 이에 영향을 받지 않음, 왜냐하면 이벤트 핸들러가 이미 진행중이기 때문.
2) I’ve dispatched an action, but the screen doesn’t update
디스패치 함수를 호출했는데 화면이 업데이트 되지 않는 경우
React will ignore your update if the next state is equal to the previous state, as determined by an Object.is comparison. This usually happens when you change an object or an array in state directly:
리액트는 Object.is 비교 방법으로 이전 state와 다음 state를 비교해 state 간 같은 경우 렌더링을 스킵
이 경우는 주로 state 내부의 객체나 배열을 직접적으로 수정했기 때문에 발생
function reducer(state, action) {
switch (action.type) {
case 'incremented_age': {
// 🚩 Wrong: mutating existing object
state.age++;
return state;
}
case 'changed_name': {
// 🚩 Wrong: mutating existing object
state.name = action.nextName;
return state;
}
// ...
}
}
3) I’m getting an error: “Too many re-renders”
무한 루프 렌더링 에러
You might get an error that says: Too many re-renders. React limits the number of renders to prevent an infinite loop. Typically, this means that you’re unconditionally dispatching an action during render, so your component enters a loop: render, dispatch (which causes a render), render, dispatch (which causes a render), and so on. Very often, this is caused by a mistake in specifying an event handler:
렌더링 중에 무조건적으로 action을 디스패치 하는 경우 무한 렌더링
// 🚩 Wrong: calls the handler during render
return <button onClick={handleClick()}>Click me</button>
// ✅ Correct: passes down the event handler
return <button onClick={handleClick}>Click me</button>
// ✅ Correct: passes down an inline function
return <button onClick={(e) => handleClick(e)}>Click me</button>
✓ useImperativeHandle
- https://react.dev/reference/react/useImperativeHandle
- https://ko.react.dev/reference/react/useImperativeHandle
- React Hook that lets you customize the handle exposed as a ref.
ref를 사용하는 핸들러를 커스터마이징하기 위한 React Hook
- Call useImperativeHandle at the top level of your component to customize the ref handle it exposes:
import { useImperativeHandle } from 'react';
useImperativeHandle(ref, createHandle, dependencies?);
- Parameters
1) ref: The ref you received as the second argument from the forwardRef render function.
forwardRef 렌더링 함수로 부터 전달받은 두 번째 인자 ref
2) createHandle: A function that takes no arguments and returns the ref handle you want to expose. That ref handle can have any type. Usually, you will return an object with the methods you want to expose.
인자가 없고, ref 핸들러를 반환. 어떤 타입이든지 가능한데 주로 메서드를 포함한 객체를 반환
3) optional dependencies: The list of all reactive values referenced inside of the createHandle code. Reactive values include props, state, and all the variables and functions declared directly inside your component body. If your linter is configured for React, it will verify that every reactive value is correctly specified as a dependency. The list of dependencies must have a constant number of items and be written inline like [dep1, dep2, dep3]. React will compare each dependency with its previous value using the Object.is comparison. If a re-render resulted in a change to some dependency, or if you omitted this argument, your createHandle function will re-execute, and the newly created handle will be assigned to the ref.
옵셔널한 의존성 : createHandle에서 참조된 값 리스트.
Reactive한 값으로 컴포넌트 body에 선언된 props, state, 모든 변수나 함수를 포함
의존성은 반드시 상수와 배열 형태로 구성, 리액트는 각 의존성의 이전 값과 현재 값을 Object.is 방식으로 비교
의존성에 변화가 생기거나, 이 의존성을 작성하지 않으면 createHandle 함수는 매번 실행되고 ref에 새로운 createHandle 할당
- Returns
useImperativeHandle returns undefined.
undefined 리턴
- Usage
1) Exposing a custom ref handle to the parent component.
부모 컴포넌트에서 ref 핸들러 사용
` case 1
import { forwardRef } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
return <input {...props} ref={ref} />; // ref : <input> DOM node
});
` case 2
import { forwardRef, useRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => {
return {
focus() {
inputRef.current.focus();
},
scrollIntoView() {
inputRef.current.scrollIntoView();
},
};
}, []);
return <input {...props} ref={inputRef} />;
});
Note that in the code above, the ref is no longer forwarded to the <input>.
For example, suppose you don’t want to expose the entire <input> DOM node, but you want to expose two of its methods: focus and scrollIntoView. To do this, keep the real browser DOM in a separate ref. Then use useImperativeHandle to expose a handle with only the methods that you want the parent component to call:
Now, if the parent component gets a ref to MyInput, it will be able to call the focus and scrollIntoView methods on it. However, it will not have full access to the underlying <input> DOM node.
위 코드에서 ref는 더이상 input을 가리키지 않음
예를 들자면, 전체 input DOM 노드를 가리키는 것 대신 focus와 scrollIntoView 메서드만 보여주고 싶을 때 useImeperativeHandle을 사용
그러면 부모 컴포넌트에서 자식 컴포넌트의 ref를 받을 때, 전체의 DOM 노드에 대해 접근하는 것이 아니라 메서드만 접근
대신, useImperatieHandle 내부 별도의 ref에 진짜 브라우저 DOM을 저장
2) Exposing your own imperative methods
커스텀 imperative 메서드
The methods you expose via an imperative handle don’t have to match the DOM methods exactly. For example, this Post component exposes a scrollAndFocusAddComment method via an imperative handle. This lets the parent Page scroll the list of comments and focus the input field when you click the button:
DOM 메서드가 아닌 메서드를 사용하고 싶을 때. 예를 들면, 스크롤과 포커스를 동시에 하는 메서드