skip to Main Content

I am encountering a problem with my Redux setup in React Native where I have two reducers: one for authentication and another for menu. While the auth reducer persists data across the entire app without any issues, the menu reducer seems to have a problem. After the getMenu() action is called, the menu state initially populates data correctly, but then immediately becomes undefined. I’m seeking advice on how to troubleshoot and resolve this issue. I want to be able to persist the menu data throughout the entire application.

Code:

//store.js
import { configureStore, combineReducers } from "@reduxjs/toolkit";
import {
  persistStore,
  persistReducer,
} from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { combineReducers } from "redux";

import authReducer from "./auth/authReducer";
import menuReducer from "./menu/menuReducer";

const rootPersistConfig = {
  key: "root",
  storage: AsyncStorage,
  keyPrefix: "redux-",
  whitelist: [],
};

const rootReducer = combineReducers({
  auth: authReducer,
  menu: menuReducer,
});

const persistedReducer = persistReducer(rootPersistConfig, rootReducer);

export const store = configureStore({
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
  reducer: persistedReducer,
});

export const persistor = persistStore(store);
//menuReducer.js
import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  menu: null,
  loading: false,
  error: null,
};

const menuSlice = createSlice({
  name: "menu",
  initialState,
  reducers: {
     setLoading: (state, action) => {
      state.loading = action.payload;
    },
    setError: (state, action) => {
      state.error = action.payload;
      state.loading = false;
    },
    setMenu: (state, action) => {
      state.menu = action.payload;
      state.loading = false;
    },
    clearMenu: (state) => {
      state.menu = null;
    },
    clearError: (state) => {
      state.error = null;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase("menu/getMenu/pending", (state) => {
        state.loading = true;
      })
      .addCase("menu/getMenu/fulfilled", (state, action) => {
        state.loading = false;
        state.menu = action.payload;
      })
      .addCase("menu/getMenu/rejected", (state, action) => {
        state.loading = false;
        state.error = action.payload;
      });
  },
});
export const { setLoading, setError, setMenu, clearMenu, clearError } =
  menuSlice.actions;
export default menuSlice.reducer;

Here is how i update the menu state with data:

//menuAction.js
export const getMenu = createAsyncThunk(
  "menu/getMenu",
  async ({ storeNumber, token }, { dispatch }) => {
    try {
      dispatch(setLoading(true));
      const menu = await GetMenuApi(storeNumber, token);
      dispatch(setMenu(menu));
      dispatch(clearError());
      dispatch(setLoading(false));
    } catch (error) {
      dispatch(setError(error));
    }
  }
);
//App.js
export default function App(){
  const { menu } = useSelector((state) => state.menu);
  const dispatch = useDispatch();
  const storeNumber = 1;
  const token = 'abc'

  console.log(menu) //logs correct data then immediately logs undefined

  const getMenuFunc= () => {
    dispatch(getMenu({ storeNumber, token }));
  };

  return(
   <View style={{flex:1}}>
    <Button onPress={getMenuFunc} title="get menu"/>
   </View>
  );
}

After invoking the getMenuFunc() function, the console.log(menu) correctly logs the menu data the first time. However, upon subsequent invocations of getMenuFunc(), the menu variable becomes undefined. Interestingly, upon app reload via Expo and subsequent invocation of getMenuFunc(), the data is retrieved again, only to become undefined once more.

4

Answers


  1. Have you wrapped your root component with PersistGate? if not then you need to wrap it because it delays the rendering of your app’s UI until your persisted state has been retrieved and saved to redux. otherwise, your app will load before the state is saved to the redux store hence you will get undefined when you try to access the state because it does not exist on the store.

    Please check the link below for more https://www.npmjs.com/package/redux-persist#:~:text=If%20you%20are%20using%20react%2C%20wrap%20your%20root%20component%20with%20PersistGate.%20This%20delays%20the%20rendering%20of%20your%20app%27s%20UI%20until%20your%20persisted%20state%20has%20been%20retrieved%20and%20saved%20to%20redux.%20NOTE%20the%20PersistGate%20loading%20prop%20can%20be%20null%2C%20or

    Login or Signup to reply.
  2. First of all, I think that there is a confusion of RTK usage 🙂

    In your slice, under the reducers key, you’re re-inventing the wheel of the extraReducer + createAsyncThunk

    1st solution (not the best IMO) : don’t use createAsyncThunk

    You can write your thunk like that.
    If you choose this solution, you can also remove extraReducers.
    The issue is that action dispatched by createAsyncThunk are in conflict with the one you defined

    export const getMenu = async ({ storeNumber, token }) => ({ dispatch }) => {
        try {
          dispatch(setLoading(true));
          const menu = await GetMenuApi(storeNumber, token);
          dispatch(setMenu(menu));
          dispatch(clearError());
          dispatch(setLoading(false));
        } catch (error) {
          dispatch(setError(error));
        }
      };
    

    2nd solution : RTK for the win

    If you check the doc of createAsyncThunk, you’ll see that 3 action are generated => the one that you defined under extraReducers key. But more important, menu/getMenu/pending is dispatched directly when the thunk is dispatched and menu/getMenu/fulfilled when there was no error.

    With that in mind, you have a conflict

    So we’ll have the following code

    // Thunk is now super simple => put as much logic as possible in reducers ;)
    // https://redux.js.org/style-guide/#put-as-much-logic-as-possible-in-reducers
    export const getMenu = createAsyncThunk(
      "menu/getMenu",
      async ({ storeNumber, token }, { dispatch }) => {
          return await GetMenuApi(storeNumber, token);
      }
    );
    
    const menuSlice = createSlice({
      name: "menu",
      initialState,
      reducers: {
        clearMenu: (state) => {
          state.menu = null;
        },
      },
      extraReducers: (builder) => {
        // Tip : instead of string, you can pass the thunk.(pending|fulfilled|error)
        // in order to avoid typo
        builder
          .addCase(getMenu.pending, (state) => {
            state.loading = true;
          })
          .addCase(getMenu.fulfilled, (state, action) => {
            state.loading = false;
            // The `payload` is what was returned form the thunk
            state.menu = action.payload;
            
            // This was previously done by the `clearError` action
            state.error = null;
          })
          .addCase(getMenu.rejected, (state, action) => {
            state.loading = false;
            // If you don't use `rejectWithValue`, error will be under `error` key
            state.error = action.error;
          });
      },
    });
    
    Login or Signup to reply.
  3. Issue

    Your getMenu action isn’t returning any payload value, so the setMenu action is dispatched and the state is updated with the menu value, only to be wiped out by the getMenu.fulfilled action that has an undefined payload value.

    export const getMenu = createAsyncThunk(
      "menu/getMenu",
      async ({ storeNumber, token }, { dispatch }) => {
        try {
          dispatch(setLoading(true));
          const menu = await GetMenuApi(storeNumber, token);
          dispatch(setMenu(menu)); // (1) <-- has menu payload
          dispatch(clearError());
          dispatch(setLoading(false));
          // (2) <-- missing return implicitly returns undefined payload
        } catch (error) {
          dispatch(setError(error));
        }
      }
    );
    
    const menuSlice = createSlice({
      name: "menu",
      initialState,
      reducers: {
         setLoading: (state, action) => {
          state.loading = action.payload;
        },
        setError: (state, action) => {
          state.error = action.payload;
          state.loading = false;
        },
        setMenu: (state, action) => {
          state.menu = action.payload; // <-- (1) menu payload 🙂
          state.loading = false;
        },
        clearMenu: (state) => {
          state.menu = null;
        },
        clearError: (state) => {
          state.error = null;
        },
      },
      extraReducers: (builder) => {
        builder
          .addCase("menu/getMenu/pending", (state) => {
            state.loading = true;
          })
          .addCase("menu/getMenu/fulfilled", (state, action) => {
            state.loading = false;
            state.menu = action.payload; // (2) <-- undefined payload 🙁
          })
          .addCase("menu/getMenu/rejected", (state, action) => {
            state.loading = false;
            state.error = action.payload;
          });
      },
    });
    

    You are making more work for yourself though. Just use the thunk actions directly and ensure you properly return values from your getMenu action.

    Solution

    Update getMenu to correctly return the menu as a resolved/fulfilled payload. Do the same thing for errors.

    Example:

    export const getMenu = createAsyncThunk(
      "menu/getMenu",
      ({ storeNumber, token }, { rejectWithValue }) => {
        try {
          return GetMenuApi(storeNumber, token); // <-- return menu payload
        } catch (error) {
          rejectWithValue(error); // <-- return rejection value
        }
      }
    );
    
    const menuSlice = createSlice({
      name: "menu",
      initialState,
      reducers: {
        ...
      },
      extraReducers: (builder) => {
        builder
          .addCase(getMenu.pending, (state) => {
            state.loading = true;
          })
          .addCase(getMenu.fulfilled, (state, action) => {
            state.loading = false;
            state.menu = action.payload; // <-- menu payload 😁
            state.error = null;
          })
          .addCase(getMenu.rejected, (state, action) => {
            state.loading = false;
            state.error = action.payload; // <-- menu fetch error
          });
      },
    });
    
    Login or Signup to reply.
  4. Async thunks should be used for handling asynchronous logic such as making API calls. Dispatching actions should be handled in the "extrareducers" section of the slice where you define how your state should respond to different actions.

     //Define the async thunk to fetch menuData
    export const getMenu = createAsyncThunk(
      'menu/getMenu',
          async ((storeNumber, token), { dispatch }) => {
            // Async logic, API call
            try {
             const menu = await GetMenuApi(storeNumber, token);
              return menu; // Return data to be used in the reducer
            } catch (error) {
              throw error; // Re-throw the error to handle it in the rejected action
            }
          }
        );
    
    //App.js
        const menuSlice = createSlice({
      name: 'menu',
      initialState,
      reducers: {
        // Add any synchronous actions here
      },
      extraReducers: (builder) => {
        builder
          .addCase(getMenu.pending, (state) => {
            state.loading = true;
            state.error = null; // Clear any existing error
          })
          .addCase(getMenu.fulfilled, (state, action) => {
            state.loading = false;
            state.menu = action.payload; // Update the state with the fetched data
          })
          .addCase(getMenu.rejected, (state, action) => {
            state.loading = false;
            state.error = action.error.message; // Update the error state with the error message
          });
      },
    });
    

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search