Reputation: 3888
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:
BINGO SUBSCRIBE a
multiple timesDATA from store
that is inside the storeBINGO THEN a null idle
that is in the then
after calling dispatchI 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
Reputation: 3386
Why I see "BINGO SUBSCRIBE a" that is inside the store.subscribe multiple times?
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:
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])
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
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
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