├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── spec ├── .eslintrc └── immutableRenderSpec.js └── src ├── immutableRenderDecorator.js ├── immutableRenderMixin.js ├── index.js ├── shallowEqualImmutable.js └── shouldComponentUpdate.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "globals": { 4 | "describe": true, 5 | "it": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | *.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | .babelrc 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Clint Ayres 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-immutable-render-mixin 2 | ============================ 3 | 4 | ## Users are urged to use [PureRenderMixin](http://facebook.github.io/react/docs/pure-render-mixin.html) with [facebook/immutable-js](https://github.com/facebook/immutable-js). If performance is still an issue an examination of your usage of Immutable.js should be your first path towards a solution. This library was created from experimentations with Immutable that were ultimately erroneous; improper usage of Immutable.js :hankey:. Users should be able to achieve maximum performance simply using PureRenderMixin. 5 | 6 | This library exposes 4 distinct options for immutable rendering: 7 | 8 | * Mixin for `React.createClass` support 9 | * HoC ( _decorator_ ) for `React.Component` 10 | * shouldComponentUpdate function used by the mixin and HoC 11 | * shallowEqualImmutable function to allow custom `shouldComponentUpdate` implementations 12 | 13 | This library when used as a mixin/decorator replaces the [PureRenderMixin](http://facebook.github.io/react/docs/pure-render-mixin.html) when using [facebook/immutable-js](https://github.com/facebook/immutable-js) library with [React](https://github.com/facebook/react) 14 | 15 | This Mixin and HoC implements `shouldComponentUpdate` method using prop and state equality with Immutable.is(). 16 | 17 | We also expose the `shallowEqualImmutable` to allow developers to craft a custom `shouldComponentUpdate` method as needed. 18 | 19 | Installation 20 | ------------ 21 | 22 | ```sh 23 | npm i react-immutable-render-mixin 24 | ``` 25 | 26 | Usage as Mixin 27 | ----- 28 | 29 | ```js 30 | import immutableRenderMixin from 'react-immutable-render-mixin'; 31 | 32 | React.createClass({ 33 | mixins: [immutableRenderMixin], 34 | 35 | render: function() { 36 | return
foo
; 37 | } 38 | }); 39 | ``` 40 | 41 | Usage as a HoC 42 | ----- 43 | 44 | ```js 45 | import React from 'react'; 46 | import { immutableRenderDecorator } from 'react-immutable-render-mixin'; 47 | 48 | class Test extends React.Component { 49 | render() { 50 | return
; 51 | } 52 | } 53 | 54 | export default immutableRenderDecorator(Test); 55 | ``` 56 | 57 | Usage as Decorator 58 | ----- 59 | 60 | ```js 61 | import React from 'react'; 62 | import { immutableRenderDecorator } from 'react-immutable-render-mixin'; 63 | 64 | @immutableRenderDecorator 65 | class Test extends React.Component { 66 | render() { 67 | return
; 68 | } 69 | } 70 | ``` 71 | 72 | Usage with default `shouldComponentUpdate` 73 | ----- 74 | 75 | ```js 76 | import React from 'react'; 77 | import { shouldComponentUpdate } from 'react-immutable-render-mixin'; 78 | 79 | class Test extends React.Component { 80 | constructor(props) { 81 | super(props); 82 | this.shouldComponentUpdate = shouldComponentUpdate.bind(this); 83 | } 84 | 85 | render() { 86 | return
; 87 | } 88 | } 89 | ``` 90 | 91 | Usage with a custom `shouldComponentUpdate` 92 | ----- 93 | 94 | ```js 95 | import React from 'react'; 96 | import { shallowEqualImmutable } from 'react-immutable-render-mixin'; 97 | 98 | class Test extends React.Component { 99 | shouldComponentUpdate(nextProps, nextState) { 100 | return !shallowEqualImmutable(this.props, nextProps) || !shallowEqualImmutable(this.state, nextState); 101 | } 102 | 103 | render() { 104 | return
; 105 | } 106 | } 107 | ``` 108 | 109 | Usage with <= ES5 110 | ----- 111 | 112 | Exports: 113 | 114 | ```js 115 | var immutableRenderMixin = require('react-immutable-render-mixin').default; 116 | 117 | var immutableRenderDecorator = require('react-immutable-render-mixin').immutableRenderDecorator; 118 | 119 | var shallowEqualImmutable = require('react-immutable-render-mixin').shallowEqualImmutable; 120 | 121 | var shouldComponentUpdate = require('react-immutable-render-mixin').shouldComponentUpdate; 122 | ``` 123 | 124 | Full Example: 125 | 126 | ```js 127 | var immutableRenderMixin = require('react-immutable-render-mixin').default; 128 | 129 | React.createClass({ 130 | mixins: [immutableRenderMixin], 131 | 132 | render: function() { 133 | return
foo
; 134 | } 135 | }); 136 | ``` 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-immutable-render-mixin", 3 | "version": "0.9.7", 4 | "description": "React PureRenderMixin replacement for immutable-js library", 5 | "homepage": "https://github.com/jurassix/react-immutable-render-mixin", 6 | "bugs": "https://github.com/jurassix/react-immutable-render-mixin/issues", 7 | "scripts": { 8 | "build": "npm run clean && npm run lint && npm run build:lib && npm run build:spec", 9 | "build:lib": "mkdirp lib && babel src -d lib", 10 | "build:spec": "mkdirp lib/spec && babel spec -d lib/spec", 11 | "test": "npm run build && mocha --recursive lib/spec", 12 | "clean": "rimraf lib", 13 | "lint": "eslint src && eslint spec", 14 | "prepublish": "npm run build" 15 | }, 16 | "keywords": [ 17 | "react", 18 | "mixin", 19 | "immutable-js", 20 | "immutability", 21 | "react-component" 22 | ], 23 | "main": "./lib/index.js", 24 | "author": "clint ayres", 25 | "license": "MIT", 26 | "peerDependencies": { 27 | "immutable": ">=2.0.10", 28 | "react": "*" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/jurassix/react-immutable-render-mixin.git" 33 | }, 34 | "devDependencies": { 35 | "babel-cli": "^6.3.17", 36 | "babel-preset-es2015": "^6.3.13", 37 | "chai": "^3.4.1", 38 | "eslint": "^1.10.3", 39 | "eslint-config-airbnb": "^2.1.0", 40 | "mkdirp": "^0.5.1", 41 | "mocha": "^2.3.4", 42 | "react": "^0.14.7", 43 | "rimraf": "^2.4.4" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "new-cap": [2, {"capIsNewExceptions": ["Immutable.List", "Immutable.Map", "Immutable.Set"]}] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /spec/immutableRenderSpec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Immutable from 'immutable'; 3 | 4 | import immutableRenderMixin, { immutableRenderDecorator, shallowEqualImmutable } from '../index'; 5 | 6 | import mixin from '../immutableRenderMixin'; 7 | import decorator from '../immutableRenderDecorator'; 8 | import shallowEqual from '../shallowEqualImmutable'; 9 | import shouldComponentUpdate from '../shouldComponentUpdate'; 10 | 11 | describe('ImmutableRenderMixin', () => { 12 | describe('exports', () => { 13 | it('should expose correct default export', () => { 14 | expect(immutableRenderMixin).to.deep.equal(mixin); 15 | }); 16 | it('should expose decorator on export', () => { 17 | expect(immutableRenderDecorator).to.deep.equal(decorator); 18 | }); 19 | it('should expose shallowEqual function on export', () => { 20 | expect(shallowEqualImmutable).to.deep.equal(shallowEqual); 21 | }); 22 | it('should expose mixin as default for <= ES5', () => { 23 | const indexDefault = require('../index').default; 24 | expect(immutableRenderMixin).to.deep.equal(indexDefault); 25 | }); 26 | it('should expose decorator on export for <= ES5', () => { 27 | const indexDecorator = require('../index').immutableRenderDecorator; 28 | expect(immutableRenderDecorator).to.deep.equal(indexDecorator); 29 | }); 30 | it('should expose shallowEqual function for <= ES5', () => { 31 | const indexShallowEqual = require('../index').shallowEqualImmutable; 32 | expect(shallowEqualImmutable).to.deep.equal(indexShallowEqual); 33 | }); 34 | }); 35 | }); 36 | 37 | describe('shallowEqualImmutable', () => { 38 | const equals = [ 39 | ['string', 'string'], 40 | [true, true], 41 | [0, -0], // Immutable.is assumes 0 and -0 are the same value, matching the behavior of ES6 Map key equality. 42 | [Immutable.List([1, 2, 3]), Immutable.List([1, 2, 3])], 43 | [Immutable.Map({ a: 1, b: 1, c: 1 }), Immutable.Map({ a: 1, b: 1, c: 1 })], 44 | [Immutable.Set([NaN]), Immutable.Set([NaN])], 45 | ]; 46 | 47 | const obj = { a: 1, b: 2, c: 3 }; 48 | const map1 = Immutable.Map(obj); 49 | obj.a = 10; 50 | const map2 = Immutable.Map(obj); 51 | const notEquals = [ 52 | ['string', 'other string'], 53 | [Immutable.List([1, 2, 3]), Immutable.List([3, 2, 1])], 54 | [Immutable.List([1]), Immutable.Set([1])], 55 | [map1, map2], 56 | ]; 57 | 58 | it('can determines whether two arguments are the same value or two Immutable Iterable that have equivalent values', () => { 59 | equals.forEach(pair => 60 | expect(shallowEqualImmutable(pair[0], pair[1])).to.be.true 61 | ); 62 | 63 | notEquals.forEach(pair => 64 | expect(shallowEqualImmutable(pair[0], pair[1])).to.be.false 65 | ); 66 | }); 67 | 68 | it('should return true if two arguments not equal but all their items do', () => { 69 | equals.forEach(pair => { 70 | const obj1 = { key: pair[0] }; 71 | const obj2 = { key: pair[1] }; 72 | expect(shallowEqualImmutable(obj1, obj2)).to.be.true; // eslint-disable-line no-unused-expressions 73 | }); 74 | 75 | expect(shallowEqualImmutable({}, {})).to.be.true; // eslint-disable-line no-unused-expressions 76 | 77 | notEquals.forEach(pair => { 78 | const obj1 = { key: pair[0] }; 79 | const obj2 = { key: pair[1] }; 80 | expect(shallowEqualImmutable(obj1, obj2)).to.be.false; // eslint-disable-line no-unused-expressions 81 | }); 82 | }); 83 | 84 | it('should return false if two arguments has different number of keys', () => { 85 | const obj1 = { a: equals[0], b: equals[1] }; 86 | const obj2 = { a: equals[0], b: equals[1], c: equals[2] }; 87 | expect(shallowEqualImmutable(obj1, obj2)).to.be.false; // eslint-disable-line no-unused-expressions 88 | }); 89 | }); 90 | 91 | describe('shouldComponentUpdate', () => { 92 | it('can determines whether new props / states and current one are equivalent to', () => { 93 | const obj = { 94 | props: { a: Immutable.List([1, 2, 3]) }, 95 | state: Immutable.Map({ a: 1, b: 2 }), 96 | shouldComponentUpdate, 97 | }; 98 | 99 | expect( // eslint-disable-line no-unused-expressions 100 | obj.shouldComponentUpdate({ a: Immutable.List([1, 2, 3]) }, Immutable.Map({ a: 1, b: 2 })) 101 | ).to.be.false; // props and state are equal, should not update 102 | 103 | expect( // eslint-disable-line no-unused-expressions 104 | obj.shouldComponentUpdate({ a: Immutable.List([1, 2, 3]) }, Immutable.Map({ a: 1, b: 3 })) 105 | ).to.be.true; 106 | }); 107 | }); 108 | 109 | describe('immutableRenderDecorator', () => { 110 | it('can behavior like a HoC', () => { 111 | class TestComponent {} 112 | const Enhanced = immutableRenderDecorator(TestComponent); 113 | expect(Enhanced.prototype.shouldComponentUpdate).equal(shouldComponentUpdate); 114 | }); 115 | 116 | it('should accept functional components', () => { 117 | const FunctionalComponent = () => null; 118 | const DecoratedComponent = immutableRenderDecorator(FunctionalComponent); 119 | 120 | expect(DecoratedComponent.prototype.shouldComponentUpdate).equal(shouldComponentUpdate); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /src/immutableRenderDecorator.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import shouldComponentUpdate from './shouldComponentUpdate'; 3 | 4 | /** 5 | * Makes the given component "pure". 6 | * 7 | * @param object Target Component. 8 | */ 9 | export default function immutableRenderDecorator(Target) { 10 | class Wrapper extends Component { 11 | render() { 12 | return React.createElement(Target, this.props, this.props.children); 13 | } 14 | } 15 | 16 | Wrapper.prototype.shouldComponentUpdate = shouldComponentUpdate; 17 | 18 | return Wrapper; 19 | } 20 | -------------------------------------------------------------------------------- /src/immutableRenderMixin.js: -------------------------------------------------------------------------------- 1 | import shouldComponentUpdate from './shouldComponentUpdate'; 2 | 3 | export default { 4 | shouldComponentUpdate, 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import shouldComponentUpdate from './shouldComponentUpdate'; 2 | import shallowEqualImmutable from './shallowEqualImmutable'; 3 | import immutableRenderMixin from './immutableRenderMixin'; 4 | import immutableRenderDecorator from './immutableRenderDecorator'; 5 | 6 | export { 7 | immutableRenderMixin as default, 8 | immutableRenderDecorator, 9 | shouldComponentUpdate, 10 | shallowEqualImmutable, 11 | }; 12 | -------------------------------------------------------------------------------- /src/shallowEqualImmutable.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | const is = Immutable.is.bind(Immutable); 4 | 5 | export default function shallowEqualImmutable(objA, objB) { 6 | if (objA === objB || is(objA, objB)) { 7 | return true; 8 | } 9 | 10 | if (typeof objA !== 'object' || objA === null || 11 | typeof objB !== 'object' || objB === null) { 12 | return false; 13 | } 14 | 15 | const keysA = Object.keys(objA); 16 | const keysB = Object.keys(objB); 17 | 18 | if (keysA.length !== keysB.length) { 19 | return false; 20 | } 21 | 22 | // Test for A's keys different from B. 23 | const bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB); 24 | for (let i = 0; i < keysA.length; i++) { 25 | if (!bHasOwnProperty(keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { 26 | return false; 27 | } 28 | } 29 | 30 | return true; 31 | } 32 | -------------------------------------------------------------------------------- /src/shouldComponentUpdate.js: -------------------------------------------------------------------------------- 1 | import shallowEqualImmutable from './shallowEqualImmutable'; 2 | 3 | export default function shouldComponentUpdate(nextProps, nextState) { 4 | return !shallowEqualImmutable(this.props, nextProps) || !shallowEqualImmutable(this.state, nextState); 5 | } 6 | --------------------------------------------------------------------------------