Initial commit from Create Next App

This commit is contained in:
Joshua Seigler 2020-02-18 13:52:21 -05:00
commit 5684ef36ec
10 changed files with 7925 additions and 0 deletions

24
.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
.env*
.vscode
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

56
README.md Normal file
View file

@ -0,0 +1,56 @@
# Redux example
This example shows how to integrate Redux in Next.js.
Usually splitting your app state into `pages` feels natural but sometimes you'll want to have global state for your app. This is an example on how you can use redux that also works with Next.js's universal rendering approach.
## Deploy your own
Deploy the example using [ZEIT Now](https://zeit.co/now):
[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/new/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-redux)
## How to use
### Using `create-next-app`
Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npm init next-app --example with-redux with-redux-app
# or
yarn create next-app --example with-redux with-redux-app
```
### Download manually
Download the example:
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-redux
cd with-redux
```
Install it and run:
```bash
npm install
npm run dev
# or
yarn
yarn dev
```
Deploy it to the cloud with [ZEIT Now](https://zeit.co/new?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
## 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 `lib/redux.js`. Since the `IndexPage` component is wrapped in `withRedux` the redux context will be automatically initialized and provided to `IndexPage`.
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.

41
components/clock.js Normal file
View file

@ -0,0 +1,41 @@
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' : ''}>
{formatTime(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

36
components/counter.js Normal file
View file

@ -0,0 +1,36 @@
import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
const useCounter = () => {
const count = useSelector(state => state.count)
const dispatch = useDispatch()
const increment = () =>
dispatch({
type: 'INCREMENT',
})
const decrement = () =>
dispatch({
type: 'DECREMENT',
})
const reset = () =>
dispatch({
type: 'RESET',
})
return { count, increment, decrement, reset }
}
const Counter = () => {
const { count, increment, decrement, reset } = useCounter()
return (
<div>
<h1>
Count: <span>{count}</span>
</h1>
<button onClick={increment}>+1</button>
<button onClick={decrement}>-1</button>
<button onClick={reset}>Reset</button>
</div>
)
}
export default Counter

72
lib/redux.js Normal file
View file

@ -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
lib/useInterval.js Normal file
View file

@ -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

7576
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

18
package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "with-redux",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-redux": "^7.1.0",
"redux": "^3.6.0",
"redux-devtools-extension": "^2.13.2"
},
"license": "ISC"
}

39
pages/index.js Normal file
View file

@ -0,0 +1,39 @@
import React from 'react'
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'
const IndexPage = () => {
// Tick the time every second
const dispatch = useDispatch()
useInterval(() => {
dispatch({
type: 'TICK',
light: true,
lastUpdate: Date.now(),
})
}, 1000)
return (
<>
<Clock />
<Counter />
</>
)
}
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(),
})
return {}
}
export default withRedux(IndexPage)

44
store.js Normal file
View file

@ -0,0 +1,44 @@
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
const initialState = {
lastUpdate: 0,
light: false,
count: 0,
}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'TICK':
return {
...state,
lastUpdate: action.lastUpdate,
light: !!action.light,
}
case 'INCREMENT':
return {
...state,
count: state.count + 1,
}
case 'DECREMENT':
return {
...state,
count: state.count - 1,
}
case 'RESET':
return {
...state,
count: initialState.count,
}
default:
return state
}
}
export const initializeStore = (preloadedState = initialState) => {
return createStore(
reducer,
preloadedState,
composeWithDevTools(applyMiddleware())
)
}