Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is no longer necessary

- `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
20 changes: 8 additions & 12 deletions packages/wrapper/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from 'next';

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

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

Expand All @@ -22,19 +21,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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may not even be necessary, but it's probably a good idea to keep a global reference around in case getInitialProps is called client side. Far as I could test, client side getInitialProps used the existing store and state


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

if (getIsServer()) {
Expand All @@ -48,15 +44,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 +64,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 @@ -141,7 +138,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 @@ -232,7 +229,6 @@ export type Context = NextPageContext | AppContext | GetStaticPropsContext | Get
export interface Config<S extends Store> {
serializeState?: (state: ReturnType<S['getState']>) => any;
deserializeState?: (state: any) => ReturnType<S['getState']>;
storeKey?: string;
debug?: boolean;
}

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

import * as React from 'react';
import {useDispatch} 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';

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 +37,32 @@ 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 () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking this is sufficient to test the store is the same on client-side getInitialProps

const wrapper = createWrapper(makeStore);
let renderer: any = null;

const Page: React.ComponentType<any> & {getInitialProps: any} = () => {
const dispatch = useDispatch();

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

return null;
};
Page.getInitialProps = wrapper.getInitialPageProps(store => () =>
// when invoked below, verify that state modification is retained in getInitialProps
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