0

I'm attempting to set up a redux store that is accessible from multiple components. I'm new to this, but I understand that normally one would wrap the root app component with the provider to make the same store context available everywhere.

However, I'm working within a legacy app, where we're tacking on React components here and there and there's no site-wide common root app component, only separate components trees that know nothing about each other. I thought a redux store would be a good solution to this, but now realize that there's some context handling going on behind the scenes and I'm not sure how to wire up each provider with the same context. I found some basic info on providing a context, but I'm not sure if this solves my problem or not and it's getting into the weeds, especially using Typescript.

Specifically, I have a mini-cart in the header and several pages that want to interact with that component. I have things working on both sides, but they are affecting different stores.

MiniCart.tsx:

import store from '../../common/stores/store';
...
<Provider store={store}>
    <MiniCart />
</Provider>

PageThatNeedsToAddToMiniCart.tsx:

import store from '../../common/stores/store';
...
<Provider store={store}>
    <PageThatNeedsToAddToMiniCart />
</Provider>

--- EDIT 1 ---

hooks.ts:

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from './store'

type DispatchFunc = () => AppDispatch
const useAppDispatch: DispatchFunc = useDispatch
const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

export { useAppDispatch, useAppSelector }

miniCartSlice.tsx:

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from './store';

interface MiniCart {
    SubTotal: number;
}

interface MiniCartState {
    SubTotal: number;
}

const initialState: UserCart = {
    SubTotal: 0,
}

const miniCartSlice = createSlice({
    name: 'miniCart',
    initialState: initialState,
    reducers: {
        productAdded: (state, action: PayloadAction<UserCart>) => {
            return state = action.payload;
        }
    }
});

const selectSubTotal = (state: RootState) => state.miniCart.SubTotal;

export const { productAdded } = miniCartSlice.actions;
export { selectSubTotal };
export default miniCartSlice.reducer;

store.tsx:

import { configureStore } from '@reduxjs/toolkit';
import miniCartReducer from './miniCartSlice';

const store = configureStore({
    reducer: {
        miniCart: miniCartReducer
    }
})

type RootState = ReturnType<typeof store.getState>;
type AppDispatch = typeof store.dispatch;

export type { RootState, AppDispatch }
export default store;

PageThatNeedsToAddToMiniCart.tsx:

const dispatch = useAppDispatch();
...

dispatch(
    productAdded(result.UpdatedCart)
);

MiniCart.tsx:

//This value is not updated when the other component dispatches "productAdded". I expected it to trigger a rerender.
const subTotal = useAppSelector(selectSubTotal); 
...
return (
    <div> 
        <p>{`Subtotal: ${subTotal} `}</p>
    </div>
);

Maybe I'm misunderstanding what I'm seeing debugging, but the two components appear to access separate stores.

--- EDIT 2 ---

It turns out that, while I'm configuring the store once, bundling the apps separately results in separate stores. I ended up going with Jacob's answer for simplicity, but may end up going with his more robust suggestion of redux-micro-frontend down the road.

12
  • 1
    Redux by itself has no connection to React, the connection is handled by React-Redux. What this means for you is it's possible to create your store external to either of your react apps and then pass that same store to both apps. This will allow both react apps to update based on actions/state updates triggered by the other. Commented Mar 27, 2023 at 20:47
  • 2
    Isn't what Jacob is describing already what you are doing with the code examples you provided? A Provider component provides a store object to the sub-ReactTree it is wrapping? What exactly is the issue with what you are trying to do here? What isn't working? Is the issue that "I have things working on both sides, but they are affecting different stores." - you should have one single app store if you are wanting any consumers to read/update the same state. Commented Mar 27, 2023 at 21:34
  • 2
    The connect Higher Order Component only connects a component to the nearest Provider component providing the store. I guess what I'm asking is if the issue is because you are using two different app stores. What I think both Jacom and I are suggesting is to pass the same store to both providers. Does that make sense? Commented Mar 27, 2023 at 22:21
  • 1
    Looks like the exported store reference is the same for any consumers that import it. It's only configured once then exported. Commented Mar 27, 2023 at 22:37
  • 2
    Even if you are using the same imported file if you are bundling your apps separately you will most likely end up with two stores. A dodgy test for this would be to just attach a reference of the store to the window for each app, such as window.APP_A_STORE and window.APP_B_STORE, and then manually check their reference equality once the apps have rendered (window.APP_A_STORE === window.APP_B_STORE). Commented Mar 27, 2023 at 23:43

1 Answer 1

2

If you create 3 separate bundles (App A, App B and Store) you could share the store via an event.

App A

// Wait to start the application until the store is created.
window.addEventListener('createStore', function(event) {
    const root = createRoot(document.getElementById('root')!);

    // pull store from the event details
    const store = event.detail;

    root.render(
        <StrictMode>
            <Provider store={store}>
                <AppA />
            </Provider>
        </StrictMode>
    )
});

App B

// Wait to start the application until the store is created.
window.addEventListener('createStore', function(event) {
    const root = createRoot(document.getElementById('root')!);

    // pull store from the event details
    const store = event.detail;

    root.render(
        <StrictMode>
            <Provider store={store}>
                <AppB />
            </Provider>
        </StrictMode>
    )
});

Store

// Wait for page to load (might be needed so that when the react 
// applications respond to the custom event the DOM elements that
// they target are definitely created).
document.addEventListener('DOMContentLoaded', function(event) {
    const reducer = /* CONFIGURE ROOT REDUCER HERE */;
    const initialState = /* CONFIGURE INITIAL STATE HERE */;
    const middleware = /* CONFIGURE INITIAL MIDDLEWARE HERE */;
    
    // Create redux store.
    const store = createStore(reducer, initialState, middleware)
    
    // Create custom event with reference to store attached.
    const createStoreEvent = new CustomEvent('createStore', { detail: store });
    
    // dispatch createStore event
    window.dispatchEvent(createStoreEvent);
});

While this is pretty similar to just slapping the store on the window, it is slightly harder to grab the store reference (can't just start typing window. in the console and see what pops up).

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.