Skip to content

Commit 5260736

Browse files
Tomekmularczyktimneutkens
authored andcommitted
example with-redux-observable (vercel#3272)
* example with-redux-observable * fix styling with js standard-style
1 parent 45e26f2 commit 5260736

10 files changed

Lines changed: 262 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)