Search code examples
reduxreact-reduxredux-toolkit

Why initial state is not synced from previous tab in react redux-toolkit app using redux-state-sync?


I have an example stackblitz application using redux-state-sync that describes the problem that a window does not get initial state from other window like it should when function initStateWithPrevTab is called.

Expected behavior (confirmed also in my old Angular app):

  1. Open a "main window" with route / and dispatch a state change
  2. Open another app window with a route /properties/
  3. The /properties/ window asks for the initial state
  4. The main window replies with it's state (a state has been changed in step 1)
  5. /properties/ window receives the (changed) state from main window.

However my linked example app works like this:

  1. Open a "main window" with route / and dispatch a state change
  2. Open another app window with a route /properties/
  3. Now the /properties/ window has initial state without the change dispatched in main window in step 1.
  4. Dispatch another state change in main window
  5. The state change is now synced to the /properties/ window

The /properties/-window receives the entire state from "main window" and it dispatches receiveIniteState but it doesn't have any effect.

So ONLY the initial state from previous tab ("main window") is not synced to the new window (/properties/). All the state changes AFTER the /properties/ window is opened are synced without problem. The code looks like this:

store.ts

export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export const store = configureStore({
  reducer: {
    appProperties: appPropertiesReducer,
  },
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(createStateSyncMiddleware()),
});
initStateWithPrevTab(store);

AppProperties.slice.ts

interface AppPropertiesState {
  propertiesId: string;
}
const initialState: AppPropertiesState = {
  propertiesId: '0',
};
const propertiesSlice = createSlice({
  name: 'app-properties',
  initialState,
  reducers: {
    setPropertiesId(state, action: PayloadAction<string>) {
      state.propertiesId = action.payload;
    },
  },
});
export const { setPropertiesId } = propertiesSlice.actions;
export const selectPropertiesId = (state: RootState) =>
  state.appProperties.propertiesId;
export const getInitialState = propertiesSlice.getInitialState;
export default propertiesSlice.reducer;

index.ts

const root = createRoot(document.getElementById('app'));
root.render(
  <StrictMode>
    <App name="StackBlitz" />
  </StrictMode>
);

App.tsx

export const App: FC<{ name: string }> = ({ name }) => {
  return (
    <div>
      <Provider store={store}>
        <BrowserRouter>
          <Routes>
            <Route path="/" element={<MainView />} />
            <Route path="properties" element={<Properties />} />
          </Routes>
        </BrowserRouter>
      </Provider>
    </div>
  );
};

MainView.tsx

let i = 0;
export function MainView() {
  function onSetPropertiesId(): void {
    store.dispatch(setPropertiesId((++i).toString()));
  }
  const propertiesId = useSelector(selectPropertiesId);
  return (
    <div>
      <p>Click the button increment propertiesId</p>
      <button onClick={onSetPropertiesId}>Increment</button>
      <p>Properties Id : {propertiesId}</p>
    </div>
  );
}

Properties.tsx

export function Properties() {
  const [content, setContent] = useState<ReactNode | undefined>(undefined);
  const propertiesId = useSelector(selectPropertiesId);
  useEffect(() => {
    setContent(<span>PropertiesId : {propertiesId}</span>);
  }, [propertiesId]);
  return <div>{content}</div>;
}

How to reproduce it in the example stackblitz application:

  1. Open https://stackblitz-starters-uadxdg.stackblitz.io
  2. Press the Increment button -> now the value in state for this window is 1.
  3. Open https://stackblitz-starters-uadxdg.stackblitz.io/properties -> now the value is 0 even though it should get the initial value (1) from the 1st window AND NOT from it's local initial state (0).

After that syncing works. So the question is that why 2nd window doesn't init the state from previous window even though initStateWithPrevTab is called? As mentioned before the initial state syncing in the library works and I have no problem in my old Angular app.


Solution

  • This can be fixed by combining reducers and moving initStateWithPrevTab to App.tsx.

    store.ts

    export const store = configureStore({
      reducer: withReduxStateSync(
        combineReducers({
          appProperties: appPropertiesReducer
        })
      ),
      middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware().concat(createStateSyncMiddleware()),
    });
    

    App.tsx

    export const App: FC<{ name: string }> = ({ name }) => {
      // function call moved here
      initStateWithPrevTab(store);
      return (
        <div>
          <Provider store={store}>
            <BrowserRouter>
              <Routes>
                <Route path="/" element={<MainView />} />
                <Route path="/properties" element={<Properties />} />
              </Routes>
            </BrowserRouter>
          </Provider>
        </div>
      );
    };