slevin
slevin

Reputation: 3888

How to properly get when the redux dispatch is done, to then render a Component

I use react 18.2.0, redux 4.2.0 and redux thunk

I want to wait until my dispatch is done, so I can take the data it got from the server and render a component with that data

I try to do the following in my component:

import store from '../redux/store'; 

const featureInfoA = useSelector(featureInfo) 
const featureErrorA = useSelector(featureError) 
const featureStatusA = useSelector(featureStatus) 
    
const clickFeatureFromList = (id) =>  { 
  //start dispatching
  dispatch(fetchFeatureInfoById({ id, category })) 
    .then((e) => {
      console.log('BINGO THEN ', featureInfoA, featureErrorA, featureStatusA);
    })
}

//check store when a specific state changed and then get its data and render a component
store.subscribe(() => {
  const state = store.getState()
  if (state.features.status == 'succeeded' || state.features.status == 'failed') {
    console.log('BINGO SUBSCRIBE ', featureInfoA);
    accordionDetailsRootGlobal.render(<ContextInfo featureData={featureInfoA} />);
  } 
})

The issue is that in my console I see:

I think that the log of the store should be first

Why I see "BINGO SUBSCRIBE a" that is inside the store.subscribe multiple times?

Why I never see the final data that came from the server? "BINGO THEN a null idle" contains the initial state, even though "DATA from store" brought back data.

By the way, this my store, for features

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

const FEATURE_URL = 'http://localhost:3500/map'

//status : idle, loading, succeeded, failed
const initialState = {
    info:'a',
    status:'idle',
    error:null
}

export const fetchFeatureInfoById = createAsyncThunk('feature/fetchInfoById', (originalData) => {
    console.log('fetchFeatureInfoById originalData ', originalData);
    fetch(FEATURE_URL + '/feature/' + originalData.id + '/' + originalData.category)
    .then((response) => response.json())
    .then((data) => {
        console.log('DATA from store ', data);
        return data;
    })
    .catch((error) => {
        return error.message;
    });
})

export const featuresSlice = createSlice({
    name: 'features',
    initialState,
    reducers: { 
        featureInfoById: {
            reducer(state, action) {
                //do something with the id 
                console.log('feature state ', ' -- state : ',state.status, ' -- action : ', action);
            },
            prepare(category, id) {
                return{
                    payload:{
                        category,
                        id
                    }
                }
            }
        }
    },
    extraReducers(builder) {
        builder
        .addCase(fetchFeatureInfoById.pending, (state, action) => {
            state.status = 'loading'
        })
        .addCase(fetchFeatureInfoById.rejected, (state, action) => {
            state.status = 'failed'
            state.error = action.error.message
        })
        .addCase(fetchFeatureInfoById.fulfilled, (state, action) => {
            console.log('fulfilled data store ', state, action);
            state.status = 'succeeded'
            state.info = action.palyload
        })
    }
  }) 
   
  export const featureInfo = (state) => state.features.info;
  export const featureError = (state) => state.features.error;
  export const featureStatus = (state) => state.features.status;
  export const {featureInfoById} = featuresSlice.actions
  export default featuresSlice.reducer

I just want to get the data after the dispatch is done, to render a component with them. Please help me understand what I am doing wrong and how can I fix that.

Upvotes: 1

Views: 1233

Answers (3)

Berci
Berci

Reputation: 3386

Why I see "BINGO SUBSCRIBE a" that is inside the store.subscribe multiple times?

The docs mention:

Adds a change listener. It will be called any time an action is dispatched, and some part of the state tree may potentially have changed.

So basically each time any action is dispatched and any part of the state tree might have been changed it will be triggered. This refers to your entire store (not just featuresSlice).

Another potential reason is that you are subscribing each time the component is mounted, but never unsubscribing (so you might have multiple subscriptions at the same time). If you really want to keep this, a fix would be to subscribe inside an useEffect unsubscribe on unmount (something like):

useEffect(() => {
  const unsubscribeFn = store.subscribe(() => {...})
  return unsubscribeFn
}

Why I never see the final data that came from the server? "BINGO THEN a null idle" contains the initial state, even though "DATA from store" brought back data.

This is most likely because your fetchFeatureInfoById is not returning anything (please note that the return data; part is inside a callback, but the main function doesn't return anything) so in your extra reducer action.payload has no value.

So what you want to do in order to be able to properly set the slice state from the slice extra reducer is returning the fetch result from the fetchFeatureInfoById function. So basically your function would look something like:

export const fetchFeatureInfoById = createAsyncThunk('feature/fetchInfoById', (originalData) => {
    console.log('fetchFeatureInfoById originalData ', originalData);
    // Add the return keyword before the fetch from the next line
    return fetch(FEATURE_URL + '/feature/' + originalData.id + '/' + originalData.category)
    .then((response) => response.json())
    .then((data) => {
        console.log('DATA from store ', data);
        return data;
    })
    .catch((error) => {
        return error.message;
    });
})

PS: Without a reproducible example is not 100% this will fix all your problems, but it should at the very least point you in the right direction.


Extra suggestions:

  1. (related to first answer): I think you can at least use an useEffect? It would be way more efficient to only trigger the an effect when accordionDetailsRootGlobal changes or featureInfoA (so basically have an useEffect with this 2 dependencies and render the ContextInfo when one of this 2 change, rather than rendering on every store update). Something like:
  useEffect(() => {
    if (featureInfoA)
      accordionDetailsRootGlobal.render(<ContextInfo featureData={featureInfoA} />)
  }, [accordionDetailsRootGlobal, featureInfoA])
  1. inside fetchFeatureInfoById on the catch you don't throw any error. This will prevent .addCase(fetchFeatureInfoById.rejected ... from being called (since, even if your fetch fails, the function won't throw any error). So you should probably remove the catch part, or throw an error inside it.

Upvotes: 2

Sepanta
Sepanta

Reputation: 146

I Think this might be a better approach to deal with dispatch when you want to observe your data after you request successfully handled

   const clickFeatureFromList =async (id) =>  { 
    
      //start dispatching
     const result = await dispatch(fetchFeatureInfoById({ id, category }))
     if (fetchFeatureInfoById.fulfilled.match(result)) { 
     const {payload} = result
     console.log('BINGO THEN ', featureInfoA, featureErrorA, featureStatusA);

   }
    }

Upvotes: 0

LMG
LMG

Reputation: 96

You don't need to do the store.subscribe thing.

The selector is supposed to update and it will cause a render of your component. So, initially featureInfoA is undefined but after the promise is resolved it will update the store which will trigger a render in your component

import store from '../redux/store'; 

const featureInfoA = useSelector(featureInfo) 
const featureErrorA = useSelector(featureError) 
const featureStatusA = useSelector(featureStatus) 
    
const clickFeatureFromList =(id) =>  { 
  //start dispatching
  dispatch(fetchFeatureInfoById({id, category})) 
}

// add some logic here to check if featureInfoA has value
if(featureErrorA) return <div>Error</div>

if(featureStatusA === "loading") return <div>Loading...</div>

if(featureInfoA) return <div>{featureInfoA}</div>

Upvotes: 0

Related Questions