Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added example for usage with rematch (#4095)
* Usage with rematch * Run lint fix * Add counter-display example and readme
- Loading branch information
1 parent
e3a9c95
commit 3556a3a
Showing
11 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-rematch) | ||
|
||
# Rematch example | ||
|
||
## How to use | ||
|
||
### Using `create-next-app` | ||
|
||
Download [`create-next-app`](https://github.com/segmentio/create-next-app) to bootstrap the example: | ||
|
||
```bash | ||
npx create-next-app --example with-rematch with-rematch-app | ||
# or | ||
yarn create next-app --example with-rematch with-rematch-app | ||
``` | ||
|
||
### Download manually | ||
|
||
Download the example [or clone the repo](https://github.com/zeit/next.js): | ||
|
||
```bash | ||
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-rematch | ||
cd with-rematch | ||
``` | ||
|
||
Install it and run: | ||
|
||
```bash | ||
npm install | ||
npm run dev | ||
``` | ||
|
||
Deploy it to the cloud with [now](https://zeit.co/now) ([download](https://zeit.co/download)) | ||
|
||
```bash | ||
now | ||
``` | ||
|
||
## The idea behind the example | ||
|
||
This example has two pages. The first page has a counter which can be incremented synchronously or asynchronously. The second page is a page which shows a list of github users. It fetches data from the github api using this [endpoint](api.github.com/users). | ||
|
||
Since rematch is utility which uses redux under the hood, some elements like `store.js` and `withRematch` are very similar to the `with-redux` example. Please go through the `with-redux` example [here](https://github.com/zeit/next.js/tree/master/examples/with-redux) before reading further if you are not familiar with how redux is integrated with nextjs. Rematch is just an extension for Redux so a lot of elements are the same. | ||
|
||
**Directory structure** | ||
|
||
Besides the `pages` directory, there is a directory called shared which holds all of the code belonging to rematch. `Rematch` has a lot lesser boilerplate than `Redux` because it is able to put actions(including async actions), _models_ and reducers together. Hence, a `models` directory is present, which contains the logic for `counter` and `github` users. | ||
|
||
Some features of this example are : | ||
|
||
* Pages are connected to rematch using `withRematch` util. These pages are capable of accessing values from the store and dispatching changes | ||
* Components are inside the `shared/components` folder. The `counter-display` component is connected to the store using the `connect` function to show how components which are not pages, can connect with Rematch. | ||
* The file `shared/store` exports an initStore function which is used by `withRematch` to create store universally on the server and on the client. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "with-rematch", | ||
"version": "1.0.0", | ||
"scripts": { | ||
"dev": "next", | ||
"build": "next build", | ||
"start": "next start" | ||
}, | ||
"dependencies": { | ||
"@rematch/core": "0.6.0", | ||
"isomorphic-unfetch": "2.0.0", | ||
"next": "5.1.0", | ||
"react": "16.3.0", | ||
"react-dom": "16.3.0", | ||
"react-redux": "5.0.7", | ||
"redux": "3.7.2" | ||
}, | ||
"license": "ISC" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import React, { Component } from 'react' | ||
import Link from 'next/link' | ||
import { dispatch } from '@rematch/core' | ||
import { initStore } from '../shared/store' | ||
import withRematch from '../shared/utils/withRematch' | ||
import Header from '../shared/components/header' | ||
import CounterDisplay from '../shared/components/counter-display' | ||
|
||
class Github extends Component { | ||
static async getInitialProps ({ isServer, initialState }) { | ||
if (isServer) { | ||
await dispatch.github.fetchUsers() | ||
} | ||
return {} | ||
} | ||
render () { | ||
return ( | ||
<div> | ||
<Header /> | ||
<h1> Github users </h1> | ||
<p> | ||
Server rendered github user list. You can also reload the users from | ||
the api by clicking the <b>Get users</b> button below. | ||
</p> | ||
{this.props.isLoading ? <p>Loading ...</p> : null} | ||
<p> | ||
<button onClick={this.props.fetchUsers}>Get users</button> | ||
</p> | ||
{this.props.userList.map(user => ( | ||
<div key={user.login}> | ||
<Link href={user.html_url} passHref> | ||
<a> | ||
<img height='45' width='45' src={user.avatar_url} /> | ||
<span> Username - {user.login}</span> | ||
</a> | ||
</Link> | ||
<br /> | ||
</div> | ||
))} | ||
<br /> | ||
|
||
<CounterDisplay /> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
const mapState = state => ({ | ||
userList: state.github.users, | ||
isLoading: state.github.isLoading | ||
}) | ||
|
||
const mapDispatch = ({ github: { fetchUsers } }) => ({ | ||
fetchUsers: () => fetchUsers() | ||
}) | ||
|
||
export default withRematch(initStore, mapState, mapDispatch)(Github) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import React, { Component } from 'react' | ||
import { dispatch } from '@rematch/core' | ||
import { initStore } from '../shared/store' | ||
import withRematch from '../shared/utils/withRematch' | ||
import Header from '../shared/components/header' | ||
|
||
class Home extends Component { | ||
render () { | ||
return ( | ||
<div> | ||
<Header /> | ||
<h1> Counter </h1> | ||
<h3>The count is {this.props.counter}</h3> | ||
<p> | ||
<button onClick={this.props.increment}>increment</button> | ||
<button onClick={() => dispatch.counter.increment(1)}> | ||
increment (using dispatch function) | ||
</button> | ||
<button onClick={this.props.incrementBy(5)}>increment by 5</button> | ||
<button onClick={this.props.incrementAsync}>incrementAsync</button> | ||
</p> | ||
<br /> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
const mapState = state => ({ | ||
counter: state.counter | ||
}) | ||
|
||
const mapDispatch = ({ counter: { increment, incrementAsync } }) => ({ | ||
increment: () => increment(1), | ||
incrementBy: amount => () => increment(amount), | ||
incrementAsync: () => incrementAsync(1) | ||
}) | ||
|
||
export default withRematch(initStore, mapState, mapDispatch)(Home) |
31 changes: 31 additions & 0 deletions
31
examples/with-rematch/shared/components/counter-display.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import React, { Component } from 'react' | ||
import { connect } from 'react-redux' | ||
|
||
class CounterDisplay extends Component { | ||
render () { | ||
return ( | ||
<div> | ||
<h3> Counter </h3> | ||
<p> | ||
This counter is connected via the <b>connect</b> function. Components | ||
which are not pages can be connected using the connect function just | ||
like redux components. | ||
</p> | ||
<p>Current value {this.props.counter} </p> | ||
<p> | ||
<button onClick={this.props.incrementBy3}>Increment</button> | ||
</p> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
const mapState = state => ({ | ||
counter: state.counter | ||
}) | ||
|
||
const mapDispatch = ({ counter: { increment, incrementAsync } }) => ({ | ||
incrementBy3: () => increment(3) | ||
}) | ||
|
||
export default connect(mapState, mapDispatch)(CounterDisplay) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import React, { Component } from 'react' | ||
import Link from 'next/link' | ||
|
||
class Header extends Component { | ||
render () { | ||
return ( | ||
<div> | ||
<nav> | ||
<ul> | ||
<li> | ||
<Link href='/' passHref> | ||
<a>Home</a> | ||
</Link> | ||
</li> | ||
<li> | ||
<Link href='/github-users' passHref> | ||
<a>Async Example </a> | ||
</Link> | ||
</li> | ||
</ul> | ||
</nav> | ||
</div> | ||
) | ||
} | ||
} | ||
|
||
export default Header |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
const counter = { | ||
state: 0, // initial state | ||
reducers: { | ||
// handle state changes with pure functions | ||
increment (state, payload) { | ||
return state + payload | ||
} | ||
}, | ||
effects: { | ||
// handle state changes with impure functions. | ||
// use async/await for async actions | ||
async incrementAsync (payload, rootState) { | ||
await new Promise(resolve => setTimeout(resolve, 1000)) | ||
this.increment(payload) | ||
} | ||
} | ||
} | ||
|
||
export default counter |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import fetch from 'isomorphic-unfetch' | ||
|
||
const github = { | ||
state: { | ||
users: [], | ||
isLoading: false | ||
}, // initial state | ||
reducers: { | ||
requestUsers (state) { | ||
return { | ||
users: [], | ||
isLoading: true | ||
} | ||
}, | ||
receiveUsers (state, payload) { | ||
return { | ||
isLoading: false, | ||
users: payload | ||
} | ||
} | ||
}, | ||
effects: { | ||
// handle state changes with impure functions. | ||
// use async/await for async actions | ||
async fetchUsers (payload, rootState) { | ||
try { | ||
this.requestUsers() | ||
const response = await fetch('https://api.github.com/users') | ||
const users = await response.json() | ||
this.receiveUsers(users) | ||
} catch (err) { | ||
console.log(err) | ||
this.receiveUsers([]) | ||
} | ||
} | ||
} | ||
} | ||
|
||
export default github |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import counter from './counter' | ||
import github from './github' | ||
|
||
export { counter } | ||
export { github } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { init } from '@rematch/core' | ||
import { counter, github } from './models' | ||
|
||
// rematch store with initialValue set to 5 | ||
export const initStore = (initialState = { counter: 5 }) => { | ||
return init({ | ||
models: { | ||
counter, | ||
github | ||
}, | ||
redux: { | ||
initialState | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import React from 'react' | ||
import { connect, Provider } from 'react-redux' | ||
|
||
const __NEXT_REMATCH_STORE__ = '__NEXT_REMATCH_STORE__' | ||
|
||
// https://github.com/iliakan/detect-node | ||
const checkServer = () => | ||
Object.prototype.toString.call(global.process) === '[object process]' | ||
|
||
const getOrCreateStore = (initStore, initialState) => { | ||
// Always make a new store if server | ||
if (checkServer() || typeof window === 'undefined') { | ||
return initStore(initialState) | ||
} | ||
|
||
// Memoize store in global variable if client | ||
if (!window[__NEXT_REMATCH_STORE__]) { | ||
window[__NEXT_REMATCH_STORE__] = initStore(initialState) | ||
} | ||
return window[__NEXT_REMATCH_STORE__] | ||
} | ||
|
||
export default (...args) => Component => { | ||
// First argument is initStore, the rest are redux connect arguments and get passed | ||
const [initStore, ...connectArgs] = args | ||
|
||
const ComponentWithRematch = (props = {}) => { | ||
const { store, initialProps, initialState } = props | ||
|
||
// Connect page to redux with connect arguments | ||
const ConnectedComponent = connect.apply(null, connectArgs)(Component) | ||
|
||
// Wrap with redux Provider with store | ||
// Create connected page with initialProps | ||
return React.createElement( | ||
Provider, | ||
{ | ||
store: | ||
store && store.dispatch | ||
? store | ||
: getOrCreateStore(initStore, initialState) | ||
}, | ||
React.createElement(ConnectedComponent, initialProps) | ||
) | ||
} | ||
|
||
ComponentWithRematch.getInitialProps = async (props = {}) => { | ||
const isServer = checkServer() | ||
const store = getOrCreateStore(initStore) | ||
|
||
// Run page getInitialProps with store and isServer | ||
const initialProps = Component.getInitialProps | ||
? await Component.getInitialProps({ ...props, isServer, store }) | ||
: {} | ||
|
||
return { | ||
store, | ||
initialState: store.getState(), | ||
initialProps | ||
} | ||
} | ||
|
||
return ComponentWithRematch | ||
} |