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.
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.
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
});
},
});