Skip to content

Commit

Permalink
Merge b8f568b into b6bb7cd
Browse files Browse the repository at this point in the history
  • Loading branch information
kirill-konshin committed Sep 15, 2021
2 parents b6bb7cd + b8f568b commit 1f88711
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 130 deletions.
52 changes: 30 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,33 +131,53 @@ export const wrapper = createWrapper(makeStore, {debug: true});

</details>

## `wrapper.useWrappedStore`

It is highly recommended to use `pages/_app` to wrap all pages at once, otherwise due to potential race conditions you may get `Cannot update component while rendering another component`:

```typescript
import React, {FC} from 'react';
import {Provider} from 'react-redux';
import {AppProps} from 'next/app';
import {wrapper} from '../components/store';

const WrappedApp: FC<AppProps> = ({Component, pageProps}) => <Component {...pageProps} />;
const MyApp: FC<AppProps> = ({Component, ...rest}) => {
const {store, props} = wrapper.useWrappedStore(rest);
return (
<Provider store={store}>
<Component {...props.pageProps} />
</Provider>
);
};

export default wrapper.withRedux(WrappedApp);
export default MyApp;
```

<details>
<summary>Same code in JavaScript (without types)</summary>

```js
import React from 'react';
import React, {FC} from 'react';
import {Provider} from 'react-redux';
import {wrapper} from '../components/store';

const MyApp = ({Component, pageProps}) => <Component {...pageProps} />;
const MyApp = ({Component, ...rest}) => {
const {store, props} = wrapper.useWrappedStore(rest);
return (
<Provider store={store}>
<Component {...props.pageProps} />
</Provider>
);
};

export default wrapper.withRedux(MyApp);
export default MyApp;
```

</details>

You can also use class-based App wrapper.
## Legacy `withRedux`

Instead of `wrapper.useWrappedStore` you can also use legacy HOC, that can work with class-based components.

