Skip to content

Commit

Permalink
Merge pull request #24 from yahoo/higher_order
Browse files Browse the repository at this point in the history
Higher order immutable container
  • Loading branch information
Marcelo Eden committed Jun 23, 2015
2 parents 074ac00 + 6cd130d commit 17c5cad
Show file tree
Hide file tree
Showing 6 changed files with 307 additions and 31 deletions.
48 changes: 40 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,45 @@ This package provides easy to use mixins/utils for both fluxible stores and reac
$ npm install --save fluxible-immutable-utils
```

## `createImmutableContainer`

This method creates an immutable higher order component.

```js

var MyComponent = React.createClass({
displayName: 'MyComponent',

...
});

var createImmutableContainer = require('fluxible-immutable-utils').createImmutableContainer;

// Wraps your component in an immutable container.
// Prevents renders when props are the same
module.exports = createImmutableContainer(MyComponent);

// Wraps your component in an immutable container that listens to stores
// and pass its state down as props
module.exports = createImmutableContainer(MyComponent, {
stores: [SomeStore],
getStateFromStores: {
SomeStore: function (store) {
return {
someState: store.state;
}
}
}
});
```

## `ComponentMixin`
A mixin that provides convenience methods for using Immutable.js inside of react components. Note that this mixin uses the initializeComponent method for setup, and any components that use this mixin should define a 'getStateOnChange' function for generating component state (see below).

This mixin has several purposes:
- Checks that the objects in state/props of each component are an Immutable Map.
- Implements a default shouldComponentUpdate method.
- Provides a convenience method for dealing with state changes/component
- Provides a convenience method for dealing with state changes/component
initialization.

### Immutalizing State
Expand Down Expand Up @@ -76,14 +108,14 @@ module.exports = React.createClass({
var myObject = {
foo: 'bar'
};
<MyReactComponent
<MyReactComponent
someKey={myObject} // fine, because we set this key to be ignored
aNonImmutableObject={myObject} // will cause a console.warn statement because we are passing a non-immutable object
/>
```

#### Configuring the Mixin
If you are using third party libraries/have a special case where you don't want the mixin to consider some of the keys of props/state, you have two options. First, you can set the ignoreImmutableCheck object to skip the check for immutability for any keys you decide. Second, if you want the mixin to also ignore a key when checking for data equality in props/state, you can set the key value to the flag `SKIP_SHOULD_UPDATE`. You must set these values inside a component's `statics` field (or in a config, see below), and they must be set seperately for props/state. You can also turn off all warnings by settings the ignoreAllWarnings flag.
If you are using third party libraries/have a special case where you don't want the mixin to consider some of the keys of props/state, you have two options. First, you can set the ignoreImmutableCheck object to skip the check for immutability for any keys you decide. Second, if you want the mixin to also ignore a key when checking for data equality in props/state, you can set the key value to the flag `SKIP_SHOULD_UPDATE`. You must set these values inside a component's `statics` field (or in a config, see below), and they must be set seperately for props/state. You can also turn off all warnings by settings the ignoreAllWarnings flag.

**Example**

Expand All @@ -104,15 +136,15 @@ module.exports = React.createClass({
state: {
anotherKey: 'SKIP_SHOULD_UPDATE' // don't check anotherKey for immutablility in props, AND don't check its value is shouldComponentUpdate
}

}
},

...rest of component follows...
});
```

If you want to just pass around a common config, then use:
If you want to just pass around a common config, then use:
```jsx
var ImmutableMixin = require('fluxible-immutable-utils').createComponentMixin(myConfig);
```
Expand All @@ -122,19 +154,19 @@ Where myConfig has the same structure as the statics above.

A helper method similar to `React.createClass` but for creating immutable [stores](https://facebook.github.io/flux/docs/overview.html#stores). Internally it wraps a call to [`'fluxible/utils/createStore`](https://github.com/yahoo/fluxible/blob/v0.2.9/utils/createStore.js).

The main use case for this method is to reduce boilerplate when implementing immutable [`fluxible`](fluxible.io) stores.
The main use case for this method is to reduce boilerplate when implementing immutable [`fluxible`](fluxible.io) stores.

