8000 example with-redux-observable (#3272) · JavaScriptExpert/next.js@5260736 · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

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

File tree

10 files changed

+262
-0
lines changed

10 files changed

+262
-0
lines changed
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)
0