Search code examples
react-nativereact-hooksredux-toolkit

Wait for redux-toolkit variable to update


What I want to do:

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.

How I'm trying to do it:

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.

The problem I encounter:

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.

More info context to the problem:

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:

  • Only call the dispatch function once in the parent component.
    • This isn't great as I only want the API to be called when the components are needed.
  • Wait until render by setting a delay (setTimeout)
    • You don't know the render duration so this isn't a great solution

I'm hoping there might be a better solution or that there is a mistake in my implementation.


Solution

  • 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;}});