Skip to content

Commit

Permalink
Merge pull request #2 from jayphelps/rename-files
Browse files Browse the repository at this point in the history
Tweak docs, add example app
  • Loading branch information
benlesh committed May 12, 2016
2 parents a1cf32e + 2c51ccd commit ab33766
Show file tree
Hide file tree
Showing 15 changed files with 226 additions and 64 deletions.
87 changes: 27 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

# redux-observable (beta)

Creates [RxJS 5](http://github.com/ReactiveX/RxJS)-based middleware for
[Redux](http://github.com/reactjs/redux).
[RxJS 5](http://github.com/ReactiveX/RxJS)-based middleware for
[Redux](http://github.com/reactjs/redux). Compose and cancel async actions and more.

- Dispatch a function that returns an observable of actions, a promise of action or iterable of actions.
- Function is provided a stream of all actions, useful for composition with the current dispatched observable
- Function is provided a stream of all future actions, useful for composition with the current dispatched observable, especially for cancellation.
(think things like `takeUntil` or `zip`)
- Function is also provided a reference to the store which can be used to get the state or even dispatch.

Expand All @@ -16,7 +16,7 @@ NOTE: This has a peer dependencies of `rxjs@5.0.*` and `redux`, which will have
as well.

```sh
npm i -S redux-observable
npm install --save redux-observable
```

## Usage
Expand All @@ -38,7 +38,16 @@ dispatch(() => (function* () {
for (let i = 0; i < 10; i++) {
yield { type: 'SOME_GENERATED_ACTION', value: i };
}
}()))
}()));

// Of couse, you'll usually create action factories instead:

const asyncAction = () => (
(actions, store) => Rx.Observable.of({ type: 'ASYNC_ACTION_FROM_RX' }).delay(1000)
);

dispatch(asyncAction());

```

### Cancellation
Expand All @@ -48,9 +57,13 @@ by leveraging the first argument to your dispatched function, which returns all
you can use `takeUntil` to abort the async action cleanly and via composition.

```js
dispatch((actions) => Observable.timer(1000)
.map(() => ({ type: 'TIMER_COMPLETE'}))
.takeUntil(actions.filter(a => a.type === 'ABORT_TIMER')))
dispatch(
(actions) => Observable.timer(1000)
.map(() => ({ type: 'TIMER_COMPLETE'}))
.takeUntil(
actions.filter(a => a.type === 'ABORT_TIMER')
)
);

// elsewhere in your code you can abort with a simple dispatch
dispatch({ type: 'ABORT_TIMER' });
Expand Down Expand Up @@ -84,58 +97,12 @@ dispatch = ((actions?: Observable<Action>, store?: ReduxStore) => Observable<Act

### Example

Below is a basic example of it how it might work in React.
A full example is available in [examples/basic](examples/basic)

```js
import { Component } from 'react';
import { connect } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { reduxObservable } from 'redux-observable';
import * as Rx from 'rxjs';

// Just the plain redux reducer.
const reducer = (state = {}, action) => {
switch (action.type) {
case 'DATA_LOADING':
return { ...state, loading: true };
case 'DATA_LOADED':
return { ...state, loading: false, data: action.data };
case 'ABORT_LOAD':
return { ...state, loading: false };
}
return state;
};

// making a store
const store = createStore(reducer, applyMiddleware(reduxObservable()));

// set up async dispatches here
const loadData = () => (actions, store) => Observable.of('hello world')
.delay(1000)
.map(data => ({ type: 'DATA_LOADED', data })
.startWith({ type: 'DATA_LOADING' })
.takeUntil(actions.filter(a => a.type === 'ABORT_LOAD'));

// plain old action
const abortLoad = () => ({ type: 'ABORT_LOAD' });

const mapStateToProps = ({ data, loading }) => ({ data, loading });

const mapDispatchToProps = (dispatch) => ({
loadData: () => dispatch(loadData()),
abortLoad: () => dispatch(abortLoad())
});

const MyComponent = ({ loading, data, loadData, abortLoad }) => (
<div>
<button onClick={loadData}>load data</button>
<button onClick={abortLoad}>abort load</button>
<div>Loading: {loading}</div>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
* * *

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
```
##### Incompatible w/ redux-thunk

Since redux-observable uses dispached functions, this middlware is *incompatible with redux-thunk*. At this time, this is unavoidable since providing the function a stream of future actions for cancellation is imperative.

:shipit:
:shipit:
6 changes: 6 additions & 0 deletions examples/basic/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": ["es2015", "react"],
"plugins": [
"transform-object-rest-spread"
]
}
9 changes: 9 additions & 0 deletions examples/basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Basic Example

### Run it

```bash
npm install
npm start
# Listening at localhost:3000
```
9 changes: 9 additions & 0 deletions examples/basic/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<head>
<title>Example</title>
</head>
<body>
<div id="app"></div>
<script src="/static/bundle.js"></script>
</body>
</html>
36 changes: 36 additions & 0 deletions examples/basic/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "redux-observable-example-basic",
"version": "1.0.0",
"description": "Example usage of redux-observable",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"repository": {
"type": "git",
"url": "https://github.com/blesh/redux-observable.git"
},
"author": "",
"license": "MIT",
"dependencies": {
"babel-runtime": "^6.6.1",
"react": "^15.0.2",
"react-dom": "^15.0.2",
"react-redux": "^4.4.5",
"redux": "^3.5.2",
"redux-observable": "*",
"rxjs": "^5.0.0-beta.7"
},
"devDependencies": {
"babel-cli": "^6.7.7",
"babel-core": "^6.7.7",
"babel-loader": "^6.2.4",
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"node-libs-browser": "^0.5.2",
"react-hot-loader": "^1.2.7",
"webpack": "^1.9.11",
"webpack-dev-server": "^1.9.0"
}
}
16 changes: 16 additions & 0 deletions examples/basic/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var webpack = require('webpack');
var WebpackDevServer = require('webpack-dev-server');
var config = require('./webpack.config');

