diff --git a/examples/with-redux-thunk/README.md b/examples/with-redux-thunk/README.md index c8533d44a739b..80f4282727684 100644 --- a/examples/with-redux-thunk/README.md +++ b/examples/with-redux-thunk/README.md @@ -45,15 +45,20 @@ 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: -For simplicity and readability, Reducers, Actions, and Store creators are all in the same file: `store.js` +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` + +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 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..6bdd9221010c3 100644 --- a/examples/with-redux-thunk/components/clock.js +++ b/examples/with-redux-thunk/components/clock.js @@ -1,25 +1,27 @@ -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..c57ea3608e2f2 100644 --- a/examples/with-redux-thunk/components/examples.js +++ b/examples/with-redux-thunk/components/examples.js @@ -1,15 +1,18 @@ +import React from 'react' import { useSelector } from 'react-redux' import Clock from './clock' import Counter from './counter' -export default () => { - const lastUpdate = useSelector(state => state.lastUpdate) - const light = useSelector(state => state.light) +const Examples = () => { + const lastUpdate = useSelector(state => state.timer.lastUpdate) + const light = useSelector(state => state.timer.light) return ( -
+
) } + +export default Examples 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..e16d1e9e3efe2 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 App from 'next/app' +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..fdcc4194353c5 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 React, { PureComponent } 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)) +class Index extends PureComponent { + 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'