From 76f43d71cbf8ff8a32906eba53753a697e6470e4 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 10 Apr 2020 13:09:11 -0700 Subject: [PATCH 1/5] Update with-redux-thunk example to include HMR --- examples/with-redux-thunk/README.md | 18 +++-- examples/with-redux-thunk/actions.js | 23 ++++++ examples/with-redux-thunk/components/clock.js | 42 +++++----- .../with-redux-thunk/components/counter.js | 8 +- .../with-redux-thunk/components/examples.js | 6 +- .../with-redux-thunk/lib/with-redux-store.js | 26 ++----- examples/with-redux-thunk/pages/_app.js | 6 +- examples/with-redux-thunk/pages/index.js | 26 +++++-- .../pages/show-redux-state.js | 26 +++++++ examples/with-redux-thunk/reducers.js | 43 ++++++++++ examples/with-redux-thunk/store.js | 78 ++++--------------- examples/with-redux-thunk/types.js | 5 ++ 12 files changed, 181 insertions(+), 126 deletions(-) create mode 100644 examples/with-redux-thunk/actions.js create mode 100644 examples/with-redux-thunk/pages/show-redux-state.js create mode 100644 examples/with-redux-thunk/reducers.js create mode 100644 examples/with-redux-thunk/types.js diff --git a/examples/with-redux-thunk/README.md b/examples/with-redux-thunk/README.md index c8533d44a739b..2fd61ba4ec0f1 100644 --- a/examples/with-redux-thunk/README.md +++ b/examples/with-redux-thunk/README.md @@ -45,15 +45,19 @@ Deploy it to the cloud with [ZEIT Now](https://zeit.co/import?filter=next.js&utm ## Notes -In the first example we are going to display a digital clock that updates every second. The first render is happening in the server and then the browser will take over. To illustrate this, the server rendered clock will have a different background color (black) than the client one (grey). +The Redux `Provider` is implemented in `pages/_app.js`. The `MyApp` component is wrapped in a `withReduxStore` function, the redux `store` will be initialized in the function and then passed down to `MyApp` as `this.props.initialReduxState`, which will then be utilized by the `Provider` component. -The Redux `Provider` is implemented in `pages/_app.js`. Since the `MyApp` component is wrapped in `withReduxStore` the redux store will be automatically initialized and provided to `MyApp`, which in turn passes it off to `react-redux`'s `Provider` component. +Every initial server-side request will utilize a new `store`. However, every `Router` or `Link` action will persist the same `store` as a user navigates through the `pages`. To demonstrate this example, we can navigate back and forth to `/show-redux-state` using the provided `Link`s. However, if we navigate directly to `/show-redux-state` (or refresh the page), this will cause a server-side render, which will then utilize a new store. -`index.js` have access to the redux store using `connect` from `react-redux`. -`counter.js` and `examples.js` have access to the redux store using `useSelector` and `useDispatch` from `react-redux@^7.1.0` +In the `clock` component, we are going to display a digital clock that updates every second. The first render is happening on the server and then the browser will take over. To illustrate this, the server rendered clock will initially have a black background; then, once the component has been mounted in the browser, it changes from black to a grey background. -On the server side every request initializes a new store, because otherwise different user data can be mixed up. On the client side the same store is used, even between page changes. +In the `counter` component, we are going to display a user-interactive counter that can be increased or decreased when the provided buttons are pressed. -The example under `components/counter.js`, shows a simple incremental counter implementing a common Redux pattern. Again, the first render is happening in the server and instead of starting the count at 0, it will dispatch an action in redux that starts the count at 1. This continues to highlight how each navigation triggers a server render first and then a client render when switching pages on the client side +This example includes two different ways to access the `store` or to `dispatch` actions: +1.) `pages/index.js` will utilize `connect` from `react-redux` to `dispatch` the `startClock` redux action once the component has been mounted in the browser. -For simplicity and readability, Reducers, Actions, and Store creators are all in the same file: `store.js` +2.) `components/counter.js` and `components/examples.js` have access to the redux store using `useSelector` and can dispatch actions using `useDispatch` from `react-redux@^7.1.0` + +You can either use the `connect` function to access redux state and/or dispatch actions or use the hook variations: `useSelector` and `useDispatch`. It's up to you. + +This example also includes hot-reloading when one of the `reducers` has changed. However, there is one caveat with this implementation: If you're using the `Redux DevTools` browser extension, then all previously recorded actions will be recreated when a reducer has changed (in other words, if you increment the counter by 1 using the `+1` button, and then change the increment action to add 10 in the reducer, Redux DevTools will playback all actions and adjust the counter state by 10 to reflect the reducer change). Therefore, to avoid this issue, the store has been set up to reset to back initial state upon a reducer change. If you wish to persist redux state regardless (or you don't have the extension installed), then in `store.js` change (line 19) `store.replaceReducer(createNextReducer(initialState))` to `store.replaceReducer(createNextReducer)`. diff --git a/examples/with-redux-thunk/actions.js b/examples/with-redux-thunk/actions.js new file mode 100644 index 0000000000000..a0ab26810c3dd --- /dev/null +++ b/examples/with-redux-thunk/actions.js @@ -0,0 +1,23 @@ +import * as types from './types' + +// INITIALIZES CLOCK ON SERVER +export const serverRenderClock = isServer => dispatch => + dispatch({ + type: types.TICK, + payload: { light: !isServer, ts: Date.now() }, + }) + +// INITIALIZES CLOCK ON CLIENT +export const startClock = () => dispatch => + setInterval(() => { + dispatch({ type: types.TICK, payload: { light: true, ts: Date.now() } }) + }, 1000) + +// INCREMENT COUNTER BY 1 +export const incrementCount = () => ({ type: types.INCREMENT }) + +// DECREMENT COUNTER BY 1 +export const decrementCount = () => ({ type: types.DECREMENT }) + +// RESET COUNTER +export const resetCount = () => ({ type: types.RESET }) diff --git a/examples/with-redux-thunk/components/clock.js b/examples/with-redux-thunk/components/clock.js index 9fb286e174819..50a49693f6923 100644 --- a/examples/with-redux-thunk/components/clock.js +++ b/examples/with-redux-thunk/components/clock.js @@ -1,25 +1,25 @@ -export default ({ lastUpdate, light }) => { - return ( -
- {format(new Date(lastUpdate))} - -
- ) -} +const pad = n => (n < 10 ? `0${n}` : n) const format = t => `${pad(t.getUTCHours())}:${pad(t.getUTCMinutes())}:${pad(t.getUTCSeconds())}` -const pad = n => (n < 10 ? `0${n}` : n) +const Clock = ({ lastUpdate, light }) => ( +
+ {format(new Date(lastUpdate))} + +
+) + +export default Clock diff --git a/examples/with-redux-thunk/components/counter.js b/examples/with-redux-thunk/components/counter.js index a85d2f23c3f31..9b9a9ed14b370 100644 --- a/examples/with-redux-thunk/components/counter.js +++ b/examples/with-redux-thunk/components/counter.js @@ -1,9 +1,9 @@ import React from 'react' import { useSelector, useDispatch } from 'react-redux' -import { incrementCount, decrementCount, resetCount } from '../store' +import { incrementCount, decrementCount, resetCount } from '../actions' -export default () => { - const count = useSelector(state => state.count) +const Counter = () => { + const count = useSelector(state => state.counter) const dispatch = useDispatch() return ( @@ -17,3 +17,5 @@ export default () => { ) } + +export default Counter diff --git a/examples/with-redux-thunk/components/examples.js b/examples/with-redux-thunk/components/examples.js index 4db5ac94dd4f6..700a811d5f79b 100644 --- a/examples/with-redux-thunk/components/examples.js +++ b/examples/with-redux-thunk/components/examples.js @@ -3,11 +3,11 @@ import Clock from './clock' import Counter from './counter' export default () => { - const lastUpdate = useSelector(state => state.lastUpdate) - const light = useSelector(state => state.light) + const lastUpdate = useSelector(state => state.timer.lastUpdate) + const light = useSelector(state => state.timer.light) return ( -
+
diff --git a/examples/with-redux-thunk/lib/with-redux-store.js b/examples/with-redux-thunk/lib/with-redux-store.js index 163daf7eb5a30..285dbfad20ee6 100644 --- a/examples/with-redux-thunk/lib/with-redux-store.js +++ b/examples/with-redux-thunk/lib/with-redux-store.js @@ -1,12 +1,11 @@ import React from 'react' -import { initializeStore } from '../store' +import initializeStore from '../store' -const isServer = typeof window === 'undefined' const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__' function getOrCreateStore(initialState) { // Always make a new store if server, otherwise state is shared between requests - if (isServer) { + if (typeof window === 'undefined') { return initializeStore(initialState) } @@ -22,29 +21,20 @@ export default App => { static async getInitialProps(appContext) { // Get or Create the store with `undefined` as initialState // This allows you to set a custom default initialState - const reduxStore = getOrCreateStore() + const store = getOrCreateStore() // Provide the store to getInitialProps of pages - appContext.ctx.reduxStore = reduxStore - - let appProps = {} - if (typeof App.getInitialProps === 'function') { - appProps = await App.getInitialProps(appContext) - } + appContext.ctx.store = store return { - ...appProps, - initialReduxState: reduxStore.getState(), + ...(App.getInitialProps ? await App.getInitialProps(appContext) : {}), + initialReduxState: store.getState(), } } - constructor(props) { - super(props) - this.reduxStore = getOrCreateStore(props.initialReduxState) - } - render() { - return + const { initialReduxState } = this.props + return } } } diff --git a/examples/with-redux-thunk/pages/_app.js b/examples/with-redux-thunk/pages/_app.js index 0c88a44d65423..df5b482157dcf 100644 --- a/examples/with-redux-thunk/pages/_app.js +++ b/examples/with-redux-thunk/pages/_app.js @@ -1,13 +1,13 @@ import App from 'next/app' import React from 'react' -import withReduxStore from '../lib/with-redux-store' import { Provider } from 'react-redux' +import withReduxStore from '../lib/with-redux-store' class MyApp extends App { render() { - const { Component, pageProps, reduxStore } = this.props + const { Component, pageProps, store } = this.props return ( - + ) diff --git a/examples/with-redux-thunk/pages/index.js b/examples/with-redux-thunk/pages/index.js index a243fb7391550..38a3d5896dc59 100644 --- a/examples/with-redux-thunk/pages/index.js +++ b/examples/with-redux-thunk/pages/index.js @@ -1,19 +1,18 @@ import React from 'react' import { connect } from 'react-redux' -import { startClock, serverRenderClock } from '../store' +import Link from 'next/link' +import { startClock, serverRenderClock } from '../actions' import Examples from '../components/examples' class Index extends React.Component { - static getInitialProps({ reduxStore, req }) { - const isServer = !!req - reduxStore.dispatch(serverRenderClock(isServer)) + static getInitialProps({ store, req }) { + store.dispatch(serverRenderClock(!!req)) return {} } componentDidMount() { - const { dispatch } = this.props - this.timer = startClock(dispatch) + this.timer = this.props.startClock() } componentWillUnmount() { @@ -21,8 +20,19 @@ class Index extends React.Component { } render() { - return + return ( + <> + + + Click to see current Redux State + + + ) } } -export default connect()(Index) +const mapDispatchToProps = { + startClock, +} + +export default connect(null, mapDispatchToProps)(Index) diff --git a/examples/with-redux-thunk/pages/show-redux-state.js b/examples/with-redux-thunk/pages/show-redux-state.js new file mode 100644 index 0000000000000..1994764f0fd55 --- /dev/null +++ b/examples/with-redux-thunk/pages/show-redux-state.js @@ -0,0 +1,26 @@ +import React from 'react' +import { connect } from 'react-redux' +import Link from 'next/link' + +const codeStyle = { + background: '#ebebeb', + width: 400, + padding: 10, + border: '1px solid grey', + marginBottom: 10, +} + +const ShowReduxState = state => ( + <> +
+      {JSON.stringify(state, null, 4)}
+    
+ + Go Back Home + + +) + +const mapDispatchToProps = state => state + +export default connect(mapDispatchToProps)(ShowReduxState) diff --git a/examples/with-redux-thunk/reducers.js b/examples/with-redux-thunk/reducers.js new file mode 100644 index 0000000000000..74b7876ebd3b7 --- /dev/null +++ b/examples/with-redux-thunk/reducers.js @@ -0,0 +1,43 @@ +import { combineReducers } from 'redux' +import * as types from './types' + +// COUNTER REDUCER +const counterReducer = (state = 0, { type }) => { + switch (type) { + case types.INCREMENT: + return state + 1 + case types.DECREMENT: + return state - 1 + case types.RESET: + return 0 + default: + return state + } +} + +// INITIAL TIMER STATE +const initialTimerState = { + lastUpdate: 0, + light: false, +} + +// TIMER REDUCER +const timerReducer = (state = initialTimerState, { type, payload }) => { + switch (type) { + case types.TICK: + return { + lastUpdate: payload.ts, + light: !!payload.light, + } + default: + return state + } +} + +// COMBINED REDUCERS +const reducers = { + counter: counterReducer, + timer: timerReducer, +} + +export default combineReducers(reducers) diff --git a/examples/with-redux-thunk/store.js b/examples/with-redux-thunk/store.js index 3835647cca5fb..082638e2c04d0 100644 --- a/examples/with-redux-thunk/store.js +++ b/examples/with-redux-thunk/store.js @@ -1,72 +1,24 @@ import { createStore, applyMiddleware } from 'redux' import { composeWithDevTools } from 'redux-devtools-extension' import thunkMiddleware from 'redux-thunk' +import reducers from './reducers' -const exampleInitialState = { - lastUpdate: 0, - light: false, - count: 0, -} +// CREATING INITIAL STORE +export default initialState => { + const store = createStore( + reducers, + initialState, + composeWithDevTools(applyMiddleware(thunkMiddleware)) + ) -export const actionTypes = { - TICK: 'TICK', - INCREMENT: 'INCREMENT', - DECREMENT: 'DECREMENT', - RESET: 'RESET', -} + // IF REDUCERS WERE CHANGED, RELOAD WITH INITIAL STATE + if (module.hot) { + module.hot.accept('./reducers', () => { + const createNextReducer = require('./reducers').default -// REDUCERS -export const reducer = (state = exampleInitialState, action) => { - switch (action.type) { - case actionTypes.TICK: - return Object.assign({}, state, { - lastUpdate: action.ts, - light: !!action.light, - }) - case actionTypes.INCREMENT: - return Object.assign({}, state, { - count: state.count + 1, - }) - case actionTypes.DECREMENT: - return Object.assign({}, state, { - count: state.count - 1, - }) - case actionTypes.RESET: - return Object.assign({}, state, { - count: exampleInitialState.count, - }) - default: - return state + store.replaceReducer(createNextReducer(initialState)) + }) } -} - -// ACTIONS -export const serverRenderClock = isServer => dispatch => { - return dispatch({ type: actionTypes.TICK, light: !isServer, ts: Date.now() }) -} - -export const startClock = dispatch => { - return setInterval(() => { - dispatch({ type: actionTypes.TICK, light: true, ts: Date.now() }) - }, 1000) -} -export const incrementCount = () => { - return { type: actionTypes.INCREMENT } -} - -export const decrementCount = () => { - return { type: actionTypes.DECREMENT } -} - -export const resetCount = () => { - return { type: actionTypes.RESET } -} - -export function initializeStore(initialState = exampleInitialState) { - return createStore( - reducer, - initialState, - composeWithDevTools(applyMiddleware(thunkMiddleware)) - ) + return store } diff --git a/examples/with-redux-thunk/types.js b/examples/with-redux-thunk/types.js new file mode 100644 index 0000000000000..05cfd696391d0 --- /dev/null +++ b/examples/with-redux-thunk/types.js @@ -0,0 +1,5 @@ +// REDUX ACTION TYPES +export const TICK = 'TICK' +export const INCREMENT = 'INCREMENT' +export const DECREMENT = 'DECREMENT' +export const RESET = 'RESET' From 9b0aab5382be2994449563a2d3ee58e01ee39bc1 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 10 Apr 2020 13:14:40 -0700 Subject: [PATCH 2/5] Update README --- examples/with-redux-thunk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/with-redux-thunk/README.md b/examples/with-redux-thunk/README.md index 2fd61ba4ec0f1..07de4d629c6c3 100644 --- a/examples/with-redux-thunk/README.md +++ b/examples/with-redux-thunk/README.md @@ -60,4 +60,4 @@ This example includes two different ways to access the `store` or to `dispatch` You can either use the `connect` function to access redux state and/or dispatch actions or use the hook variations: `useSelector` and `useDispatch`. It's up to you. -This example also includes hot-reloading when one of the `reducers` has changed. However, there is one caveat with this implementation: If you're using the `Redux DevTools` browser extension, then all previously recorded actions will be recreated when a reducer has changed (in other words, if you increment the counter by 1 using the `+1` button, and then change the increment action to add 10 in the reducer, Redux DevTools will playback all actions and adjust the counter state by 10 to reflect the reducer change). Therefore, to avoid this issue, the store has been set up to reset to back initial state upon a reducer change. If you wish to persist redux state regardless (or you don't have the extension installed), then in `store.js` change (line 19) `store.replaceReducer(createNextReducer(initialState))` to `store.replaceReducer(createNextReducer)`. +This example also includes hot-reloading when one of the `reducers` has changed. However, there is one caveat with this implementation: If you're using the `Redux DevTools` browser extension, then all previously recorded actions will be recreated when a reducer has changed (in other words, if you increment the counter by 1 using the `+1` button, and then change the increment action to add 10 in the reducer, Redux DevTools will playback all actions and adjust the counter state by 10 to reflect the reducer change). Therefore, to avoid this issue, the store has been set up to reset back initial state upon a reducer change. If you wish to persist redux state regardless (or you don't have the extension installed), then in `store.js` change (line 19) `store.replaceReducer(createNextReducer(initialState))` to `store.replaceReducer(createNextReducer)`. From 7429a9baee2ae3c7b394d18d0c92ab9688aa3db4 Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 10 Apr 2020 13:16:48 -0700 Subject: [PATCH 3/5] Fix clock component --- examples/with-redux-thunk/components/clock.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/with-redux-thunk/components/clock.js b/examples/with-redux-thunk/components/clock.js index 50a49693f6923..6bdd9221010c3 100644 --- a/examples/with-redux-thunk/components/clock.js +++ b/examples/with-redux-thunk/components/clock.js @@ -1,3 +1,5 @@ +import React from 'react' + const pad = n => (n < 10 ? `0${n}` : n) const format = t => From 8e03e79b1ff6677d968989320511fef9ffd3322e Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 10 Apr 2020 13:18:56 -0700 Subject: [PATCH 4/5] Fix example component --- examples/with-redux-thunk/components/examples.js | 5 ++++- examples/with-redux-thunk/pages/_app.js | 2 +- examples/with-redux-thunk/pages/index.js | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/with-redux-thunk/components/examples.js b/examples/with-redux-thunk/components/examples.js index 700a811d5f79b..c57ea3608e2f2 100644 --- a/examples/with-redux-thunk/components/examples.js +++ b/examples/with-redux-thunk/components/examples.js @@ -1,8 +1,9 @@ +import React from 'react' import { useSelector } from 'react-redux' import Clock from './clock' import Counter from './counter' -export default () => { +const Examples = () => { const lastUpdate = useSelector(state => state.timer.lastUpdate) const light = useSelector(state => state.timer.light) @@ -13,3 +14,5 @@ export default () => {
) } + +export default Examples diff --git a/examples/with-redux-thunk/pages/_app.js b/examples/with-redux-thunk/pages/_app.js index df5b482157dcf..e16d1e9e3efe2 100644 --- a/examples/with-redux-thunk/pages/_app.js +++ b/examples/with-redux-thunk/pages/_app.js @@ -1,6 +1,6 @@ -import App from 'next/app' import React from 'react' import { Provider } from 'react-redux' +import App from 'next/app' import withReduxStore from '../lib/with-redux-store' class MyApp extends App { diff --git a/examples/with-redux-thunk/pages/index.js b/examples/with-redux-thunk/pages/index.js index 38a3d5896dc59..fdcc4194353c5 100644 --- a/examples/with-redux-thunk/pages/index.js +++ b/examples/with-redux-thunk/pages/index.js @@ -1,10 +1,10 @@ -import React from 'react' +import React, { PureComponent } from 'react' import { connect } from 'react-redux' import Link from 'next/link' import { startClock, serverRenderClock } from '../actions' import Examples from '../components/examples' -class Index extends React.Component { +class Index extends PureComponent { static getInitialProps({ store, req }) { store.dispatch(serverRenderClock(!!req)) From 9c6fe3954975c3be2710d2f08a587d2c5b46626d Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 10 Apr 2020 17:56:21 -0700 Subject: [PATCH 5/5] Fix README --- examples/with-redux-thunk/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/with-redux-thunk/README.md b/examples/with-redux-thunk/README.md index 07de4d629c6c3..80f4282727684 100644 --- a/examples/with-redux-thunk/README.md +++ b/examples/with-redux-thunk/README.md @@ -54,6 +54,7 @@ In the `clock` component, we are going to display a digital clock that updates e In the `counter` component, we are going to display a user-interactive counter that can be increased or decreased when the provided buttons are pressed. This example includes two different ways to access the `store` or to `dispatch` actions: + 1.) `pages/index.js` will utilize `connect` from `react-redux` to `dispatch` the `startClock` redux action once the component has been mounted in the browser. 2.) `components/counter.js` and `components/examples.js` have access to the redux store using `useSelector` and can dispatch actions using `useDispatch` from `react-redux@^7.1.0`