Installation
# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit
# or
npx create-react-app my-app --template redux
ConfigureStore
// Typical example
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({ reducer: rootReducer })
// The store now has redux-thunk added and the Redux DevTools Extension is turned on
// Full example
const store = configureStore({
reducer,
middleware,
devTools: process.env.NODE_ENV !== 'production',
preloadedState,
enhancers: [reduxBatch]
})
Reducer
If a reducer is only one, it is used as the default root reducer:
const store = configureStore({reducer})
If an object of slice reducers, given as an object like:
{
reducer:
{
users: usersReducers,
posts: postsReducers
}
}
configureStore
under the hood will automatically create a root reducer combining all the reducers.
createAction
// Typescript signature
function createAction(type, prepareAction?)
// Native redux
const INCREMENT = 'counter/increment'
function increment(amount) {
return {
type: INCREMENT,
payload: amount
}
}
const action = increment(3)
// { type: 'counter/increment', payload: 3 }
// With Redux toolkit
const increment = createAction('counter/increment')
let action = increment()
// { type: 'counter/increment' }
action = increment(3)
// returns { type: 'counter/increment', payload: 3 }
console.log(increment.toString())
// 'counter/increment'
console.log(`The action type is: ${increment}`)
// 'The action type is: counter/increment'
Note action typing style counter/increment
which keeps consistent with objects created by createSlice
.
By default, increment
accepts a single argument that is to be passed in to payload
. To customize the contents of payload to pass in rather than the single value, a "prepare" callback function can be passed in as second argument as below:
import v4 from 'uuid/v4'
const addTodo = createAction('todos/add', function prepare(text) {
return {
payload: {
text,
id: v4(),
createdAt: new Date().toISOString()
}
}
})
console.log(addTodo('Write more docs'))
/**
* {
* type: 'todos/add',
* payload: {
* text: 'Write more docs',
* id: '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed',
* createdAt: '2019-10-03T07:53:36.581Z'
* }
* }
**/
createReducer
A navtive Redux reducer looks as below:
function counterReducer(state = 0, action) {
switch (action.type) {
case 'increment':
return state + action.payload
case 'decrement':
return state - action.payload
default:
return state
}
}
where all switch
cases need to be hardcopied; boiler-plate-y and error-prone.
createReducer
with the signature below provides a more efficient API.
// Typescript signature
export function createReducer<S>(
initialState: S,
mapOrBuilderCallback:
| CaseReducers<S, any>
| ((builder: ActionReducerMapBuilder<S>) => void)
): Reducer<S> {}
// The native Redux example above is equivalent to
const counterReducer = createReducer(0, {
increment: (state, action) => state + action.payload,
decrement: (state, action) => state - action.payload
})
As shown in typescript definition for createReducer
, a builder
callback function can be given instead of an actions-containing Reducer object, i.e., CaseReducers
. This is relevant when typescript is used and not covered herein yet.
Combined with createAction
, it can be written as below:
const increment = createAction('increment')
const decrement = createAction('decrement')
const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload
})
Note that the keys of CaseReducers
are each given in [] which is called "computed property" syntax
introduced in ES6. With this, it automatically calls toString
method which returns the value in action.type
. Therefore, the following are equivalent to each other:
[increment]: (state, action) => state + action.payload
and
[increment.type]: (state, action) => state + action.payload
State Mutation/Updates
To keep the immutability of states, spread operator is typically used when updating states upon action.
const addTodo = createAction('todos/add')
const toggleTodo = createAction('todos/toggle')
const todosReducer = createReducer([], {
[addTodo]: (state, action) => {
const todo = action.payload
return [...state, todo]
},
[toggleTodo]: (state, action) => {
const index = action.payload
const todo = state[index]
return [
...state.slice(0, index),
{ ...todo, completed: !todo.completed }
...state.slice(index + 1)
]
}
})
Another way to achieve this that is viable when using createReducer
is to use native javascript methods such as push
as below:
const addTodo = createAction('todos/add')
const toggleTodo = createAction('todos/toggle')
const todosReducer = createReducer([], {
[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)
},
[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
}*
})
immer
, the built-in library in createReducer
, guarantees under the hood that new state gets passed in as new object copy.
Note that when immer-style mutation is practces, it shall not return
createSlice
// Typescript signature
function createSlice({
// An object of "case reducers". Key names will be used to generate actions.
reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
// The initial state for the reducer
initialState: any,
// A name, used in action types
name: string,
// An additional object of "case reducers". Keys should be other action types.
extraReducers?:
| Object<string, ReducerFunction>
| ((builder: ActionReducerMapBuilder<State>) => void)
})
This is the most powerful/magical API as it *" accepts an initial state, an object full of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state."*, due to which it is the most confusing API as a lot of things happen under the hood.
Three arguments are mandatory: reducers
, initialState
, and name
; extraReducers
is optional.
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo(state, action) {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
toggleTodo(state, action) {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
})
export const { addTodo, toggleTodo } = todosSlice.actions
export default todosSlice.reducer
Note reducers written in ES6 syntax can be re-written in ES5 style using arrow functions as below:
reducers: {
addTodo: (state, action) => {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
Based on name
, initialState
, and reducers
, createSlice
returns the object as below:
{
name: "todos",
reducer: (state, action) => newState,
actions: {
addTodo: (payload) => ({type: "todos/addTodo", payload}),
toggleTodo: (payload) => ({type: "todos/toggleTodo", payload})
},
caseReducers: {
addTodo: (state, action) => newState,
toggleTodo: (state, action) => newState,
}
}
Note it is todosSlice.reducer
that gets exported for further use.
const incrementBy = createAction<number>('incrementBy')
const decrementBy = createAction<number>('decrementBy')
const counter = createSlice({
name: 'counter',
initialState: 0 as number,
reducers: {
increment: state => state + 1,
decrement: state => state - 1,
multiply: {
reducer: (state, action: PayloadAction<number>) => state * action.payload,
prepare: (value: number) => ({ payload: value || 2 }) // fallback if the payload is a falsy value
}
},
// "builder callback API", recommended for TypeScript users
extraReducers: builder => {
builder.addCase(incrementBy, (state, action) => {
return state + action.payload
})
builder.addCase(decrementBy, (state, action) => {
return state - action.payload
})
}
})
As shown above, createAction
is carried out for all given reducers without explicit action declaration, e.g., increment
, decrement
.multiply
for which a custom payload is to be defined, reducer
and prepare
keys are used.
let nextTodoId = 0
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer(state, action) {
const { id, text } = action.payload
state.push({ id, text, completed: false })
},
prepare(text) {
return { payload: { text, id: nextTodoId++ } }
}
}
}
}
extraReducers
... to be updated.
References
'프레임워크 > React' 카테고리의 다른 글
VS Code ES7 React/Redux/React-Native/JS snippets (0) | 2020.06.07 |
---|