Skip to content

Commit

Permalink
Refactor redux observable example (#3495)
Browse files Browse the repository at this point in the history
* move imports into files using lettable operators, remove rxjs-library

* refactor to be more in keeping with redux conventions

from the single reducer.js, I split the functionality into actionTypes
(actionTypes.js), actions (actions.js), and epics (epics.js). Most of
the fetching should be done in an epic, but that requires introducing a
new action and so was
better in a separate commit.

* switch to fetching on the front-end via an epic

The fetching previously was triggered using an api call that had side
effects, but was triggered from inside of an epic and was not an action.
Now calls on the front-end all of the api calls are occuring via an
action through fetchCharacterEpic. This does not remove the api.js file
as I have not yet been able to get the epic to trigger correctly on the
server-side, thus the api.fetchCharacter call is awaited in
getInitialProps for initialising the state serverSide.

* remove need for the serverSide api by directly handling the dispatch

This still seems to be an incomplete solution to the problem as it
circumvents the standard redux event flow on the serverside. However, it
does obey the spirit of the redux event flow (as it passes an Observable
of an action into the epic to then trigger other actions). Additionally,
this removes the problem of code duplication.

* update README.md and move lib/ to redux/

* Fix linting
  • Loading branch information
mpacer authored and timneutkens committed Feb 4, 2018
1 parent 4d9cf19 commit 3bbfbfa
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 103 deletions.
58 changes: 45 additions & 13 deletions examples/with-redux-observable/README.md
Expand Up @@ -29,24 +29,56 @@ 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.

This 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.
The main problem with integrating Redux, Redux-Observable and Next.js is
probably making initial requests on a server. That's because, the
`getInitialProps` hook runs on the server-side before epics have been made available by just dispatching actions.

However, we can access and execute epics directly. In order to do so, we need to
pass them an Observable of an action and they will return an Observable:

```js
static async getInitialProps({ store, isServer }) {
const resultAction = await rootEpic(
of(actions.fetchCharacter(isServer)),
store
).toPromise(); // we need to convert Observable to Promise
store.dispatch(resultAction)};
```

Note: we are not using `AjaxObservable` from the `rxjs` library; as of rxjs
v5.5.6, it will not work on both the server- and client-side. Instead we call
the default export from
[universal-rx-request](https://www.npmjs.com/package/universal-rx-request) (as
`ajax`).

We transform the Observable we get from `ajax` into a Promise in order to await
its resolution. That resolution should be a action (since the epic returns
Observables of actions). We immediately dispatch that action to the store.

This server-side solution allows compatibility with Next. It may not be
something you wish to emulate. In other situations, calling or awaiting epics
directly and passing their result to the store would be an anti-pattern. You
should only trigger epics by dispatching actions. This solution may not
generalise to resolving more complicated sets of actions.

The layout of the redux related functionality is split between:

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.
- actions (in `redux/actions.js`)
- actionTypes (in `redux/actionTypes.js`)
- epics (in `redux/epics.js`)
- reducer (in `redux/reducer.js`)

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).
and organized in `redux/index.js`.

