React Native: редуктор становится неопределенным после первоначальной выборки

Я столкнулся с проблемой при настройке 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.

Судя по вашему описанию, неясно, используете ли вы только один магазин Redux или два магазина. Под «магазином» вы подразумеваете два редуктора: «auth» и «меню»? О какой начальной выборке вы говорите? Это не полный минимально воспроизводимый пример . Можете ли вы отредактировать, чтобы прояснить проблему и включить весь соответствующий код, с которым вы работаете?

Drew Reese 06.03.2024 19:09

@DrewReese Спасибо за ваш отзыв. Я пересмотрел свой вопрос, чтобы внести больше ясности. Я действительно использую два отдельных хранилища Redux: одно для аутентификации, а другое для меню. Первоначальная выборка означает получение данных меню из API при инициализации приложения. Я внес некоторые изменения в вопрос и добавил соответствующий код.

Brian 06.03.2024 19:48
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
2
104
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

Вы обернули корневой компонент 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
      });
  },
});

Другие вопросы по теме