4.6.8 • Published 1 year ago

fusion-plugin-rpc-redux-react v4.6.8

Weekly downloads
234
License
MIT
Repository
github
Last release
1 year ago

fusion-plugin-rpc-redux-react

Build status

A plugin for integrating web-rpc, redux, and react.


Installation

yarn add fusion-plugin-rpc-redux-react

Example

// src/main.js
import App from 'fusion-react';
import UniversalEvents, {UniversalEventsToken} from 'fusion-plugin-universal-events';
import Redux, {ReduxToken, ReducerToken} from 'fusion-plugin-react-redux';
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';

export default () => {
  const app = new App(root);

  app.register(RPCToken, RPC);
  app.register(UniversalEventsToken, UniversalEvents);
  __NODE__
    ? app.register(RPCHandlersToken, handlers);
    : app.register(FetchToken, fetch);
  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}));
  }
}

// src/root.js
import React from 'react';
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>
  );
}

const hoc = compose(
  withRPCRedux('increment'),
  connect(({count, loading, error}) => ({count, loading, error})),
);
export default hoc(Example);

Usage with Reactors

redux-reactors is a library that allows you to colocate Redux actions and reducers

The fusion-plugin-rpc-redux-react package provides a withRPCReactor HOC which facilitates implementing a Redux store using reactors.

To use it, register the fusion-plugin-react-redux plugin with reactorEnhancer from redux-reactors:

// src/main.js
import App from 'fusion-react';
import Redux, {
  ReduxToken,
  ReducerToken,
  EnhancerToken
} from 'fusion-plugin-react-redux';
import RPC, {RPCToken, RPCHandlersToken} from 'fusion-plugin-rpc-redux-react';
import {FetchToken} from 'fusion-tokens';
import {reactorEnhancer} from 'redux-reactors';
import fetch from 'unfetch';

import reducer from './redux';
import handlers from './rpc';

export default () => {
  const app = new App();

  app.register(ReduxToken, Redux);
  app.register(ReducerToken, reducer);
  app.register(EnhancerToken, reactorEnhancer);

  app.register(RPCToken, RPC);
  app.register(RPCHandlersToken, handlers);
  app.register(FetchToken, fetch);
  return app;
}

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

Because redux-reactors is implemented as a Redux enhancer, it doesn't require building reducers in the traditional Redux way. Thus, the root reducer can simply be the identity function:

// src/redux.js
export default state => state;

Here's how to implement a reactor:

// src/reactors/increment.js
import {withRPCReactor} from 'fusion-plugin-rpc-redux-react';

export const incrementReactor = withRPCReactor('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});
});

incrementReactor: Component => Component is a React HOC. It defines three actions: start, success and failure, which correspond to the respective statuses of a HTTP request.

In the example above, when increment is called, the start action is dispatched, which runs a reducer that sets state.loading to true, state.error to false and keeps state.count intact. If the request completes successfully, state.loading is set to false, and state.count is updated with a new value. Similarly, if the request fails, state.error is set.

In addition to defining action/reducer pairs, the incrementReactor HOC also maps RPC methods to React props.

Reactors typically need to be used in conjunction with connect from react-redux, in order to map state to React.

Below is an example of consuming the state and RPC methods that are made available from the Redux store and the RPC plugin.

// src/components/example.js
import React from 'react';
import {connect} from 'react-redux';
import {compose} from 'redux';
import {incrementReactor} from './reactors/increment.js'

function Example({count, loading, error, increment}) {
  return (
    <div>
      <p>Count: {count}</p>
      <p>
        <button onClick={() => increment()}>Increment</button>
      </p>
      {loading && 'Loading...'}
      {error}
    </div>
  );
}

const hoc = compose(
  incrementReactor,
  connect(({count, loading, error}) => ({count, loading, error})),
);
export default hoc(Example);

Differences between reactors and vanilla Redux

