Questions about using createAysncThunk

Viewed 935

I am new to redux and redux toolkit. I am reading this tutorial from redux toolkit official document. https://redux-toolkit.js.org/api/createAsyncThunk. This is an example it provides

import { createAsyncThunk, createSlice, unwrapResult } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, { getState, requestId }) => {
    const { currentRequestId, loading } = getState().users
    if (loading !== 'pending' || requestId !== currentRequestId) {
      return
    }
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

const usersSlice = createSlice({
  name: 'users',
  initialState: {
    entities: [],
    loading: 'idle',
    currentRequestId: undefined,
    error: null
  },
  reducers: {},
  extraReducers: {
    [fetchUserById.pending]: (state, action) => {
      if (state.loading === 'idle') {
        state.loading = 'pending'
        state.currentRequestId = action.meta.requestId
      }
    },
    [fetchUserById.fulfilled]: (state, action) => {
      const { requestId } = action.meta
      if (state.loading === 'pending' && state.currentRequestId === requestId) {
        state.loading = 'idle'
        state.entities.push(action.payload)
        state.currentRequestId = undefined
      }
    },
    [fetchUserById.rejected]: (state, action) => {
      const { requestId } = action.meta
      if (state.loading === 'pending' && state.currentRequestId === requestId) {
        state.loading = 'idle'
        state.error = action.error
        state.currentRequestId = undefined
      }
    }
  }
})

const UsersComponent = () => {
  const { users, loading, error } = useSelector(state => state.users)
  const dispatch = useDispatch()

  const fetchOneUser = async userId => {
    try {
      const resultAction = await dispatch(fetchUserById(userId))
      const user = unwrapResult(resultAction)
      showToast('success', `Fetched ${user.name}`)
    } catch (err) {
      showToast('error', `Fetch failed: ${err.message}`)
    }
  }

  // render UI here
}

my question is, since we have handled the case where the promise rejects in the lifecycle action creator i.e.

[fetchUserById.rejected]: (state, action) => {
      const { requestId } = action.meta
      if (state.loading === 'pending' && state.currentRequestId === requestId) {
        state.loading = 'idle'
        state.error = action.error
        state.currentRequestId = undefined
      }
    }

Then why is that, in UsersComponent, we still need to try and catch the error thrown by the dispatch.Can't we tell if the request failed or not from the error state? To be clearer, what about I write the UsersComponent this following way:


const UsersComponent = () => {
  const { users, loading, error } = useSelector(state => state.users)
  const dispatch = useDispatch()

  useEffect(() => {
    dispatch(fetchUserById(userId))
  },[dispatch])

  if(error) {
    showToast('error', `Fetch failed: ${error.message}`)
  } else if(loading) {
    showToast('loading')
  } else {
    showToast('success', `Fetched ${user.name}`)
  }

  // render UI here
}

Additionally, I wonder when the fetchUserById.rejected would be dispatched? According to the doc, createAsyncThunk will always return a resolved promise. If we hit this condition in the callback of createAsyncThunk

if (loading !== 'pending' || requestId !== currentRequestId) {
      return
    }

then the lifecycle action gets dispatched would be fulfilled right? What does it take for the rejected lifecyle action gets dispatched? Only when an exception is thrown inside of the callback?

My last question is, if I'd like to check if the data I got back is of the right shape, for example, I want to make sure if has a property called data on it. How can I achieve that? If I rewrite the callback in createAsyncThunk to be


const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, { getState, requestId }) => {
    const { currentRequestId, loading } = getState().users
    if (loading !== 'pending' || requestId !== currentRequestId) {
      return
    }
    const response = await userAPI.fetchById(userId)
    if(!response.data) throw new Error('No data')
    return response.data
  }
)

will the rejected lifecycle action gets dispatched when there is actually no data property on the response?

1 Answers

As for the first part of your question: Yes, you could do that on a subsequent render. But if you just write

  if(error) {
    showToast('error', `Fetch failed: ${error.message}`)
  } else if(loading) {
    showToast('loading')
  } else {
    showToast('success', `Fetched ${user.name}`)
  }

that would show a toast on every subsequent render of that component. And in React, that component will re-render every time a parent is rendered, so you will probably see a lot of Toasts.

You can of course also use useEfect for that and then do it that way, if you like that better. That's just a style thing, some people like one way better, some the other.

A rejected action would for example be dispatched when userAPI.fetchById would throw an error, maybe due to network problems.

If just return is called, it will actually return undefined and lead to a fulfilled action with undefined as payload.

Related