:warning: Next.js provides [generic `getInitialProps`](https://github.com/vercel/next.js/blob/canary/packages/next/pages/_app.tsx#L21) when using `class MyApp extends App` which will be picked up by wrapper, so you **must not extend `App`** as you'll be opted out of Automatic Static Optimization: https://err.sh/next.js/opt-out-auto-static-optimization. Just export a regular Functional Component as in the example above.

Expand Down Expand Up @@ -275,10 +295,7 @@ import {wrapper, State} from '../store';

export const getStaticProps = wrapper.getStaticProps((store) => ({preview}) => {
console.log('2. Page.getStaticProps uses the store to dispatch things');
store.dispatch({
type: 'TICK',
payload: 'was set in other page ' + preview,
});
store.dispatch({type: 'TICK', payload: 'was set in other page ' + preview});
});

// you can also use `connect()` instead of hooks
Expand All @@ -300,10 +317,7 @@ import {wrapper} from '../store';

export const getStaticProps = wrapper.getStaticProps((store) => ({preview}) => {
console.log('2. Page.getStaticProps uses the store to dispatch things');
store.dispatch({
type: 'TICK',
payload: 'was set in other page ' + preview,
});
store.dispatch({type: 'TICK', payload: 'was set in other page ' + preview});
});

// you can also use `connect()` instead of hooks
Expand Down Expand Up @@ -386,10 +400,7 @@ const Page: NextPage = () => {

Page.getInitialProps = wrapper.getInitialPageProps((store) => ({pathname, req, res}) => {
console.log('2. Page.getInitialProps uses the store to dispatch things');
store.dispatch({
type: 'TICK',
payload: 'was set in error page ' + pathname,
});
store.dispatch({type: 'TICK', payload: 'was set in error page ' + pathname});
});

export default Page;
Expand All @@ -410,10 +421,7 @@ const Page = () => {

Page.getInitialProps = wrapper.getInitialPageProps((store) => ({pathname, req, res}) => {
console.log('2. Page.getInitialProps uses the store to dispatch things');
store.dispatch({
type: 'TICK',
payload: 'was set in error page ' + pathname,
});
store.dispatch({type: 'TICK', payload: 'was set in error page ' + pathname});
});

export default Page;
Expand Down
12 changes: 10 additions & 2 deletions packages/demo-redux-toolkit/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React, {FC} from 'react';
import {Provider} from 'react-redux';
import {AppProps} from 'next/app';
import {wrapper} from '../store';

const MyApp: FC<AppProps> = ({Component, pageProps}) => <Component {...pageProps} />;
const MyApp: FC<AppProps> = ({Component, ...rest}) => {
const {store, props} = wrapper.useWrappedStore(rest);
return (
<Provider store={store}>
<Component {...props.pageProps} />
</Provider>
);
};

export default wrapper.withRedux(MyApp);
export default MyApp;
207 changes: 101 additions & 106 deletions packages/wrapper/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import App, {AppContext, AppInitialProps} from 'next/app';
import React from 'react';
import React, {useEffect, useMemo, useRef} from 'react';
import {Provider} from 'react-redux';
import {Store} from 'redux';
import {
Expand Down Expand Up @@ -41,12 +41,12 @@ export declare type MakeStore<S extends Store> = (context: Context) => S;

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

let sharedClientStore: any;
let clientSharedStore: any;

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

if (getIsServer()) {
Expand All @@ -64,11 +64,11 @@ const initStore = <S extends Store>({makeStore, context}: InitStoreOptions<S>):
}

// Memoize store if client
if (!sharedClientStore) {
sharedClientStore = createStore();
if (!clientSharedStore) {
clientSharedStore = createStore();
}

return sharedClientStore;
return clientSharedStore;
};

export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config: Config<S> = {}) => {
Expand Down Expand Up @@ -126,11 +126,7 @@ export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config:
const getInitialAppProps =
<P extends {} = any>(callback: AppCallback<S, P>): GetInitialAppProps<P> =>
async (context: AppContext) => {
const {initialProps, initialState} = await makeProps({
callback,
context,
addStoreToContext: true,
});
const {initialProps, initialState} = await makeProps({callback, context, addStoreToContext: true});
return {
...initialProps,
initialState,
Expand All @@ -140,10 +136,7 @@ export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config:
const getStaticProps =
<P extends {} = any>(callback: GetStaticPropsCallback<S, P>): GetStaticProps<P> =>
async (context) => {
const {initialProps, initialState} = await makeProps({
callback,
context,
});
const {initialProps, initialState} = await makeProps({callback, context});
return {
...initialProps,
props: {
Expand All @@ -158,106 +151,107 @@ export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config:
async (context) =>
await getStaticProps(callback as any)(context); // just not to repeat myself

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

const hasInitialProps = 'getInitialProps' in Component;

//TODO Check if pages/_app was wrapped so there's no need to wrap a page itself
return class Wrapper extends React.Component<any, any> {
static displayName = displayName;
const hydrate = (store: S, state: any) => {
if (!state) {
return;
}
store.dispatch({
type: HYDRATE,
payload: getDeserializedState<S>(state, config),
} as any);
};

static getInitialProps = hasInitialProps ? Component.getInitialProps : undefined;
const useHybridHydrate = (store: S, state: any) => {
const firstRender = useRef<boolean>(true);

public store: S = null as any;
useEffect(() => {
firstRender.current = false;
}, []);

constructor(props: any, context: any) {
super(props, context);
this.hydrate(props, context);
useMemo(() => {
// synchronous for server or first time render
if (getIsServer() || firstRender.current) {
hydrate(store, state);
}
}, [store, state]);

hydrate({initialState, initialProps, ...props}: any, context: any) {
// this happens when App has page with getServerSideProps/getStaticProps, initialState will be dumped twice:
// one incomplete and one complete
const initialStateFromGSPorGSSR = props?.pageProps?.initialState;

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

if (config.debug) {
console.log('4. WrappedApp created new store with', displayName, {
initialState,
initialStateFromGSPorGSSR,
});
}
}

if (initialState) {
this.store.dispatch({
type: HYDRATE,
payload: getDeserializedState<S>(initialState, config),
} as any);
}

// ATTENTION! This code assumes that Page's getServerSideProps is executed after App.getInitialProps
// so we dispatch in this order
if (initialStateFromGSPorGSSR) {
this.store.dispatch({
type: HYDRATE,
payload: getDeserializedState<S>(initialStateFromGSPorGSSR, config),
} as any);
}
useEffect(() => {
// asynchronous for client subsequent navigation
if (!getIsServer()) {
hydrate(store, state);
}
}, [store, state]);
};

shouldComponentUpdate(nextProps: Readonly<any>, nextState: Readonly<any>, nextContext: any): boolean {
if (
nextProps?.pageProps?.initialState !== this.props?.pageProps?.initialState ||
nextProps?.initialState !== this.props?.initialState
) {
this.hydrate(nextProps, nextContext);
}
const useWrappedStore = (
{initialState, initialProps, ...props}: any,
displayName = 'useWrappedStore',
): {store: S; props: any} => {
// this happens when App has page with getServerSideProps/getStaticProps, initialState will be dumped twice:
// one incomplete and one complete
const initialStateFromGSPorGSSR = props?.pageProps?.initialState;

return true;
}
if (config.debug) {
console.log('4.', displayName, 'created new store with', {
initialState,
initialStateFromGSPorGSSR,
});
}

render() {
const {initialState, initialProps, ...props} = this.props;

let resultProps: any = props;

// order is important! Next.js overwrites props from pages/_app with getStaticProps from page
// @see https://github.com/zeit/next.js/issues/11648
if (initialProps && initialProps.pageProps) {
resultProps.pageProps = {
...initialProps.pageProps, // this comes from wrapper in _app mode
...props.pageProps, // this comes from gssp/gsp in _app mode
};
}

const initialStateFromGSPorGSSR = props?.pageProps?.initialState;

// just some cleanup to prevent passing it as props, we need to clone props to safely delete initialState
if (initialStateFromGSPorGSSR) {
resultProps = {...props, pageProps: {...props.pageProps}};
delete resultProps.pageProps.initialState;
}

// unwrap getInitialPageProps
if (resultProps?.pageProps?.initialProps) {
resultProps.pageProps = {
...resultProps.pageProps,
...resultProps.pageProps.initialProps,
};
delete resultProps.pageProps.initialProps;
}

return (
<Provider store={this.store}>
<Component {...initialProps} {...resultProps} />
</Provider>
);
}
const store = useMemo<S>(() => initStore<S>({makeStore}), []);

useHybridHydrate(store, initialState);
useHybridHydrate(store, initialStateFromGSPorGSSR);

let resultProps: any = props;

// order is important! Next.js overwrites props from pages/_app with getStaticProps from page
// @see https://github.com/zeit/next.js/issues/11648
if (initialProps && initialProps.pageProps) {
resultProps.pageProps = {
...initialProps.pageProps, // this comes from wrapper in _app mode
...props.pageProps, // this comes from gssp/gsp in _app mode
};
}

// just some cleanup to prevent passing it as props, we need to clone props to safely delete initialState
if (initialStateFromGSPorGSSR) {
resultProps = {...props, pageProps: {...props.pageProps}};
delete resultProps.pageProps.initialState;
}

// unwrap getInitialPageProps
if (resultProps?.pageProps?.initialProps) {
resultProps.pageProps = {...resultProps.pageProps, ...resultProps.pageProps.initialProps};
delete resultProps.pageProps.initialProps;
}

return {store, props: {...initialProps, ...resultProps}};
};

const withRedux = (Component: NextComponentType | App | any) => {
console.warn(
'/!\\ You are using legacy implementaion. Please update your code: use createWrapper() and wrapper.useWrappedStore().',
);

//TODO Check if pages/_app was wrapped so there's no need to wrap a page itself
const WrappedComponent = (props: any) => {
const {store, props: combinedProps} = useWrappedStore(props, WrappedComponent.displayName);

return (
<Provider store={store}>
<Component {...combinedProps} />
</Provider>
);
};

WrappedComponent.displayName = `withRedux(${Component.displayName || Component.name || 'Component'})`;

if ('getInitialProps' in Component) {
WrappedComponent.getInitialProps = Component.getInitialProps;
}

return WrappedComponent;
};

return {
Expand All @@ -266,6 +260,7 @@ export const createWrapper = <S extends Store>(makeStore: MakeStore<S>, config:
getInitialAppProps,
getInitialPageProps,
withRedux,
useWrappedStore,
};
};

Expand Down

0 comments on commit 1f88711

Please sign in to comment.