Quick Start 페이지에서 설명한 대로 Redux 툴킷의 목표는 Redux 사용을 단순화하는 것입니다.
Redux로 하고자 하는 모든 것에 대한 완전한 솔루션은 아니지만
Redux로 앱을 작성하는데 필요한 많은 코드를 자동으로 만들어줍니다.
스토어 설정
- 루트 리듀서 함수 가져오기 / 만들기
- 비동기, 부수효과 논리 처리를 위한 미들웨어 설정
- Redux DevTools 확장 설정
- 애플리케이션이 개발용인지 프로덕션용인지에 따른 로직 변경
수동 스토어 설정
Redux 문서의 Configuring Your Store 페이지의 다음 예는 일반적인 스토어 설정 프로세스를 보여줍니다.import { applyMiddleware, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureStore(preloadedState) {
const middlewares = [loggerMiddleware, thunkMiddleware]
const middlewareEnhancer = applyMiddleware(...middlewares)
const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
const composedEnhancers = composeWithDevTools(...enhancers)
const store = createStore(rootReducer, preloadedState, composedEnhancers)
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
- 기본 Redux createStore 함수는 위치가 정해진 인수(rootReducer, preloadedState, Enhancer)를 사용합니다.
- 어떤 매개변수가 어느 것인지 잊어버리기 쉽습니다.
- 여러 설정을 추가하려는 경우 미들웨어 및 인핸서를 설정하는 프로세스가 혼란스러울 수 있습니다.
- Redux DevTools Extension 문서는 처음에 확장 도구가 사용 가능한지 확인하기 위해 전역 네임스페이스를 확인하는 일부 손으로 작성한 코드를 사용하도록 합니다.
- 많은 사용자가 이러한 스니펫을 복붙하여 설정 코드를 읽기 어렵게 만듭니다.
- some hand-written code that checks the global namespace to see if the extension is available
configureStore로 스토어 설정 간소화
configureStore는 다음과 같은 방법으로 이러한 문제를 해결합니다.- 가독성을 향상시키는 "명명된" 매개변수가 있는 옵션 객체 사용
- 스토어에 추가하려는 미들웨어 및 인핸서 어레이를 제공하면 자동으로 applyMiddleware 및 compose을 호출해 줍니다.
- Redux DevTools Extension을 자동으로 활성화 합니다.
- redux-thunk는 컴포넌트 외부의 동기 및 비동기 논리 액션 처리에 가장 일반적으로 사용되는 미들웨어입니다.
- 개발 단계에서 상태를 변경하거나 직렬화할 수 없는 값을 사용하는 것과 같은 일반적인 실수를 확인하는 미들웨어를 추가합니다.
스토어 설정 코드를 더 짧고 읽기 쉽게 해주며, 동시에 좋은 디폴트 동작을 얻을 수 있음을 의미합니다.
configureStore API를 사용하는 가장 간단한 방법은 루트 리듀서 함수를 reducer라는 이름의 매개변수로 전달하는 것입니다.
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './reducers'
const store = configureStore({
reducer: rootReducer,
})
export default store
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
},
})
export default store
이것은 1 depth 레벨의 리듀서에서만 동작합니다.
리듀서를 중첩하려면 CombineReducers를 직접 호출해야 합니다.
스토어 설정을 사용자 정의해야 하는 경우 추가 옵션을 전달할 수 있습니다.
Redux Toolkit을 사용하는 핫 리로딩 예제는 다음과 같습니다.
import { configureStore } from '@reduxjs/toolkit'
import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'
export default function configureAppStore(preloadedState) {
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(loggerMiddleware),
preloadedState,
enhancers: [monitorReducersEnhancer],
})
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
}
return store
}
리듀서 작성하기
- 액션 객체의 타입 필드를 보고 어떻게 응답해야 하는지 확인합니다.
- 상태를 변경해야 하는 부분의 복사본을 만들고 해당 복사본만 수정하여 상태를 변경할 수 없도록 업데이트합니다.
리듀서 작성과 관련된 다른 일반적인 문제는 상태를 변경할 수 없도록 업데이트하는 것과 관련이 있습니다.
JavaScript의 객체는 불변이 아니므로 중첩된 데이터를 불변하도록 손으로 업데이트하는 것은 어렵고 실수하기 쉽습니다.
createReducer로 리듀서 단순화
다음은 createReducer를 사용하는 방법에 대한 몇 가지 예입니다.
switch 문과 변경할 수 없는 업데이트를 사용하는 일반적인 "Todo" 리듀서부터 시작하겠습니다.
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO': {
return state.concat(action.payload)
}
case 'TOGGLE_TODO': {
const { index } = action.payload
return state.map((todo, i) => {
if (i !== index) return todo
return {
...todo,
completed: !todo.completed,
}
})
}
case 'REMOVE_TODO': {
return state.filter((todo, i) => i !== action.payload.index)
}
default:
return state
}
}
state.concat(), state.map(), 객체 스프레드 연산자를 통해 불변을 적용합니다.
createReducer를 사용하면 해당 예제를 상당히 줄일 수 있습니다.
불변처럼 상태를 "변경"하는 능력은 깊이 중첩된 상태를 업데이트하려고 할 때 특히 유용합니다. (by immer)
const todosReducer = createReducer([], (builder) => {
builder
.addCase('ADD_TODO', (state, action) => {
// "mutate" the array by calling push()
state.push(action.payload)
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index]
// "mutate" the object by overwriting a field
todo.completed = !todo.completed
})
.addCase('REMOVE_TODO', (state, action) => {
// Can still return an immutably-updated value if we want to
return state.filter((todo, i) => i !== action.payload.index)
})
})
createReducer 사용 시 고려할 점
- "변이" 코드는 createReducer 함수 내에서만 올바르게 작동합니다.
- Immer를 사용하면 draft 상태를 "변경"함과 동시에 새 상태 값을 반환하는 것을 혼합할 수 없습니다.
액션 생성자 작성하기
Redux는 액션 객체를 생성하는 프로세스를 캡슐화하는 "액션 생성자" 함수를 작성하도록 권장합니다.
(write "action creator" functions)
꼭 필요한 것은 아니지만 Redux 사용 표준입니다.
대부분의 액션 생성자는 매우 간단합니다.
일부 매개변수를 사용하여 특정 타입 필드와 액션 내부에 매개변수가 있는 액션 객체를 반환합니다.
해당 매개변수는 일반적으로 액션 객체를 구성하기 위한 Flux 표준 액션 규칙의 일부인 페이로드라는 필드에 넣습니다.
일반적인 액션 생성자는 다음처럼 생겼습니다.
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: { text },
}
}
createAction으로 액션 생성자 정의하기
직접 액션 제작자를 작성하는 것은 지겹습니다.
Redux toolkit은 주어진 액션 타입을 사용하는 액션 생성자를 생성하고
해당 인수를 페이로드 필드로 바꾸는 createAction이라는 함수를 제공합니다.
const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})
createAction은 또한 결과 페이로드 필드를 사용자 정의하고 선택적으로 메타 필드를 추가할 수 있는 "ready 콜백" 인수를 허용합니다.
ready 콜백으로 액션 생성자를 정의하는 방법에 대한 자세한 내용은 createAction API 참조를 참조하세요.
액션 생성자를 액션 타입으로 사용하기
- 첫째, createAction은 액션 생성자의 toString() 메서드를 재정의합니다.
- 액션 생성자 자체를 "액션 타입" 참조로 사용할 수 있음을 의미합니다.
- 둘째, 액션 타입은 액션 생성자의 타입 필드에도 정의되어 있습니다.
const actionCreator = createAction('SOME_ACTION_TYPE')
console.log(actionCreator.toString())
// "SOME_ACTION_TYPE"
console.log(actionCreator.type)
// "SOME_ACTION_TYPE"
const reducer = createReducer({}, (builder) => {
// actionCreator.toString() will automatically be called here
// also, if you use TypeScript, the action type will be correctly inferred
builder.addCase(actionCreator, (state, action) => {})
// Or, you can reference the .type field:
// if using TypeScript, the action type cannot be inferred that way
builder.addCase(actionCreator.type, (state, action) => {})
})
즉, 별도의 액션 타입 변수를 작성하여 사용하거나,
const SOME_ACTION_TYPE = "SOME_ACTION_TYPE"과 같은 액션 타입의 이름과 값을 반복할 필요가 없습니다.
불행히도, 문자열에 대한 암시적 변환은 switch 문에선 대해 발생하지 않습니다.
switch 문에서 이러한 액션 생성자 중 하나를 사용하려면 actionCreator.toString()을 직접 호출해야 합니다.
const actionCreator = createAction('SOME_ACTION_TYPE')
const reducer = (state = {}, action) => {
switch (action.type) {
// ERROR: this won't work correctly!
case actionCreator: {
break
}
// CORRECT: this will work as expected
case actionCreator.toString(): {
break
}
// CORRECT: this will also work right
case actionCreator.type: {
break
}
}
}
- 이 경우 타입 단언하거나 (actionCreator as string)
- type 필드를 사용해야 할 수 있습니다.
상태의 슬라이스 만들기
import { combineReducers } from 'redux'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'
const rootReducer = combineReducers({
users: usersReducer,
posts: postsReducer,
})
- 초기 값을 포함하여 상태를 "소유"합니다
- 해당 상태가 업데이트되는 방법을 정의합니다
- 상태 업데이트로 이어지는 특정 액션을 정의합니다
- 리듀서 함수를 첫번째 파일에 정의하고,
- 액션 생성자를 두 번째 파일에 정의합니다.
- 액션 타입을 세 번째 파일에 정의하고, 위의 두 파일에서 가져와 사용합니다.
- 두 함수 모두 동일한 액션 타입을 참조해야 하기 때문입니다.
// postsConstants.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
// postsActions.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
// postsReducer.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// omit implementation
}
default:
return state
}
}
- 두 위치 모두에서 액션 타입을 인라인 문자열로 작성할 수 있습니다.
- 액션 생성자는 훌륭하지만 Redux에 필수는 아닙니다. (객체 인라인)
- 여러 파일을 작성하는 유일한 이유는 코드가 수행하는 액션(기능,작업)에 따라 코드를 구분하는 것이 일반적이기 때문입니다.
// postsDuck.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'
export function addPost(id, title) {
return {
type: CREATE_POST,
payload: { id, title },
}
}
const initialState = []
export default function postsReducer(state = initialState, action) {
switch (action.type) {
case CREATE_POST: {
// Omit actual code
break
}
default:
return state
}
}
여러 파일이 필요하지 않고 액션 타입 상수의 중복 가져오기를 제거할 수 있기 때문에 작업이 간소화됩니다.
그러나 여전히 액션 타입과 액션 생성자를 직접 작성해야 합니다.
객체 안에 함수 정의
const keyName = "ADD_TODO4";
const reducerObject = {
// Explicit quotes for the key name, arrow function for the reducer
"ADD_TODO1" : (state, action) => { }
// Bare key with no quotes, function keyword
ADD_TODO2 : function(state, action){ }
// Object literal function shorthand
ADD_TODO3(state, action) { }
// Computed property
[keyName] : (state, action) => { }
}
createSlice로 슬라이스 단순화하기
Redux 툴킷에는
사용자가 제공하는 리듀서 함수의 이름을 기반으로 액션 타입 및 액션 생성자를 자동 생성하는 createSlice 함수가 포함되어 있습니다.
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
console.log(postsSlice)
/*
{
name: 'posts',
actions : {
createPost,
updatePost,
deletePost,
},
reducer
}
*/
const { createPost } = postsSlice.actions
console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "posts/createPost", payload : {id : 123, title : "Hello World"}}
슬라이스를 내보내기 / 슬라이스 사용하기
대부분의 경우 슬라이스를 정의하고 해당 액션 생성자와 리듀서를 내보내고 싶을 것입니다.
권장되는 방법은 ES6 구조 분해 및 내보내기 구문을 사용하는 것입니다.
const postsSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
createPost(state, action) {},
updatePost(state, action) {},
deletePost(state, action) {},
},
})
// Extract the action creators object and the reducer
const { actions, reducer } = postsSlice
// Extract and export each action creator by name
export const { createPost, updatePost, deletePost } = actions
// Export the reducer, either as a default or named export
export default reducer
이렇게 정의된 슬라이스는 액션 생성자와 리듀서를 정의하고 내보내기 위한 "Redux Ducks" 패턴과 개념이 매우 유사합니다.
그러나 슬라이스를 가져오고 내보낼 때 알아야 할 몇 가지 잠재적인 단점이 있습니다.
첫째, Redux 액션 타입은 단일 슬라이스에만 적용되는 것이 아닙니다.
둘째, "순환 참조" 문제가 발생할 수 있습니다.
- createAction을 사용하여 별도의 파일에 몇 가지 일반적인 액션 타입을 정의하고
- 해당 액션 생성자를 각 슬라이스 파일로 가져오고
- extraReducers 인수를 사용하여 처리할 수 있습니다.
(How to fix circular dependency issues in JS)
비동기 로직과 데이터 가져오기
- 동기식으로 액션을 전달하고,
- 루트 리듀서 함수를 호출하여 상태를 업데이트하고,
- 무언가가 변경되었음을 UI에 알리는 방법만 알고 있습니다.
스토어에 디스패치하거나 상태값을 확인하여 비동기 논리가 저장소와 상호 작용하도록 하려면 어떻게 해야 할까요?
바로 여기에서 Redux 미들웨어(Redux middleware)가 등장합니다. 스토어를 확장하고 다음을 수행할 수 있습니다.
- 액션이 전달될 때 추가 논리 실행(예: 액션 및 상태 기록)
- 디스패치된 액션 버리기, 수정, 지연, 교체 또는 일시 중지
- dispatch 및 getState에 대한 액세스 권한이 있는 추가 코드 작성
- 함수 및 프로미스과 같은 일반 작업 개체 이외의 다른 값을 가로채서 대신 실제 액션 개체를 전달하여 받는 방법을 디스패치에 가르칩니다.
미들웨어를 사용하는 가장 일반적인 이유는 다양한 종류의 비동기 논리가 저장소와 상호 작용할 수 있도록 하는 것입니다.
(The most common reason to use middleware is to allow different kinds of async logic to interact with the store)
이를 통해 액션을 전달하고 스토어 상태를 확인할 수 있는 코드를 작성하면서 해당 논리를 UI와 분리할 수 있습니다.
Redux에는 여러 종류의 비동기 미들웨어가 있으며 각각 다른 구문을 사용하여 논리를 작성할 수 있습니다.
가장 일반적인 비동기 미들웨어는 다음과 같습니다.
- redux-thunk
- 비동기 논리를 직접 포함할 수 있는 일반 함수를 작성할 수 있습니다.
- redux-saga
- 미들웨어에서 실행할 수 있는 동작 설명을 반환하는 제너레이터 함수를 사용합니다.
- redux-observable
- RxJS 옵저버블 라이브러리를 사용하여 액션을 처리하는 함수 체인을 생성합니다.
Redux 툴킷의 RTK 쿼리는 Redux 앱을 위해 특별히 제작된 데이터 가져오기 및 캐싱 솔루션이며
데이터 가져오기를 관리하기 위해 썽크 또는 리듀서를 작성할 필요가 없습니다.
Redux Toolkit configureStore 기능은 기본적으로 썽크 미들웨어를 자동으로 설정하므로
(automatically sets up the thunk middleware by default)
애플리케이션 코드의 일부로 썽크 작성을 즉시 시작할 수 있습니다.
비동기 로직을 슬라이스로 정의하기
썽크는 일반적으로 dispatch(dataLoaded(response.data))와 같은 일반 액션을 전달합니다.
많은 Redux 앱은 "타입 별 폴더" 접근 방식을 사용하여 코드를 구성했습니다.
그 구조에서 썽크 액션 생성자는 일반적으로 일반 액션 생성자와 함께 "액션" 파일에 정의됩니다.
// First, define the reducer and action creators via `createSlice`
const usersSlice = createSlice({
name: 'users',
initialState: {
loading: 'idle',
users: [],
},
reducers: {
usersLoading(state, action) {
// Use a "state machine" approach for loading state instead of booleans
if (state.loading === 'idle') {
state.loading = 'pending'
}
},
usersReceived(state, action) {
if (state.loading === 'pending') {
state.loading = 'idle'
state.users = action.payload
}
},
},
})
// Destructure and export the plain action creators
export const { usersLoading, usersReceived } = usersSlice.actions
// Define a thunk that dispatches those action creators
const fetchUsers = () => async (dispatch) => {
dispatch(usersLoading())
const response = await usersAPI.fetchAll()
dispatch(usersReceived(response.data))
}
리덕스 데이터 가져오기 패턴
- 요청이 진행 중임을 나타내기 위해 요청 전에 "start" 액션이 전달됩니다.
- 이는 로드 상태를 추적하거나 중복 요청을 건너뛸 수 있도록 하거나 UI에 로드 표시기를 표시하는 데 사용할 수 있습니다.
- 비동기 요청이 이루어집니다
- 요청 결과에 따라 비동기 논리는 결과 데이터가 포함된 "success" 액션이나 오류 세부 정보가 포함된 "실패" 액션을 전달합니다.
- 리듀서 로직은 두 경우 모두 로딩 상태를 지우고 성공 사례의 결과 데이터를 처리하거나 UI에 나타내기 위해 오류 값을 저장합니다.
- 요청 결과에 따라 비동기 논리는 결과 데이터가 포함된 "success" 액션이나 오류 세부 정보가 포함된 "실패" 액션을 전달합니다.
이러한 단계는 필수는 아니지만 Redux 자습서에서 제안된 패턴으로 권장됩니다.
(recommended in the Redux tutorials as a suggested pattern)
일반적인 구현은 다음과 같습니다.
const getRepoDetailsStarted = () => ({
type: "repoDetails/fetchStarted"
})
const getRepoDetailsSuccess = (repoDetails) => ({
type: "repoDetails/fetchSucceeded",
payload: repoDetails
})
const getRepoDetailsFailed = (error) => ({
type: "repoDetails/fetchFailed",
error
})
const fetchIssuesCount = (org, repo) => async dispatch => {
dispatch(getRepoDetailsStarted())
try {
const repoDetails = await getRepoDetails(org, repo)
dispatch(getRepoDetailsSuccess(repoDetails))
} catch (err) {
dispatch(getRepoDetailsFailed(err.toString()))
}
}
- 세 가지 다른 경우에 대해 고유한 액션타입을 정의해야 합니다.
- 이러한 각 액션 타입에는 일반적으로 해당 액션 생성자 함수가 있습니다.
- 올바른 순서로 올바른 액션을 전달하는 썽크를 작성해야 합니다.
createAsyncThunk는 액션 타입 및 액션 생성자를 생성하고 해당 액션을 전달하는 썽크를 생성하여 이 패턴을 추상화합니다.
createAsyncThunk와 비동기 요청
- API 요청을 수행하는 데 필요한 실제 로직
- Redux 액션 히스토리 로그에 표시되는 액션 타입 이름,
- 리듀서가 가져온 데이터를 처리하는 방법에
가장 관심이 있을 것입니다.
여러 액션 타입을 정의하고 올바른 순서로 액션을 전달하는 반복적인 세부 사항은 중요하지 않습니다.
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// First, create the thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload)
})
},
})
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123))
썽크 액션 생성자는 페이로드 작성자 콜백에 첫 번째 인수로 전달되는 단일 인수를 허용합니다.
페이로드 생성자는 또한 일반적으로 표준 Redux 썽크 함수에 전달되는 매개변수가 포함된 thunkAPI 개체와
자동 생성된 고유한 임의 요청 ID 문자열 및 AbortController.signal object를 받습니다.
interface ThunkAPI {
dispatch: Function
getState: Function
extra?: any
requestId: string
signal: AbortSignal
}
정규화된 데이터 다루기
- 이것은 일반적으로 id의 키를 가진 객체로 컬렉션을 저장하는 동시에
- 해당 id의 정렬된 배열을 저장하여 수행됩니다.
직접 코딩해서 정규화하기
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
return response.data
})
export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
// reduce the collection by the id property into a shape of { 1: { ...user }}
const byId = action.payload.users.reduce((byId, user) => {
byId[user.id] = user
return byId
}, {})
state.entities = byId
state.ids = Object.keys(byId)
})
},
})
우리는 이 코드를 직접 작성할 수 있지만, 여러 타입의 데이터를 처리하는 경우에는 반복적입니다.
또한 이 예제에서는 항목을 업데이트하지 않고 상태로 로드하는 작업만 처리합니다.
normalizr와 함께 정규화
Redux 없이 단독으로 사용할 수 있지만 매우 일반적으로 Redux와 함께 사용됩니다.
일반적인 사용법은 API 응답에서 컬렉션을 형식화한 다음 리듀서에서 처리하는 것입니다.
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { normalize, schema } from 'normalizr'
import userAPI from './userAPI'
const userEntity = new schema.Entity('users')
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// Normalize the data before passing it to our reducer
const normalized = normalize(response.data, [userEntity])
return normalized.entities
})
export const slice = createSlice({
name: 'users',
initialState: {
ids: [],
entities: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.entities = action.payload.users
state.ids = Object.keys(action.payload.users)
})
},
})
직접 작성한 버전과 마찬가지로 이것은 상태에 항목을 추가하거나 나중에 업데이트하는 것을 처리하지 않습니다.
수신된 모든 항목을 로드하는 것뿐입니다.
createEntityAdapter와 함께 정규화
import {
createSlice,
createAsyncThunk,
createEntityAdapter,
} from '@reduxjs/toolkit'
import userAPI from './userAPI'
export const fetchUsers = createAsyncThunk('users/fetchAll', async () => {
const response = await userAPI.fetchAll()
// In this case, `response.data` would be:
// [{id: 1, first_name: 'Example', last_name: 'User'}]
return response.data
})
export const updateUser = createAsyncThunk('users/updateOne', async (arg) => {
const response = await userAPI.updateUser(arg)
// In this case, `response.data` would be:
// { id: 1, first_name: 'Example', last_name: 'UpdatedLastName'}
return response.data
})
export const usersAdapter = createEntityAdapter()
// By default, `createEntityAdapter` gives you `{ ids: [], entities: {} }`.
// If you want to track 'loading' or other keys, you would initialize them here:
// `getInitialState({ loading: false, activeRequestId: null })`
const initialState = usersAdapter.getInitialState()
export const slice = createSlice({
name: 'users',
initialState,
reducers: {
removeUser: usersAdapter.removeOne,
},
extraReducers: (builder) => {
builder.addCase(fetchUsers.fulfilled, usersAdapter.upsertMany)
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
const { id, ...changes } = payload
usersAdapter.updateOne(state, { id, changes })
})
},
})
const reducer = slice.reducer
export default reducer
export const { removeUser } = slice.actions
정규화 라이브러리와 함께 createEntityAdapter 사용하기
// features/articles/articlesSlice.js
import {
createSlice,
createEntityAdapter,
createAsyncThunk,
createSelector,
} from '@reduxjs/toolkit'
import fakeAPI from '../../services/fakeAPI'
import { normalize, schema } from 'normalizr'
// Define normalizr entity schemas
export const userEntity = new schema.Entity('users')
export const commentEntity = new schema.Entity('comments', {
commenter: userEntity,
})
export const articleEntity = new schema.Entity('articles', {
author: userEntity,
comments: [commentEntity],
})
const articlesAdapter = createEntityAdapter()
export const fetchArticle = createAsyncThunk(
'articles/fetchArticle',
async (id) => {
const data = await fakeAPI.articles.show(id)
// Normalize the data so reducers can load a predictable payload, like:
// `action.payload = { users: {}, articles: {}, comments: {} }`
const normalized = normalize(data, articleEntity)
return normalized.entities
}
)
export const slice = createSlice({
name: 'articles',
initialState: articlesAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Handle the fetch result by inserting the articles here
articlesAdapter.upsertMany(state, action.payload.articles)
})
},
})
const reducer = slice.reducer
export default reducer
// features/users/usersSlice.js
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'
const usersAdapter = createEntityAdapter()
export const slice = createSlice({
name: 'users',
initialState: usersAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// And handle the same fetch result by inserting the users here
usersAdapter.upsertMany(state, action.payload.users)
})
},
})
const reducer = slice.reducer
export default reducer
// features/comments/commentsSlice.js
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
import { fetchArticle } from '../articles/articlesSlice'
const commentsAdapter = createEntityAdapter()
export const slice = createSlice({
name: 'comments',
initialState: commentsAdapter.getInitialState(),
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchArticle.fulfilled, (state, action) => {
// Same for the comments
commentsAdapter.upsertMany(state, action.payload.comments)
})
},
})
const reducer = slice.reducer
export default reducer
createEntityAdapter와 함께 selector 사용하기
엔티티 어댑터는 가장 일반적인 셀렉터를 생성하는 셀렉터 팩토리를 제공합니다.
위의 예를 사용하여 다음과 같이 usersSlice에 셀렉터를 추가할 수 있습니다.
// Rename the exports for readability in component usage
export const {
selectById: selectUserById,
selectIds: selectUserIds,
selectEntities: selectUserEntities,
selectAll: selectAllUsers,
selectTotal: selectTotalUsers,
} = usersAdapter.getSelectors((state) => state.users)
import React from 'react'
import { useSelector } from 'react-redux'
import { selectTotalUsers, selectAllUsers } from './usersSlice'
import styles from './UsersList.module.css'
export function UsersList() {
const count = useSelector(selectTotalUsers)
const users = useSelector(selectAllUsers)
return (
<div>
<div className={styles.row}>
There are <span className={styles.value}>{count}</span> users.{' '}
{count === 0 && `Why don't you fetch some more?`}
</div>
{users.map((user) => (
<div key={user.id}>
<div>{`${user.first_name} ${user.last_name}`}</div>
</div>
))}
</div>
)
}
대체 ID 필드 지정
기본적으로 createEntityAdapter는 데이터의 entity.id 필드에 고유 ID가 있다고 가정합니다.
데이터 세트가 해당 ID를 다른 필드에 저장하는 경우 적절한 필드를 반환하는 selectId 인수를 전달할 수 있습니다.
// In this instance, our user data always has a primary key of `idx`
const userData = {
users: [
{ idx: 1, first_name: 'Test' },
{ idx: 2, first_name: 'Two' },
],
}
// Since our primary key is `idx` and not `id`,
// pass in an ID selector to return that field instead
export const usersAdapter = createEntityAdapter({
selectId: (user) => user.idx,
})
엔터티 정렬하기
createEntityAdapter는 상태의 ID 컬렉션을 정렬하는 데 활용할 수 있는 sortComparer 인수를 제공합니다.
이것은 정렬 순서를 보장하고 데이터가 미리 정렬되지 않은 경우에 매우 유용할 수 있습니다.
// In this instance, our user data always has a primary key of `id`, so we do not need to provide `selectId`.
const userData = {
users: [
{ id: 1, first_name: 'Test' },
{ id: 2, first_name: 'Banana' },
],
}
// Sort by `first_name`. `state.ids` would be ordered as
// `ids: [ 2, 1 ]`, since 'B' comes before 'T'.
// When using the provided `selectAll` selector, the result would be sorted:
// [{ id: 2, first_name: 'Banana' }, { id: 1, first_name: 'Test' }]
export const usersAdapter = createEntityAdapter({
sortComparer: (a, b) => a.first_name.localeCompare(b.first_name),
})
직렬화 불가능한 데이터로 작업하기
Redux의 핵심 사용 원칙 중 하나는 직렬화할 수 없는 값을 상태나 액션에 넣으면 안 된다는 것입니다.
(you should not put non-serializable values in state or actions)
직렬화 가능성 검사 미들웨어(serializability dev check middleware)는
액션이나 상태에서 직렬화할 수 없는 값을 감지할 때마다 자동으로 경고합니다.
실수로 직렬화 불가능한 값을 상태에 넣지 하지 않도록 이 미들웨어를 활성 상태로 두는 것이 좋습니다.
그러나 이러한 경고를 해제해야 하는 경우 특정 액션 타입 또는 액션 및 상태의 필드를 무시하도록 설정하여
미들웨어를 사용자 지정할 수 있습니다.
configureStore({
//...
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
// Ignore these action types
ignoredActions: ['your/action/type'],
// Ignore these field paths in all actions
ignoredActionPaths: ['meta.arg', 'payload.timestamp'],
// Ignore these paths in the state
ignoredPaths: ['items.dates'],
},
}),
})
Redux-Presist 사용하기
import { configureStore } from '@reduxjs/toolkit'
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import { PersistGate } from 'redux-persist/integration/react'
import App from './App'
import rootReducer from './reducers'
const persistConfig = {
key: 'root',
version: 1,
storage,
}
const persistedReducer = persistReducer(persistConfig, rootReducer)
const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}),
})
let persistor = persistStore(store)
ReactDOM.render(
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<App />
</PersistGate>
</Provider>,
document.getElementById('root')
)
import { PURGE } from "redux-persist";
...
extraReducers: (builder) => {
builder.addCase(PURGE, (state) => {
customEntityAdapter.removeAll(state);
});
}
const persistConfig = {
key: "root",
version: 1,
storage,
blacklist: [pokemonApi.reducerPath],
};
아래 게시물도 참조하면 도움이 됩니다.
- Redux Toolkit #121: How to use this with Redux-Persist?
- Redux-Persist #988: non-serializable value error
Use with React-Redux-Firebase
설정은 다음과 같습니다. (직렬화 가능성 검사 무시)
import { configureStore } from '@reduxjs/toolkit'
import {
getFirebase,
actionTypes as rrfActionTypes,
} from 'react-redux-firebase'
import { constants as rfConstants } from 'redux-firestore'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [
// just ignore every redux-firebase and react-redux-firebase action type
...Object.keys(rfConstants.actionTypes).map(
(type) => `${rfConstants.actionsPrefix}/${type}`
),
...Object.keys(rrfActionTypes).map(
(type) => `@@reactReduxFirebase/${type}`
),
],
ignoredPaths: ['firebase', 'firestore'],
},
thunk: {
extraArgument: {
getFirebase,
},
},
}),
})
export default store
'FrontEnd' 카테고리의 다른 글
[Babel] 바벨 플러그인을 작성하며 AST 배우기 (0) | 2022.09.14 |
---|---|
[typescript] d.ts 파일을 js 프로젝트에서 사용할 수 있을까? (3) | 2022.09.14 |
Redux Toolkit : Immer와 함께 사용하기 (0) | 2022.09.12 |
[번역] Mobx로 배우는 반응형 프로그래밍(reactive programming)의 원리 (0) | 2022.09.12 |
[30초 CSS] CSS 속성 상속과 !important는 무관하다 (0) | 2022.09.02 |