[Redux Toolkit] configureStore, createReducer, createAction

진실·2022년 6월 15일

공식 문서를 보고 기본적인 사용법을 정리해보고자 한다.

configureStore

parameter

configureStore({
  reducer : ,
  middleware ?: ,
  devTools ?: ,
  preloadedState ?:,
  enhaners ?:,
})
  • reducer : "reducer 함수" or "slice reducer들을 모은 객체". 후자의 경우 combineReducer로 알아서 넘어간다.
  • middleware : 사용할 미들웨어 배열. getDefaultMiddleware를 인자로 받아서, 여기에 concat하는 식으로 미들웨어를 추가한다.
  • devTools : boolean, 개발자도구를 사용할 것인지.
  • preloadedState : 초기 state,
  • enhancers : enhancer배열.

example

// todos/todosReducer.ts
import {Reducer} from '@reduxjs/toolkit'
declare const reducer : Reducer<{}>
export default reducer

// visibility/visibilityRedcuer.ts
import {Reducer} from '@reduxjs/toolkit'
declar const reducer : Reducer<{}>
export default reducer

// store.ts
import {configureStore} from '@reduxjs/toolkit'

import logger from 'redux-logger'

import {batchedSubscribe} from 'redux-batched-subscribe'

import todosReducer from './todos/todosReducer'
import visibilityReducer from './visibility/visibilityReducer'

const reducer = {
  todos : todosReducer,
  visibility : visibilityReducer
}

const preloadedState = {
  todos : [
    {
      text : 'Eat Food',
      completed : true,
    },
    {
      text : 'Exercise',
      completed : false,
    },
  ],
  visibilityFilter : 'SHOW_COMPLETED',
}

const debounceNotify = _.debounce(notify => notify())

