Skip to content

Commit

Permalink
Update with-redux-thunk example to include HMR (#11816)
Browse files Browse the repository at this point in the history
* Update with-redux-thunk example to include HMR

* Update README

* Fix clock component

* Fix example component

* Fix README
  • Loading branch information
Matt Carlotta committed Apr 14, 2020
1 parent 682120b commit ffa4089
Show file tree
Hide file tree
Showing 12 changed files with 190 additions and 129 deletions.
19 changes: 12 additions & 7 deletions examples/with-redux-thunk/README.md
Expand Up @@ -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)`.
23 changes: 23 additions & 0 deletions 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 })
42 changes: 22 additions & 20 deletions examples/with-redux-thunk/components/clock.js
@@ -1,25 +1,27 @@
export default ({ lastUpdate, light }) => {
return (
<div className={light ? 'light' : ''}>
{format(new Date(lastUpdate))}
<style jsx>{`
div {
padding: 15px;
display: inline-block;
color: #82fa58;
font: 50px menlo, monaco, monospace;
background-color: #000;
}
import React from 'react'

.light {
background-color: #999;
}
`}</style>
</div>
)
}
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 }) => (
<div className={light ? 'light' : ''}>
{format(new Date(lastUpdate))}
<style jsx>{`
div {
padding: 15px;
display: inline-block;
color: #82fa58;
font: 50px menlo, monaco, monospace;
background-color: #000;
}
.light {
background-color: #999;
}
`}</style>
</div>
)

export default Clock
8 changes: 5 additions & 3 deletions 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 (
Expand All @@ -17,3 +17,5 @@ export default () => {
</div>
)
}

export default Counter
11 changes: 7 additions & 4 deletions 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 (
<div>
<div style={{ marginBottom: 10 }}>
<Clock lastUpdate={lastUpdate} light={light} />
<Counter />
</div>
)
}

export default Examples
26 changes: 8 additions & 18 deletions 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)
}

Expand All @@ -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 <App {...this.props} reduxStore={this.reduxStore} />
const { initialReduxState } = this.props
return <App {...this.props} store={getOrCreateStore(initialReduxState)} />
}
}
}
8 changes: 4 additions & 4 deletions 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 (
<Provider store={reduxStore}>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
Expand Down
30 changes: 20 additions & 10 deletions examples/with-redux-thunk/pages/index.js
@@ -1,28 +1,38 @@
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() {
clearInterval(this.timer)
}

render() {
return <Examples />
return (
<>
<Examples />
<Link href="/show-redux-state">
<a>Click to see current Redux State</a>
</Link>
</>
)
}
}

export default connect()(Index)
const mapDispatchToProps = {
startClock,
}

export default connect(null, mapDispatchToProps)(Index)
26 changes: 26 additions & 0 deletions 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 => (
<>
<pre style={codeStyle}>
<code>{JSON.stringify(state, null, 4)}</code>
</pre>
<Link href="/">
<a>Go Back Home</a>
</Link>
</>
)

const mapDispatchToProps = state => state

export default connect(mapDispatchToProps)(ShowReduxState)
43 changes: 43 additions & 0 deletions 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)

0 comments on commit ffa4089

Please sign in to comment.