redux toolkit 관련 참고용 코드들
- REACT & NODE
- 2021. 5. 9.
toolkit-saga
https://velog.io/@suyeonme/React-Redux-toolkit에서-미들웨어-사용하기
toolkit 설명
https://kyounghwan01.github.io/blog/React/redux/redux-toolkit/#종합-예제
rn 과 toolkit 관련 블로그
https://post.naver.com/viewer/postView.nhn?volumeNo=29438367&memberNo=10070839
redux-toolkit 사용하면 combineReducer 와 devtools 를 사용하지 않아도 이미 적용되어 있다.
하지만 configureStore 에 devTools: process.env.NODE_ENV !== 'production', 이런 옵션으로 넣어줄 수 있다.
createSlice 에서
reducers 는 해당 액션함수의 이름으로 자동으로 액션도 생성되며 리듀서도 만들어준다.
extraReducers 의 경우 액션을 만들고 해당 액션의 type 값을 키로하는 리듀서를 만들어준다.
extraReducers 도 리듀서이다. 따라서 slice.caseReducers.xxx 를 중간중간에 호출(dispatch 하듯) 하는 것은 하면 안된다. 또한 side effect 도 들어가면 안된다. 즉, 순수함수 룰을 지켜주어야 한다. extreaReducers 의 builder.addCase 의 키값으로 액션의 타입값을 지정하는 것일 뿐 이것은 createAsyncThunk 와 같은 부수효과 처리용 단위 로직이 아니다. 그냥 리듀서이다.
www.youtube.com/watch?v=L6O1wsydyeg&list=PLM0LBHjz37LW0zVaEjpeCmw-WgglfXWnI&index=7
soyoung210.github.io/redux-toolkit/api/createEntityAdapter/
jbee.io/react/react-redux-normalize/
egghead.io/lessons/javascript-redux-normalizing-the-state-shape
github.com/gaearon/todos/tree/12-wrapping-dispatch-to-log-actions
redux.js.org/recipes/structuring-reducers/normalizing-state-shape
www.pluralsight.com/guides/deeply-nested-objectives-redux
stackoverflow.com/questions/64095957/redux-normalized-nested-data-organization-inside-createslice
const tags = data.reduce((prev, curr) => [...prev, curr.tags], []).flat()
const likes = data.reduce((prev, curr) => [...prev, curr.likes], []).flat()
const comments = data.map(({ id, body }) => ({ id, body }))
return { comments, likes, tags }
const mappedData = data.map((comment) => ({
...comment,
tags: comment.tags.map((tag) => ({ ...tag, commentId: comment.id })),
likes: comment.likes.map((like) => ({ ...like, commentId: comment.id })),
}));
const tags = mappedData.reduce((prev, curr) => [...prev, curr.tags], []).flat()
const likes = mappedData.reduce((prev, curr) => [...prev, curr.likes], []).flat()
const comments = mappedData.map(({ id, body, likes, tags }) => ({
id,
body,
likesIds: likes.map((like) => like.id),
tagsIds: tags.map((tag) => tag.id),
}))
return { comments, likes, tags }
import { schema } from "normalizr";
// definite 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]
});
import {
createSlice,
createEntityAdapter,
createAsyncThunk,
createSelector
} from "@reduxjs/toolkit";
import fakeAPI from "../../services/fakeAPI";
import { normalize } from "normalizr";
import { articleEntity } from "../../schemas";
const articlesAdapter = createEntityAdapter();
export const fetchArticle = createAsyncThunk(
"articles/fetchArticle",
async id => {
const data = await fakeAPI.articles.show(id);
// normalize the data so reducers can responded to a predictable payload, in this case: `action.payload = { users: {}, articles: {}, comments: {} }`
const normalized = normalize(data, articleEntity);
return normalized.entities;
}
);
export const slice = createSlice({
name: "articles",
initialState: articlesAdapter.getInitialState(),
reducers: {},
extraReducers: {
[fetchArticle.fulfilled]: (state, action) => {
articlesAdapter.upsertMany(state, action.payload.articles);
}
}
});
const reducer = slice.reducer;
export default reducer;
export const {
selectById: selectArticleById,
selectIds: selectArticleIds,
selectEntities: selectArticleEntities,
selectAll: selectAllArticles,
selectTotal: selectTotalArticles
} = articlesAdapter.getSelectors(state => state.articles);
export const selectCommentsByArticleId = articleId =>
createSelector(
[
state => selectArticleById(state, articleId), // select the current article
state => state.comments.ids.map(id => state.comments.entities[id]) // this is the same as selectAllComments
],
(article, comments) => {
// return the comments for the given article only
return Object.keys(comments)
.map(c => comments[c])
.filter(comment => article.comments.includes(comment.id));
}
);
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: {
[fetchArticle.fulfilled]: (state, action) => {
commentsAdapter.upsertMany(state, action.payload.comments);
}
}
});
const reducer = slice.reducer;
export default reducer;
export const {
selectById: selectCommentById,
selectIds: selectCommentIds,
selectEntities: selectCommentEntities,
selectAll: selectAllComments,
selectTotal: selectTotalComments
} = commentsAdapter.getSelectors(state => state.comments);
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) => {
usersAdapter.upsertMany(state, action.payload.users);
});
}
});
const reducer = slice.reducer;
export default reducer;
export const {
selectById: selectUserById,
selectIds: selectUserIds,
selectEntities: selectUserEntities,
selectAll: selectAllUsers,
selectTotal: selectTotalUsers
} = usersAdapter.getSelectors(state => state.users);
redux-toolkit.js.org/usage/usage-guide
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)
})
},
})
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
// 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: {
[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: {
[fetchArticle.fulfilled]: (state, action) => {
// Same for the comments
commentsAdapter.upsertMany(state, action.payload.comments)
},
},
})
const reducer = slice.reducer
export default reducer
// 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>
)
}
const todosSlice = createSlice({
name : 'todos',
initialState : todosInitialState,
reducers : {
create : {
reducer: (state, { payload } : PayloadAction<{id : string; desc: string; isComplete : boolean; }>) => {
state.push(payload);
},
prepare: ({ desc } : { desc : string; }) => ({
payload : {
id : uuid(),
desc,
isComplete : false
}
})
},
edit : (state, { payload }: PayloadAction<{ id : string; desc : string; }>) => {
const todoToEdit = state.find(todo => todo.id === payload.id);
if(todoToEdit) todoToEdit.desc = payload.desc;
},
toggle : (state, { payload }:PayloadAction<{ id : string; }>) => {
const todoToEdit = state.find(todo => todo.id === payload.id);
if(todoToEdit) todoToEdit.isComplete = !todoToEdit.isComplete;
},
remove : (state, { payload }:PayloadAction<{ id : string; }> ) =>{
const index = state.findIndex(todo => todo.id === payload.id);
if(index !== -1) state.splice(index, 1);
}
}
})
redux-toolkit.js.org/usage/usage-with-typescript
import { configureStore } from '@reduxjs/toolkit'
import additionalMiddleware from 'additional-middleware'
import logger from 'redux-logger'
// @ts-ignore
import untypedMiddleware from 'untyped-middleware'
import rootReducer from './rootReducer'
export type RootState = ReturnType<typeof rootReducer>
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.prepend(
// correctly typed middlewares can just be used
additionalMiddleware,
// you can also type middlewares manually
untypedMiddleware as Middleware<
(action: Action<'specialAction'>) => number,
RootState
>
)
// prepend and concat calls can be chained
.concat(logger),
})
export type AppDispatch = typeof store.dispatch
export default store
import { configureStore, MiddlewareArray } from '@reduxjs/toolkit'
configureStore({
reducer: rootReducer,
middleware: new MiddlewareArray().concat(additionalMiddleware, logger),
})
configureStore({
reducer: rootReducer,
middleware: [additionalMiddleware, logger] as const,
})
function isNumberValueAction(action: AnyAction): action is PayloadAction<{ value: number }> {
return typeof action.payload.value === 'number'
}
createReducer({ value: 0 }, builder =>
builder.addMatcher(isNumberValueAction, (state, action) => {
state.value += action.payload.value
})
})
const slice = createSlice({
name: 'test',
initialState: 0,
reducers: {
increment: (state, action: PayloadAction<number>) => state + action.payload,
},
})
// now available:
slice.actions.increment(2)
// also available:
slice.caseReducers.increment(0, { type: 'increment', payload: 5 })
type State = number
const increment: CaseReducer<State, PayloadAction<number>> = (state, action) =>
state + action.payload
createSlice({
name: 'test',
initialState: 0,
reducers: {
increment,
},
})
type SliceState = { state: 'loading' } | { state: 'finished'; data: string }
// First approach: define the initial state using that type
const initialState: SliceState = { state: 'loading' }
createSlice({
name: 'test1',
initialState, // type SliceState is inferred for the state of the slice
reducers: {},
})
// Or, cast the initial state as necessary
createSlice({
name: 'test2',
initialState: { state: 'loading' } as SliceState,
reducers: {},
})
const blogSlice = createSlice({
name: 'blogData',
initialState,
reducers: {
receivedAll: {
reducer(
state,
action: PayloadAction<Page[], string, { currentPage: number }>
) {
state.all = action.payload
state.meta = action.meta
},
prepare(payload: Page[], currentPage: number) {
return { payload, meta: { currentPage } }
},
},
},
})
interface MyData {
// ...
}
const fetchUserById = createAsyncThunk(
'users/fetchById',
// Declare the type your function argument here:
async (userId: number) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`)
// Inferred return type: Promise<MyData>
return (await response.json()) as MyData
}
)
// the parameter of `fetchUserById` is automatically inferred to `number` here
// and dispatching the resulting thunkAction will return a Promise of a correctly
// typed "fulfilled" or "rejected" action.
const lastReturnedAction = await store.dispatch(fetchUserById(3))
const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
// Optional fields for defining thunkApi field types
dispatch: AppDispatch
state: State
extra: {
jwt: string
}
}
>('users/fetchById', async (userId, thunkApi) => {
const response = await fetch(`https://reqres.in/api/users/${userId}`, {
headers: {
Authorization: `Bearer ${thunkApi.extra.jwt}`,
},
})
return (await response.json()) as MyData
})
interface MyKnownError {
errorMessage: string
// ...
}
interface UserAttributes {
id: string
first_name: string
last_name: string
email: string
}
const updateUser = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
UserAttributes,
// Types for ThunkAPI
{
extra: {
jwt: string
}
rejectValue: MyKnownError
}
>('users/update', async (user, thunkApi) => {
const { id, ...userData } = user
const response = await fetch(`https://reqres.in/api/users/${id}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${thunkApi.extra.jwt}`,
},
body: JSON.stringify(userData),
})
if (response.status === 400) {
// Return the known error for future handling
return thunkApi.rejectWithValue((await response.json()) as MyKnownError)
}
return (await response.json()) as MyData
})
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: {},
error: null,
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(updateUser.fulfilled, (state, { payload }) => {
state.entities[payload.id] = payload
})
builder.addCase(updateUser.rejected, (state, action) => {
if (action.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
state.error = action.payload.errorMessage
} else {
state.error = action.error
}
})
},
})
const handleUpdateUser = async (userData) => {
const resultAction = await dispatch(updateUser(userData))
if (updateUser.fulfilled.match(resultAction)) {
const user = resultAction.payload
showToast('success', `Updated ${user.name}`)
} else {
if (resultAction.payload) {
// Since we passed in `MyKnownError` to `rejectValue` in `updateUser`, the type information will be available here.
// Note: this would also be a good place to do any handling that relies on the `rejectedWithValue` payload, such as setting field errors
showToast('error', `Update failed: ${resultAction.payload.errorMessage}`)
} else {
showToast('error', `Update failed: ${resultAction.error.message}`)
}
}
}
interface Book {
bookId: number
title: string
// ...
}
const booksAdapter = createEntityAdapter<Book>({
selectId: (book) => book.bookId,
sortComparer: (a, b) => a.title.localeCompare(b.title),
})
const booksSlice = createSlice({
name: 'books',
initialState: booksAdapter.getInitialState(),
reducers: {
bookAdded: booksAdapter.addOne,
booksReceived(state, action: PayloadAction<{ books: Book[] }>) {
booksAdapter.setAll(state, action.payload.books)
},
},
})
function createSlice({
reducers : Object<string, ReducerFunction | ReducerAndPrepareObject>,
initialState: any,
name: string,
extraReducers?:
| Object<string, ReducerFunction>
| ((builder: ActionReducerMapBuilder<State>) => void)
})
soyoung210.github.io/redux-toolkit/tutorials/advanced-tutorial/
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Comment, getComments, Issue } from 'api/githubAPI'
import { AppThunk } from 'app/store'
interface CommentsState {
commentsByIssue: Record<number, Comment[] | undefined>
loading: boolean
error: string | null
}
interface CommentLoaded {
issueId: number
comments: Comment[]
}
const initialState: CommentsState = {
commentsByIssue: {},
loading: false,
error: null
}
const comments = createSlice({
name: 'comments',
initialState,
reducers: {
getCommentsStart(state) {
state.loading = true
state.error = null
},
getCommentsSuccess(state, action: PayloadAction<CommentLoaded>) {
const { comments, issueId } = action.payload
state.commentsByIssue[issueId] = comments
state.loading = false
state.error = null
},
getCommentsFailure(state, action: PayloadAction<string>) {
state.loading = false
state.error = action.payload
}
}
})
export const {
getCommentsStart,
getCommentsSuccess,
getCommentsFailure
} = comments.actions
export default comments.reducer
export const fetchComments = (issue: Issue): AppThunk => async dispatch => {
try {
dispatch(getCommentsStart())
const comments = await getComments(issue.comments_url)
dispatch(getCommentsSuccess({ issueId: issue.number, comments }))
} catch (err) {
dispatch(getCommentsFailure(err))
}
}
redux-toolkit.js.org/usage/usage-guide#use-with-redux-persist
import { configureStore, getDefaultMiddleware } 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({
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')
)
redux.js.org/tutorials/essentials/part-6-performance-normalization#optimizing-the-posts-list
// omit other imports
import {
selectAllPosts,
fetchPosts,
selectPostIds,
selectPostById
} from './postsSlice'
let PostExcerpt = ({ postId }) => {
const post = useSelector(state => selectPostById(state, postId))
// omit rendering logic
}
export const PostsList = () => {
const dispatch = useDispatch()
const orderedPostIds = useSelector(selectPostIds)
// omit other selections and effects
if (postStatus === 'loading') {
content = <div className="loader">Loading...</div>
} else if (postStatus === 'succeeded') {
content = orderedPostIds.map(postId => (
<PostExcerpt key={postId} postId={postId} />
))
} else if (postStatus === 'error') {
content = <div>{error}</div>
}
// omit other rendering
}
'REACT & NODE' 카테고리의 다른 글
테스트 json api 제공 서비스 (0) | 2021.05.10 |
---|---|
리덕스 스타일링 가이드 요약 (0) | 2021.05.10 |
access token, refresh token 그리고... jwt? 그냥 unique token (2) | 2021.05.02 |
redux-requests (0) | 2021.04.21 |
package-lock.json vs yarn.lock (0) | 2021.04.20 |