Manage State - Extracting State Logic into Reducer

Yoseob Shin·2023년 3월 27일
0

react

목록 보기
5/6
post-thumbnail

Extracting State Logic into Reducer

Components with many state updates spread across many event handlers can get overwhelming. For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer.

Consolidate state logic with a reducer

the TaskApp component below holds an array of tasks in state and uses three different event handlers to add, remove, and edit tasks:


import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, setTasks] = useState(initialTasks);

  function handleAddTask(text) {
    setTasks([
      ...tasks,
      {
        id: nextId++,
        text: text,
        done: false,
      },
    ]);
  }

  function handleChangeTask(task) {
    setTasks(
      tasks.map((t) => {
        if (t.id === task.id) {
          return task;
        } else {
          return t;
        }
      })
    );
  }

  function handleDeleteTask(taskId) {
    setTasks(tasks.filter((t) => t.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},
];

To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a “reducer”.

Reducers are a different way to handle state. You can migrate from useState to useReducer in three steps:
1. Move from setting state to dispatching actions.
2. Write a reducer function.
3. Use the reducer from your component.

Step 1: Move from setting state to dispatching actions

Managing state with reducers is slightly different from directly setting state. Instead of telling React “what to do” by setting state, you specify “what the user just did” by dispatching “actions” from your event handlers. (The state update logic will live elsewhere!) So instead of “setting tasks” via an event handler, you’re dispatching an “added/changed/deleted a task” action. This is more descriptive of the user’s intent.

먼저 리듀서를 사용하기 위해 위의 예시 코드에서 3가지 이벤트 핸들러에서 stateSetter 함수들을 빼고 dispatch 함수를 사용해 알맞은 action object를 패싱 하도록 하자. 리듀서를 사용할때는 사이드 이팩트를 주는 핸들러 로직에 stateSetter을 사용해 "무엇을 하라" 라고 리액트에 명령을 하는거보다 과거형으로 액션 개체를 리듀서 함수에 돌려 "유저가 무었을 했었다" 라고 표현하는게 맞는거 같다.

// 일반 핸들러:
function handleAddTask(text) {
  setTasks([
    ...tasks,
    {
      id: nextId++,
      text: text,
      done: false,
    },
  ]);
}

// 리듀서를 사용할시 dispatch 함수와 액션 객체를 사용한 예시:
function handleAddTask(text) {
  dispatch({
    type: 'added',
    id: nextId++,
    text: text,
  });
}

action 객체는 일반 자바스크립트 객체이다. 일반적으론 어떠한 이벤트가 발생했는지 최소한 미니멀 하게 서술할 값만 할당해 dispatch 함수와 사용한다.

An action object can have any shape.

  1. By convention, it is common to give it a string type that describes what happened, and
  2. pass any additional information in other fields.

The type is specific to a component, so in this example either 'added' or 'added_task' would be fine. Choose a name that says what happened!

Step 2: Write a reducer function

Array.reduce((accumulator, currentValue) => {}, initialValue) 처럼 두개의 React의 reducer 함수도 두가지 인자를 받는다.(Redux도 똑같다)

중요한 key concept은 The reduce() operation lets you take an array and “accumulate” a single value out of many:

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
  (result, number) => result + number
); // 1 + 2 + 3 + 4 + 5

The function you pass to reduce is known as a “reducer”. It takes the result so far and the current item, then it returns the next result. React reducers are an example of the same idea: they take the state so far and the action, and return the next state. In this way, they accumulate actions over time into state.

You could even use the reduce() method with an initialState and an array of actions to calculate the final state by passing your reducer function to it:

// index.js 
import tasksReducer from './tasksReducer.js';

let initialState = [];
let actions = [
  {type: 'added', id: 1, text: 'Visit Kafka Museum'},
  {type: 'added', id: 2, text: 'Watch a puppet show'},
  {type: 'deleted', id: 1},
  {type: 'added', id: 3, text: 'Lennon Wall pic'},
];

let finalState = actions.reduce(tasksReducer, initialState);

// tasksReducer.js
export default function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
    .....
    }

자 다시 돌아와서:

React will set the state to what you return from the reducer.

