mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
example with-redux-observable (#3272)
* example with-redux-observable * fix styling with js standard-style
This commit is contained in:
parent
45e26f22b3
commit
5260736e33
41
examples/with-redux-observable/README.md
Normal file
41
examples/with-redux-observable/README.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Redux-Observable example
|
||||
|
||||
## How to use
|
||||
|
||||
Download the example [or clone the repo](https://github.com/zeit/next.js):
|
||||
|
||||
```bash
|
||||
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-redux-observable
|
||||
cd with-redux-observable
|
||||
```
|
||||
|
||||
Install it and run:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
|
||||
### The idea behind the example
|
||||
Example is a page that renders information about Star-Wars characters. It fetches new character
|
||||
every 3 seconds having the initial character fetched on a server.
|
||||
|
||||
Example also uses `redux-logger` to log every action.
|
||||
|
||||
![demo page](demo.png)
|
||||
|
||||
The main problem with integrating Redux, Redux-Observable and Next.js is probably making initial requests
|
||||
on a server. That's because it's not possible to wait until epics are resolved in `getInitialProps` hook.
|
||||
|
||||
In order to have best of two worlds, we can extract request logic and use it separately.
|
||||
That's what `lib/api.js` is for. It keeps functions that return configured Observable for ajax request.
|
||||
You can notice that `fetchCharacter` method is used to get initial data in `pages/index.js`
|
||||
and also in `lib/reducer.js` within an epic.
|
||||
|
||||
Other than above, configuration is pretty the same as in
|
||||
[with-redux example](https://github.com/zeit/next.js/tree/canary/examples/with-redux)
|
||||
and [redux-observable docs](https://redux-observable.js.org/). There is, however one important thing
|
||||
to note, that we are not using `AjaxObservable` from `rxjs` library because it doesn't work on Node.
|
||||
Because of this we use a library like [universal-rx-request](https://www.npmjs.com/package/universal-rx-request).
|
||||
|
43
examples/with-redux-observable/components/CharacterInfo.js
Normal file
43
examples/with-redux-observable/components/CharacterInfo.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
const CharacterInfo = ({character, error, fetchCharacter, isFetchedOnServer = false}) => (
|
||||
<div className='CharacterInfo'>
|
||||
{
|
||||
error ? <p>We encountered and error.</p>
|
||||
: <article>
|
||||
<h3>Character: {character.name}</h3>
|
||||
<p>birth year: {character.birth_year}</p>
|
||||
<p>gender: {character.gender}</p>
|
||||
<p>skin color: {character.skin_color}</p>
|
||||
<p>eye color: {character.eye_color}</p>
|
||||
</article>
|
||||
|
||||
}
|
||||
<p>
|
||||
( was character fetched on server? -
|
||||
<b>{isFetchedOnServer.toString()})</b>
|
||||
</p>
|
||||
<style jsx>{`
|
||||
article {
|
||||
background-color: #528CE0;
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
width: 250px;
|
||||
margin: 15px 0;
|
||||
color: white;
|
||||
}
|
||||
button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
character: state.character,
|
||||
error: state.error,
|
||||
isFetchedOnServer: state.isFetchedOnServer
|
||||
}),
|
||||
)(CharacterInfo)
|
BIN
examples/with-redux-observable/demo.png
Normal file
BIN
examples/with-redux-observable/demo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
12
examples/with-redux-observable/lib/api.js
Normal file
12
examples/with-redux-observable/lib/api.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Ajax actions that return Observables.
|
||||
* They are going to be used by Epics and in getInitialProps to fetch initial data.
|
||||
*/
|
||||
|
||||
import { ajax, Observable } from './rxjs-library'
|
||||
import { fetchCharacterSuccess, fetchCharacterFailure } from './reducer'
|
||||
|
||||
export const fetchCharacter = (id, isServer) =>
|
||||
ajax({ url: `https://swapi.co/api/people/${id}` })
|
||||
.map(response => fetchCharacterSuccess(response.body, isServer))
|
||||
.catch(error => Observable.of(fetchCharacterFailure(error.response.body, isServer)))
|
17
examples/with-redux-observable/lib/index.js
Normal file
17
examples/with-redux-observable/lib/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { createStore, applyMiddleware } from 'redux'
|
||||
import thunkMiddleware from 'redux-thunk'
|
||||
import { createLogger } from 'redux-logger'
|
||||
import { combineEpics, createEpicMiddleware } from 'redux-observable'
|
||||
import starwarsReducer, { fetchUserEpic } from './reducer'
|
||||
|
||||
const rootEpic = combineEpics(
|
||||
fetchUserEpic,
|
||||
)
|
||||
|
||||
export default function initStore (initialState) {
|
||||
const epicMiddleware = createEpicMiddleware(rootEpic)
|
||||
const logger = createLogger({ collapsed: true }) // log every action to see what's happening behind the scenes.
|
||||
const reduxMiddleware = applyMiddleware(thunkMiddleware, epicMiddleware, logger)
|
||||
|
||||
return createStore(starwarsReducer, initialState, reduxMiddleware)
|
||||
};
|
51
examples/with-redux-observable/lib/reducer.js
Normal file
51
examples/with-redux-observable/lib/reducer.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import * as api from './api'
|
||||
import { Observable } from './rxjs-library'
|
||||
|
||||
const FETCH_CHARACTER_SUCCESS = 'FETCH_CHARACTER_SUCCESS'
|
||||
const FETCH_CHARACTER_FAILURE = 'FETCH_CHARACTER_FAILURE'
|
||||
const START_FETCHING_CHARACTERS = 'START_FETCHING_CHARACTERS'
|
||||
const STOP_FETCHING_CHARACTERS = 'STOP_FETCHING_CHARACTERS'
|
||||
|
||||
const INITIAL_STATE = {
|
||||
nextCharacterId: 1,
|
||||
character: {},
|
||||
isFetchedOnServer: false,
|
||||
error: null
|
||||
}
|
||||
|
||||
export default function reducer (state = INITIAL_STATE, { type, payload }) {
|
||||
switch (type) {
|
||||
case FETCH_CHARACTER_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
character: payload.response,
|
||||
isFetchedOnServer: payload.isServer,
|
||||
nextCharacterId: state.nextCharacterId + 1
|
||||
}
|
||||
case FETCH_CHARACTER_FAILURE:
|
||||
return { ...state, error: payload.error, isFetchedOnServer: payload.isServer }
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const startFetchingCharacters = () => ({ type: START_FETCHING_CHARACTERS })
|
||||
export const stopFetchingCharacters = () => ({ type: STOP_FETCHING_CHARACTERS })
|
||||
|
||||
export const fetchUserEpic = (action$, store) =>
|
||||
action$.ofType(START_FETCHING_CHARACTERS)
|
||||
.mergeMap(
|
||||
action => Observable.interval(3000)
|
||||
.mergeMap(x => api.fetchCharacter(store.getState().nextCharacterId))
|
||||
.takeUntil(action$.ofType(STOP_FETCHING_CHARACTERS))
|
||||
)
|
||||
|
||||
export const fetchCharacterSuccess = (response, isServer) => ({
|
||||
type: FETCH_CHARACTER_SUCCESS,
|
||||
payload: { response, isServer }
|
||||
})
|
||||
|
||||
export const fetchCharacterFailure = (error, isServer) => ({
|
||||
type: FETCH_CHARACTER_FAILURE,
|
||||
payload: { error, isServer }
|
||||
})
|
13
examples/with-redux-observable/lib/rxjs-library.js
Normal file
13
examples/with-redux-observable/lib/rxjs-library.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
// we bundle only what is necessary from rxjs library
|
||||
import 'rxjs/add/operator/mergeMap'
|
||||
import 'rxjs/add/operator/map'
|
||||
import 'rxjs/add/operator/delay'
|
||||
import 'rxjs/add/operator/takeUntil'
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import 'rxjs/add/observable/interval'
|
||||
import ajax from 'universal-rx-request' // because standard AjaxObservable only works in browser
|
||||
|
||||
export {
|
||||
Observable,
|
||||
ajax
|
||||
}
|
25
examples/with-redux-observable/package.json
Normal file
25
examples/with-redux-observable/package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "with-redux-observable",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"dev": "next",
|
||||
"build": "next build",
|
||||
"start": "next start"
|
||||
},
|
||||
"author": "tomaszmularczyk(tomasz.mularczyk89@gmail.com)",
|
||||
"dependencies": {
|
||||
"next": "latest",
|
||||
"next-redux-wrapper": "^1.0.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-redux": "^5.0.1",
|
||||
"redux": "^3.6.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-observable": "^0.17.0",
|
||||
"redux-thunk": "^2.1.0",
|
||||
"rxjs": "^5.5.2",
|
||||
"superagent": "^3.8.1",
|
||||
"universal-rx-request": "^1.0.3"
|
||||
},
|
||||
"license": "ISC"
|
||||
}
|
47
examples/with-redux-observable/pages/index.js
Normal file
47
examples/with-redux-observable/pages/index.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import withRedux from 'next-redux-wrapper'
|
||||
import initStore from '../lib'
|
||||
import { startFetchingCharacters, stopFetchingCharacters } from '../lib/reducer'
|
||||
import * as api from '../lib/api'
|
||||
import CharacterInfo from '../components/CharacterInfo'
|
||||
|
||||
class Counter extends React.Component {
|
||||
static async getInitialProps ({ store, isServer }) {
|
||||
const nextCharacterId = store.getState().nextCharacterId
|
||||
const resultAction = await api.fetchCharacter(nextCharacterId, isServer).toPromise() // we need to convert observable to Promise
|
||||
store.dispatch(resultAction)
|
||||
|
||||
return { isServer }
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.props.startFetchingCharacters()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.stopFetchingCharacters()
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<h1>Index Page</h1>
|
||||
<CharacterInfo />
|
||||
<br />
|
||||
<nav>
|
||||
<Link href='/other'><a>Navigate to "/other"</a></Link>
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRedux(
|
||||
initStore,
|
||||
null,
|
||||
{
|
||||
startFetchingCharacters,
|
||||
stopFetchingCharacters
|
||||
},
|
||||
)(Counter)
|
13
examples/with-redux-observable/pages/other.js
Normal file
13
examples/with-redux-observable/pages/other.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
const OtherPage = () => (
|
||||
<div>
|
||||
<h1>Other Page</h1>
|
||||
<Link href='/'>
|
||||
<a>Get back to "/"</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default OtherPage
|
Loading…
Reference in a new issue