Skip to content
This repository was archived by the owner on May 17, 2019. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 183 additions & 113 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@

[![Build status](https://badge.buildkite.com/c16ece6ba0a81b30d11d69cb90b8f4d77a0967860144d12f44.svg?branch=master)](https://buildkite.com/uberopensource/fusion-plugin-rpc-redux-react)

A plugin for integrating web-rpc, redux, and react.
Provides a higher order component that connects RPC methods to Redux as well as React component props

---

### Table of contents

* [Installation](#installation)
* [Usage](#usage)
* [Setup](#setup)
* [API](#api)
* [Registration API](#registration-api)
* [Dependencies](#dependencies)
* [`withRPCRedux`](#withrpcredux)
* [`withRPCReactor`](#withrpcreactor)
* [`mock`](#mock)
* [Other examples](#other-examples)

---

Expand All @@ -14,7 +29,49 @@ yarn add fusion-plugin-rpc-redux-react

---

### Example
### Usage

```js
// src/redux/index.js
import {createRPCReducer} from 'fusion-plugin-rpc-redux-react';
export default createRPCReducer('increment', {
start: (state, action) => ({count: state.count, loading: true, error: ''});
success: (state, action) => ({count: action.payload.count, loading: false, error: ''});
failure: (state, action) => ({count: state.count, loading: false, error: action.payload.error});
});

// src/rpc/index.js
export default {
greet: async () => "hello",
}

// src/components/index.js
import React from 'react';
import {withRPCRedux} from 'fusion-plugin-rpc-redux-react';
import {connect} from 'react-redux';
import {compose} from 'redux';

function Example({greet, greeting, loading, error}) {
return (
<div>
<button onClick={greet}>Greet</button>
{loading && 'loading'}
{error}
{greeting}
</div>
);
}

const hoc = compose(
withRPCRedux('greet'),
connect(({greeting, loading, error}) => ({greeting, loading, error})),
);
export default hoc(Example);
```

---

### Setup

```js
// src/main.js
Expand All @@ -25,8 +82,9 @@ import RPC, {RPCToken, RPCHandlersToken} from 'fusion-plugin-rpc-redux-react';
import {FetchToken} from 'fusion-tokens';
import fetch from 'unfetch';

import reducer from './reducer';
import handlers from './rpc';
import root from './components/index.js'
import reducer from './redux/index.js';
import handlers from './rpc/index.js';

export default () => {
const app = new App(root);
Expand All @@ -39,51 +97,133 @@ export default () => {
app.register(ReduxToken, Redux);
app.register(ReducerToken, reducer);
}
```

// src/reducer.js
import {createRPCReducer} from 'fusion-plugin-rpc-redux-react';
export default createRPCReducer('increment', {
start: (state, action) => ({count: state.count, loading: true, error: ''});
success: (state, action) => ({count: action.payload.count, loading: false, error: ''});
failure: (state, action) => ({count: state.count, loading: false, error: action.payload.error});
});
---

// src/rpc.js
export default {
getCount() {
return 0;
},
increment() {
return db.query(/* ... */).then(n => ({count: n}));
}
}
### API

// src/root.js
import React from 'react';
#### Registration API

##### `RPC`

```js
import RPC from 'fusion-plugin-rpc-redux-react';
```

The plugin. Typically it should be registered to [`RPCToken`](#rpctoken). Installs an RPC provider at the root of the React tree.

###### `RPCToken`

```js
import {RPCToken} from 'fusion-plugin-rpc-redux-react';
```

The canonical token for the RPC plugin. Typically, it should be registered with the [RPC](#rpc) plugin.

#### Dependencies

##### `UniversalEventsToken`

Required. See [https://github.com/fusionjs/fusion-plugin-universal-events#api](https://github.com/fusionjs/fusion-plugin-universal-events#api)

##### `RPCHandlersToken`

```js
import {RPCHandlersToken} from 'fusion-plugin-rpc-redux-react';
```

Configures what RPC handlers exist. Required. Server-only.

###### Types

```flow
type RPCHandlers = Object<string, () => any>
```

You can register a value of type `RPCHandlers` or a Plugin that provides a value of type `RPCHandlers`.

##### `FetchToken`

Required. Browser-only. See [https://github.com/fusionjs/fusion-tokens#fetchtoken](https://github.com/fusionjs/fusion-tokens#fetchtoken)

##### `ReduxToken`

Required. See [https://github.com/fusionjs/fusion-plugin-react-redux](https://github.com/fusionjs/fusion-plugin-react-redux)

##### `ReducerToken`

Required. See [https://github.com/fusionjs/fusion-plugin-react-redux](https://github.com/fusionjs/fusion-plugin-react-redux)

---

#### `withRPCRedux`

```js
import {withRPCRedux} from 'fusion-plugin-rpc-redux-react';
import {connect} from 'react-redux';
import {compose} from 'redux';
```

function Example({count, loading, error, increment}) {
return (
<div>
<p>Count: {count}</p>
<p>
<button onClick={() => increment()}>Increment</button>
</p>
{loading && 'Loading...'}
{error}
</div>
);
}
Creates a higher order component with a prop mapped to the given RPC method. It can additionally configure the mapped method with parameters from state or from a transformation function.

const hoc = compose(
withRPCRedux('increment'),
connect(({count, loading, error}) => ({count, loading, error})),
);
export default hoc(Example);
```js
const hoc:HOC = withRPCRedux(rpcId: string, {
propName: ?string,
mapStateToParams: ?(state: any) => any,
transformParams: ?(params: any) => any,
})
```

* `rpcId: string` - The name of the RPC method to expose in the component's props
* `propName: ?string` - Optional. The name of the prop. Defaults to the same as `rpcId`
* `mapStateToParams: ?(state: any) => any` - populate the RPC request with parameters from Redux state
* `transformParams: ?(params: any) => any` - transforms the params
* returns `hoc: Component => Component`

#### `withRPCReactor`

```js
import {withRPCReactor} from 'fusion-plugin-rpc-redux-react';
```

Creates a higher order component by colocating global reducers to the component

```js
const hoc:HOC = withRPCReactor(rpcId: string, {
start: ?(state: any, action: Object) => any,
success: ?(state: any, action: Object) => any,
failure: ?(state: any, action: Object) => any,
}, {
propName: ?string
mapStateToParams: ?(state: any) => any,
transformParams: ?(params: any) => any,
});
```

* `rpcId: string` - The name of the RPC method to expose in the component's props
* `start: ?(state: any, action: Object) => any` - A reducer to run when the RPC call is made
* `success: ?(state: any, action: Object) => any` - A reducer to run when the RPC call succeeds
* `failure: ?(state: any, action: Object) => any` - A reducer to run when the RPC call fails
* `propName: ?string` - Optional. The name of the prop. Defaults to the same as `rpcId`
* `mapStateToParams: ?(state: any) => any` - populate the RPC request with parameters from Redux state
* `transformParams: ?(params: any) => any` - transforms the params
* returns `hoc: Component => Component`

#### mock

```js
import {mock as MockRPC} from 'fusion-plugin-rpc-redux-react';
```

The package also exports a mock RPC plugin which can be useful for testing. For example:

```js
app.register(RPCToken, mock);
```

---

### Other examples

### Usage with Reactors

[`redux-reactors`](https://github.com/ganemone/redux-reactors) is a library that allows you to colocate Redux actions and reducers
Expand Down Expand Up @@ -164,7 +304,7 @@ Below is an example of consuming the state and RPC methods that are made availab
import React from 'react';
import {connect} from 'react-redux';
import {compose} from 'redux';
import {incrementReactor} from './reactors/increment.js'
import {incrementReactor} from './reactors/increment.js';

function Example({count, loading, error, increment}) {
return (
Expand All @@ -181,7 +321,7 @@ function Example({count, loading, error, increment}) {

const hoc = compose(
incrementReactor,
connect(({count, loading, error}) => ({count, loading, error})),
connect(({count, loading, error}) => ({count, loading, error}))
);
export default hoc(Example);
```
Expand All @@ -195,73 +335,3 @@ Another point worth mentioning is that with traditional reducers, it's possible
Reactors, on the other hand, colocate a single reducer to a single action, so all state transformations pertaining to any given action are handled by a single function. This comes at the cost of flexibility: it's no longer possible to refactor the shape of the state tree without changing every affectd reducer, and it's also possible to affect unrelated parts of the state tree, for example missing properties due to an overly conservative object assignment.

However doing large refactors to the shape of the state tree isn't necessarily all that common and it's often more intuitive to see all possible state transformations for a given action in a single place. In addition to creating less boilerplate, this pattern leads to similarly intuitive tests that are also colocated by action.

---

### API

#### Dependency registration

```js
// src/main.js
import {RPCHandlersToken} from 'fusion-plugin-rpc';
import UniversalEvents, {UniversalEventsToken} from 'fusion-plugin-universal-events';
import {FetchToken} from 'fusion-tokens';
import Redux, {ReduxToken, ReducerToken} from 'fusion-plugin-react-redux';

app.register(UniversalEventsToken, UniversalEvents);
__NODE__
? app.register(RPCHandlersToken, handlers);
: app.register(FetchToken, fetch);
```

##### Required dependencies

Name | Type | Description
-|-|-
`UniversalEventsToken` | `UniversalEvents` | An event emitter plugin, such as the one provided by [`fusion-plugin-universal-events`](https://github.com/fusionjs/fusion-plugin-universal-events).
`RPCHandlersToken` | `Object<(...args: any) => Promise>` | A map of server-side RPC method implementations. Server-only.
`FetchToken` | `(url: string, options: Object) => Promise` | A [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) implementation. Browser-only.

#### `withRPCRedux`

```js
import {withRPCRedux} from 'fusion-plugin-rpc-redux-react';
const NewComponent = withRPCRedux('rpcId', {
propName: '', // optional, defaults to rpcId
mapStateToParams: (state) => ({}), // optional
transformParams(params) => ({}), // optional
})(Component)
```

#### `withRPCReactor`
```js
import {withRPCReactor} from 'fusion-plugin-rpc-redux-react';
const NewComponent = withRPCReactor('rpcId', {
start: (state, action) => newState, // optional
success: (state, action) => newState, // optional
failure: (state, action) => newState, // optional
},
{
propName: '', // optional, defaults to rpcId
mapStateToParams: (state) => ({}), // optional
transformParams(params) => ({}), // optional
})(Component);
```

### Testing

The package also exports a mock rpc plugin which can be useful for testing. For example:

```js
import {mock as MockRPC} from 'fusion-plugin-rpc-redux-react';
app.plugin(mock, {
handlers: {
getUser: (args) => {
return {
mock: 'data',
}
}
}
});
```