To move your state setting logic from your event handlers to a reducer function in this example, you will:

  1. Declare the current state (tasks) as the first argument.
  2. Declare the action object as the second argument.
  3. Return the next state from the reducer (which React will set the state to).
function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if(....).....
  .....
  // 위처럼 if 조건도 가능하지만 난 개인적으로 올드스쿨로 switch가 편하다 - 가독성이 편하기도 하고
  
  
  function tasksReducer(tasks, action) {
 	switch(action.type) {
    	case 'added':{
        	return [
            	...tasks,
                {
                id: action.id,
                text: action.text,
                done: false,
                }
            ]
        };
        case 'edited': {
        ..........
        break;
        }
    	.....
        ..
        default: {
        	throw Error(`Unknown action: + ${action.type}`);
            break;
        }
    }

We recommend wrapping each case block into the { and } curly braces so that variables declared inside of different cases don’t clash with each other. Also, a case should usually end with a return. or break If you forget to return, the code will “fall through” to the next case, which can lead to mistakes!

Step 3: Use the reducer from your component

Finally, you need to hook up the tasksReducer to your component. Import the useReducer Hook from React:

import {useReducer} from 'react';

// Then you can replace useState:
const [tasks, setTasks] = useState(initialTasks); < -- X

// with useReducer like so:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); <-- O 

The useReducer Hook is similar to useState—you must pass it an initial state and it returns a stateful value and a way to set state (in this case, the dispatch function). But it’s a little different.

나중엔 reducer 함수를 module로 import해서 separate concerns 전략을 사용해 가독성이 보기좋은 코드를 작성 할수 있다.

Comparing useState and useReducer

Reducers are not without downsides! Here’s a few ways you can compare them:

  • Code size: Generally, with useState you have to write less code upfront. With useReducer, you have to write both a reducer function and dispatch actions. However, useReducer can help cut down on the code if many event handlers modify state in a similar way.
  • Readability: useState is very easy to read when the state updates are simple. When they get more complex, they can bloat your component’s code and make it difficult to scan. In this case, useReducer lets you cleanly separate the how of update logic from the what happened of event handlers.
  • Debugging: When you have a bug with useState, it can be difficult to tell where the state was set incorrectly, and why. With useReducer, you can add a console log into your reducer to see every state update, and why it happened (due to which action). If each action is correct, you’ll know that the mistake is in the reducer logic itself. However, you have to step through more code than with useState.
  • Testing: A reducer is a pure function that doesn’t depend on your component. This means that you can export and test it separately in isolation. While generally it’s best to test components in a more realistic environment, for complex state update logic it can be useful to assert that your reducer returns a particular state for a particular initial state and action.
  • Personal preference: Some people like reducers, others don’t. That’s okay. It’s a matter of preference. You can always convert between useState and useReducer back and forth: they are equivalent!

We recommend using a reducer 1)if you often encounter bugs due to incorrect state updates in some component, and 2) want to introduce more structure to its code. You don’t have to use reducers for everything: feel free to mix and match! You can even useState and useReducer in the same component.

Writing reducers well

Keep these two tips in mind when writing reducers:

  • Reducers must be pure. Similar to state updater functions, reducers run during rendering! (Actions are queued until the next render.) This means that reducers must be pure—same inputs always result in the same output.

They should not send requests, schedule timeouts, or perform any side effects (operations that impact things outside the component). They should update objects and arrays without mutations.

  • Each action describes a single user interaction, even if that leads to multiple changes in the data. For example, if a user presses “Reset” on a form with five fields managed by a reducer, it makes more sense to dispatch one reset_form action rather than five separate set_field actions. If you log every action in a reducer, that log should be clear enough for you to reconstruct what interactions or responses happened in what order. This helps with debugging!

Recap

  • To convert from useState to useReducer:
    1. Dispatch actions from event handlers.
    2. Write a reducer function that returns the next state for a given state and action.
    3. Replace useState with useReducer.
  • Reducers require you to write a bit more code, but they help with debugging and testing.
  • Reducers must be pure.
  • Each action describes a single user interaction.
  • Use Immer if you want to write reducers in a mutating style.
profile
coder for web development + noodler at programming synthesizers for sound design as hobbyist.

0개의 댓글