Skip to content

Commit

Permalink
Simplify with-redux example
Browse files Browse the repository at this point in the history
## Changes

- [x] Remove _app.js usage
- [x] Migrate withRedux HOC to functional component
- [x] Add correct display name
- [x] Remove abstractions/boilerplate from example
- [x] Add useInterval HOC from Dan
  • Loading branch information
HaNdTriX committed Sep 27, 2019
1 parent 9152a2a commit 12791c4
Show file tree
Hide file tree
Showing 11 changed files with 190 additions and 189 deletions.
9 changes: 3 additions & 6 deletions examples/with-redux/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,10 @@ Usually splitting your app state into `pages` feels natural but sometimes you'll

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`. 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.
The Redux `Provider` is implemented in `lib/redux.js`. Since the `IndexPage` component is wrapped in `withRedux` the redux context will be automatically initialized and provided to `IndexPage`.

`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`
All components have access to the redux store using `useSelector`, `useDispatch` or `connect` from `react-redux`.

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.

The example under `components/counter.js`, shows a simple incremental counter implementing a common Redux pattern of mapping state to props. 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

For simplicity and readability, Reducers, Actions, and Store creators are all in the same file: `store.js`
The example under `components/counter.js`, shows a simple incremental counter implementing a common Redux pattern of mapping state to props. 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.
25 changes: 22 additions & 3 deletions examples/with-redux/components/clock.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
export default ({ lastUpdate, light }) => {
import React from 'react'
import { useSelector, shallowEqual } from 'react-redux'

const useClock = () => {
return useSelector(
state => ({
lastUpdate: state.lastUpdate,
light: state.light
}),
shallowEqual
)
}

const formatTime = time => {
// cut off except hh:mm:ss
return new Date(time).toJSON().slice(11, 19)
}

const Clock = () => {
const { lastUpdate, light } = useClock()
return (
<div className={light ? 'light' : ''}>
{format(new Date(lastUpdate))}
{formatTime(lastUpdate)}
<style jsx>{`
div {
padding: 15px;
Expand All @@ -19,4 +38,4 @@ export default ({ lastUpdate, light }) => {
)
}

const format = t => t.toJSON().slice(11, 19) // cut off except hh:mm:ss
export default Clock
33 changes: 16 additions & 17 deletions examples/with-redux/components/counter.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { incrementCount, decrementCount, resetCount } from '../store'

const countSelector = state => state.count

const useCounter = () => {
const count = useSelector(state => state.count)
const dispatch = useDispatch()
const increment = () => {
dispatch(incrementCount())
}
const decrement = () => {
dispatch(decrementCount())
}
const reset = () => {
dispatch(resetCount())
}

return { increment, decrement, reset }
const increment = () =>
dispatch({
type: 'INCREMENT'
})
const decrement = () =>
dispatch({
type: 'DECREMENT'
})
const reset = () =>
dispatch({
type: 'RESET'
})
return { count, increment, decrement, reset }
}