new WebpackDevServer(webpack(config), {
publicPath: config.output.publicPath,
hot: true,
historyApiFallback: true,
stats: { colors: true }
}).listen(3000, 'localhost', function (err) {
if (err) {
console.log(err);
}

console.log('Listening at localhost:3000');
});
26 changes: 26 additions & 0 deletions examples/basic/src/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as Rx from 'rxjs';

export const FETCH_USER_PENDING = 'FETCH_USER_PENDING';
export const FETCH_USER_FULFILLED = 'FETCH_USER_FULFILLED';
export const FETCH_USER_ABORTED = 'FETCH_USER_ABORTED';

export const fetchUser = () => (
(actions, store) => Rx.Observable.of({ id: 1, name: 'Bilbo Baggins', timestamp: new Date() })
// Delaying to emulate an async request, like Rx.Observable.ajax('/api/path')
.delay(1000)
// When our request comes back, we transform it into an action
// that is then automatically dispatched by the middleware
.map(
payload => ({ type: FETCH_USER_FULFILLED, payload })
)
// Abort fetching the user if someone dispatches an abort action
.takeUntil(
actions.filter(action => action.type === FETCH_USER_ABORTED)
)
// Let's us immediately update the user's state so we can display
// loading messages to the user, etc.
.startWith({ type: FETCH_USER_PENDING })
);

// Plain old action
export const abortFetchUser = () => ({ type: FETCH_USER_ABORTED });
21 changes: 21 additions & 0 deletions examples/basic/src/components/Example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchUser, abortFetchUser } from '../actions';

const Example = ({ user, fetchUser, abortFetchUser }) => (
<div>
<button onClick={fetchUser}>fetch user</button>
<button onClick={abortFetchUser}>abort fetch user</button>
<div>Loading: {`${user.isLoading}`}</div>
<pre>{JSON.stringify(user, null, 2)}</pre>
</div>
);

const mapStateToProps = ({ user }) => ({ user });

const mapDispatchToProps = (dispatch) => ({
fetchUser: () => dispatch(fetchUser()),
abortFetchUser: () => dispatch(abortFetchUser())
});

export default connect(mapStateToProps, mapDispatchToProps)(Example);
16 changes: 16 additions & 0 deletions examples/basic/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { reduxObservable } from 'redux-observable';
import rootReducer from './reducers';
import Example from './components/Example';

const store = createStore(rootReducer, applyMiddleware(reduxObservable()));

render(
<Provider store={store}>
<Example />
</Provider>,
document.getElementById('app')
)
20 changes: 20 additions & 0 deletions examples/basic/src/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { combineReducers } from 'redux';
import { FETCH_USER_PENDING, FETCH_USER_FULFILLED, FETCH_USER_ABORTED } from '../actions';

const user = (state = { isLoading: false }, action) => {
switch (action.type) {
case FETCH_USER_PENDING:
return { ...state, isLoading: true };

case FETCH_USER_FULFILLED:
return { ...state, isLoading: false, ...action.payload };

case FETCH_USER_ABORTED:
return { ...state, isLoading: false };

default:
return state;
}
};

export default combineReducers({ user });
35 changes: 35 additions & 0 deletions examples/basic/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
var path = require('path');
var webpack = require('webpack');

module.exports = {
devtool: 'eval',
entry: [
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server',
'./src/index'
],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/static/'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
],
resolve: {
extensions: ['', '.js']
},
module: {
loaders: [{
test: /\.js$/,
loaders: ['react-hot', 'babel'],
exclude: /node_modules/,
include: __dirname
}, {
test: /\.js$/,
loaders: ['babel'],
include: path.join(__dirname, '..', '..', 'src')
}]
}
};
1 change: 0 additions & 1 deletion index.js

This file was deleted.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "redux-observable",
"version": "0.3.0",
"description": "creates RxJS based middleware for Redux",
"main": "index.js",
"description": "RxJS based middleware for Redux. Compose and cancel async actions and more.",
"main": "lib/index.js",
"scripts": {
"lint": "eslint src && eslint test",
"build": "npm run lint && rm -rf lib && babel src -d lib",
Expand All @@ -22,7 +22,9 @@
"middleware",
"observable",
"thunk",
"async"
"async",
"cancel",
"action"
],
"contributors": [
{ "name": "Ben Lesh", "email": "ben@benlesh.com" },
Expand Down
File renamed without changes.
File renamed without changes.

0 comments on commit ab33766

Please sign in to comment.