Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding in synapse record system, adjusting docs, and demo page #9

Merged
merged 1 commit into from
Apr 30, 2017
Merged
Show file tree
Hide file tree
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
85 changes: 18 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,89 +1,40 @@
# `redux-synapse`

<img src="http://i.imgur.com/hAuOOkL.png" />

[![npm](https://img.shields.io/npm/dt/redux-synapse.svg)]() [![npm](https://img.shields.io/npm/v/redux-synapse.svg)]() [![npm](https://img.shields.io/npm/l/redux-synapse.svg)]()


`redux-synapse` is a library that is heavily inspired by `react-redux` and acts as an alternative for the binding of react components to the store in a more explicit manner. The primary difference is the nature in which each component must declare explicity what updates should affect the component via its higher order component; a `synapse`. With `synapse`'s it is possible to achieve a higher level of performance, than you would with alternative libraries.

A `synapse` is declared to listen to specific messages and act upon them. This is an early release of something that I intend to grow over time and build upon to make more efficient.

## Installation

Install `redux-synapse` using `npm` withte following command.

```
npm install --save redux-synapse
```
#### [Available on npm](https://www.npmjs.com/package/redux-synapse)

## What it looks like

### Provider
Much like `react-redux` we have a top level, `Provider` component, that the store should be passed too. This component is necessary for setting up our internal dictionary with subscriber lists.
## Read Docs

```js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'redux-synapse';
* `Synapse` HOC [here](/docs/Synapse.md)
* `Provider` [here](/docs/Provider.md)
* `API` [here](/docs/API.md)

ReactDOM.render(
<Provider store={store}>
<StandardComponent />
</Provider>,
document.getElementById('app')
);
```

### A Standard Component
When setting up the component via `synapse`, you pass in the standard, `mapState*` functions, as well as an array of keys, that this component should update itself on. In this example we only have one level of state as we have a single reducer, however you can see that we have added two key-paths:
* `time`
* `options-enabled`

`time` is at the top level, however `options-enabled`, says you are interested in the `options` object, and the `enabled` property.

```js
## Why `redux-synapse` exists

import { synapse } from 'redux-synapse';
### The Problem to Solve
`react` is a fantastic tool, however with larger trees you end up with an inefficient number of rerenders unless you are very strict with your `shouldComponentUpdates`, especially over frequently updated state in a standard flux model, or your own store implementation via `context`. As we know, `react-redux` utilises the `connect` higher order component to theoretically make your tree flat, so that updates are dished out directly from the store, and additional rerenders are only done if the state that we are interested in, handled via a `mapStateToProps` function, changes. This is fantastic as rerenders are expensive, and cutting them out can really solve a large number of performance problems. However in applications that are updating state frequently, such as a video based applcation, or a stock market tracker, you are going to lose performance before the rerenders even happens.

//...Component Declaration
In `react` performance would go down just based on the fact that all those components are rerendering so frequently.
In `react-redux` although we shortcut the rerenders, we are still going to visit our `mapStateToProps` of most of our components, and in essence create a new object every single time to be returned and then evaluated upon. This is fine for smaller applications but in an application with 10's or 100's of components this is going to lead to performance problems.

const mapStateToProps = (state) => {
return {
time: state.time,
};
}
### The Solution

const mapDispatchToProps = (state, dispatch) => {
return {
setTime: (time) => {
dispatch({
type: SET_TIME,
time,
});
},
};
};
This is where `redux-synapse` comes in. Using a similar syntactical solution to `react-redux`, a user can define what paths they are interested in on the state updates, and behind the scenes they are added as subscribers to those keys. If no paths are specified then it will just `subscribe` to the store like it would in `react-redux`, otherwise our `observer` behind the scenes will subscribe to updates to the store via our `reducers` and then using the paths that are specified as being updated in the reducer, will alert all necessary higher order components and trigger them to begin their own rerender cycle as opposed to visiting all components to then determine which ones should or shouldn't be updated.

export default synapse(mapStateToProps, mapDispatchToProps, ['time', 'options-enabled'])(StandardComponent);
```
### A Standard Reducer
When making changes to state, simply call `prepareNotification` with an array of the affected state keys. This will ensure that on the state being returned the interested components are updated appropriately.

```js
import { prepareNotification } from 'redux-synapse';

const defaultState = {
time: 0,
src: 'none',
options: {
enabled: true,
}
};

export default video = (state = defaultState, action) => {
switch(action.type) {
case SET_TIME:
state.time = action.time;
prepareNotification(['time']);
return state;
default:
return state;
}
};
```

21 changes: 21 additions & 0 deletions demo/components/StockItem/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { PropTypes } from 'react';

export default function StockItem({ name, currentValue, previousValue }) {
return (
<div>
<div>{name}</div>
<div>{currentValue}</div>
<div>{previousValue}</div>
<div>
<button>Buy</button>
<button>Sell</button>
</div>
</div>
);
}

StockItem.propTypes = {
name: PropTypes.string.isRequired,
currentValue: PropTypes.number.isRequired,
previousValue: PropTypes.number.isRequired,
};
8 changes: 7 additions & 1 deletion demo/components/StocksTable/StocksTable.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { PropTypes } from 'react';
import StockItem from '../StockItem';

export default class StocksTable extends React.Component {
static propTypes = {
Expand All @@ -13,7 +14,12 @@ export default class StocksTable extends React.Component {
renderStocks = () => {
return this.props.stocks.map((p, i) => {
return (
<p key={i}>{p.name}</p>
<StockItem
key={i}
name={p.name}
currentValue={p.currentValue}
previousValue={p.previousValue}
/>
);
});
}
Expand Down
1 change: 1 addition & 0 deletions demo/components/TraderCTA/TraderCTA.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default class TraderCTA extends React.Component {
render() {
return (
<div>
<h3>Trader Name: {this.props.name}</h3>
<input type="text" onChange={this.handleChange} value={this.state.name} />
<button onClick={this.handleClick}>Set Name</button>
</div>
Expand Down
4 changes: 2 additions & 2 deletions demo/components/TraderCTA/TraderCTAContainer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const mapStateToProps = (state) => {
return {
name: state.trader.name,
accountValue: state.trader.accountValue,
name: state.trader.getIn(['details', 'name']),
accountValue: state.trader.get('accountValue'),
};
};

Expand Down
2 changes: 1 addition & 1 deletion demo/components/TraderCTA/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ import { synapse } from 'redux-synapse';
import TraderCTA from './TraderCTA';
import { mapStateToProps, mapDispatchToProps } from './TraderCTAContainer';

export default synapse(mapStateToProps, mapDispatchToProps, ['trader'])(TraderCTA);
export default synapse(mapStateToProps, mapDispatchToProps, ['trader-details-name'])(TraderCTA);
7 changes: 5 additions & 2 deletions demo/records/TraderRecord.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Record } from 'immutable';
import { Map, Record } from 'immutable';

export default Record({
name: 'NONE_SET',
accountValue: 0,
details: Map({
name: 'NONE_SET',
age: 0,
}),
});
2 changes: 1 addition & 1 deletion demo/reducers/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import trader from './traderReducer';
import stocks from './stockSReducer';
import stocks from './stocksReducer';

export default {
trader,
Expand Down
20 changes: 19 additions & 1 deletion demo/reducers/stocksReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,32 @@ import { List } from 'immutable';
const defaultState = new StocksRecord({
allStocks: List([
new StockRecord({
name: 'FTSE',
name: 'FTSE 100',
currentValue: 1000,
previousValue: 750,
valueDifference: 250,
bidHistory: List([
0,
]),
}),
new StockRecord({
name: 'FTSE 250',
currentValue: 5000,
previousValue: 2500,
valueDifference: 2500,
bidHistory: List([
0,
]),
}),
new StockRecord({
name: 'FTSE 350',
currentValue: 120,
previousValue: 200,
valueDifference: -80,
bidHistory: List([
0,
]),
}),
]),
});

Expand Down
10 changes: 5 additions & 5 deletions demo/reducers/traderReducer.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import TraderRecord from '../records/TraderRecord';
import { prepareNotification } from 'redux-synapse';
import { generateSynapseRecord } from 'redux-synapse';

const trader = (state = new TraderRecord(), action) => {
const STATE_KEY = 'trader';
const defaultState = generateSynapseRecord(new TraderRecord(), STATE_KEY);
const trader = (state = defaultState, action) => {
let newState;
switch (action.type) {
case 'SET_TRADER_VALUE':
newState = state.set('accountValue', action.accountValue);
prepareNotification(['trader']);
return newState;
case 'SET_TRADER_NAME':
newState = state.set('name', action.name);
prepareNotification(['trader']);
newState = state.setIn(['details', 'name'], action.name);
return newState;
default:
return state;
Expand Down
103 changes: 103 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# API
This outlines the API that is available to use, alongside the Primary components such as the `Provider` and `Synapse`.

## `generateSynapseRecord(defaultState: any, stateKey: String, getters: Object)`

### Parameters
- `defaultState` : The default state for a reducer. It accepts, `Immutable.Record|Maps|List`s, or plain old javascript objects. However it doesn't support `Immutable.Record` implementations with custom getters.
- `stateKey` : The associated state key for which this record will be attached to. If you build a reducer and it is added to the store under the key of `"trader"`, then `"trader"` would be the value of this parameter.
- `getters` : Experimental feature for attaching `getters` onto the created `SynapseRecord`

### Outline
The `generateSynapseRecord` is a utility aimed at breaking the requirement for the `prepareNotification` paradigm that would be used inside your reducers with `redux-synapse`.

It's purpose is to remove the need for the `prepareNotification` API that is used within reducers. It does this by wrapping the provided `defaultState` in a version of an `Immutable.Record`. This hooks into all `set` and `setIn` calls that are used to manually prepare the notifications for the underlying synapse engine.

It does however require the `stateKey` that this reducer is associated with to be provided.

> The `generateSynapseRecord` API may not support custom implementations of the `Immutable.Record` class.

### Example
The below example outlines a reducer for the `trader` state key, and how the`generateSynapseRecord` API works alongside an `immutable` record.

```js
import { Record } from 'immutable'
import { generateSynapseRecord } from 'redux-synapse';

// Immutable Record of trader state
const TraderRecord = Record({
name: 'NONE_SET',
accountValue: 0,
});

// The expected property name of the reducer on the redux state
const STATE_KEY = 'trader';
const defaultState = generateSynapseRecord(new TraderRecord(), STATE_KEY);

// Our Reducer
const trader = (state = defaultState, action) => {
let newState;
switch (action.type) {
case 'SET_TRADER_VALUE':
newState = state.set('accountValue', action.accountValue);
return newState;
case 'SET_TRADER_NAME':
newState = state.set('name', action.name);
return newState;
default:
return state;
}
};

export default trader;
```

## `prepareNotification(keys: Array<String>)`

### Parameters
- `keys` : An array of keys that are used to determine which component subscriptions should be updated.

### Outline
The `prepareNotification` API should be called with an array of the top level keys that have been affected. For example if you have an object in your `redux` state with the key `video`, then you would change those properties and then call `prepareNotification` with the `video` key. This ensures that all relevant subscribers are updated, and only them. A `notify` operation is initiated at the end of the redux `reducer` cycle.

### Example


```js
import { prepareNotification } from 'redux-synapse';
import { Map } from 'immutable';

// Our default state
const defaultState = Map({
time: 0,
src: 'none',
options: Map({
enabled: true,
}),
});


// Our video reducer
export default video = (state = defaultState, action) => {
switch(action.type) {
case SET_TIME:
state = state.set('time', action.time);
// We are updating the `time` property on the `video` state key. As
// such we prepare a notification for the components that are subscribed to
// changes to the `video` state key
prepareNotification(['video-time']);
return state;
case SET_OPTIONS_ENABLED:
const options = state.options;
state = state.set('options', options.set('enabled', action.enabled));
// We have updated the `options` object, on the `video` state key. As such
// we prepare a notification for anyone that is subscribed to changes
// on the `video` state key or the `options` object.
prepareNotification(['video-options-enabled']);
default:
return state;
}
};
```
> `redux-synapse` supports a `super-explicit` mode so that in the example of nested objects (`video-options`) it would require
> an explicit subscription to the nested object, and it wouldn't update subscribers on the `video` key.
36 changes: 36 additions & 0 deletions docs/Provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,42 @@ The `Provider` works much like the `react-redux` provider as a top level compone
|---|---|---|---|
|`store`|`Object`|The store that is created by `redux`|`Yes`|
|`children`|`node`|The `React` children|`Yes`|
|`delimiter`|`String`|What string to use for the internal dictionary when delimiting keys. Defaults to `'-'` if not provided.|`No`|
|`reverseTravesal`|`Boolean`|Whether to notify each key in a provided path. Defaults to `true` if not provided.|`No`|

## Behind the scenes
The `Provider` is responsible for building the internal observer dictionary that is used to determine subscribers across state keys, and paths, from the store state tree.

## Example Usage
```js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'redux-synapse';
import { createStore } from 'redux';

const store = creatStore(...);

ReactDOM.render(
<Provider store={store}>
<StandardComponent />
</Provider>,
document.getElementById('app')
);
```

### `delimiter` Use case
You may find that you have some dynamic keys, or even keys that utilise the `'-'` in their naming. As such we allow users to change the internal delimiter to something else. Single characters are recommended.

### `reverseTraversal` Use case
Take the following key path:
`video-options-playbackspeed`

With `reverseTraversal` enabled (_by default it is_) the following keys and their subscriptions would be notified:
- video
- options
- playbackspeed

With it disabled it would only notify the following:
- playbackspeed

This allows for a much more explicit approach to defining keys for updates. It also becomes useful in large state trees with various levels, so you can effectively partition updates to entire sections of the react tree.
Loading