mirror of
https://github.com/seigler/triplebyte-react-spa
synced 2025-07-25 14:56:09 +00:00
Initial commit from Create Next App
This commit is contained in:
commit
5684ef36ec
10 changed files with 7925 additions and 0 deletions
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
56
README.md
Normal 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):
|
||||
|
||||
[](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
41
components/clock.js
Normal 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
36
components/counter.js
Normal 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
72
lib/redux.js
Normal 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
19
lib/useInterval.js
Normal 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
7576
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
18
package.json
Normal file
18
package.json
Normal 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
39
pages/index.js
Normal 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
44
store.js
Normal 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())
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue