I've got a state in my Redux store shaped like this:

type RootState = {
 PAGE_A: StatePageA,
 PAGE_B: StatePageB,
 PAGE_C: StatePageC,
 // AND SO ON...
}

Each page is a slice created with createSlice from @reduxjs/toolkit

I'm using Next.js static generation and SSR, so I need to hydrate my previously created store with the new state that comes form subsequent pages from the Next.js server.

Whatever page I'm routing to, I'll get the pre-rendered pageState, which should by of type StatePageA | StatePageB | StatePageC.

When that new state comes, I need to dispatch() a HYDRATE_PAGE action, which should replace the current page state.

This was my first idea on how to handle the HYDRATE_PAGE action.

export const HYDRATE_PAGE = createAction<{
  pageName: "PAGE_A" | "PAGE_B" | "PAGE_C",
  pageState: StatePageA | StatePageB | StatePageC
}>("HYDRATE_PAGE");

Then I would have to listen for it in every slice:

// EX: PAGE_A SLICE
// THIS WOULD HAVE TO BE DONE FOR EACH SLICE ON THE extraReducers PROPERTY

extraReducers: {
  [HYDRATE_PAGE]: (state, action) => {
    if (action.payload.pageName === "PAGE_A")
      return action.payload.pageState
  }
}

Is there a way where I can handle it in a centralized manner, without having to listen for that action in every slice reducer?

For example: a rootReducer kind of thing where I could listen for the HYDRATE_PAGE action and handle it in a single place like.

// THIS REDUCER WOULD HAVE ACCESS TO THE rootState
(state: RootState, action) => {
  const {pageName, pageState} = action.payload;
  state[pageName] = pageState;
};

Is this possible somehow?


Extra

There is this package called next-redux-wrapper, but I don't intent to use it. I'm trying to build my custom solution.

1

There are 1 best solutions below

1
On BEST ANSWER

Overview of Concept

I'm not sure that your store has the best design because generally you want data to be organized by what type of data it is rather than what page it's used on. But I'm just going to answer the question as it's presented here.

A reducer is simply a function that takes a state and an action and returns the next state. When you combine a keyed object of slice reducers using combineReducers (or configureStore, which uses combineReducers internally), you get a function that looks like:

(state: RootState | undefined, action: AnyAction) => RootState

We want to create a wrapper around that function and add an extra step that performs the HYDRATE_PAGE action.


Typing the Action

export const HYDRATE_PAGE = createAction<{
  pageName: "PAGE_A" | "PAGE_B" | "PAGE_C",
  pageState: StatePageA | StatePageB | StatePageC
}>("HYDRATE_PAGE");

The typescript types that you have in your action creator aren't as good as they can be because we want to enforce that the pageName and pageState match each other. The type that we actually want is this:

type HydratePayload = {
    [K in keyof RootState]: {
        pageName: K;
        pageState: RootState[K];
    }
}[keyof RootState];
export const HYDRATE_PAGE = createAction<HydratePayload>("HYDRATE_PAGE");

Which evaluates to a union of valid pairings:

type HydratePayload = {
    pageName: "PAGE_A";
    pageState: StatePageA;
} | {
    pageName: "PAGE_B";
    pageState: StatePageB;
} | {
    pageName: "PAGE_C";
    pageState: StatePageC;
}

Modifying the Reducer

First we will create the normal rootReducer with combineReducers. Then we create the hydratedReducer as a wrapper around it. We call rootReducer(state, action) to get the next state. Most of the time we just return that state and don't do anything to it. But if the action matches our HYDRATE_PAGE action then we modify the state by replacing the state for that slice with the one from our payload.

const rootReducer = combineReducers({
  PAGE_A: pageAReducer,
  PAGE_B: pageBReducer,
  PAGE_C: pageCReducer
});

const hydratedReducer = (state: RootState | undefined, action: AnyAction): RootState => {
  // call the reducer normally
  const nextState = rootReducer(state, action);
  // modify the next state when the action is hydrate
  if (HYDRATE_PAGE.match(action)) {
    const { pageName, pageState } = action.payload;
    return {
      ...nextState,
      [pageName]: pageState
    };
  }
  // otherwise don't do anything
  return nextState;
}

const store = configureStore({
  reducer: hydratedReducer,
});