Redux colocates all valid actions in a respective "slot" in the state tree, and colocates the structuring of the state tree via helpers such as combineReducers. This means that a reducer can be unit tested by simply calling the reducer with one of the valid actions, without having any effect on any other state that might exist in the app. The downside is that if an action needs to modify multiple "slots" in the state tree, it can be tedious to find all transformations pertaining to any given action.

Another point worth mentioning is that with traditional reducers, it's possible to refactor the state tree in such a way that doesn't make any changes to reducers or components (albeit it does require changing the reducer composition chain as well as all relevant mapStateToProps functions).

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

// 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
NameTypeDescription
UniversalEventsTokenUniversalEventsAn event emitter plugin, such as the one provided by fusion-plugin-universal-events.
RPCHandlersTokenObject<(...args: any) => Promise>A map of server-side RPC method implementations. Server-only.
FetchToken(url: string, options: Object) => PromiseA fetch implementation. Browser-only.

withRPCRedux

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

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:

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

1 year ago

4.6.7

1 year ago

4.6.6

1 year ago

4.6.5

1 year ago

4.5.3

2 years ago

4.6.1

1 year ago

4.6.0

1 year ago

4.6.3

1 year ago

4.6.2

1 year ago

4.6.4

1 year ago

4.5.2

2 years ago

4.5.1

2 years ago

4.5.0

2 years ago

4.4.17

2 years ago

4.4.16

2 years ago

4.4.14

2 years ago

4.4.13

2 years ago

4.4.12

2 years ago

4.4.15

2 years ago

4.4.11

2 years ago

4.4.9

2 years ago

4.4.8

2 years ago

4.4.7

2 years ago

4.4.10

2 years ago

4.4.3

2 years ago

4.4.2

2 years ago

4.4.5

2 years ago

4.4.4

2 years ago

4.4.6

2 years ago

4.4.1

2 years ago

4.4.0

3 years ago

4.3.16

3 years ago

4.3.15

3 years ago

4.3.14

3 years ago

4.3.13

3 years ago

4.3.12

3 years ago

4.3.11

3 years ago

4.3.10

3 years ago

4.3.9

3 years ago

4.3.8

3 years ago

4.3.7

3 years ago

4.3.6

4 years ago

4.3.4

4 years ago

4.3.3

4 years ago

4.3.2

4 years ago

4.3.1

4 years ago

4.3.0

4 years ago

4.2.0

4 years ago

4.1.0

4 years ago

4.0.8

4 years ago

4.0.7

4 years ago

4.0.6

5 years ago

4.0.5

5 years ago

4.0.4

5 years ago

4.0.3

5 years ago

4.0.2

5 years ago

4.0.1

5 years ago

4.0.0

5 years ago

3.0.1

5 years ago

3.0.0

5 years ago

2.1.1

5 years ago

2.1.0

5 years ago

2.1.0-1

5 years ago

2.1.0-0

5 years ago

2.0.6

5 years ago

2.0.6-0

5 years ago

2.0.5

5 years ago

2.0.5-0

5 years ago

2.0.4

5 years ago

2.0.4-0

5 years ago

2.0.3

5 years ago

2.0.3-1

5 years ago

2.0.3-0

5 years ago

2.0.2

5 years ago

2.0.2-0

5 years ago

2.0.1

6 years ago

2.0.0

6 years ago

1.1.2

6 years ago

1.1.1

6 years ago

1.1.0

6 years ago

1.0.4

6 years ago

1.0.4-2

6 years ago

1.0.4-1

6 years ago

1.0.3

6 years ago

1.0.2

6 years ago

1.0.1

6 years ago

1.0.0

6 years ago

0.3.3

6 years ago

0.3.2

6 years ago

0.3.1

6 years ago

0.3.0

6 years ago

0.2.4

6 years ago

0.2.3

6 years ago

0.2.2

6 years ago

0.2.1

6 years ago

0.2.0

7 years ago

0.1.9

7 years ago

0.1.8

7 years ago

0.1.7

7 years ago

0.1.5

7 years ago

0.1.4

7 years ago

0.1.3

7 years ago

0.1.2

7 years ago

0.1.1

7 years ago

0.1.0

7 years ago