Skip to content

Commit 8e098f7

Browse files
Use useMemo for hydration and avoid unnecessary hydrates (#510)
* Use useMemo for hydration and avoid unnecessary hydrates (#502) * Upgrade packages and refactor a bit * Added necessary testing dependency * Stop hydrating on server and use useLayoutEffect for client hydration * Added pokemon page with rtk's createApi * Added back dispatch in GSP in demo repo * A change in query params constitutes a new page now * Improve performance by using another hook on server * Add detail page * New approach: split gsp and gssp and hydrate based on those * Added a second type of initial state handling with more explanations * Improved useMemo comment * Make sure hydrates work when staying on the same page * Add links to demo repo to test issue (seems like no issue) * Proper gipp fix (#512) * ESLint fix * Gipp testcase and example page in RTK repo (#514) * Add GIP to _app and add GIP in page to RTK repo * Added e2e test for RTK repo * Added testcase for GIAP and GIPP to wrapper * Consistent casing and formatting in comments Fix #493 #495 #496 Co-authored-by: voinik <victor_panteleev@hotmail.com>
1 parent cd34b26 commit 8e098f7

33 files changed

+16364
-11395
lines changed

.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

README.md

Lines changed: 53 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,12 @@ import {AppProps} from 'next/app';
142142
import {wrapper} from '../components/store';
143143

144144
const MyApp: FC<AppProps> = ({Component, ...rest}) => {
145-
const {store, props} = wrapper.useWrappedStore(rest);
146-
return (
147-
<Provider store={store}>
148-
<Component {...props.pageProps} />
149-
</Provider>
150-
);
145+
const {store, props} = wrapper.useWrappedStore(rest);
146+
return (
147+
<Provider store={store}>
148+
<Component {...props.pageProps} />
149+
</Provider>
150+
);
151151
};
152152
```
153153

@@ -183,20 +183,20 @@ import {HYDRATE} from 'next-redux-wrapper';
183183

184184
// create your reducer
185185
const reducer = (state = {tick: 'init'}, action) => {
186-
switch (action.type) {
187-
case HYDRATE:
188-
const stateDiff = diff(state, action.payload) as any;
189-
const wasBumpedOnClient = stateDiff?.page?.[0]?.endsWith('X'); // or any other criteria
190-
return {
191-
...state,
192-
...action.payload,
193-
page: wasBumpedOnClient ? state.page : action.payload.page, // keep existing state or use hydrated
194-
};
195-
case 'TICK':
196-
return {...state, tick: action.payload};
197-
default:
198-
return state;
199-
}
186+
switch (action.type) {
187+
case HYDRATE:
188+
const stateDiff = diff(state, action.payload) as any;
189+
const wasBumpedOnClient = stateDiff?.page?.[0]?.endsWith('X'); // or any other criteria
190+
return {
191+
...state,
192+
...action.payload,
193+
page: wasBumpedOnClient ? state.page : action.payload.page, // keep existing state or use hydrated
194+
};
195+
case 'TICK':
196+
return {...state, tick: action.payload};
197+
default:
198+
return state;
199+
}
200200
};
201201
```
202202

@@ -431,35 +431,30 @@ import {State} from '../components/reducer';
431431

432432
// Since you'll be passing more stuff to Page
433433
declare module 'next/dist/next-server/lib/utils' {
434-
export interface NextPageContext {
435-
store: Store<State>;
436-
}
434+
export interface NextPageContext {
435+
store: Store<State>;
436+
}
437437
}
438438

439439
class MyApp extends App<AppInitialProps> {
440+
public static getInitialProps = wrapper.getInitialAppProps(store => async context => {
441+
store.dispatch({type: 'TOE', payload: 'was set in _app'});
440442

441-
public static getInitialProps = wrapper.getInitialAppProps(store => async context => {
442-
443-
store.dispatch({type: 'TOE', payload: 'was set in _app'});
444-
445-
return {
446-
pageProps: {
447-
// https://nextjs.org/docs/advanced-features/custom-app#caveats
448-
...(await App.getInitialProps(context)).pageProps,
449-
// Some custom thing for all pages
450-
pathname: ctx.pathname,
451-
},
452-
};
453-
454-
});
443+
return {
444+
pageProps: {
445+
// https://nextjs.org/docs/advanced-features/custom-app#caveats
446+
...(await App.getInitialProps(context)).pageProps,
447+
// Some custom thing for all pages
448+
pathname: ctx.pathname,
449+
},
450+
};
451+
});
455452

456-
public render() {
457-
const {Component, pageProps} = this.props;
453+
public render() {
454+
const {Component, pageProps} = this.props;
458455

459-
return (
460-
<Component {...pageProps} />
461-
);
462-
}
456+
return <Component {...pageProps} />;
457+
}
463458
}
464459

465460
export default wrapper.withRedux(MyApp);
@@ -476,28 +471,24 @@ import App from 'next/app';
476471
import {wrapper} from '../components/store';
477472

478473
class MyApp extends App {
479-
static getInitialProps = wrapper.getInitialAppProps(store => async context => {
480-
481-
store.dispatch({type: 'TOE', payload: 'was set in _app'});
482-
483-
return {
484-
pageProps: {
485-
// https://nextjs.org/docs/advanced-features/custom-app#caveats
486-
...(await App.getInitialProps(context)).pageProps,
487-
// Some custom thing for all pages
488-
pathname: ctx.pathname,
489-
},
490-
};
474+
static getInitialProps = wrapper.getInitialAppProps(store => async context => {
475+
store.dispatch({type: 'TOE', payload: 'was set in _app'});
491476

492-
});
477+
return {
478+
pageProps: {
479+
// https://nextjs.org/docs/advanced-features/custom-app#caveats
480+
...(await App.getInitialProps(context)).pageProps,
481+
// Some custom thing for all pages
482+
pathname: ctx.pathname,
483+
},
484+
};
485+
});
493486

494-
render() {
495-
const {Component, pageProps} = this.props;
487+
render() {
488+
const {Component, pageProps} = this.props;
496489

497-
return (
498-
<Component {...pageProps} />
499-
);
500-
}
490+
return <Component {...pageProps} />;
491+
}
501492
}
502493

503494
export default wrapper.withRedux(MyApp);

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
"lint:staged": "lint-staged --debug"
2020
},
2121
"devDependencies": {
22-
"eslint": "8.6.0",
23-
"eslint-config-ringcentral-typescript": "7.0.1",
22+
"eslint": "8.29.0",
23+
"eslint-config-ringcentral-typescript": "7.0.3",
2424
"husky": "7.0.4",
2525
"lerna": "4.0.0",
2626
"lint-staged": "11.1.2",

packages/demo-page/src/components/store.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import reducer, {State} from './reducer';
66
export const makeStore = (context: Context) => {
77
const store = createStore(reducer, applyMiddleware(logger));
88

9-
if (module.hot) {
10-
module.hot.accept('./reducer', () => {
9+
if ((module as any).hot) {
10+
(module as any).hot.accept('./reducer', () => {
1111
console.log('Replacing reducer');
1212
store.replaceReducer(require('./reducer').default);
1313
});

packages/demo-page/src/pages/_error.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ const ErrorPage = ({page}: any) => (
77
<>
88
<p>This is an error page, {page}.</p>
99
<nav>
10-
<Link href="/">
11-
<a>Navigate to index</a>
12-
</Link>
10+
<Link href="/">Navigate to index</Link>
1311
</nav>
1412
</>
1513
);

packages/demo-page/src/pages/index.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,9 @@ const Page: NextPage<ConnectedPageProps> = ({custom}) => {
1515
return (
1616
<div className="index">
1717
<pre>{JSON.stringify({page, custom}, null, 2)}</pre>
18-
<Link href="/other">
19-
<a>Navigate</a>
20-
</Link>
18+
<Link href="/other">Navigate</Link>
2119
{' | '}
22-
<Link href="/error">
23-
<a>Navigate to error</a>
24-
</Link>
20+
<Link href="/error">Navigate to error</Link>
2521
</div>
2622
);
2723
};

packages/demo-page/src/pages/other.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,8 @@ const OtherPage: NextPage<State> = () => {
2424
<pre>{JSON.stringify({page}, null, 2)}</pre>
2525
<nav>
2626
<button onClick={bump}>bump</button>
27-
<Link href="/">
28-
<a>Navigate to index</a>
29-
</Link>
30-
<Link href="/other2">
31-
<a>Navigate to other 2</a>
32-
</Link>
27+
<Link href="/">Navigate to index</Link>
28+
<Link href="/other2">Navigate to other 2</Link>
3329
</nav>
3430
</div>
3531
);

packages/demo-page/src/pages/other2.tsx

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,8 @@ const OtherPage: NextPage<State> = () => {
2424
<pre>{JSON.stringify({page}, null, 2)}</pre>
2525
<nav>
2626
<button onClick={bump}>bump</button>
27-
<Link href="/">
28-
<a>Navigate to index</a>
29-
</Link>
30-
<Link href="/other">
31-
<a>Navigate to other</a>
32-
</Link>
27+
<Link href="/">Navigate to index</Link>
28+
<Link href="/other">Navigate to other</Link>
3329
</nav>
3430
</div>
3531
);

packages/demo-page/src/pages/pageProps.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@ const PropsPage: NextPage<State> = props => {
1010
<p>Using Next.js default prop in a wrapped component.</p>
1111
<pre>{JSON.stringify(props)}</pre>
1212
<nav>
13-
<Link href="/">
14-
<a>Navigate to index</a>
15-
</Link>
13+
<Link href="/">Navigate to index</Link>
1614
</nav>
1715
</div>
1816
);

packages/demo-redux-toolkit/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"start": "next --port=6060"
99
},
1010
"dependencies": {
11-
"@reduxjs/toolkit": "1.6.2",
11+
"@reduxjs/toolkit": "1.8.6",
1212
"next-redux-wrapper": "*",
1313
"react": "17.0.2",
1414
"react-dom": "17.0.2",
Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,39 @@
1-
import React, {FC} from 'react';
1+
import React from 'react';
22
import {Provider} from 'react-redux';
3-
import {AppProps} from 'next/app';
4-
import {wrapper} from '../store';
3+
import App, {AppProps} from 'next/app';
4+
import {fetchSystem, wrapper} from '../store';
55

6-
const MyApp: FC<AppProps> = ({Component, ...rest}) => {
6+
interface PageProps {
7+
pageProps: {
8+
id: number;
9+
};
10+
}
11+
12+
const MyApp = ({Component, ...rest}: Omit<AppProps, 'pageProps'> & PageProps) => {
13+
console.log('rest: ', rest);
714
const {store, props} = wrapper.useWrappedStore(rest);
15+
816
return (
917
<Provider store={store}>
18+
<h1>PageProps.id: {rest.pageProps.id}</h1>
1019
<Component {...props.pageProps} />
1120
</Provider>
1221
);
1322
};
1423

24+
MyApp.getInitialProps = wrapper.getInitialAppProps(store => async (appCtx): Promise<PageProps> => {
25+
// You have to do dispatches first, before...
26+
await store.dispatch(fetchSystem());
27+
28+
// ...before calling (and awaiting!!!!) the children's getInitialProps
29+
const childrenGip = await App.getInitialProps(appCtx);
30+
return {
31+
pageProps: {
32+
// And you have to spread the children's GIP result into pageProps
33+
...childrenGip.pageProps,
34+
id: 42,
35+
},
36+
};
37+
});
38+
1539
export default MyApp;
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React from 'react';
2+
import {useDispatch, useSelector, useStore} from 'react-redux';
3+
import Link from 'next/link';
4+
import {InferGetServerSidePropsType, NextPage} from 'next';
5+
import {
6+
fetchDetail,
7+
selectDetailPageData,
8+
selectDetailPageId,
9+
selectDetailPageStateTimestamp,
10+
selectDetailPageSummary,
11+
selectSystemSource,
12+
wrapper,
13+
} from '../../store';
14+
15+
const Page: NextPage<InferGetServerSidePropsType<typeof getServerSideProps>> = ({serverTimestamp}) => {
16+
console.log('State on render', useStore().getState());
17+
console.log('Timestamp on server: ', serverTimestamp);
18+
const dispatch = useDispatch();
19+
const pageId = useSelector(selectDetailPageId);
20+
const pageSummary = useSelector(selectDetailPageSummary);
21+
const stateTimestamp = useSelector(selectDetailPageStateTimestamp);
22+
const data = useSelector(selectDetailPageData);
23+
const source = useSelector(selectSystemSource);
24+
25+
console[pageSummary ? 'info' : 'warn']('Rendered pageName: ', pageSummary);
26+
27+
if (!pageSummary || !pageId || !data) {
28+
throw new Error('Whoops! We do not have the pageId and pageSummary selector data!');
29+
}
30+
31+
return (
32+
<>
33+
<div style={{backgroundColor: 'pink', padding: '20px'}}>Timestamp on server: {serverTimestamp}</div>
34+
<div style={{backgroundColor: 'lavender', padding: '20px'}}>Timestamp in state: {stateTimestamp}</div>
35+
<div className={`page${pageId}`}>
36+
<h1>System source: {source}</h1>
37+
<h3>{pageSummary}</h3>
38+
<Link href="/subject/1">Go id=1</Link>
39+
&nbsp;&nbsp;&nbsp;&nbsp;
40+
<Link href="/subject/2">Go id=2</Link>
41+
&nbsp;&nbsp;&nbsp;&nbsp;
42+
<Link href="/detail/1">Go to details id=1</Link>
43+
&nbsp;&nbsp;&nbsp;&nbsp;
44+
<Link href="/detail/2">Go to details id=2</Link>
45+
&nbsp;&nbsp;&nbsp;&nbsp;
46+
<Link href="/gipp">Go to gipp page</Link>
47+
&nbsp;&nbsp;&nbsp;&nbsp;
48+
<Link href="/pokemon/pikachu">Go to Pokemon</Link>
49+
&nbsp;&nbsp;&nbsp;&nbsp;
50+
<Link href="/">Go to homepage</Link>
51+
</div>
52+
<button onClick={() => dispatch(fetchDetail(pageId))}>Refresh timestamp</button>
53+
</>
54+
);
55+
};
56+
57+
export const getServerSideProps = wrapper.getServerSideProps(store => async ({params}) => {
58+
const id = params?.id;
59+
if (!id || Array.isArray(id)) {
60+
throw new Error('Param id must be a string');
61+
}
62+
63+
await store.dispatch(fetchDetail(id));
64+
65+
return {
66+
props: {
67+
serverTimestamp: new Date().getTime(),
68+
},
69+
};
70+
});
71+
72+
export default Page;

0 commit comments

Comments
 (0)