function Counter () {
const count = useSelector(countSelector)
const { increment, decrement, reset } = useCounter()
const Counter = () => {
const { count, increment, decrement, reset } = useCounter()
return (
<div>
<h1>
Expand Down
20 changes: 0 additions & 20 deletions examples/with-redux/components/examples.js

This file was deleted.

72 changes: 72 additions & 0 deletions examples/with-redux/lib/redux.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react'
import { Provider } from 'react-redux'
import { initializeStore } from '../store'
import App from 'next/app'

export const withRedux = (PageComponent, { ssr = true } = {}) => {
const WithRedux = ({ initialReduxState, ...props }) => {
const store = getOrInitializeStore(initialReduxState)
return (
<Provider store={store}>
<PageComponent {...props} />
</Provider>
)
}

// Make sure people don't use this HOC on _app.js level
if (process.env.NODE_ENV !== 'production') {
const isAppHoc =
PageComponent === App || PageComponent.prototype instanceof App
if (isAppHoc) {
throw new Error('The withRedux HOC only works with PageComponents')
}
}

// Set the correct displayName in development
if (process.env.NODE_ENV !== 'production') {
const displayName =
PageComponent.displayName || PageComponent.name || 'Component'

WithRedux.displayName = `withRedux(${displayName})`
}

if (ssr || PageComponent.getInitialProps) {
WithRedux.getInitialProps = async context => {
// Get or Create the store with `undefined` as initialState
// This allows you to set a custom default initialState
const reduxStore = getOrInitializeStore()

// Provide the store to getInitialProps of pages
context.reduxStore = reduxStore

// Run getInitialProps from HOCed PageComponent
const pageProps =
typeof PageComponent.getInitialProps === 'function'
? await PageComponent.getInitialProps(context)
: {}

// Pass props to PageComponent
return {
...pageProps,
initialReduxState: reduxStore.getState()
}
}
}

return WithRedux
}

let reduxStore
const getOrInitializeStore = initialState => {
// Always make a new store if server, otherwise state is shared between requests
if (typeof window === 'undefined') {
return initializeStore(initialState)
}

// Create store if unavailable on the client and set it on the window object
if (!reduxStore) {
reduxStore = initializeStore(initialState)
}

return reduxStore
}
19 changes: 19 additions & 0 deletions examples/with-redux/lib/useInterval.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useRef } from 'react'

// https://overreacted.io/making-setinterval-declarative-with-react-hooks/
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
const handler = (...args) => savedCallback.current(...args)

if (delay !== null) {
const id = setInterval(handler, delay)
return () => clearInterval(id)
}
}, [delay])
}

export default useInterval
50 changes: 0 additions & 50 deletions examples/with-redux/lib/with-redux-store.js

This file was deleted.

4 changes: 2 additions & 2 deletions examples/with-redux/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"dependencies": {
"next": "latest",
"react": "^16.9.0",
"redux-devtools-extension": "^2.13.2",
"react-dom": "^16.9.0",
"react-redux": "^7.1.0",
"redux": "^3.6.0"
"redux": "^3.6.0",
"redux-devtools-extension": "^2.13.2"
},
"license": "ISC"
}
17 changes: 0 additions & 17 deletions examples/with-redux/pages/_app.js

This file was deleted.

62 changes: 34 additions & 28 deletions examples/with-redux/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
import React from 'react'
import { connect } from 'react-redux'
import { startClock, serverRenderClock } from '../store'
import Examples from '../components/examples'
import { useDispatch } from 'react-redux'
import { withRedux } from '../lib/redux'
import useInterval from '../lib/useInterval'
import Clock from '../components/clock'
import Counter from '../components/counter'

class Index extends React.Component {
static getInitialProps ({ reduxStore, req }) {
const isServer = !!req
// DISPATCH ACTIONS HERE ONLY WITH `reduxStore.dispatch`
reduxStore.dispatch(serverRenderClock(isServer))

return {}
}

componentDidMount () {
// DISPATCH ACTIONS HERE FROM `mapDispatchToProps`
// TO TICK THE CLOCK
this.timer = setInterval(() => this.props.startClock(), 1000)
}
const IndexPage = () => {
// Tick the time every second
const dispatch = useDispatch()
useInterval(() => {
dispatch({
type: 'TICK',
light: true,
lastUpdate: Date.now()
})
}, 1000)
return (
<>
<Clock />
<Counter />
</>
)
}

componentWillUnmount () {
clearInterval(this.timer)
}
IndexPage.getInitialProps = ({ reduxStore }) => {
// Tick the time once, so we'll have a
// valid time before first render
const { dispatch } = reduxStore
dispatch({
type: 'TICK',
light: typeof window === 'object',
lastUpdate: Date.now()
})

render () {
return <Examples />
}
return {}
}
const mapDispatchToProps = { startClock }
export default connect(
null,
mapDispatchToProps
)(Index)

export default withRedux(IndexPage)
Loading

0 comments on commit 12791c4

Please sign in to comment.