Excepting in those manners discussed above, the configuration is similar the
configuration found in [with-redux example](https://github.com/zeit/next.js/tree/canary/examples/with-redux)
and [redux-observable docs](https://redux-observable.js.org/).
12 changes: 0 additions & 12 deletions examples/with-redux-observable/lib/api.js

This file was deleted.

51 changes: 0 additions & 51 deletions examples/with-redux-observable/lib/reducer.js

This file was deleted.

13 changes: 0 additions & 13 deletions examples/with-redux-observable/lib/rxjs-library.js

This file was deleted.

17 changes: 10 additions & 7 deletions examples/with-redux-observable/pages/index.js
@@ -1,15 +1,18 @@
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 initStore from '../redux'
import CharacterInfo from '../components/CharacterInfo'
import { rootEpic } from '../redux/epics'
import * as actions from '../redux/actions'
import { of } from 'rxjs/observable/of'

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
const resultAction = await rootEpic(
of(actions.fetchCharacter(isServer)),
store
).toPromise() // we need to convert Observable to Promise
store.dispatch(resultAction)

return { isServer }
Expand Down Expand Up @@ -41,7 +44,7 @@ export default withRedux(
initStore,
null,
{
startFetchingCharacters,
stopFetchingCharacters
startFetchingCharacters: actions.startFetchingCharacters,
stopFetchingCharacters: actions.stopFetchingCharacters
},
)(Counter)
5 changes: 5 additions & 0 deletions examples/with-redux-observable/redux/actionTypes.js
@@ -0,0 +1,5 @@
export const FETCH_CHARACTER = 'FETCH_CHARACTER'
export const FETCH_CHARACTER_SUCCESS = 'FETCH_CHARACTER_SUCCESS'
export const FETCH_CHARACTER_FAILURE = 'FETCH_CHARACTER_FAILURE'
export const START_FETCHING_CHARACTERS = 'START_FETCHING_CHARACTERS'
export const STOP_FETCHING_CHARACTERS = 'STOP_FETCHING_CHARACTERS'
21 changes: 21 additions & 0 deletions examples/with-redux-observable/redux/actions.js
@@ -0,0 +1,21 @@
import * as types from './actionTypes'

export const startFetchingCharacters = () => ({
type: types.START_FETCHING_CHARACTERS
})
export const stopFetchingCharacters = () => ({
type: types.STOP_FETCHING_CHARACTERS
})
export const fetchCharacter = isServer => ({
type: types.FETCH_CHARACTER,
payload: { isServer }
})
export const fetchCharacterSuccess = (response, isServer) => ({
type: types.FETCH_CHARACTER_SUCCESS,
payload: { response, isServer }
})

export const fetchCharacterFailure = (error, isServer) => ({
type: types.FETCH_CHARACTER_FAILURE,
payload: { error, isServer }
})
57 changes: 57 additions & 0 deletions examples/with-redux-observable/redux/epics.js
@@ -0,0 +1,57 @@
import { interval } from 'rxjs/observable/interval'
import { of } from 'rxjs/observable/of'
import { takeUntil, mergeMap, catchError } from 'rxjs/operators'
import { combineEpics, ofType } from 'redux-observable'
import ajax from 'universal-rx-request' // because standard AjaxObservable only works in browser

import * as actions from './actions'
import * as types from './actionTypes'

export const fetchUserEpic = (action$, store) =>
action$.pipe(
ofType(types.START_FETCHING_CHARACTERS),
mergeMap(action => {
return interval(3000).pipe(
mergeMap(x =>
of(
actions.fetchCharacter({
isServer: store.getState().isServer
})
)
),
takeUntil(action$.ofType(types.STOP_FETCHING_CHARACTERS))
)
})
)

export const fetchCharacterEpic = (action$, store) =>
action$.pipe(
ofType(types.FETCH_CHARACTER),
mergeMap(action =>
ajax({
url: `https://swapi.co/api/people/${store.getState().nextCharacterId}`
}).pipe(
mergeMap(response =>
of(
actions.fetchCharacterSuccess(
response.body,
store.getState().isServer
)
)
),
catchError(error =>
of(
actions.fetchCharacterFailure(
error.response.body,
store.getState().isServer
)
)
)
)
)
)

export const rootEpic = combineEpics(
fetchUserEpic,
fetchCharacterEpic
)
@@ -1,17 +1,14 @@
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,
)
import { createEpicMiddleware } from 'redux-observable'
import reducer from './reducer'
import { rootEpic } from './epics'

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)
return createStore(reducer, initialState, reduxMiddleware)
};
24 changes: 24 additions & 0 deletions examples/with-redux-observable/redux/reducer.js
@@ -0,0 +1,24 @@
import * as types from './actionTypes'

const INITIAL_STATE = {
nextCharacterId: 1,
character: {},
isFetchedOnServer: false,
error: null
}

export default function reducer (state = INITIAL_STATE, { type, payload }) {
switch (type) {
case types.FETCH_CHARACTER_SUCCESS:
return {
...state,
character: payload.response,
isFetchedOnServer: payload.isServer,
nextCharacterId: state.nextCharacterId + 1
}
case types.FETCH_CHARACTER_FAILURE:
return { ...state, error: payload.error, isFetchedOnServer: payload.isServer }
default:
return state
}
}

0 comments on commit 3bbfbfa

Please sign in to comment.