const store = configureStore({
  reducer,
  middleware : (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
  devTools : process.env.NODE_ENV !== 'production',
  preloadedState,
  enhancers : [batchedSubscribe(debounceNotify),
})

createReducer

createReducer를 사용하는 데는 builder callback notation과 map object notation 두 가지가 있지만 이 포스팅에서는 builder callback notation을 기준으로 한다.

parameter

create(initialState, builderCallback)

  • initialState: State | (()=>state)
  • builderCallback : (builder:Builder) => void
    - builder 객체를 받아서 builder.addCase(actionCreatorOrType, reducer) 형태로 reducer에 case를 정의할 수 있다.

example

import {createAction, createReducer, AnyAction, PayloadAction} from '@reduxjs/toolkit'

const increment = createAction<number>('increment')
const decrement = createAction<number>('decrement')

// 인자로 넘어온 action의 payload type이 number인지 판단하고,
// 맞다면 해당 action의 type을 PayloadAction<number>로 좁힌다.
function isActionWithNumberPayload(action:AnyAction): action is PayloadAction<number>{
  return typeof action.payload == 'number'
}

const reducer = createReducer({
  counter : 0,
  sumOfNumberPayloads : 0,
  unhandledActions : 0,
}, (builder)=>{
  builder.addCase(increment, (state, action)=>{
    state.counter += action.payload
  }).addCase(decrease, (state, action)=>{
    state.counter -= action.payload
  }).addMatcher(isActionWithNumberPayload, (state, action)=>{
  
  }).addDefaultCase((state, action)=>{})
})

builder methods

위에 코드를 보면 builder.addCase, builder.addMatcher, builder.addDefaultCase 등을 통해 reducer에 case를 추가한다. 더 자세히 알아보자.

builder.addCase(actionCreator, reducer)
builder.addCase로 reducer에 case를 추가할 수 있다.

  • actionCreator : 그냥 string으로 type을 정의하거나, createAction으로 만들어진 actionCreator를 넘겨준다.
  • reducer: case reducer function ((state, action)=>{})

builder.addMatcher(matcher, reducer)
matcher를 통과한 action만 reducer로 넘어갈 수 있다.

  • mathcer : matcher function. ts에서는 반드시 type predicate function (is operator)이어야 한다.
  • reducer : 그냥 case reducer function

builder.addDefaultCase(reducer)
addCase reducer에도, addMatcher reducer에도 넘어가지 못한 action은 여기로 넘어온다.

  • reducer : 그냥 case reducer function

기타 알고 있으면 좋은 내용

direct state mutate

RTK는 immer를 써서, 객체나 배열 state의 경우 state operator 없이도 update가 가능하다. 그게 무슨말이냐

const todosReducer = createReducer([] as Todo[], (builder) => {
  builder
    .addCase(addTodo, (state, action) => {
      const todo = action.payload
      return [...state, todo]
    })
    .addCase(toggleTodo, (state, action) => {
      const index = action.payload
      const todo = state[index]
      return [
        ...state.slice(0, index),
        { ...todo, completed: !todo.completed },
        ...state.slice(index + 1),
      ]
    })
})

기존의 redux reducer는 shallow copy 문제 때문에 원래 배열, 객체를 spread operator로 가져오고 리턴하는 형식이었다.

const todosReducer = createReducer([] as Todo[], (builder) => {
  builder
    .addCase(addTodo, (state, action) => {
      // This push() operation gets translated into the same
      // extended-array creation as in the previous example.
      const todo = action.payload
      state.push(todo)
    })
    .addCase(toggleTodo, (state, action) => {
      // The "mutating" version of this case reducer is much
      //  more direct than the explicitly pure one.
      const index = action.payload
      const todo = state[index]
      todo.completed = !todo.completed
    })
})

다만, RTK는 내부적으로 immer를 사용하므로 state.push(todo)처럼 shallow copy문제 없이 직관적으로 코드를 작성할 수 있다.

다만 주의해야할 점은, immer의 고질적인 특성때문에, state 인자의 변경 혹은 새 state 리턴이 동시에 이루어져서는 안된다는 것이다. 예를들면,

const todosReducer = createReducer([] as Todo[], (builder) => {
  builder.addCase(toggleTodo, (state, action) => {
    const index = action.payload
    const todo = state[index]

    // This case reducer both mutates the passed-in state...
    todo.completed = !todo.completed

    // ... and returns a new value. This will throw an
    // exception. In this example, the easiest fix is
    // to remove the `return` statement.
    return [...state.slice(0, index), todo, ...state.slice(index + 1)]
  })
})

return [...state.slice(0, index), todo, ...state.slice(index + 1)]
이렇게 state인자를 바꾸면서 state를 리턴하지 말라는 것이다. 왜냐구? 그냥 immer가 그렇다.

Multiple Case Reducer Execution

addCase, addMatcher 등을 달다 보면 addCase, addMatcher 둘 다에 걸리는 action이 있을 수 있다. 이해가 안간다면 다음을 보라.

import { createReducer } from '@reduxjs/toolkit'

const reducer = createReducer(0, (builder) => {
  builder
    .addCase('increment', (state) => state + 1)
    .addMatcher(
      (action) => action.type.startsWith('i'),
      (state) => state * 5
    )
    .addMatcher(
      (action) => action.type.endsWith('t'),
      (state) => state + 2
    )
})

console.log(reducer(0, { type: 'increment' }))
// Returns 7, as the 'increment' case and both matchers all ran in sequence:
// - case 'increment": 0 => 1
// - matcher starts with 'i': 1 => 5
// - matcher ends with 't': 5 => 7

여러 개에 매치 될 때의 순서는 다음과 같다.

  • 먼저 addCase reducer function을 수행한다.
  • 그 다음 matcher를 순서대로 수행한다.

createAction

createAction은 action creator를 리턴한다!

parameter

function createAction(type, prepareAction?)

type

type은 그냥 문자열 넘겨주면 된다.

prepareAction

얘는 필수 인자는 아니다. 개발을 하다 보면, action이 생성될 때 항상 기본적으로 id를 부여하고 싶을 수도 있고, 그때의 timestamp를 찍고 싶을 수도 있다. 이때 사용하는 게 prepare Action이다.

import { createAction, nanoid } from '@reduxjs/toolkit'

const addTodo = createAction('todos/add', function prepare(text: string) {
  return {
    payload: {
      text,
      id: nanoid(),
      createdAt: new Date().toISOString(),
    },
  }
})

console.log(addTodo('Write more docs'))
/**
 * {
 *   type: 'todos/add',
 *   payload: {
 *     text: 'Write more docs',
 *     id: '4AJvwMSWEHCchcWYga3dj',
 *     createdAt: '2019-10-03T07:53:36.581Z'
 *   }
 * }
 **/

action을 만들 때 prepare callback이 호출돼서, 기본적으로 위와 같은 형태의 payload를 만든다.

profile
반갑습니다.

0개의 댓글