I want to render 4 components in a parent component. How they look like and what they do depends on the settings. The settings can be retrieved from the backend with an API.
There is a redux store that handles the API request to fetch the settings. Once the data is fetched (once) then the fetched data can be used for all 4 components. The fetching happens when a component is rendered.
All 4 components call the fetch API (it should only be called once) because the redux variable (fetchStatus) only updates at the end of a render and the components don't know (until next render) that the fetchStatus is already set to "loading". The components still read that the fetchstatus is "idle" and thus call the fetch API as well.
I have a (simplified) slice:
interface settingsState {
settings: Settings;
fetchStatus: CallStatus;
fetchError: null | any;
}
const _initialState: settingsState = {
userSettings: null,
fetchStatus: CallStatus.IDLE,
fetchError: null,
};
export const activeSettingsSlice = createSlice({
name: ‘settings',
initialState: _initialState,
reducers: {},
extraReducers(builder) {
builder.addCase(fetchSettings.pending, (state, _) => {
state.fetchStatus = CallStatus.LOADING;
console.log('loading settings');
console.log('fetchstatus after setting in redux:',state.fetchStatus);
});
builder.addCase(fetchActiveSettings.fulfilled, (state, action) => {
state.fetchStatus = CallStatus.SUCCESS;
state.settings = action.payload.settings;
console.log('fetch settings success');
});
builder.addCase(fetchActiveSettings.rejected, (state, action) => {
state.fetchStatus = CallStatus.FAIL;
state.fetchError = action.payload;
});
},
});
export const fetchSettings = createAsyncThunk('settings/fetch', async (_, thunkApi) => {
new SettingsApi(…).getSettings(…)
);
});
And I have four components like this rendered in the same parent component at the same time:
export default function DemoComponent() {
const fetchStatus = useSelector((state) => state.settings.fetchStatus);
const fetchError = useSelector((state) => state.settings.fetchError);
const dispatch = useDispatch();
useEffect(() => {
if (fetchStatus === CallStatus.IDLE) {
console.log('fetchstatus when should be idle:', fetchStatus);
dispatch(fetchSettings());
}
}, [fetchStatus, dispatch]);
useEffect(() => {
console.log('status fetchstatus:', fetchStatus);
}, [fetchStatus]);
return <></>;
}
However, in my backend I see that the API is called three times. The API should only be called once (because if it's fetched once, all the other components can read the fetched data (settings)). If I check the console ouput of react-native I see this:
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0 // we just set this variable to 1, and it’s still 0 (so it hasn’t rendered yet right?)
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0
LOG fetchstatus when should be idle: 0
LOG loading settings
LOG fetchstatus after setting in redux: 1
LOG status fetchstatus: 0
LOG status fetchstatus: 1
LOG status fetchstatus: 1
LOG status fetchstatus: 1
LOG status fetchstatus: 1
LOG fetch settings success
LOG status fetchstatus: 2
LOG status fetchstatus: 2
LOG status fetchstatus: 2
LOG status fetchstatus: 2
LOG fetch settings success
LOG fetch settings success
LOG fetch settings success
To me it seems clear that fetchStatus in DemoComponent has yet to render. So these are the possible solutions I see:
I'm hoping there might be a better solution or that there is a mistake in my implementation.
Thanks to the comment from @Linda Paiste, AsyncThunk has a third optional variable that allows you to add a condition to the asyncThunk where you can also read the Rootstate variables from the slice. See also https://github.com/reduxjs/redux-toolkit/pull/513
The new asyncThunk:
export const fetchSettings = createAsyncThunk('settings/fetch', async (_, thunkApi) => {
new SettingsApi(…).getSettings(…)
},{condition(args,{getState})=>{return (getState as Rootstate).settings.fetchStatus !== CallStatus.LOADING;}});