공식 문서를 보고 기본적인 사용법을 정리해보고자 한다.
configureStore({
reducer : ,
middleware ?: ,
devTools ?: ,
preloadedState ?:,
enhaners ?:,
})
// 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를 사용하는 데는 builder callback notation과 map object notation 두 가지가 있지만 이 포스팅에서는 builder callback notation을 기준으로 한다.
create(initialState, builderCallback)
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.addCase, builder.addMatcher, builder.addDefaultCase 등을 통해 reducer에 case를 추가한다. 더 자세히 알아보자.
builder.addCase(actionCreator, reducer)
builder.addCase로 reducer에 case를 추가할 수 있다.
builder.addMatcher(matcher, reducer)
matcher를 통과한 action만 reducer로 넘어갈 수 있다.
builder.addDefaultCase(reducer)
addCase reducer에도, addMatcher reducer에도 넘어가지 못한 action은 여기로 넘어온다.
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가 그렇다.
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
여러 개에 매치 될 때의 순서는 다음과 같다.
createAction은 action creator를 리턴한다!
function createAction(type, prepareAction?)
type은 그냥 문자열 넘겨주면 된다.
얘는 필수 인자는 아니다. 개발을 하다 보면, 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를 만든다.