How to create redux store within UseEffect, correct pattern?

578 views Asked by At

What is the correct way to create redux store within useEffect?

I have a use case that a child App.js will be exported as web-component (e.g. a javascript file), then embed (e.g. reference this file in html header) by a parent react app.

One flow is that the parent app will render App.js twice or N times. I put store creation inside useEffect + empty dependency, so this will be called once, regardless how many times the parent app renders the child app.

Once my store is ready, I render rest of the component + pass the store to context.

In Component.js, I look up the state and want to use them.

The error I have: https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts

const state = store.getState();, getState does not exist. It seems that either store is not ready when I tried to look up the state.

So I wonder what is the correct way to create store within useEffect?

App.js
export default function App() {
    const store = useRef(null);
    const [storeReady, setStoreReady] = useState(false);
    const [token, setToken] = useState('');  

    useEffect(() => {
        store.current = createReduxStore();
        setStoreReady(true);

        return () => {
            store.current = null;
        };
    }, []);

    useEffect(() => {
        if (storeReady) {
            // get token and set in state
            token(auth())
        }
    }, [auth, storeReady]);

    // store.current
    if (store.current !== null) {
        //test
        console.log(
        '++++++ store.current not null, condi',
        store.current !== null,
        store.current
        );

        return (
            <>
                <StoreContext.Provider value={store.current}>
                    <MyApp
                        token={token}
                    />
                </StoreContext.Provider>
            </>
        );
    } else {
        return null;
    }
}

Store.js
export function createReduxStore(initialState = {}) {
    const sagaMiddleware = createSagaMiddleware();
    const store = createStore(
        rootReducer,
        initialState,
        composeWithDevTools(applyMiddleware(sagaMiddleware))
    );
    sagaMiddleware.run(rootSaga);
    return store;
}

Component.js
export default function MyApp() {

    const mapState = useCallback(
        state => ({
        history:
            state !== undefined && state.idv !== undefined ? state.idv.history : [],
        status:
            state !== undefined && state.idv !== undefined
            ? state.idv.status
            : initialStatus,
        error:
            state !== undefined && state.idv !== undefined ? state.idv.error : ''
        }),
        []
    );

    
    const {history, status, error} = useMappedState(mapState);

}

// https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts
  function useMappedState<TResult>(
    mapState: (state: TState) => TResult,
    equalityCheck: (a: TResult, b: TResult) => boolean = defaultEqualityCheck,
  ): TResult {
    const store = useContext(StoreContext);
    if (!store) {
      throw new MissingProviderError();
    }

    // We don't keep the derived state but call mapState on every render with current state.
    // This approach guarantees that useMappedState returns up-to-date derived state.
    // Since mapState can be expensive and must be a pure function of state we memoize it.
    const memoizedMapState = useMemo(() => memoizeSingleArg(mapState), [
      mapState,
    ]);

    // getState is undefined
    const state = store.getState();
    const derivedState = memoizedMapState(state);

Update 1, but not working @backtick

export default function App() {
    const duckStore = { getState() {} };
    const store = useRef(duckStore);
    const [token, setToken] = useState('');  

    useEffect(() => {
        store.current = createReduxStore();
        return () => {
            store.current = duckStore;
        };
    }, []);

    // store.current
    if (store.current !== null) {
        return (
            <>
                <StoreContext.Provider value={store.current}>
                    <MyApp />
                </StoreContext.Provider>
            </>
        );
    } else {
        return null;
    }
}

Store.js
export function createReduxStore(initialState = {}) {
    const sagaMiddleware = createSagaMiddleware();
    const store = createStore(
        rootReducer,
        initialState,
        composeWithDevTools(applyMiddleware(sagaMiddleware))
    );
    sagaMiddleware.run(rootSaga);
    return store;
}

Component.js
export default function MyApp() {
    // There is no store variable here, useMappedState is able to get the store value.

    const mapState = useCallback(
        state => ({
        history:
            state !== undefined && state.idv !== undefined ? state.idv.history : [],
        status:
            state !== undefined && state.idv !== undefined
            ? state.idv.status
            : initialStatus,
        error:
            state !== undefined && state.idv !== undefined ? state.idv.error : ''
        }),
        []
    );

    
    const {history, status, error} = useMappedState(mapState);

}
1

There are 1 answers

4
backtick On

useMappedState calls the store.getState() on every render, even the first one when the store isn't ready. Just duck-type the store ref's initial value:

// module scope
const duckStore = { getState() {} };

// in component
const store = useRef(duckStore);

// in useEffect cleanup
return () => { store.current = duckStore; };