Skip to content

Commit

Permalink
Fix hot reloading issues by removing the store from window
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielOrtel committed Feb 11, 2021
1 parent 193e2a3 commit e516388
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 29 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,6 @@ The `createWrapper` function accepts `makeStore` as its first argument. The `mak
`createWrapper` also optionally accepts a config object as a second parameter:
- `storeKey` (optional, string) : the key used on `window` to persist the store on the client
- `debug` (optional, boolean) : enable debug logging
- `serializeState` and `deserializeState`: custom functions for serializing and deserializing the redux state, see
[Custom serialization and deserialization](#custom-serialization-and-deserialization).
Expand Down
28 changes: 15 additions & 13 deletions packages/wrapper/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import {
GetStaticPropsContext,
NextComponentType,
NextPageContext,
NextPage,
} from 'next';
import {BaseContext, AppType} from 'next/dist/next-server/lib/utils';

export const HYDRATE = '__NEXT_REDUX_WRAPPER_HYDRATE__';
export const STOREKEY = '__NEXT_REDUX_WRAPPER_STORE__';

const getIsServer = () => typeof window === 'undefined';

Expand All @@ -22,19 +23,16 @@ const getDeserializedState = <S extends Store>(initialState: any, {deserializeSt
const getSerializedState = <S extends Store>(state: any, {serializeState}: Config<S> = {}) =>
serializeState ? serializeState(state) : state;

const getStoreKey = <S extends Store>({storeKey}: Config<S> = {}) => storeKey || STOREKEY;

export declare type MakeStore<S extends Store> = (context: Context) => S;

export interface InitStoreOptions<S extends Store> {
makeStore: MakeStore<S>;
context: Context;
config: Config<S>;
}

const initStore = <S extends Store>({makeStore, context, config}: InitStoreOptions<S>): S => {
const storeKey = getStoreKey(config);
let store: any;

const initStore = <S extends Store>({makeStore, context}: InitStoreOptions<S>): S => {
const createStore = () => makeStore(context);

if (getIsServer()) {
Expand All @@ -48,15 +46,16 @@ const initStore = <S extends Store>({makeStore, context, config}: InitStoreOptio
if (!req.__nextReduxWrapperStore) req.__nextReduxWrapperStore = createStore();
return req.__nextReduxWrapperStore;
}

return createStore();
}

// Memoize store if client
if (!(storeKey in window)) {
(window as any)[storeKey] = createStore();
if (!store) {
store = createStore();
}

return (window as any)[storeKey];
return store;
};

export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config: Config<S> = {}) => {
Expand All @@ -67,7 +66,7 @@ export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config:
callback: Callback<S, any>;
context: any;
}): Promise<WrapperProps> => {
const store = initStore({context, makeStore, config});
const store = initStore({context, makeStore});

if (config.debug) console.log(`1. getProps created store with state`, store.getState());

Expand Down Expand Up @@ -118,7 +117,7 @@ export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config:
callback: GetServerSidePropsCallback<S, P>,
): GetServerSideProps<P> => async context => await getStaticProps(callback as any)(context); // just not to repeat myself

const withRedux = (Component: NextComponentType | App | any) => {
const withRedux = (Component: NextPage | AppType | any) => {
const displayName = `withRedux(${Component.displayName || Component.name || 'Component'})`;

const hasInitialProps = 'getInitialProps' in Component;
Expand All @@ -141,7 +140,7 @@ export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config:
const initialStateFromGSPorGSSR = props?.pageProps?.initialState;

if (!this.store) {
this.store = initStore({makeStore, config, context});
this.store = initStore({makeStore, context});

if (config.debug)
console.log('4. WrappedApp created new store with', displayName, {
Expand Down Expand Up @@ -227,12 +226,15 @@ export default <S extends Store>(makeStore: MakeStore<S>, config: Config<S> = {}
return createWrapper(makeStore, config).withRedux;
};

export type Wrapped<C extends BaseContext = NextPageContext, IP = {}, P = {}> = React.ComponentType<P> & {
getInitialProps?(context: C): IP | Promise<IP>;
};

export type Context = NextPageContext | AppContext | GetStaticPropsContext | GetServerSidePropsContext;

export interface Config<S extends Store> {
serializeState?: (state: ReturnType<S['getState']>) => any;
deserializeState?: (state: any) => ReturnType<S['getState']>;
storeKey?: string;
debug?: boolean;
}

Expand Down
50 changes: 38 additions & 12 deletions packages/wrapper/tests/client.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@
**/

import * as React from 'react';
import {useDispatch, Provider, useSelector, DefaultRootState} from 'react-redux';
import {create, act} from 'react-test-renderer';
import {DummyComponent, wrapper, child, makeStore} from './testlib';
import {createWrapper} from '../src';
import {Store} from 'redux';
import {ReactElement, useState} from 'react';
import {NextPage} from 'next';

const w: {testStoreKey?: Store} = window as any;
let store: Store;

const defaultState = {reduxStatus: 'init'};
const modifiedState = {...defaultState, modified: true};

describe('client integration', () => {
afterEach(() => {
delete w.testStoreKey;
});

describe('existing store is taken from window', () => {
beforeEach(() => {
w.testStoreKey = makeStore();
store = makeStore();
});

test('withRedux', async () => {
const WrappedPage: any = wrapper.withRedux(DummyComponent);
expect(child(<WrappedPage initialState={w.testStoreKey?.getState()} />)).toEqual(
expect(child(<WrappedPage initialState={store.getState()} />)).toEqual(
'{"props":{},"state":{"reduxStatus":"init"}}',
);
});
Expand All @@ -38,11 +39,36 @@ describe('client integration', () => {
});
});

test('store is available in window when created', async () => {
const wrapper = createWrapper(makeStore, {storeKey: 'testStoreKey'});
const Page = () => null;
Page.getInitialProps = wrapper.getInitialPageProps(store => () => null);
test('store is available when calling getInitialProps client-side and references the existing store on client', async () => {
const wrapper = createWrapper(makeStore);
let renderer: any = null;

const Page: React.ComponentType<any> & {getInitialProps: any} = () => {
const [, setData] = useState<DefaultRootState | null>(null);
const selector = useSelector(state => state);
const dispatch = useDispatch();

React.useEffect(() => {
dispatch({type: 'MODIFY_STATE'});
}, []);

React.useEffect(() => {
setData(selector);
});

return null;
};
Page.getInitialProps = wrapper.getInitialPageProps(store => () =>
expect(store.getState()).toEqual(modifiedState),
);

const Wrapped: any = wrapper.withRedux(Page);

act(() => {
renderer = create(<Wrapped />);
});

// expected when invoked above
await wrapper.withRedux(Page)?.getInitialProps({} as any);
expect(w.testStoreKey?.getState()).toEqual(defaultState);
});
});
2 changes: 1 addition & 1 deletion packages/wrapper/tests/server.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ describe('custom serialization', () => {
},
});

const WrappedApp = wrapper.withRedux(DummyComponent);
const WrappedApp: any = wrapper.withRedux(DummyComponent);

expect(child(<WrappedApp {...props} />)).toEqual(
'{"props":{},"state":{"reduxStatus":"init","serialized":true,"deserialized":true}}',
Expand Down
6 changes: 4 additions & 2 deletions packages/wrapper/tests/testlib.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,18 @@ export const reducer = (state: State = {reduxStatus: 'init'}, action: AnyAction)
case 'FOO': // sync
case 'FOO_FULFILLED': // async
return {reduxStatus: action.payload};
case 'MODIFY_STATE':
return {...state, modified: true};
default:
return state;
}
};

export const makeStore = () => createStore(reducer, undefined, applyMiddleware(promiseMiddleware));

export const wrapper = createWrapper(makeStore, {storeKey: 'testStoreKey'});
export const wrapper = createWrapper(makeStore);

export const DummyComponent = (props: any) => {
export const DummyComponent: React.ComponentType<any> = (props: any) => {
const state = useSelector((state: State) => state);
return <div>{JSON.stringify({props, state})}</div>;
};
Expand Down

0 comments on commit e516388

Please sign in to comment.