From 3556a3a63f81a09b553317a0a02a7dfcccddb258 Mon Sep 17 00:00:00 2001 From: Bhargav Ponnapalli Date: Tue, 3 Apr 2018 12:39:30 +0530 Subject: [PATCH] Added example for usage with rematch (#4095) * Usage with rematch * Run lint fix * Add counter-display example and readme --- examples/with-rematch/README.md | 53 +++++++++++++++ examples/with-rematch/package.json | 19 ++++++ examples/with-rematch/pages/github-users.js | 57 +++++++++++++++++ examples/with-rematch/pages/index.js | 38 +++++++++++ .../shared/components/counter-display.js | 31 +++++++++ .../with-rematch/shared/components/header.js | 27 ++++++++ .../with-rematch/shared/models/counter.js | 19 ++++++ examples/with-rematch/shared/models/github.js | 39 +++++++++++ examples/with-rematch/shared/models/index.js | 5 ++ examples/with-rematch/shared/store.js | 15 +++++ .../with-rematch/shared/utils/withRematch.js | 64 +++++++++++++++++++ 11 files changed, 367 insertions(+) create mode 100644 examples/with-rematch/README.md create mode 100644 examples/with-rematch/package.json create mode 100644 examples/with-rematch/pages/github-users.js create mode 100644 examples/with-rematch/pages/index.js create mode 100644 examples/with-rematch/shared/components/counter-display.js create mode 100644 examples/with-rematch/shared/components/header.js create mode 100644 examples/with-rematch/shared/models/counter.js create mode 100644 examples/with-rematch/shared/models/github.js create mode 100644 examples/with-rematch/shared/models/index.js create mode 100644 examples/with-rematch/shared/store.js create mode 100644 examples/with-rematch/shared/utils/withRematch.js diff --git a/examples/with-rematch/README.md b/examples/with-rematch/README.md new file mode 100644 index 00000000..2aff9032 --- /dev/null +++ b/examples/with-rematch/README.md @@ -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. diff --git a/examples/with-rematch/package.json b/examples/with-rematch/package.json new file mode 100644 index 00000000..8bdae15f --- /dev/null +++ b/examples/with-rematch/package.json @@ -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" +} diff --git a/examples/with-rematch/pages/github-users.js b/examples/with-rematch/pages/github-users.js new file mode 100644 index 00000000..0fb41991 --- /dev/null +++ b/examples/with-rematch/pages/github-users.js @@ -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 ( +
+
+

Github users

+

+ Server rendered github user list. You can also reload the users from + the api by clicking the Get users button below. +

+ {this.props.isLoading ?

Loading ...

: null} +

+ +

+ {this.props.userList.map(user => ( +
+ + + + Username - {user.login} + + +
+
+ ))} +
+ + +
+ ) + } +} + +const mapState = state => ({ + userList: state.github.users, + isLoading: state.github.isLoading +}) + +const mapDispatch = ({ github: { fetchUsers } }) => ({ + fetchUsers: () => fetchUsers() +}) + +export default withRematch(initStore, mapState, mapDispatch)(Github) diff --git a/examples/with-rematch/pages/index.js b/examples/with-rematch/pages/index.js new file mode 100644 index 00000000..79a78a61 --- /dev/null +++ b/examples/with-rematch/pages/index.js @@ -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 ( +
+
+

Counter

+

The count is {this.props.counter}

+

+ + + + +

+
+
+ ) + } +} + +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) diff --git a/examples/with-rematch/shared/components/counter-display.js b/examples/with-rematch/shared/components/counter-display.js new file mode 100644 index 00000000..f6beda2d --- /dev/null +++ b/examples/with-rematch/shared/components/counter-display.js @@ -0,0 +1,31 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' + +class CounterDisplay extends Component { + render () { + return ( +
+

Counter

+

+ This counter is connected via the connect function. Components + which are not pages can be connected using the connect function just + like redux components. +

+

Current value {this.props.counter}

+

+ +

+
+ ) + } +} + +const mapState = state => ({ + counter: state.counter +}) + +const mapDispatch = ({ counter: { increment, incrementAsync } }) => ({ + incrementBy3: () => increment(3) +}) + +export default connect(mapState, mapDispatch)(CounterDisplay) diff --git a/examples/with-rematch/shared/components/header.js b/examples/with-rematch/shared/components/header.js new file mode 100644 index 00000000..e3c04b4f --- /dev/null +++ b/examples/with-rematch/shared/components/header.js @@ -0,0 +1,27 @@ +import React, { Component } from 'react' +import Link from 'next/link' + +class Header extends Component { + render () { + return ( +
+ +
+ ) + } +} + +export default Header diff --git a/examples/with-rematch/shared/models/counter.js b/examples/with-rematch/shared/models/counter.js new file mode 100644 index 00000000..5a641df2 --- /dev/null +++ b/examples/with-rematch/shared/models/counter.js @@ -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 diff --git a/examples/with-rematch/shared/models/github.js b/examples/with-rematch/shared/models/github.js new file mode 100644 index 00000000..7436698e --- /dev/null +++ b/examples/with-rematch/shared/models/github.js @@ -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 diff --git a/examples/with-rematch/shared/models/index.js b/examples/with-rematch/shared/models/index.js new file mode 100644 index 00000000..14082a26 --- /dev/null +++ b/examples/with-rematch/shared/models/index.js @@ -0,0 +1,5 @@ +import counter from './counter' +import github from './github' + +export { counter } +export { github } diff --git a/examples/with-rematch/shared/store.js b/examples/with-rematch/shared/store.js new file mode 100644 index 00000000..d968768b --- /dev/null +++ b/examples/with-rematch/shared/store.js @@ -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 + } + }) +} diff --git a/examples/with-rematch/shared/utils/withRematch.js b/examples/with-rematch/shared/utils/withRematch.js new file mode 100644 index 00000000..5b6b9468 --- /dev/null +++ b/examples/with-rematch/shared/utils/withRematch.js @@ -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 +}