The helper adds a new property and method to the created store
* `_state` {[Map](http://facebook.github.io/immutable-js/docs/#/Map)} - The root `Immutable` where all data in the store will be saved.

* `setState(newState, [event], [payload])` {Function} - This method replaces `this._state` with `newState` (unless they were the same) and then calls `this.emit(event, payload)`.
* `setState(newState, [event], [payload])` {Function} - This method replaces `this._state` with `newState` (unless they were the same) and then calls `this.emit(event, payload)`.
* If `event` is *falsy* it will call `this.emitChange(payload)`
* The method also ensures that `_state` remains immutable by auto-converting `newState` to an immutable object.

and creates defaults for the following [fluxible store](http://fluxible.io/api/stores.html) methods
* [`initialize()`](http://fluxible.io/api/stores.html#constructor) - The default implementations creates a `_state` property on the store and initializes it to [`Immutable.Map`](http://facebook.github.io/immutable-js/docs/#/Map)

* [`rehydrate(state)`](http://fluxible.io/api/stores.html#rehydrate-state-) - The default implementation hydrates `_state`
* [`rehydrate(state)`](http://fluxible.io/api/stores.html#rehydrate-state-) - The default implementation hydrates `_state`

* [`dehydrate()`](http://fluxible.io/api/stores.html#dehydrate-) - The default implementation simply returns `_state` which is `Immutable` (due to all `Immutable` objects implementing a [`toJSON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON_behavior) function, `_state` can be directly passed to `JSON.stringify`)

Expand Down
40 changes: 32 additions & 8 deletions internals/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,38 @@
*/
'use strict';

module.exports = {
merge: function merge(dest, src) {
dest || (dest = {});
var Immutable = require('immutable');
var isImmutable = Immutable.Iterable.isIterable;
var isReactElement = require('react/addons').isValidElement;

function isNonImmutable(item) {
return (
item
&& typeof item === 'object'
&& !isReactElement(item)
&& !isImmutable(item)
);
}

function warnNonImmutable(component, prop) {
console.warn('Component ' +
'"' + component.constructor.displayName + '"' +
' received non-immutable object for ' +
'"' + prop + '"');
}

src && typeof src === 'object' && Object.keys(src).forEach(function mergeCb(prop) {
dest[prop] = src[prop];
});
function merge(dest, src) {
dest || (dest = {});

return dest;
}
src && typeof src === 'object' && Object.keys(src).forEach(function mergeCb(prop) {
dest[prop] = src[prop];
});

return dest;
}

module.exports = {
merge: merge,
isNonImmutable: isNonImmutable,
warnNonImmutable: warnNonImmutable
};
79 changes: 79 additions & 0 deletions lib/createImmutableContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2015, Yahoo Inc. All rights reserved.
* Copyrights licensed under the New BSD License.
* See the accompanying LICENSE file for terms.
*/

'use strict';

var React = require('react');
var connectToStores = require('fluxible/addons/connectToStores');
var utils = require('../internals/utils');
var shallowEqual = require('react/lib/shallowEqual');

function getIgnoredProps(ignore) {
if (!Array.isArray(ignore)) {
return ignore || {};
}

var ignoredProps = {};

ignore.forEach(function (prop) {
ignoredProps[prop] = true;
});

return ignoredProps;
}

module.exports = function createImmutableContainer(Component, options) {
options = options || {};
var ignore = options.ignore;
var ignoreWarnings = options.ignoreWarnings;
var getStateFromStores = options.getStateFromStores;
var stores = options.stores || Object.keys(getStateFromStores || {});
var componentName = Component.displayName || Component.name;

var ImmutableComponent = React.createClass({
displayName: componentName + ':Immutable',

getDefaultProps: function () {
return {};
},

checkImmutable: function (prop) {
if (
!this._ignoredProps[prop] &&
utils.isNonImmutable(this.props[prop])
) {
utils.warnNonImmutable(this, prop);
}
},

checkAllImmutable: function () {
if (!ignoreWarnings) {
Object.keys(this.props).forEach(this.checkImmutable);
}
},

componentWillMount: function () {
this._ignoredProps = getIgnoredProps(ignore);
this.checkAllImmutable();
},

componentWillUpdate: function () {
this.checkAllImmutable();
},

shouldComponentUpdate: function (nextProps) {
return !shallowEqual(this.props, nextProps);
},

render: function () {
return React.createElement(Component, this.props);
}
});

return stores.length ?
connectToStores(ImmutableComponent, stores, getStateFromStores) :
ImmutableComponent;
};
12 changes: 2 additions & 10 deletions lib/createImmutableMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@
*/
'use strict';

var Immutable = require('immutable');
var isImmutable = Immutable.Iterable.isIterable;
var isReactElement = require('react/addons').isValidElement;
var GET_STATE_FUNCTION = 'getStateOnChange';
var utils = require('../internals/utils');

var GET_STATE_FUNCTION = 'getStateOnChange';
var IGNORE_IMMUTABLE_CHECK = 'ignoreImmutableCheck';
var IGNORE_EQUALITY_CHECK_FLAG = 'SKIP_SHOULD_UPDATE';

Expand All @@ -23,11 +19,7 @@ var IGNORE_EQUALITY_CHECK_FLAG = 'SKIP_SHOULD_UPDATE';
* @return {Boolean} True if non-immutable object, else false.
*/
function checkNonImmutableObject(key, item, component) {
if (item
&& typeof item === 'object'
&& !isReactElement(item)
&& !isImmutable(item)
) {
if (utils.isNonImmutable(item)) {
console.warn('WARN: component: ' + component.constructor.displayName
+ ' received non-immutable object: ' + key);
}
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@
"devtest": "mocha tests --recursive --reporter spec",
"test": "npm run lint && npm run cover"
},
"peerDependencies": {
"react": "^0.13"
},
"dependencies": {
"immutable": "^3.3.0",
"fluxible": "^0.4.0"
},
"peerDependencies": {
"react": "<=0.13.x"
},
"devDependencies": {
"chai": "^2.0.0",
"coveralls": "^2.11.1",
"eslint": "^0.17.0",
"istanbul": "^0.3.2",
"jsx-test": "^0.4.2",
"jsx-test": "^0.6",
"mocha": "^2.0",
"pre-commit": "^1.0",
"react": "<=0.13.x",
"react": "^0.13",
"sinon": "^1.14.1"
},
"pre-commit": [
Expand Down
Loading

0 comments on commit 17c5cad

Please sign in to comment.