Skip to content

Commit

Permalink
Use useMemo for hydration and avoid unnecessary hydrates (#510)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
kirill-konshin and voinik committed Dec 28, 2022
1 parent cd34b26 commit 8e098f7
Show file tree
Hide file tree
Showing 33 changed files with 16,364 additions and 11,395 deletions.
1 change: 1 addition & 0 deletions .yarnrc.yml
@@ -0,0 +1 @@
nodeLinker: node-modules
115 changes: 53 additions & 62 deletions README.md
Expand Up @@ -142,12 +142,12 @@ import {AppProps} from 'next/app';
import {wrapper} from '../components/store';

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

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

// create your reducer
const reducer = (state = {tick: 'init'}, action) => {
switch (action.type) {
case HYDRATE:
const stateDiff = diff(state, action.payload) as any;
const wasBumpedOnClient = stateDiff?.page?.[0]?.endsWith('X'); // or any other criteria
return {
...state,
...action.payload,
page: wasBumpedOnClient ? state.page : action.payload.page, // keep existing state or use hydrated
};
case 'TICK':
return {...state, tick: action.payload};
default:
return state;
}
switch (action.type) {
case HYDRATE:
const stateDiff = diff(state, action.payload) as any;
const wasBumpedOnClient = stateDiff?.page?.[0]?.endsWith('X'); // or any other criteria
return {
...state,
...action.payload,
page: wasBumpedOnClient ? state.page : action.payload.page, // keep existing state or use hydrated
};
case 'TICK':
return {...state, tick: action.payload};
default:
return state;
}
};
```

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

// Since you'll be passing more stuff to Page
declare module 'next/dist/next-server/lib/utils' {
export interface NextPageContext {
store: Store<State>;
}
export interface NextPageContext {
store: Store<State>;
}
}

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

public static getInitialProps = wrapper.getInitialAppProps(store => async context => {

store.dispatch({type: 'TOE', payload: 'was set in _app'});

return {
pageProps: {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
...(await App.getInitialProps(context)).pageProps,
// Some custom thing for all pages
pathname: ctx.pathname,
},
};

});
return {
pageProps: {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
...(await App.getInitialProps(context)).pageProps,
// Some custom thing for all pages
pathname: ctx.pathname,
},
};
});

public render() {
const {Component, pageProps} = this.props;
public render() {
const {Component, pageProps} = this.props;

return (
<Component {...pageProps} />
);
}
return <Component {...pageProps} />;
}
}

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

class MyApp extends App {
static getInitialProps = wrapper.getInitialAppProps(store => async context => {

store.dispatch({type: 'TOE', payload: 'was set in _app'});

return {
pageProps: {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
...(await App.getInitialProps(context)).pageProps,
// Some custom thing for all pages
pathname: ctx.pathname,
},
};
static getInitialProps = wrapper.getInitialAppProps(store => async context => {
store.dispatch({type: 'TOE', payload: 'was set in _app'});

});
return {
pageProps: {
// https://nextjs.org/docs/advanced-features/custom-app#caveats
...(await App.getInitialProps(context)).pageProps,
// Some custom thing for all pages
pathname: ctx.pathname,
},
};
});

render() {
const {Component, pageProps} = this.props;
render() {
const {Component, pageProps} = this.props;

return (
<Component {...pageProps} />
);
}
return <Component {...pageProps} />;
}
}

export default wrapper.withRedux(MyApp);
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -19,8 +19,8 @@
"lint:staged": "lint-staged --debug"
},
"devDependencies": {
"eslint": "8.6.0",
"eslint-config-ringcentral-typescript": "7.0.1",
"eslint": "8.29.0",
"eslint-config-ringcentral-typescript": "7.0.3",
"husky": "7.0.4",
"lerna": "4.0.0",
"lint-staged": "11.1.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/demo-page/src/components/store.tsx
Expand Up @@ -6,8 +6,8 @@ import reducer, {State} from './reducer';
export const makeStore = (context: Context) => {
const store = createStore(reducer, applyMiddleware(logger));

if (module.hot) {
module.hot.accept('./reducer', () => {
if ((module as any).hot) {
(module as any).hot.accept('./reducer', () => {
console.log('Replacing reducer');
store.replaceReducer(require('./reducer').default);
});
Expand Down
4 changes: 1 addition & 3 deletions packages/demo-page/src/pages/_error.tsx
Expand Up @@ -7,9 +7,7 @@ const ErrorPage = ({page}: any) => (
<>
<p>This is an error page, {page}.</p>
<nav>
<Link href="/">
<a>Navigate to index</a>
</Link>
<Link href="/">Navigate to index</Link>
</nav>
</>
);
Expand Down
8 changes: 2 additions & 6 deletions packages/demo-page/src/pages/index.tsx
Expand Up @@ -15,13 +15,9 @@ const Page: NextPage<ConnectedPageProps> = ({custom}) => {
return (
<div className="index">
<pre>{JSON.stringify({page, custom}, null, 2)}</pre>
<Link href="/other">
<a>Navigate</a>
</Link>
<Link href="/other">Navigate</Link>
{' | '}
<Link href="/error">
<a>Navigate to error</a>
</Link>
<Link href="/error">Navigate to error</Link>
</div>
);
};
Expand Down
8 changes: 2 additions & 6 deletions packages/demo-page/src/pages/other.tsx
Expand Up @@ -24,12 +24,8 @@ const OtherPage: NextPage<State> = () => {
<pre>{JSON.stringify({page}, null, 2)}</pre>
<nav>
<button onClick={bump}>bump</button>
<Link href="/">
<a>Navigate to index</a>
</Link>
<Link href="/other2">
<a>Navigate to other 2</a>
</Link>
<Link href="/">Navigate to index</Link>
<Link href="/other2">Navigate to other 2</Link>
</nav>
</div>
);
Expand Down
8 changes: 2 additions & 6 deletions packages/demo-page/src/pages/other2.tsx
Expand Up @@ -24,12 +24,8 @@ const OtherPage: NextPage<State> = () => {
<pre>{JSON.stringify({page}, null, 2)}</pre>
<nav>
<button onClick={bump}>bump</button>
<Link href="/">
<a>Navigate to index</a>
</Link>
<Link href="/other">
<a>Navigate to other</a>
</Link>
<Link href="/">Navigate to index</Link>
<Link href="/other">Navigate to other</Link>
</nav>
</div>
);
Expand Down
4 changes: 1 addition & 3 deletions packages/demo-page/src/pages/pageProps.tsx
Expand Up @@ -10,9 +10,7 @@ const PropsPage: NextPage<State> = props => {
<p>Using Next.js default prop in a wrapped component.</p>
<pre>{JSON.stringify(props)}</pre>
<nav>
<Link href="/">
<a>Navigate to index</a>
</Link>
<Link href="/">Navigate to index</Link>
</nav>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/demo-redux-toolkit/package.json
Expand Up @@ -8,7 +8,7 @@
"start": "next --port=6060"
},
"dependencies": {
"@reduxjs/toolkit": "1.6.2",
"@reduxjs/toolkit": "1.8.6",
"next-redux-wrapper": "*",
"react": "17.0.2",
"react-dom": "17.0.2",
Expand Down
32 changes: 28 additions & 4 deletions packages/demo-redux-toolkit/pages/_app.tsx
@@ -1,15 +1,39 @@
import React, {FC} from 'react';
import React from 'react';
import {Provider} from 'react-redux';
import {AppProps} from 'next/app';
import {wrapper} from '../store';
import App, {AppProps} from 'next/app';
import {fetchSystem, wrapper} from '../store';

const MyApp: FC<AppProps> = ({Component, ...rest}) => {
interface PageProps {
pageProps: {
id: number;
};
}

const MyApp = ({Component, ...rest}: Omit<AppProps, 'pageProps'> & PageProps) => {
console.log('rest: ', rest);
const {store, props} = wrapper.useWrappedStore(rest);

return (
<Provider store={store}>
<h1>PageProps.id: {rest.pageProps.id}</h1>
<Component {...props.pageProps} />
</Provider>
);
};

MyApp.getInitialProps = wrapper.getInitialAppProps(store => async (appCtx): Promise<PageProps> => {
// You have to do dispatches first, before...
await store.dispatch(fetchSystem());

// ...before calling (and awaiting!!!!) the children's getInitialProps
const childrenGip = await App.getInitialProps(appCtx);
return {
pageProps: {
// And you have to spread the children's GIP result into pageProps
...childrenGip.pageProps,
id: 42,
},
};
});

export default MyApp;
72 changes: 72 additions & 0 deletions packages/demo-redux-toolkit/pages/detail/[id].tsx
@@ -0,0 +1,72 @@
import React from 'react';
import {useDispatch, useSelector, useStore} from 'react-redux';
import Link from 'next/link';
import {InferGetServerSidePropsType, NextPage} from 'next';
import {
fetchDetail,
selectDetailPageData,
selectDetailPageId,
selectDetailPageStateTimestamp,
selectDetailPageSummary,
selectSystemSource,
wrapper,
} from '../../store';

const Page: NextPage<InferGetServerSidePropsType<typeof getServerSideProps>> = ({serverTimestamp}) => {
console.log('State on render', useStore().getState());
console.log('Timestamp on server: ', serverTimestamp);
const dispatch = useDispatch();
const pageId = useSelector(selectDetailPageId);
const pageSummary = useSelector(selectDetailPageSummary);
const stateTimestamp = useSelector(selectDetailPageStateTimestamp);
const data = useSelector(selectDetailPageData);
const source = useSelector(selectSystemSource);

console[pageSummary ? 'info' : 'warn']('Rendered pageName: ', pageSummary);

if (!pageSummary || !pageId || !data) {
throw new Error('Whoops! We do not have the pageId and pageSummary selector data!');
}

return (
<>
<div style={{backgroundColor: 'pink', padding: '20px'}}>Timestamp on server: {serverTimestamp}</div>
<div style={{backgroundColor: 'lavender', padding: '20px'}}>Timestamp in state: {stateTimestamp}</div>
<div className={`page${pageId}`}>
<h1>System source: {source}</h1>
<h3>{pageSummary}</h3>
<Link href="/subject/1">Go id=1</Link>
&nbsp;&nbsp;&nbsp;&nbsp;
<Link href="/subject/2">Go id=2</Link>
&nbsp;&nbsp;&nbsp;&nbsp;
<Link href="/detail/1">Go to details id=1</Link>
&nbsp;&nbsp;&nbsp;&nbsp;
<Link href="/detail/2">Go to details id=2</Link>
&nbsp;&nbsp;&nbsp;&nbsp;
<Link href="/gipp">Go to gipp page</Link>
&nbsp;&nbsp;&nbsp;&nbsp;
<Link href="/pokemon/pikachu">Go to Pokemon</Link>
&nbsp;&nbsp;&nbsp;&nbsp;
<Link href="/">Go to homepage</Link>
</div>
<button onClick={() => dispatch(fetchDetail(pageId))}>Refresh timestamp</button>
</>
);
};

export const getServerSideProps = wrapper.getServerSideProps(store => async ({params}) => {
const id = params?.id;
if (!id || Array.isArray(id)) {
throw new Error('Param id must be a string');
}

await store.dispatch(fetchDetail(id));

return {
props: {
serverTimestamp: new Date().getTime(),
},
};
});

export default Page;

0 comments on commit 8e098f7

Please sign in to comment.