1
0
Fork 0
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:
Tomek 2017-11-13 20:37:43 +01:00 committed by Tim Neutkens
parent 45e26f22b3
commit 5260736e33
10 changed files with 262 additions and 0 deletions

View 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).

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View 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)))

View 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)
};

View 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 }
})

View 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
}

View 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"
}

View 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)

View 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