user10568173
user10568173

Reputation: 71

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

We have written a React app using Redux via Redux Toolkit. So far so fine. Now the React app shall be rendered into multiple different elements (each element shall get a new app instance) on the same page. The rendering part is straight forward: We just call ReactDOM.render(...) for each element. The Redux part again brings some headache. To create a new Redux store instance for each app instance, we call the configureStore function for each React app instance. Our slices look similiar to this:

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

Please note, that currently we create and export each slice statically and only once. Here comes my first question: Is this actually valid when creating multiple store instances or do we actually need to create also new slice instances for each app/store instance? For the simple counter example provided, doing not so, seems to work, but as soon as we use an AsyncThunk as in the example below the whole thing breaks.

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

I believe the breaking starts here because of interdifferences between the events fired from dispatching the AsyncThunk. Thereby I think the solution is to call the createAsyncThunk function for each app/store/slice instance. Are there any best practices for doing so? Of course this breaks the beauty and functionality of static exports and requires kind of a mapping, hence I'm asking.

Upvotes: 0

Views: 1287

Answers (1)

user10568173
user10568173

Reputation: 71

My original suspicion that the AsyncThunk-part was responsible for the interferences between the stores of the different React app instances was wrong. The source was something different not visible in the examples provided in my question. We use memoized selectors via createSelector from reselect. Those were created and exported like the rest statically which in fact is a problem when working with multiple store/app instances. This way all instances use the same memoized selector which again doesn't work correctly thereby, since in the worst scenario the stored values of the dependency selectors are coming from the use from another store/app instance. This again can lead to endless rerenderings and recomputations.

The solution I came up with, is to create the memoized selectors for each app instance freshly. Therefore I generate a unique id for each app instance which is stored permanently in the related Redux store. When creating the store for an app instance I create also new memoized selectors instances and store them in a object which is stored in a static dictionary using the appId as the key. To use the memoized selectors in our components I wrote a hook which uses 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;
};

Then the selectors can be used in the components like this:

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

and the related dictionary and lookup process would look like this:

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];
};

Upvotes: 1

Related Questions