如何将 Redux Toolkit 与多个 React App 实例一起使用?

How do I use Redux Toolkit with multiple React App instances?

我们已经通过 Redux Toolkit 使用 Redux 编写了一个 React 应用程序。到目前为止还好。 现在,React 应用程序将在同一页面上呈现为多个不同的元素(每个元素都将获得一个新的应用程序实例)。 渲染部分很简单:我们只需为每个元素调用 ReactDOM.render(...)。 Redux 部分再次让人头疼。 要为每个应用程序实例创建一个新的 Redux 存储实例,我们为每个 React 应用程序实例调用 configureStore 函数。我们的切片看起来与此类似:

import { createSlice } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'

// Define a type for the slice state
interface CounterState {
  value: number
}

// Define the initial state using that type
const initialState: CounterState = {
  value: 0,
}

const counterSlice = createSlice({
  name: 'counter',
  // `createSlice` will infer the state type from the `initialState` argument
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    }
  },
});

export const increment = (): AppThunk => async (
  dispatch: AppDispatch
) => {
  dispatch(indicatorsOrTopicsSlice.actions.increment());
};

export const decrement = (): AppThunk => async (
  dispatch: AppDispatch
) => {
  dispatch(indicatorsOrTopicsSlice.actions.decrement());
};

// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counter.value

export default counterSlice.reducer

请注意,目前我们只静态创建和导出每个切片一次。这是我的第一个问题:这在创建多个商店实例时是否真的有效,或者我们是否真的需要为每个 app/store 实例创建新的切片实例? 对于提供的简单计数器示例,不这样做似乎可行,但是一旦我们像下面的示例那样使用 AsyncThunk,整个事情就会中断。

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'

// First, create the thunk
const fetchUserById = createAsyncThunk(
  'users/fetchByIdStatus',
  async (userId, thunkAPI) => {
    const response = await userAPI.fetchById(userId)
    return response.data
  }
)

// Then, handle actions in your reducers:
const usersSlice = createSlice({
  name: 'users',
  initialState: { entities: [], isLoading: false, hasErrors: false },
  reducers: {
    // standard reducer logic, with auto-generated action types per reducer
  },
  extraReducers: (builder) => {
    builder.addCase(fetchUserById.pending, (state, action) => {
      state.isLoading = true;
    });
    builder.addCase(fetchUserById.rejected, (state, action) => {
      state.isLoading = false;
      state.hasErrors = true;
    });
    builder.addCase(fetchUserById.fulfilled, (state, action) => {
      // Add user to the state array
      state.entities.push(action.payload);
      state.isLoading = false;
      state.hasErrors = true;
    });
  },
});

我认为中断是从这里开始的,因为调度 AsyncThunk 所触发的事件之间存在相互差异。 因此,我认为解决方案是为每个 app/store/slice 实例调用 createAsyncThunk 函数。这样做有什么最佳做法吗?当然,这破坏了静态导出的美观和功能,并且需要某种映射,因此我要问。

我最初怀疑 AsyncThunk 部分是造成不同 React 应用程序实例的商店之间的干扰的原因是错误的。 在我的问题中提供的示例中,来源是不可见的。 我们通过 reselect 中的 createSelector 使用记忆化选择器。这些是静态创建和导出的,这在处理多个 store/app 实例时实际上是一个问题。这样所有实例都使用相同的记忆选择器,这再次无法正常工作,因为在最坏的情况下,依赖选择器的存储值来自另一个 store/app 实例的使用。这又会导致无休止的重新渲染和重新计算。

我想到的解决方案是为每个应用程序实例重新创建记忆选择器。因此,我为每个永久存储在相关 Redux 存储中的应用程序实例生成一个唯一的 ID。在为应用程序实例创建商店时,我还创建了新的记忆选择器实例并将它们存储在一个对象中,该对象使用 appId 作为键存储在静态字典中。 为了在我们的组件中使用记忆选择器,我写了一个钩子,它使用 React.memo:

import { useMemo } from "react";
import { useSelector } from "react-redux";
import { selectAppId } from "../redux/appIdSlice";
import { getMemoizedSelectors } from "../redux/memoizedSelectors";

// Hook for using created memoized selectors
export const useMemoizedSelectors = () => {
  const appId = useSelector(selectAppId);
  const allMemoizedSelectors = useMemo(() => {
    return getMemoizedSelectors(appId);
  }, [appId]);

  return allMemoizedSelectors;
};

然后可以像这样在组件中使用选择器:

function MyComponent(): ReactElement {
  const {
    selectOpenTodos,
  } = useMemoizedSelectors().todos;
  const openTodos = useSelector(selectOpenTodos);
  // ...
}

相关的字典和查找过程如下所示:

import { createTodosMemoizedSelectors } from "./todosSlice";

/**
 * We must create and store memoized selectors for each app instance on its own,
 * else they will not work correctly, because memoized value would be used for all instances.
 * This dictionary holds for each appId (the key) the related created memoized selectors.
 */
const memoizedSelectors: {
  [key: string]: ReturnType<typeof createMemoizedSelectors>;
} = {};

/**
 * Calls createMemoizedSelectors for all slices providing
 * memoizedSelectors and stores resulting selectors
 * structured by slice-name in an object.
 * @returns object with freshly created memoized selectors of all slices (providing such selectors)
 */
const createMemoizedSelectors = () => ({
  todos: createTodosMemoizedSelectors(),
});

/**
 * Creates fresh memoized selectors for given appId.
 * @param appId the id of the app the memoized selectors shall be created for
 */
export const initMemoizedSelectors = (appId: string) => {
  if (memoizedSelectors[appId]) {
    console.warn(
      `Created already memoized selectors for given appId: ${appId}`
    );
    return;
  }
  memoizedSelectors[appId] = createMemoizedSelectors();
};

/**
 * Returns created memoized selectors for given appId.
 */
export const getMemoizedSelectors = (appId: string) => {
  return memoizedSelectors[appId];
};