Я столкнулся с проблемой при настройке Redux в React Native, где у меня есть два редуктора: один для аутентификации, а другой для меню. В то время как редуктор аутентификации сохраняет данные во всем приложении без каких-либо проблем, у редуктора меню, похоже, есть проблема. После вызова действия getMenu() состояние меню сначала правильно заполняет данные, но затем сразу же становится неопределенным. Мне нужен совет о том, как устранить и решить эту проблему. Я хочу иметь возможность сохранять данные меню во всем приложении.
Код:
//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;
Вот как я обновляю состояние меню данными:
//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.info(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>
);
}
После вызова функции getMenuFunc()console.info(menu) правильно записывает данные меню с первого раза. Однако при последующих вызовах getMenuFunc() переменная menu становится undefined. Интересно, что при перезагрузке приложения через Expo и последующем вызове getMenuFunc() данные извлекаются снова, только чтобы снова стать undefined.
@DrewReese Спасибо за ваш отзыв. Я пересмотрел свой вопрос, чтобы внести больше ясности. Я действительно использую два отдельных хранилища Redux: одно для аутентификации, а другое для меню. Первоначальная выборка означает получение данных меню из API при инициализации приложения. Я внес некоторые изменения в вопрос и добавил соответствующий код.





Вы обернули корневой компонент PersistGate? если нет, то вам нужно обернуть его, потому что он задерживает рендеринг пользовательского интерфейса вашего приложения до тех пор, пока ваше постоянное состояние не будет получено и сохранено в Redux. в противном случае ваше приложение загрузится до того, как состояние будет сохранено в хранилище Redux, поэтому вы получите неопределенное значение при попытке получить доступ к состоянию, поскольку оно не существует в хранилище.
Пожалуйста, перейдите по ссылке ниже, чтобы узнать больше https://www.npmjs.com/package/redux-persist#:~:text=If%20you%20are%20using%20react%2C%20wrap%20your%20root%20comComponent% 20with%20PersistGate.%20Это%20задерживает%20%20рендеринг%20из%20вашего%20приложения%27s%20UI%20до тех пор, пока%20ваше%20сохраняется%20состояние%20%20было%20получено%20и%20сохранено%20в%20редукс.%20NOTE%20the% 20PersistGate%20loading%20prop%20can%20be%20null%2C%20или
Во-первых, мне кажется, что произошла путаница в использовании RTK :)
В своем фрагменте, под клавишей reducers, вы заново изобретаете колесо extraReducer + createAsyncThunk.
1-е решение (не самое лучшее, по моему мнению): не используйте createAsyncThunk
Вы можете написать свое мнение так.
Если вы выберете это решение, вы также можете удалить extraReducers.
Проблема в том, что действие, отправленное createAsyncThunk, конфликтует с тем, которое вы определили.
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));
}
};
Второе решение: RTK для победы
Если вы проверите документ createAsyncThunk, вы увидите, что сгенерировано 3 действия => то, которое вы определили под ключом extraReducers. Но что еще более важно, menu/getMenu/pending отправляется сразу, когда отправляется переходник, и menu/getMenu/fulfilled, когда не было ошибки.
Имея это в виду, у вас есть конфликт
Итак, у нас будет следующий код
// 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;
});
},
});
Ваше действие getMenu не возвращает никакого значения полезной нагрузки, поэтому действие setMenu отправляется, а состояние обновляется значением menu только для того, чтобы быть уничтожено действием getMenu.fulfilled, которое имеет неопределенное значение полезной нагрузки.
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;
});
},
});
Однако вы делаете больше работы для себя. Просто используйте действия thunk напрямую и убедитесь, что вы правильно возвращаете значения из действия getMenu.
Обновите getMenu, чтобы правильно вернуть меню как решенную/выполненную полезную нагрузку. Сделайте то же самое для ошибок.
Пример:
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
});
},
});
Асинхронные переходники следует использовать для обработки асинхронной логики, например вызовов API. Действия по диспетчеризации должны обрабатываться в разделе «extrareducers» среза, где вы определяете, как ваше состояние должно реагировать на различные действия.
//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
});
},
});
Судя по вашему описанию, неясно, используете ли вы только один магазин Redux или два магазина. Под «магазином» вы подразумеваете два редуктора: «auth» и «меню»? О какой начальной выборке вы говорите? Это не полный минимально воспроизводимый пример . Можете ли вы отредактировать, чтобы прояснить проблему и включить весь соответствующий код, с которым вы работаете?