ref –
- https://blog.logrocket.com/using-redux-toolkits-createasyncthunk/
- https://redux-toolkit.js.org/api/createAsyncThunk
Within createSlice, synchronous requests made to the store are handled in the reducers object while extraReducers handles asynchronous requests, which is our main focus.
For example…with reducers
They are functions that receive the current state, and an action object that was dispatched.
src/features/posts/postsSlice.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const postsSlice = createSlice({ name: 'posts', initialState, reducers: { reactionAdded(state, action) { const { postId, reaction } = action.payload const existingPost = state.entities[postId] if (existingPost) { existingPost.reactions[reaction]++ } }, increaseCount(state, action) { console.log('postsSlice.js', 'increaseCount'); state.count = state.count + 1 } }, ... ... ... |
We export our reducers from postsSlice.actions so others can use it to dispatch.
src/features/posts/postsSlice.js
1 |
export const { increaseCount, reactionAdded } = postsSlice.actions |
In the UI layer, they are dispatched like so:
src/features/posts/ReactionButtons.js
1 |
dispatch(reactionAdded({ postId: post.id, reaction: name })) |
On the other hand…what if we want the async version of it?
Asynchronous requests created with createAsyncThunk accept three parameters:
- an action type string
- a callback function (referred to as a payloadCreator)
- options object
Taking the previous code block as a Redux store for a blog application, let’s examine getPosts:
We have our initial state.
1 2 3 4 5 |
const initialState = { status: 'idle', //'idle' | 'loading' | 'succeeded' | 'failed' error: null, count: 0 }) |
1 2 3 4 5 6 |
// these get dispatched like so: store.dispatch(fetchPosts()) export const fetchPosts = createAsyncThunk('posts/fetchPosts', async () => { const response = await axios.get(POSTS_URL) return response.data }) |
posts/fetchPosts is the action type string in this case.
Whenever this function is dispatched from a component within our application, createAsyncThunk generates promise lifecycle action types using this string as a prefix:
- pending: posts/fetchPosts/pending
- fulfilled: posts/fetchPosts/fulfilled
- rejected: posts/fetchPosts/rejected
On its initial call, createAsyncThunk dispatches the posts/getPosts/pending lifecycle action type. The payloadCreator then executes to return either a result or an error.
In the event of an error, posts/getPosts/rejected is dispatched and createAsyncThunk should either return a rejected promise containing an Error instance, a plain descriptive message, or a resolved promise with a RejectWithValue argument as returned by the thunkAPI.rejectWithValue function (more on thunkAPI and error handling momentarily).
If our data fetch is successful, the posts/getPosts/fulfilled action type gets dispatched.
src/features/posts/postsSlice.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
const postsSlice = createSlice({ name: 'posts', initialState, reducers: { // matched with createAction ... ... }, extraReducers(builder) { // matched with createAsyncThunk builder .addCase(fetchPosts.pending, (state, action) => { // pending state.status = 'loading' }) .addCase(fetchPosts.fulfilled, (state, action) => { // fulfilled state.status = 'succeeded' // Adding date and reactions let min = 1; const loadedPosts = action.payload.map(post => { // do business logic here return post; }); // Add any fetched posts to the array postsAdapter.upsertMany(state, loadedPosts) }) .addCase(fetchPosts.rejected, (state, action) => {. // rejected state.status = 'failed' state.error = action.error.message }); ... ... export const getPostsStatus = (state) => state.posts.status; |
Notice that we change the status property of state. How and where is state used?
First, notice getPostsStatus. In PostsList, we call useSelector on getPostsStatus.
useSelector is simply a hook to access Redux store’s state. This hook takes a selector function (getPostsStatus) as an argument. The selector is called with the store state.
In our case, it is state.posts.
Furthermore since we want to look at status property, then we’re accessing state.posts.status.
In other words, if state.posts.status === ‘loading’…
src/features/posts/PostsList.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
const PostsList = () => { const postStatus = useSelector(getPostsStatus); let content; if (postStatus === 'loading') { content = <p>"Loading..."</p>; } return ( <section> {content} </section> ) } export default PostsList |