Search code examples
reactjsasynchronousreduxstate

How to use async React-Redux state properly


I fetched some data from my api by react-redux. My problem is that, since it is async I have to wait for the state to update its inital value in order to use them in the app. For example I have to use

products && products.length && products[n].img

syntax not to get undefined error when I try to access the fetched data. But when I use them at the first render just as

products[n].img

the app gives undefined as it should because redux fetches the data asynchronously. How can I bypass these steps so that I can use my desired state immediately?

React code

import React, { useEffect } from "react";
import {useDispatch, useSelector} from 'react-redux'
import { listPoduct } from "../actions/productActions";

const Examples = () => {

    const dispatch = useDispatch()

    const productList = useSelector(state => state.productList)
    const {loading, error, products} = productList

    useEffect(()=>{
        dispatch(listPoduct())
    },[dispatch])

    console.log(products && products.length && products[0].img)

    return(
        <div>
          ...
        </div>
    )
}

export default Examples

Action

export function listPoduct() {
    return (dispatch) => {
        const baseUrl = "/api/images"
        fetch(`${baseUrl}`)
            .then(res => res.json())
            .then(res => {
                dispatch({
                    type: PRODUCT_LIST_SUCCESS,
                    payload: res
                })
            })
    }
}

Reducer

export const productListReducer = (state = { products: [] }, action) => {
    switch (action.type) {
        case PRODUCT_LIST_REQUEST:
            return {loading:true, products:[]}
        case PRODUCT_LIST_SUCCESS:
            return {loading:false, products: action.payload}
        case PRODUCT_LIST_FAIL:
            return {loading:false, error: action.payload}
        default:
            return state
    }
}

Store

import {createStore, combineReducers, applyMiddleware} from 'redux' 
import thunk from 'redux-thunk'
import {composeWithDevTools} from 'redux-devtools-extension' 
import {productListReducer, productDetailsReducer} from './reducers/productReducer'

const reducer = combineReducers({
    productList: productListReducer,
    productDetails: productDetailsReducer
})

const initialState = {}

const middleware = [thunk]

const store = createStore(
    reducer,
    initialState,
    composeWithDevTools(applyMiddleware(...middleware))
  )

export default store 

Solution

  • The short answer is that you cannot. Sadly. Your request is asynchronous, so there's just no data available immediately. In your particular case, my advice would be to render some kind of spinner-loader conditionally (if loading is set to true) and only the loader. In this case, if you have loading set to true, you will not reach the place where you can actually read the data (you will render-and-return before). And once loading switches back to false, you can now display the data safely as the request is finished and the data is in the right place.

    The same applies to the failure state (as there's also no data available if the request failed).

    Here's your modified code (as an example):

    const Examples = () => {
    
        const dispatch = useDispatch()
    
        const productList = useSelector(state => state.productList)
        const {loading, error, products} = productList
    
        useEffect(()=>{
            dispatch(listPoduct())
        },[dispatch]);
    
        if (loading) {
            return (<div>Loading...</div>);
        }
    
        if (error) {
            return (<div>Error: {error}</div>);
        }
    
        console.log(products && products.length && products[0].img)
    
        return(
            <div>
              ...
            </div>
        )
    }