ref – https://blog.logrocket.com/using-redux-toolkits-createasyncthunk/
Redux store possesses great state management features, but it doesn’t know how to deal with asynchronous logic.
Redux can’t handle asynchronous logic simply because it doesn’t know what you want to do with the data you fetched.
Middleware has since been used in Redux applications to perform asynchronous tasks.
Redux Thunk’s middleware being the most popular package.
With Redux Toolkit,
Redux Thunk is included by default.
This allows createAsyncThunk to perform delayed, asynchronous logic before sending the processed result to the reducers.
The Slice
We create the Redux slice via createSlice function.
A slice is a function that contains your store and reducer functions used to modify store data
Its name is posts, and we initialize it with some default data.
As you can see we create a getPosts that uses createAsyncThunk function. We initialize its action type string as ‘posts/getPosts’.
This is used in reducers in switch statements to catch the data to be stored.
Then we implement the async functionality.
We do our async feature in there and then return the data.
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 |
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit' const initialState = { entities: [], loading: false, } const getPosts = createAsyncThunk( 'posts/getPosts', //action type string async (thunkAPI) => { // callback function (called payloadCreator) const res = await fetch('https://jsonplaceholder.typicode.com/posts').then( (data) => data.json() ) return res }) export const postSlice = createSlice({ name: 'posts', initialState, reducers: {}, extraReducers: {}, }) export const postReducer = postSlice.reducer |
- Synchronous Requests – Within createSlice, synchronous requests made to the store are handled in the reducers object
- Async Requests – Within createSlice, extraReducers handles asynchronous requests, which is our main focus
Asynchronous requests created with createAsyncThunk accept three parameters:
- an action type string
- a callback function (referred to as a payloadCreator)
- and an options object
1 2 3 4 5 6 7 |
const getPosts = createAsyncThunk( 'posts/getPosts', //1 - action type string async (thunkAPI) => { // 2 - payloadCreator then executes to return either a result or an error. const res = await fetch('https://jsonplaceholder.typicode.com/posts').then((data) => data.json()) return res } ) |
ref – https://redux-toolkit.js.org/api/createAsyncThunk#payloadcreator
It’s important to note that payloadCreator accepts only two parameters:
- custom argument that may be used in your request
- thunkAPI is an object containing all of the parameters that are normally passed to a Redux Thunk function — like dispatch and getState. Take a look at all the acceptable parameters.
The payloadCreator function will be called with two arguments:
arg: a single value, containing the first parameter that was passed to the thunk action creator when it was dispatched. This is useful for passing in values like item IDs that may be needed as part of the request. If you need to pass in multiple values, pass them together in an object when you dispatch the thunk, like
1dispatch(fetchUsers({id: 52345, status: 'active', sortBy: 'name'})).thunkAPI: an object containing all of the parameters that are normally passed to a Redux thunk function, as well as additional other options:
- dispatch: the Redux store dispatch method
- getState: the Redux store getState method
For example, in this payloadCreator, we have the main object goalData, which has all the data packaged into an object, so that it can contain various properties and their values.
Then we have thunkAPI, which has getState, and dispatch. In our example, it uses the getState, and then uses the auth slice to access its user’s token value:
123456789async (goalData, thunkAPI) => {try {const token = thunkAPI.getState().auth.user.tokenreturn await goalService.createGoal(goalData, token)} catch (error) {// take care of errorreturn thunkAPI.rejectWithValue(message)}}Whenever the payloadCreator is dispatched from a component within our application, createAsyncThunk generates promise lifecycle action types using this string as a prefix:
pending: posts/getPosts/pending
fulfilled: posts/getPosts/fulfilled
rejected: posts/getPosts/rejectedThe three lifecycle action types mentioned earlier can then be evaluated in extraReducers, where we make our desired changes to the store. In this case, let’s populate entities with some data and appropriately set the loading state in each action type:
123456789101112131415161718192021export const postSlice = createSlice({name: 'posts',initialState,reducers: {},extraReducers: {[getPosts.pending]: (state) => {state.loading = true},[getPosts.fulfilled]: (state, { payload }) => {state.loading = falsestate.entities = payload},[getPosts.rejected]: (state) => {state.loading = false},},})Dispatching it in your UI
By using useSelector and useDispatch from react-redux, we can read state from a Redux store and dispatch any action from a component, respectively.
1234567891011121314151617181920212223mport { useEffect } from 'react'import { useDispatch, useSelector } from 'react-redux'import { getPosts } from '../redux/features/posts/postThunk'export default function Home() {const dispatch = useDispatch()const { entities, loading } = useSelector((state) => state.posts)useEffect(() => {dispatch(getPosts()) // HERE!}, [])if (loading) return <p>Loading...</p>return (<div><h2>Blog Posts</h2>{entities.map((post) => (<p key={post.id}>{post.title}</p>))}</div>)}Example
Given a fetch function in a service file.
We have a function that takes in data, and a token.
This function will be used in a payloadCreator.frontend/src/features/goals/goalService.js
123456789const createGoal = async (goalData, token) => {const config = {headers: {Authorization: `Bearer ${token}`,},}const response = await axios.post(API_URL, goalData, config)return response.data}Thus, we receive this data and token from thunkAPI:
frontend/src/features/goals/goalSlice.js
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253const initialState = {goals: [],isError: false,isSuccess: false,isLoading: false,message: '',}export const createGoal = createAsyncThunk('goals/create',// we implement the async function.async (goalData, thunkAPI) => {try {const token = thunkAPI.getState().auth.user.tokenreturn await goalService.createGoal(goalData, token)} catch (error) {const message =(error.response &&error.response.data &&error.response.data.message) ||error.message ||error.toString()return thunkAPI.rejectWithValue(message)}})export const goalSlice = createSlice({name: 'goal',initialState,reducers: {reset: (state) => initialState,},extraReducers: (builder) => { // we take care of the promise's status herebuilder.addCase(createGoal.pending, (state) => {state.isLoading = true}).addCase(createGoal.fulfilled, (state, action) => {state.isLoading = falsestate.isSuccess = truestate.goals.push(action.payload)}).addCase(createGoal.rejected, (state, action) => {state.isLoading = falsestate.isError = truestate.message = action.payload})},})export const { reset } = goalSlice.actionsexport default goalSlice.reducer1) So we import an action function from our slice.
2) We then dispatch that action.12345678910111213141516171819202122// we take in an action from our sliceimport { createGoal } from '../features/goals/goalSlice'function GoalForm() {const [text, setText] = useState('')const dispatch = useDispatch()const onSubmit = (e) => {e.preventDefault()// when the form is submitted, we dispatch createGoal.dispatch(createGoal({ text }))}return (/* JSX here */)}export default GoalForm