redux toolkit 관련 참고용 코드들

반응형

 

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 와 같은 부수효과 처리용 단위 로직이 아니다. 그냥 리듀서이다.

 

 

 

codesandbox.io/s/rtk-entities-basic-example-with-normalizr-bm3ie?file=/src/features/articles/articlesSlice.js

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

 

Redux - Normalized nested data organization inside createSlice

I have a deeply nested data object that comes back from my API which looks like the JSON below. I am using Redux toolkit's createSlice to create a slice of a trip So currently in my createSlice, I ...

stackoverflow.com

 

 

 

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
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY