├── .travis.yml ├── .gitignore ├── prop-types.js ├── .babelrc ├── mixins.js ├── hooks.js ├── src ├── context.js ├── utils │ ├── prop-types.js │ └── helpers.js ├── hooks.js ├── mixins.js └── higher-order.js ├── higher-order.js ├── .npmignore ├── index.js ├── test ├── setup.js ├── hook.jsx ├── mixins.jsx └── higher-order.jsx ├── LICENSE.txt ├── package.json ├── README.md └── docs ├── hooks.md ├── mixins.md └── higher-order.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist-modules 4 | TODO.md 5 | *.log 6 | build/ 7 | -------------------------------------------------------------------------------- /prop-types.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist-modules/utils/prop-types.js').default; 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-1" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /mixins.js: -------------------------------------------------------------------------------- 1 | var mixins = require('./dist-modules/mixins.js'); 2 | exports.root = mixins.root; 3 | exports.branch = mixins.branch; 4 | -------------------------------------------------------------------------------- /hooks.js: -------------------------------------------------------------------------------- 1 | var wrappers = require('./dist-modules/hooks.js'); 2 | exports.useRoot = wrappers.useRoot; 3 | exports.useBranch = wrappers.useBranch; 4 | -------------------------------------------------------------------------------- /src/context.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // context shared between higher-order and hooks 4 | export default React.createContext(); 5 | -------------------------------------------------------------------------------- /higher-order.js: -------------------------------------------------------------------------------- 1 | var wrappers = require('./dist-modules/higher-order.js'); 2 | exports.root = wrappers.root; 3 | exports.branch = wrappers.branch; 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .gitignore 4 | .npmignore 5 | .travis.yml 6 | build/ 7 | test/ 8 | *.log 9 | src 10 | .babelrc 11 | .eslintrc 12 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | higherOrder: require('./higher-order.js'), 3 | mixins: require('./mixins.js'), 4 | hooks: require('./hooks.js'), 5 | PropTypes: require('./dist-modules/utils/prop-types.js').default, 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/prop-types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Baobab-React Custom Prop Types 3 | * =============================== 4 | * 5 | * PropTypes used to propagate context safely. 6 | */ 7 | import {isBaobabTree} from './helpers'; 8 | 9 | function errorMessage(propName, what) { 10 | return `prop type \`${propName}\` is invalid; it must be ${what}.`; 11 | } 12 | 13 | export default { 14 | baobab(props, propName) { 15 | if (!(propName in props)) 16 | return; 17 | 18 | if (!isBaobabTree(props[propName])) 19 | return new Error(errorMessage(propName, 'a Baobab tree')); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom').jsdom; 2 | 3 | var exposedProperties = ['window', 'navigator', 'document']; 4 | 5 | global.document = jsdom(''); 6 | global.window = document.defaultView; 7 | Object.keys(document.defaultView).forEach((property) => { 8 | if (typeof global[property] === 'undefined') { 9 | exposedProperties.push(property); 10 | global[property] = document.defaultView[property]; 11 | } 12 | }); 13 | 14 | global.navigator = { 15 | userAgent: 'node.js' 16 | }; 17 | 18 | function throwMessage(msg) { 19 | throw Error(msg); 20 | } 21 | 22 | console.error = console.warn = throwMessage; 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2016 Guillaume Plique (Yomguithereal) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Baobab-React Helpers 3 | * ===================== 4 | * 5 | * Miscellaneous helper functions. 6 | */ 7 | 8 | /** 9 | * Simple curry function. 10 | */ 11 | export function curry(fn, arity) { 12 | return function f1(...args) { 13 | if (args.length >= arity) { 14 | return fn.apply(null, args); 15 | } 16 | else { 17 | return function f2(...args2) { 18 | return f1.apply(null, args.concat(args2)); 19 | }; 20 | } 21 | }; 22 | } 23 | 24 | /** 25 | * Solving the mapping given to a higher-order construct. 26 | */ 27 | export function solveMapping(mapping, props, context) { 28 | if (typeof mapping === 'function') 29 | mapping = mapping(props, context); 30 | 31 | return mapping; 32 | } 33 | 34 | /** 35 | * Determines if the given tree is a Baobab tree. 36 | * FIXME: if Baobab ever implements something like Array.isArray we should use 37 | * that instead of relying in the internal _identity = '[object Baobab]' value. 38 | * See https://github.com/Yomguithereal/baobab/blob/master/src/baobab.js#L111 39 | */ 40 | export function isBaobabTree(tree) { 41 | return !!(tree && typeof tree.toString === 'function' && tree.toString() === '[object Baobab]'); 42 | } 43 | -------------------------------------------------------------------------------- /src/hooks.js: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState, useEffect} from 'react'; 2 | import {isBaobabTree} from './utils/helpers'; 3 | import Baobab from 'baobab'; 4 | import BaobabContext from './context'; 5 | 6 | const makeError = Baobab.helpers.makeError, 7 | isPlainObject = Baobab.type.object; 8 | 9 | function invalidMapping(name, mapping) { 10 | throw makeError( 11 | 'baobab-react/hooks.useBranch: given cursors mapping is invalid (check the "' + name + '" component).', 12 | {mapping} 13 | ); 14 | } 15 | 16 | export function useRoot(tree) { 17 | if (!isBaobabTree(tree)) 18 | throw makeError( 19 | 'baobab-react/hooks.useRoot: given tree is not a Baobab.', 20 | {target: tree} 21 | ); 22 | 23 | const [state, setState] = useState(() => { 24 | return ({children}) => { 25 | return React.createElement(BaobabContext.Provider, { 26 | value: {tree} 27 | }, children); 28 | }; 29 | }); 30 | 31 | useEffect(() => { 32 | setState(() => { 33 | return ({children}) => { 34 | return React.createElement(BaobabContext.Provider, { 35 | value: {tree} 36 | }, children); 37 | }; 38 | }); 39 | }, [tree]); 40 | 41 | return state; 42 | } 43 | 44 | export function useBranch(cursors) { 45 | if (!isPlainObject(cursors) && typeof cursors !== 'function') 46 | invalidMapping(name, cursors); 47 | 48 | const context = useContext(BaobabContext); 49 | 50 | if (!context || !isBaobabTree(context.tree)) 51 | throw makeError( 52 | 'baobab-react/hooks.useBranch: tree is not available.' 53 | ); 54 | 55 | const [state, setState] = useState(() => { 56 | const mapping = typeof cursors === 'function' ? cursors(context) : cursors; 57 | const obj = context.tree.project(mapping); 58 | obj.dispatch = (fn, ...args) => fn(context.tree, ...args); 59 | return obj; 60 | }); 61 | 62 | useEffect(() => { 63 | const mapping = typeof cursors === 'function' ? cursors(context) : cursors; 64 | const watcher = context.tree.watch(mapping); 65 | 66 | watcher.on('update', () => { 67 | const obj = watcher.get(); 68 | obj.dispatch = (fn, ...args) => fn(context.tree, ...args); 69 | setState(obj); 70 | }); 71 | 72 | return () => watcher.release(); 73 | }, [cursors]); 74 | 75 | return state; 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baobab-react", 3 | "version": "4.0.2", 4 | "description": "React integration for Baobab.", 5 | "main": "./index.js", 6 | "scripts": { 7 | "prepublish": "babel ./src --out-dir dist-modules", 8 | "lint": "eslint ./src", 9 | "test": "mocha -R spec --require ./test/setup.js --compilers jsx:babel-register ./test", 10 | "build": "npm run build-mixins && npm run build-higher-order && npm run build-hooks", 11 | "build-mixins": "mkdir -p build && browserify -x baobab -t babelify ./src/mixins.js -o build/mixins.js", 12 | "build-higher-order": "mkdir -p build && browserify -x baobab -x react -t babelify ./src/higher-order.js -o build/higher-order.js", 13 | "build-hooks": "mkdir -p build && browserify -x baobab -x react -t babelify ./src/hooks.js -o build/hooks.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Yomguithereal/baobab-react" 18 | }, 19 | "keywords": [ 20 | "baobab", 21 | "react" 22 | ], 23 | "author": { 24 | "name": "Guillaume Plique", 25 | "url": "http://github.com/Yomguithereal" 26 | }, 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/Yomguithereal/baobab-react/issues" 30 | }, 31 | "homepage": "https://github.com/Yomguithereal/baobab-react", 32 | "devDependencies": { 33 | "@yomguithereal/eslint-config": "^2.1.0", 34 | "babel-cli": "^6.6.4", 35 | "babel-core": "^6.6.4", 36 | "babel-preset-es2015": "^6.6.0", 37 | "babel-preset-react": "^6.5.0", 38 | "babel-preset-stage-1": "^6.5.0", 39 | "babel-register": "^6.6.0", 40 | "babelify": "^7.2.0", 41 | "baobab": "^2.3.3", 42 | "browserify": "^13.0.0", 43 | "create-react-class": "^15.6.3", 44 | "enzyme": "^3.9.0", 45 | "enzyme-adapter-react-16": "^1.11.2", 46 | "eslint": "^2.2.0", 47 | "jsdom": "^8.1.0", 48 | "mocha": "^2.2.4", 49 | "react": "^16.8.6", 50 | "react-dom": "^16.8.6", 51 | "react-test-renderer": "^16.8.6" 52 | }, 53 | "eslintConfig": { 54 | "extends": [ 55 | "@yomguithereal/eslint-config/es6" 56 | ], 57 | "parserOptions": { 58 | "ecmaVersion": 9, 59 | "sourceType": "module", 60 | "ecmaFeatures": { 61 | "jsx": true 62 | } 63 | } 64 | }, 65 | "dependencies": { 66 | "deep-equal": "^1.0.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/mixins.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Baobab-React Mixins 3 | * ==================== 4 | * 5 | * Old style react mixins. 6 | */ 7 | import PropTypes from './utils/prop-types'; 8 | import {solveMapping} from './utils/helpers'; 9 | import Baobab from 'baobab'; 10 | 11 | const makeError = Baobab.helpers.makeError; 12 | 13 | /** 14 | * Helpers 15 | */ 16 | function displayName(instance) { 17 | return (instance.constructor || {}).displayName || 'Component'; 18 | } 19 | 20 | /** 21 | * Root mixin 22 | */ 23 | const RootMixin = { 24 | 25 | // Component prop types 26 | propTypes: { 27 | tree: PropTypes.baobab 28 | }, 29 | 30 | // Context prop types 31 | childContextTypes: { 32 | tree: PropTypes.baobab 33 | }, 34 | 35 | // Handling child context 36 | getChildContext() { 37 | return { 38 | tree: this.props.tree 39 | }; 40 | } 41 | }; 42 | 43 | /** 44 | * Branch mixin 45 | */ 46 | const BranchMixin = { 47 | 48 | // Retrieving the tree from context 49 | contextTypes: { 50 | tree: PropTypes.baobab 51 | }, 52 | 53 | // Building initial state 54 | getInitialState() { 55 | const name = displayName(this); 56 | 57 | if (this.cursors) { 58 | this.__cursorsMapping = this.cursors; 59 | 60 | const mapping = solveMapping( 61 | this.__cursorsMapping, 62 | this.props, 63 | this.context 64 | ); 65 | 66 | // If the solved mapping is not valid, we throw 67 | if (!mapping) 68 | throw makeError( 69 | 'baobab-react/mixins.branch: given mapping is invalid (check the "' + name + '" component).', 70 | {mapping} 71 | ); 72 | 73 | // Creating the watcher 74 | this.__watcher = this.context.tree.watch(mapping); 75 | 76 | // Building initial state 77 | return this.__watcher.get(); 78 | } 79 | 80 | return null; 81 | }, 82 | 83 | // On component mount 84 | componentWillMount() { 85 | 86 | // Creating dispatcher 87 | this.dispatch = (fn, ...args) => fn(this.context.tree, ...args); 88 | 89 | if (!this.__watcher) 90 | return; 91 | 92 | const handler = () => { 93 | if (this.__watcher) 94 | this.setState(this.__watcher.get()); 95 | }; 96 | 97 | this.__watcher.on('update', handler); 98 | }, 99 | 100 | // On component unmount 101 | componentWillUnmount() { 102 | if (!this.__watcher) 103 | return; 104 | 105 | // Releasing facet 106 | this.__watcher.release(); 107 | this.__watcher = null; 108 | }, 109 | 110 | // On new props 111 | componentWillReceiveProps(props) { 112 | if (!this.__watcher || typeof this.__cursorsMapping !== 'function') 113 | return; 114 | 115 | const name = displayName(this); 116 | 117 | // Refreshing the watcher 118 | const mapping = solveMapping(this.__cursorsMapping, props, this.context); 119 | 120 | if (!mapping) 121 | throw makeError( 122 | 'baobab-react/mixins.branch: given mapping is invalid (check the "' + name + '" component).', 123 | {mapping} 124 | ); 125 | 126 | this.__watcher.refresh(mapping); 127 | this.setState(this.__watcher.get()); 128 | } 129 | }; 130 | 131 | export {RootMixin as root, BranchMixin as branch}; 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Yomguithereal/baobab-react.svg)](https://travis-ci.org/Yomguithereal/baobab-react) 2 | 3 | # baobab-react 4 | 5 | Welcome to [baobab](https://github.com/Yomguithereal/baobab)'s [React](https://facebook.github.io/react/) integration (from v2.0.0 and onwards). 6 | 7 | Implemented patterns: 8 | 9 | * [Hooks](docs/hooks.md) 10 | * [Higher order components](docs/higher-order.md) (curried so also usable as ES7 decorators) 11 | * [Mixins](docs/mixins.md) 12 | 13 | ## Summary 14 | 15 | * [Installation](#installation) 16 | * [On root & branches](#on-root--branches) 17 | * [Patterns](#patterns) 18 | * [Hooks](#hooks) 19 | * [Mixins](#mixins) 20 | * [Higher Order Components](#higher-order-components) 21 | * [Common pitfalls](#common-pitfalls) 22 | * [Contribution](#contribution) 23 | * [License](#license) 24 | 25 | ## Installation 26 | 27 | You can install `baobab-react` through npm: 28 | 29 | ``` 30 | npm install baobab-react 31 | ``` 32 | 33 | *Peer dependencies* 34 | 35 | This library necessitate that you install `baobab >= 2.0.0` and `react >= 0.13.x` (plus `react-dom >= 0.14.x` if required). 36 | 37 | Then require the desired pattern and only this one will be loaded (meaning that your browserify/webpack bundle, for instance, won't load unnecessary files and end up bloated). 38 | 39 | *Example* 40 | 41 | ```js 42 | var mixins = require('baobab-react/mixins'); 43 | ``` 44 | 45 | ## On root & branches 46 | 47 | In order to keep component definitions detached from any particular instance of Baobab, the mixins, higher order components etc. are divided into two: 48 | 49 | * The **Root** aims at passing a baobab tree through context so that child component (branches) may use it. Typically, your app's top-level component will probably be a root. 50 | * The **Branches**, bound to cursors, get their data from the tree given by the root. 51 | 52 | This is necessary so that isomorphism can remain an enjoyable stroll in the park (your UI would remain a pure function). 53 | 54 | ## Patterns 55 | 56 | ### Hooks 57 | 58 | [Dedicated documentation](docs/hooks.md) 59 | 60 | ### Higher Order Components 61 | 62 | [Dedicated documentation](docs/higher-order.md) 63 | 64 | ### Mixins 65 | 66 | [Dedicated documentation](docs/mixins.md) 67 | 68 | ## Common pitfalls 69 | 70 | **Controlled input state** 71 | 72 | If you need to store a react controlled input's state into a baobab tree, remember you have to commit changes synchronously through the `tree.commit` method or else you'll observe nasty cursor jumps in some cases. 73 | 74 | ```js 75 | var PropTypes = require('baobab-react/prop-types').PropTypes; 76 | 77 | var Input = React.createClass({ 78 | mixins: [mixins.branch], 79 | cursors: { 80 | inputValue: ['inputValue'] 81 | }, 82 | contextTypes: { 83 | tree: PropTypes.baobab 84 | }, 85 | onChange: function(e) { 86 | var newValue = e.target.value; 87 | 88 | // If one edits the tree normally, i.e. asynchronously, the cursor will hop 89 | this.cursor.set(newValue); 90 | 91 | // One has to commit synchronously the update for the input to work correctly 92 | this.cursor.set(newValue); 93 | this.context.tree.commit(); 94 | }, 95 | render: function() { 96 | return ; 97 | } 98 | }); 99 | ``` 100 | 101 | ## Contribution 102 | 103 | Contributions are obviously welcome. 104 | 105 | Be sure to add unit tests if relevant and pass them all before submitting your pull request. 106 | 107 | ```bash 108 | # Installing the dev environment 109 | git clone git@github.com:Yomguithereal/baobab-react.git 110 | cd baobab-react 111 | npm install 112 | 113 | # Running the tests 114 | npm test 115 | 116 | # Linting 117 | npm run lint 118 | 119 | # Building a independent version 120 | npm run build 121 | 122 | # or per pattern 123 | npm run build-mixins 124 | npm run build-higher-order 125 | npm run build-hooks 126 | ``` 127 | 128 | ## License 129 | MIT 130 | -------------------------------------------------------------------------------- /src/higher-order.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Baobab-React Higher Order Component 3 | * ==================================== 4 | * 5 | * ES6 state of the art higher order component. 6 | */ 7 | import React from 'react'; 8 | import Baobab from 'baobab'; 9 | import {curry, isBaobabTree, solveMapping} from './utils/helpers'; 10 | import deepEqual from 'deep-equal'; 11 | import BaobabContext from './context'; 12 | 13 | const makeError = Baobab.helpers.makeError, 14 | isPlainObject = Baobab.type.object; 15 | 16 | /** 17 | * Helpers 18 | */ 19 | function displayName(Component) { 20 | return Component.name || Component.displayName || 'Component'; 21 | } 22 | 23 | function invalidMapping(name, mapping) { 24 | throw makeError( 25 | 'baobab-react/higher-order.branch: given cursors mapping is invalid (check the "' + name + '" component).', 26 | {mapping} 27 | ); 28 | } 29 | 30 | /** 31 | * Root component 32 | */ 33 | function root(tree, Component) { 34 | if (!isBaobabTree(tree)) 35 | throw makeError( 36 | 'baobab-react/higher-order.root: given tree is not a Baobab.', 37 | {target: tree} 38 | ); 39 | 40 | if (typeof Component !== 'function') 41 | throw Error('baobab-react/higher-order.root: given target is not a valid React component.'); 42 | 43 | const name = displayName(Component); 44 | 45 | const value = {tree}; 46 | 47 | const ComposedComponent = class extends React.Component { 48 | // Render shim 49 | render() { 50 | return ( 51 | 52 | 53 | 54 | ); 55 | } 56 | }; 57 | 58 | ComposedComponent.displayName = 'Rooted' + name; 59 | 60 | return ComposedComponent; 61 | } 62 | 63 | /** 64 | * Branch component 65 | */ 66 | function branch(cursors, Component) { 67 | if (typeof Component !== 'function') 68 | throw Error('baobab-react/higher-order.branch: given target is not a valid React component.'); 69 | 70 | const name = displayName(Component); 71 | 72 | if (!isPlainObject(cursors) && typeof cursors !== 'function') 73 | invalidMapping(name, cursors); 74 | 75 | const ComposedComponent = class extends React.Component { 76 | // Building initial state 77 | constructor(props, context) { 78 | super(props, context); 79 | 80 | // Creating dispatcher 81 | this.dispatcher = (fn, ...args) => fn(this.context.tree, ...args); 82 | 83 | if (!cursors) 84 | return; 85 | 86 | const mapping = solveMapping(cursors, props, context); 87 | 88 | if (!mapping) 89 | invalidMapping(name, mapping); 90 | 91 | if (!this.context || !isBaobabTree(this.context.tree)) 92 | throw makeError( 93 | 'baobab-react/higher-order.branch: tree is not available.' 94 | ); 95 | 96 | // Creating the watcher 97 | const watcher = this.context.tree.watch(mapping); 98 | 99 | const handler = () => { 100 | this.setState({derived: this.state.watcher.get()}); 101 | }; 102 | 103 | watcher.on('update', handler); 104 | 105 | // Hydrating initial state 106 | this.state = { 107 | watcher, 108 | tree: context.tree, 109 | derived: watcher.get(), 110 | }; 111 | } 112 | 113 | static getDerivedStateFromProps(props, {watcher, tree, mapping}) { 114 | if (!cursors) 115 | return; 116 | 117 | const newMapping = solveMapping(cursors, props, {tree}); 118 | 119 | if (!newMapping) 120 | invalidMapping(name, newMapping); 121 | 122 | if (deepEqual(mapping, newMapping)) return; 123 | 124 | // Refreshing the watcher 125 | watcher.refresh(newMapping); 126 | return {mapping, derived: watcher.get()}; 127 | } 128 | 129 | // Render shim 130 | render() { 131 | const {decoratedComponentRef, ...props} = this.props; 132 | const suppl = {dispatch: this.dispatcher}; 133 | 134 | return ; 135 | } 136 | 137 | // On component unmount 138 | componentWillUnmount() { 139 | if (!this.state.watcher) 140 | return; 141 | 142 | // Releasing watcher 143 | this.state.watcher.release(); 144 | } 145 | }; 146 | 147 | ComposedComponent.displayName = 'Branched' + name; 148 | 149 | ComposedComponent.contextType = BaobabContext; 150 | 151 | return ComposedComponent; 152 | } 153 | 154 | // Currying the functions so that they could be used as decorators 155 | const curriedRoot = curry(root, 2), 156 | curriedBranch = curry(branch, 2); 157 | 158 | export {curriedRoot as root, curriedBranch as branch}; 159 | -------------------------------------------------------------------------------- /test/hook.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Baobab-React Mixins Unit Tests 3 | * =============================== 4 | * 5 | */ 6 | import assert from 'assert'; 7 | import React from 'react'; 8 | import enzyme, {mount} from 'enzyme'; 9 | import Baobab from 'baobab'; 10 | import {useRoot, useBranch} from '../src/hooks'; 11 | import Adapter from 'enzyme-adapter-react-16'; 12 | 13 | enzyme.configure({adapter: new Adapter()}); 14 | 15 | /** 16 | * Components. 17 | */ 18 | const BasicRoot = function({tree, children}) { 19 | const Root = useRoot(tree); 20 | return ( 21 | 22 |
23 | {children} 24 |
25 |
26 | ); 27 | } 28 | 29 | /** 30 | * Test suite. 31 | */ 32 | describe('Hook', function() { 33 | describe('api', function() { 34 | it('root should throw an error if the passed argument is not a tree.', function() { 35 | assert.throws(function() { 36 | mount(); 37 | }, /baobab-react/); 38 | }); 39 | 40 | it('branch should throw an error if the passed argument is not valid.', function() { 41 | const tree = new Baobab({name: 'John'}, {asynchronous: false}); 42 | 43 | const Child = () => { 44 | const data = useBranch(); 45 | return Hello {data.name}; 46 | } 47 | 48 | assert.throws(() => { 49 | mount(); 50 | }, /baobab-react/); 51 | }); 52 | }); 53 | 54 | describe('context', function() { 55 | it('the tree should be propagated through context.', function() { 56 | const tree = new Baobab({path: ['name'], name: 'John'}, {asynchronous: false}); 57 | 58 | const Child = () => { 59 | const data = useBranch(context => ({ 60 | name: context.tree.get('path'), 61 | })); 62 | return Hello {data.name}; 63 | } 64 | 65 | const wrapper = mount(); 66 | 67 | assert.strictEqual(wrapper.text(), 'Hello John'); 68 | }); 69 | 70 | it('should fail if the tree is not passed through context.', function() { 71 | const Child = () => { 72 | const data = useBranch({name: ['name']}); 73 | return Hello John; 74 | } 75 | 76 | assert.throws(function() { 77 | mount(); 78 | }, /baobab-react/); 79 | }); 80 | }); 81 | 82 | describe('binding', function() { 83 | it('should be possible to bind several cursors to a component.', function() { 84 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 85 | 86 | const Child = () => { 87 | const data = useBranch({ 88 | name: ['name'], 89 | surname: ['surname'] 90 | }); 91 | return ( 92 | 93 | Hello {data.name} {data.surname} 94 | 95 | ); 96 | } 97 | 98 | const wrapper = mount(); 99 | 100 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 101 | }); 102 | 103 | it('should be possible to register paths using typical Baobab polymorphisms.', function() { 104 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 105 | 106 | const Child = () => { 107 | const data = useBranch({ 108 | name: 'name', 109 | surname: 'surname' 110 | }); 111 | return ( 112 | 113 | Hello {data.name} {data.surname} 114 | 115 | ); 116 | } 117 | 118 | const wrapper = mount(); 119 | 120 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 121 | }); 122 | 123 | it('bound components should update along with the cursor.', function(done) { 124 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 125 | 126 | const Child = () => { 127 | const data = useBranch({ 128 | name: 'name', 129 | surname: 'surname' 130 | }); 131 | return ( 132 | 133 | Hello {data.name} {data.surname} 134 | 135 | ); 136 | } 137 | 138 | const wrapper = mount(); 139 | 140 | tree.set('surname', 'the Third'); 141 | 142 | setTimeout(() => { 143 | assert.strictEqual(wrapper.text(), 'Hello John the Third'); 144 | done(); 145 | }, 50); 146 | }); 147 | 148 | it('should be possible to set cursors with a function.', function(done) { 149 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 150 | 151 | const Child = props => { 152 | const data = useBranch(() => { 153 | return { 154 | name: ['name'], 155 | surname: props.path 156 | }; 157 | }); 158 | return ( 159 | 160 | Hello {data.name} {data.surname} 161 | 162 | ); 163 | } 164 | 165 | const wrapper = mount(); 166 | 167 | tree.set('surname', 'the Third'); 168 | 169 | setTimeout(() => { 170 | assert.strictEqual(wrapper.text(), 'Hello John the Third'); 171 | done(); 172 | }, 50); 173 | }); 174 | }); 175 | 176 | describe('actions', function() { 177 | it('should be possible to dispatch actions.', function() { 178 | const tree = new Baobab({counter: 0}, {asynchronous: false}); 179 | 180 | const inc = function(state, by = 1) { 181 | state.apply('counter', nb => nb + by); 182 | }; 183 | 184 | const Counter = function() { 185 | const {counter, dispatch} = useBranch({counter: 'counter'}); 186 | 187 | return ( 188 | dispatch(inc)} 189 | onChange={() => dispatch(inc, 2)}> 190 | Counter: {counter} 191 | 192 | ); 193 | }; 194 | 195 | const wrapper = mount(); 196 | 197 | assert.strictEqual(wrapper.text(), 'Counter: 0'); 198 | wrapper.find('span').simulate('click'); 199 | assert.strictEqual(wrapper.text(), 'Counter: 1'); 200 | wrapper.find('span').simulate('change'); 201 | assert.strictEqual(wrapper.text(), 'Counter: 3'); 202 | }); 203 | }); 204 | }); 205 | -------------------------------------------------------------------------------- /test/mixins.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Baobab-React Mixins Unit Tests 3 | * =============================== 4 | * 5 | */ 6 | import assert from 'assert'; 7 | import React from 'react'; 8 | import createReactClass from 'create-react-class'; 9 | import {mount} from 'enzyme'; 10 | import Baobab from 'baobab'; 11 | import * as mixins from '../src/mixins'; 12 | 13 | /** 14 | * Components. 15 | */ 16 | const DummyRoot = createReactClass({ 17 | mixins: [mixins.root], 18 | render() { 19 | return
; 20 | } 21 | }); 22 | 23 | const Root = createReactClass({ 24 | mixins: [mixins.root], 25 | render() { 26 | return
{this.props.children}
; 27 | } 28 | }); 29 | 30 | /** 31 | * Test suite. 32 | */ 33 | describe('Mixins', function() { 34 | 35 | describe('context', function() { 36 | 37 | // NOTE: the commented tests do not work from React v15.2.0 & onwards 38 | 39 | // it('should fail if passing a wrong tree to the root mixin.', function() { 40 | 41 | // assert.throws(function() { 42 | // mount(); 43 | // }, /Baobab/); 44 | // }); 45 | 46 | it('the tree should be propagated through context.', function() { 47 | const tree = new Baobab({name: 'John'}, {asynchronous: false}); 48 | 49 | const Child = createReactClass({ 50 | mixins: [mixins.branch], 51 | render() { 52 | return Hello {this.context.tree.get('name')}; 53 | } 54 | }); 55 | 56 | const wrapper = mount(); 57 | 58 | assert.strictEqual(wrapper.text(), 'Hello John'); 59 | }); 60 | 61 | // it('should fail if the tree is not passed through context.', function() { 62 | // const Child = createReactClass({ 63 | // mixins: [mixins.branch], 64 | // render() { 65 | // return Hello John; 66 | // } 67 | // }); 68 | 69 | // assert.throws(function() { 70 | // mount(); 71 | // }, /Baobab/); 72 | // }); 73 | }); 74 | 75 | describe('binding', function() { 76 | it('should be possible to bind several cursors to a component.', function() { 77 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 78 | 79 | const Child = createReactClass({ 80 | mixins: [mixins.branch], 81 | cursors: { 82 | name: ['name'], 83 | surname: ['surname'] 84 | }, 85 | render: function() { 86 | 87 | return ( 88 | 89 | Hello {this.state.name} {this.state.surname} 90 | 91 | ); 92 | } 93 | }); 94 | 95 | const wrapper = mount(); 96 | 97 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 98 | }); 99 | 100 | it('should be possible to register paths using typical Baobab polymorphisms.', function() { 101 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 102 | 103 | const Child = createReactClass({ 104 | mixins: [mixins.branch], 105 | cursors: { 106 | name: 'name', 107 | surname: 'surname' 108 | }, 109 | render: function() { 110 | 111 | return ( 112 | 113 | Hello {this.state.name} {this.state.surname} 114 | 115 | ); 116 | } 117 | }); 118 | 119 | const wrapper = mount(); 120 | 121 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 122 | }); 123 | 124 | it('bound components should update along with the cursor.', function(done) { 125 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 126 | 127 | const Child = createReactClass({ 128 | mixins: [mixins.branch], 129 | cursors: { 130 | name: ['name'], 131 | surname: ['surname'] 132 | }, 133 | render: function() { 134 | 135 | return ( 136 | 137 | Hello {this.state.name} {this.state.surname} 138 | 139 | ); 140 | } 141 | }); 142 | 143 | const wrapper = mount(); 144 | 145 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 146 | 147 | tree.set('surname', 'the Third'); 148 | 149 | setTimeout(() => { 150 | assert.strictEqual(wrapper.text(), 'Hello John the Third'); 151 | done(); 152 | }, 50); 153 | }); 154 | 155 | it('should be possible to set cursors with a function.', function(done) { 156 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 157 | 158 | const Child = createReactClass({ 159 | mixins: [mixins.branch], 160 | cursors(props) { 161 | return { 162 | name: ['name'], 163 | surname: props.path 164 | }; 165 | }, 166 | render: function() { 167 | 168 | return ( 169 | 170 | Hello {this.state.name} {this.state.surname} 171 | 172 | ); 173 | } 174 | }); 175 | 176 | const wrapper = mount(); 177 | 178 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 179 | 180 | tree.set('surname', 'the Third'); 181 | 182 | setTimeout(() => { 183 | assert.strictEqual(wrapper.text(), 'Hello John the Third'); 184 | done(); 185 | }, 50); 186 | }); 187 | }); 188 | 189 | describe('actions', function() { 190 | 191 | it('should be possible to dispatch actions.', function() { 192 | const tree = new Baobab({counter: 0}, {asynchronous: false}); 193 | 194 | const inc = function(state, by = 1) { 195 | state.apply('counter', nb => nb + by); 196 | }; 197 | 198 | const Counter = createReactClass({ 199 | mixins: [mixins.branch], 200 | cursors: { 201 | counter: 'counter' 202 | }, 203 | render() { 204 | const dispatch = this.dispatch; 205 | 206 | return ( 207 | dispatch(inc)} 208 | onChange={() => dispatch(inc, 2)}> 209 | Counter: {this.state.counter} 210 | 211 | ); 212 | } 213 | }); 214 | 215 | const wrapper = mount(); 216 | 217 | assert.strictEqual(wrapper.text(), 'Counter: 0'); 218 | wrapper.find('span').simulate('click'); 219 | assert.strictEqual(wrapper.text(), 'Counter: 1'); 220 | wrapper.find('span').simulate('change'); 221 | assert.strictEqual(wrapper.text(), 'Counter: 3'); 222 | }); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | In this example, we'll build a simplistic React app showing a list of colors to see how one could integrate **Baobab** with React by using hooks. 4 | 5 | ### Summary 6 | 7 | * [Hooks](#hooks) 8 | * [Summary](#summary) 9 | * [Creating the app's state](#creating-the-apps-state) 10 | * [Rooting our top-level component](#rooting-our-top-level-component) 11 | * [Branching our list](#branching-our-list) 12 | * [Actions](#actions) 13 | * [Dynamically set the list's path using props](#dynamically-set-the-lists-path-using-props) 14 | * [Clever vs. dumb components](#clever-vs-dumb-components) 15 | 16 | ### Creating the app's state 17 | 18 | Let's create a **Baobab** tree to store our colors' list: 19 | 20 | *state.js* 21 | 22 | ```js 23 | import Baobab from 'baobab'; 24 | 25 | const tree = new Baobab({ 26 | colors: ['yellow', 'blue', 'orange'] 27 | }); 28 | 29 | export default tree; 30 | ``` 31 | 32 | ### Rooting our top-level component 33 | 34 | Now that the tree is created, we should bind our React app to it by "rooting" our top-level component. 35 | 36 | Under the hood, this component will simply propagate the tree to its descendants using React's [Context](https://reactjs.org/docs/context.html) so that "branched" component may subscribe to updates of parts of the tree afterwards. 37 | 38 | *main.jsx* 39 | 40 | ```jsx 41 | import React, {Component} from 'react'; 42 | import {render} from 'react-dom'; 43 | import {useRoot} from 'baobab-react/hooks'; 44 | import tree from './state'; 45 | 46 | // We will write this component later 47 | import List from './list.jsx'; 48 | 49 | // Creating our top-level component 50 | const App = function({store}) { 51 | // useRoot takes the baobab tree and provides a component bound to the tree 52 | const Root = useRoot(store); 53 | return ( 54 | 55 | 56 | 57 | ); 58 | } 59 | 60 | // Rendering the app 61 | render(, document.querySelector('#mount')); 62 | ``` 63 | 64 | ### Branching our list 65 | 66 | Now that we have "rooted" our top-level `App` component, let's create the component displaying our colors' list and branch it to the tree's data. 67 | 68 | *list.jsx* 69 | 70 | ```jsx 71 | import React, {Component} from 'react'; 72 | import {useBranch} from 'baobab-react/hooks'; 73 | 74 | const List = function() { 75 | // branch by mapping the desired data to cursors 76 | const {colors} = useBranch({ 77 | colors: ['colors'] 78 | }); 79 | 80 | function renderItem(color) { 81 | return
  • {color}
  • ; 82 | } 83 | 84 | return
      {colors.map(renderItem)}
    ; 85 | } 86 | 87 | export default List; 88 | ``` 89 | 90 | Our app would now render something of the kind: 91 | 92 | ```html 93 |
    94 |
      95 |
    • yellow
    • 96 |
    • blue
    • 97 |
    • orange
    • 98 |
    99 |
    100 | ``` 101 | 102 | But let's add a color to the list: 103 | 104 | ```js 105 | tree.push('colors', 'purple'); 106 | ``` 107 | 108 | And the list component will automatically update and to render the following: 109 | 110 | ```html 111 |
    112 |
      113 |
    • yellow
    • 114 |
    • blue
    • 115 |
    • orange
    • 116 |
    • purple
    • 117 |
    118 |
    119 | ``` 120 | 121 | Now you just need to add an action layer on top of that so that app's state can be updated and you've got yourself an atomic Flux! 122 | 123 | ### Actions 124 | 125 | Here is what we are trying to achieve: 126 | 127 | ``` 128 | ┌────────────────────┐ 129 | ┌──────────── │ Central State │ ◀───────────┐ 130 | │ │ (Baobab tree) │ │ 131 | │ └────────────────────┘ │ 132 | Renders Updates 133 | │ │ 134 | │ │ 135 | ▼ │ 136 | ┌────────────────────┐ ┌────────────────────┐ 137 | │ View │ │ Actions │ 138 | │ (React Components) │ ────────Triggers───────▶ │ (Functions) │ 139 | └────────────────────┘ └────────────────────┘ 140 | ``` 141 | 142 | For the time being we only have a central state stored by a Baobab tree and a view layer composed of React components. 143 | 144 | What remains to be added is a way for the user to trigger actions and update the central state. 145 | 146 | To do so `baobab-react` proposes to create simple functions as actions: 147 | 148 | *actions.js* 149 | 150 | ```js 151 | export function addColor(tree, color) { 152 | tree.push('colors', color); 153 | } 154 | ``` 155 | 156 | Now let's add a simple button so that a user may add colors: 157 | 158 | *list.jsx* 159 | 160 | ```jsx 161 | import React, {useState} from 'react'; 162 | import {useBranch} from 'baobab-react/hooks'; 163 | import * as actions from './actions'; 164 | 165 | const List = function() { 166 | const [inputColor, setColor] = useState(null); 167 | // Subscribing to the relevant data in the tree 168 | const {colors, dispatch} = useBranch({ 169 | colors: ['colors'] 170 | }); 171 | 172 | // Adding a color on click 173 | const handleClick = () => { 174 | // A dispatcher is available through `props.dispatch` 175 | dispatch( 176 | actions.addColor, 177 | inputColor 178 | ); 179 | 180 | // Resetting the input 181 | setColor(null); 182 | }; 183 | 184 | return ( 185 |
    186 |
      {colors.map(renderItem)}
    187 | setColor(e.target.value)} /> 190 | 191 |
    192 | ); 193 | }; 194 | 195 | export default List; 196 | ``` 197 | 198 | ### Dynamically set the list's path using props 199 | 200 | Sometimes, you might find yourself needing cursors paths changing along with your component's props. 201 | 202 | For instance, given the following state: 203 | 204 | *state.js* 205 | 206 | ```js 207 | import Baobab from 'baobab'; 208 | 209 | const tree = new Baobab({ 210 | colors: ['yellow', 'blue', 'orange'], 211 | alternativeColors: ['purple', 'orange', 'black'] 212 | }); 213 | 214 | export default tree; 215 | ``` 216 | 217 | You might want to have a list rendering either one of the colors' lists. 218 | 219 | Fortunately, you can do so by passing a function taking both props and context of the components and returning a valid mapping: 220 | 221 | *list.jsx* 222 | 223 | ```jsx 224 | import React, {Component} from 'react'; 225 | import {useBranch} from 'baobab-react/hooks'; 226 | 227 | const List = function(props) { 228 | // Using a function so that your cursors' path can use the component's props etc. 229 | const {colors} = useBranch({ 230 | colors: [props.alternative ? 'alternativeColors' : 'colors'] 231 | }); 232 | 233 | function renderItem(color) { 234 | return
  • {color}
  • ; 235 | } 236 | 237 | return
      {colors.map(renderItem)}
    ; 238 | } 239 | 240 | export default List; 241 | ``` 242 | 243 | ### Clever vs. dumb components 244 | 245 | Now you know everything to use a Baobab tree efficiently with React. 246 | 247 | However, the example app shown above is minimalist and should probably not be organized thusly in a real-life scenario. 248 | 249 | Indeed, whenever possible, one should try to separate "clever" components, that know about the tree's existence from "dumb" components, completely oblivious of it. 250 | 251 | Knowing when to branch/wrap a component and let some components ignore the existence of the tree is the key to a maintainable and scalable application. 252 | 253 | **Example** 254 | 255 | *Clever component* 256 | 257 | This component does know that a tree provides him with data. 258 | 259 | ```js 260 | import React, {Component} from 'react'; 261 | import {useBranch} from 'baobab-react/hooks'; 262 | import List from './list.jsx'; 263 | 264 | class ListWrapper extends Component { 265 | const {colors} = useBranch({ 266 | colors: ['colors'] 267 | }); 268 | return ; 269 | } 270 | 271 | export default ListWrapper; 272 | ``` 273 | 274 | *Dumb component* 275 | 276 | This component should stay unaware of the tree so it can remain generic and be used elsewhere easily. 277 | 278 | ```js 279 | import React, {Component} from 'react'; 280 | 281 | export default class List extends Component { 282 | render() { 283 | 284 | function renderItem(value) { 285 | return
  • {value}
  • ; 286 | } 287 | 288 | return
      {this.props.items.map(renderItem)}
    ; 289 | } 290 | } 291 | ``` -------------------------------------------------------------------------------- /test/higher-order.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Baobab-React Mixins Unit Tests 3 | * =============================== 4 | * 5 | */ 6 | import assert from 'assert'; 7 | import React, {Component} from 'react'; 8 | import enzyme, {mount} from 'enzyme'; 9 | import Baobab from 'baobab'; 10 | import BaobabContext from '../src/context'; 11 | import {root, branch} from '../src/higher-order'; 12 | import Adapter from 'enzyme-adapter-react-16'; 13 | 14 | enzyme.configure({adapter: new Adapter()}); 15 | 16 | /** 17 | * Components. 18 | */ 19 | class DummyRoot extends Component { 20 | render() { 21 | return
    ; 22 | } 23 | } 24 | 25 | class BasicRoot extends Component { 26 | render() { 27 | return ( 28 |
    29 | {this.props.children} 30 |
    31 | ); 32 | } 33 | } 34 | 35 | /** 36 | * Test suite. 37 | */ 38 | describe('Higher Order', function() { 39 | 40 | describe('api', function() { 41 | it('both root & branch should be curried.', function() { 42 | const rootTest = root(new Baobab()), 43 | branchTest = branch({}); 44 | 45 | assert(typeof rootTest === 'function'); 46 | assert(typeof branchTest === 'function'); 47 | 48 | const rootWithComponentTest = root(new Baobab(), DummyRoot), 49 | branchWithComponentTest = branch({}, DummyRoot); 50 | 51 | assert(typeof rootWithComponentTest === 'function'); 52 | assert(typeof branchWithComponentTest === 'function'); 53 | 54 | const rootThenComponentTest = root(new Baobab())(DummyRoot), 55 | branchThenComponentTest = branch({})(DummyRoot); 56 | 57 | assert(typeof rootThenComponentTest === 'function'); 58 | assert(typeof branchThenComponentTest === 'function'); 59 | }); 60 | 61 | it('root should throw an error if the passed argument is not a tree.', function() { 62 | assert.throws(function() { 63 | root(null, DummyRoot); 64 | }, /Baobab/); 65 | }); 66 | 67 | it('branch should throw an error if the passed argument is not valid.', function() { 68 | assert.throws(function() { 69 | branch(null, DummyRoot); 70 | }, /invalid/); 71 | }); 72 | 73 | it('both root & branch should throw if the target is not a valid React component.', function() { 74 | assert.throws(function() { 75 | root(new Baobab(), null); 76 | }, /component/); 77 | 78 | assert.throws(function() { 79 | branch({}, null); 80 | }, /component/); 81 | }); 82 | }); 83 | 84 | describe('context', function() { 85 | it('the tree should be propagated through context.', function() { 86 | const tree = new Baobab({name: 'John'}, {asynchronous: false}); 87 | 88 | const Root = root(tree, BasicRoot); 89 | 90 | class Child extends Component { 91 | render() { 92 | return Hello {this.context.tree.get('name')}; 93 | } 94 | } 95 | 96 | Child.contextType = BaobabContext; 97 | 98 | const wrapper = mount(); 99 | 100 | assert.strictEqual(wrapper.text(), 'Hello John'); 101 | }); 102 | 103 | it('should fail if the tree is not passed through context.', function() { 104 | class Child extends Component { 105 | render() { 106 | return Hello John; 107 | } 108 | } 109 | 110 | const BranchedChild = branch({}, Child); 111 | 112 | assert.throws(function() { 113 | mount(); 114 | }, /baobab-react/); 115 | }); 116 | }); 117 | 118 | describe('binding', function() { 119 | it('should be possible to bind several cursors to a component.', function() { 120 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 121 | 122 | class Child extends Component { 123 | render() { 124 | return ( 125 | 126 | Hello {this.props.name} {this.props.surname} 127 | 128 | ); 129 | } 130 | } 131 | 132 | const Root = root(tree, BasicRoot); 133 | 134 | const BranchedChild = branch({ 135 | name: ['name'], 136 | surname: ['surname'] 137 | }, Child); 138 | 139 | const wrapper = mount(); 140 | 141 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 142 | }); 143 | 144 | it('should be possible to register paths using typical Baobab polymorphisms.', function() { 145 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 146 | 147 | class Child extends Component { 148 | render() { 149 | return ( 150 | 151 | Hello {this.props.name} {this.props.surname} 152 | 153 | ); 154 | } 155 | } 156 | 157 | const Root = root(tree, BasicRoot); 158 | 159 | const BranchedChild = branch({ 160 | name: 'name', 161 | surname: 'surname' 162 | }, Child); 163 | 164 | const wrapper = mount(); 165 | 166 | assert.strictEqual(wrapper.text(), 'Hello John Talbot'); 167 | }); 168 | 169 | it('bound components should update along with the cursor.', function(done) { 170 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 171 | 172 | class Child extends Component { 173 | render() { 174 | return ( 175 | 176 | Hello {this.props.name} {this.props.surname} 177 | 178 | ); 179 | } 180 | } 181 | 182 | const Root = root(tree, BasicRoot); 183 | 184 | const BranchedChild = branch({ 185 | name: 'name', 186 | surname: 'surname' 187 | }, Child); 188 | 189 | const wrapper = mount(); 190 | 191 | tree.set('surname', 'the Third'); 192 | 193 | setTimeout(() => { 194 | assert.strictEqual(wrapper.text(), 'Hello John the Third'); 195 | done(); 196 | }, 50); 197 | }); 198 | 199 | it('should be possible to set cursors with a function.', function(done) { 200 | const tree = new Baobab({name: 'John', surname: 'Talbot'}, {asynchronous: false}); 201 | 202 | class Child extends Component { 203 | render() { 204 | return ( 205 | 206 | Hello {this.props.name} {this.props.surname} 207 | 208 | ); 209 | } 210 | } 211 | 212 | const Root = root(tree, BasicRoot); 213 | 214 | const BranchedChild = branch(props => { 215 | return { 216 | name: ['name'], 217 | surname: props.path 218 | }; 219 | }, Child); 220 | 221 | const wrapper = mount(); 222 | 223 | tree.set('surname', 'the Third'); 224 | 225 | setTimeout(() => { 226 | assert.strictEqual(wrapper.text(), 'Hello John the Third'); 227 | done(); 228 | }, 50); 229 | }); 230 | 231 | it('wrapper component should allow setting a ref on the wrapped component using the decoratedComponentRef prop.', function(done) { 232 | const tree = new Baobab({counter: 0}, {asynchronous: false}); 233 | 234 | class Counter extends Component { 235 | render() { 236 | return ( 237 | 238 | Counter: {this.props.counter} 239 | 240 | ); 241 | } 242 | } 243 | 244 | const Root = root(tree, BasicRoot); 245 | 246 | const BranchedCounter = branch({counter: 'counter'}, Counter); 247 | 248 | const wrapper = mount(); 249 | 250 | function checkIfNodeIsCounter(instance) { 251 | assert(instance instanceof Counter); 252 | done(); 253 | } 254 | }); 255 | }); 256 | 257 | describe('actions', function() { 258 | it('should be possible to dispatch actions.', function() { 259 | const tree = new Baobab({counter: 0}, {asynchronous: false}); 260 | 261 | const inc = function(state, by = 1) { 262 | state.apply('counter', nb => nb + by); 263 | }; 264 | 265 | class Counter extends Component { 266 | render() { 267 | const dispatch = this.props.dispatch; 268 | 269 | return ( 270 | dispatch(inc)} 271 | onChange={() => dispatch(inc, 2)}> 272 | Counter: {this.props.counter} 273 | 274 | ); 275 | } 276 | } 277 | 278 | const Root = root(tree, BasicRoot); 279 | 280 | const BranchedCounter = branch({counter: 'counter'}, Counter); 281 | 282 | const wrapper = mount(); 283 | 284 | assert.strictEqual(wrapper.text(), 'Counter: 0'); 285 | wrapper.find('span').simulate('click'); 286 | assert.strictEqual(wrapper.text(), 'Counter: 1'); 287 | wrapper.find('span').simulate('change'); 288 | assert.strictEqual(wrapper.text(), 'Counter: 3'); 289 | }); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /docs/mixins.md: -------------------------------------------------------------------------------- 1 | # Mixins 2 | 3 | In this example, we'll build a simplistic React app showing a list of colors to see how one could integrate **Baobab** with React by using mixins. 4 | 5 | ### Summary 6 | 7 | * [Creating the app's state](#creating-the-apps-state) 8 | * [Rooting our top-level component](#rooting-our-top-level-component) 9 | * [Branching our list](#branching-our-list) 10 | * [Actions](#actions) 11 | * [Dynamically set the list's path using props](#dynamically-set-the-lists-path-using-props) 12 | * [Accessing the tree](#accessing-the-tree) 13 | * [Clever vs. dumb components](#clever-vs-dumb-components) 14 | 15 | ### Creating the app's state 16 | 17 | Let's create a **Baobab** tree to store our colors' list: 18 | 19 | *state.js* 20 | 21 | ```js 22 | var Baobab = require('baobab'); 23 | 24 | module.exports = new Baobab({ 25 | colors: ['yellow', 'blue', 'orange'] 26 | }); 27 | ``` 28 | 29 | ### Rooting our top-level component 30 | 31 | Now that the tree is created, we should bind our React app to it by "rooting" our top-level component. 32 | 33 | Under the hood, this component will simply propagate the tree to its descendants using React's context so that "branched" component may subscribe to updates of parts of the tree afterwards. 34 | 35 | *main.jsx* 36 | 37 | ```jsx 38 | var React = require('react'), 39 | mixins = require('baobab-react/mixins'), 40 | tree = require('./state.js'), 41 | 42 | // We will write this component later 43 | List = require('./list.jsx'); 44 | 45 | // Creating our top-level component 46 | var App = React.createClass({ 47 | 48 | // Let's bind the component to the tree through the `root` mixin 49 | mixins: [mixins.root], 50 | 51 | render: function() { 52 | return ; 53 | } 54 | }); 55 | 56 | // Rendering the app and giving the tree to the `App` component through props 57 | React.render(, document.querySelector('#mount')); 58 | ``` 59 | 60 | ### Branching our list 61 | 62 | Now that we have "rooted" our top-level `App` component, let's create the component displaying our colors' list and branch it to the tree's data. 63 | 64 | *list.jsx* 65 | 66 | ```jsx 67 | var React = require('react'), 68 | mixins = require('baobab-react/mixins'); 69 | 70 | var List = React.createClass({ 71 | 72 | // Let's branch the component 73 | mixins: [mixins.branch], 74 | 75 | // Mapping the paths we want to get from the tree. 76 | // Associated data will be bound to the component's state 77 | cursors: { 78 | colors: ['colors'] 79 | }, 80 | 81 | render() { 82 | 83 | // Our colors are now available through the component's state 84 | var colors = this.state.colors; 85 | 86 | function renderItem(color) { 87 | return
  • {color}
  • ; 88 | } 89 | 90 | return
      {colors.map(renderItem)}
    ; 91 | } 92 | }); 93 | 94 | module.exports = List; 95 | ``` 96 | 97 | Our app would now render something of the kind: 98 | 99 | ```html 100 |
    101 |
      102 |
    • yellow
    • 103 |
    • blue
    • 104 |
    • orange
    • 105 |
    106 |
    107 | ``` 108 | 109 | But let's add a color to the list: 110 | 111 | ```js 112 | tree.push('colors', 'purple'); 113 | ``` 114 | 115 | And the list component will automatically update and to render the following: 116 | 117 | ```html 118 |
    119 |
      120 |
    • yellow
    • 121 |
    • blue
    • 122 |
    • orange
    • 123 |
    • purple
    • 124 |
    125 |
    126 | ``` 127 | 128 | Now you just need to add an action layer on top of that so that app's state can be updated and you've got yourself an atomic Flux! 129 | 130 | ### Actions 131 | 132 | Here is what we are trying to achieve: 133 | 134 | ``` 135 | ┌────────────────────┐ 136 | ┌──────────── │ Central State │ ◀───────────┐ 137 | │ │ (Baobab tree) │ │ 138 | │ └────────────────────┘ │ 139 | Renders Updates 140 | │ │ 141 | │ │ 142 | ▼ │ 143 | ┌────────────────────┐ ┌────────────────────┐ 144 | │ View │ │ Actions │ 145 | │ (React Components) │ ────────Triggers───────▶ │ (Functions) │ 146 | └────────────────────┘ └────────────────────┘ 147 | ``` 148 | 149 | For the time being we only have a central state stored by a Baobab tree and a view layer composed of React components. 150 | 151 | What remains to be added is a way for the user to trigger actions and update the central state. 152 | 153 | To do so `baobab-react` proposes to create simple functions as actions: 154 | 155 | *actions.js* 156 | 157 | ```js 158 | exports.addColor = function(tree, color) { 159 | tree.push('colors', color); 160 | }; 161 | ``` 162 | 163 | Now let's add a simple button so that a user may add colors: 164 | 165 | *list.jsx* 166 | 167 | ```jsx 168 | var React = require('react'), 169 | mixins = require('baobab-react/mixins'), 170 | actions = require('./actions.js'); 171 | 172 | var List = React.createClass({ 173 | mixins: [mixins.branch], 174 | 175 | cursors: { 176 | colors: ['colors'] 177 | }, 178 | 179 | getInitialState: function() { 180 | return {inputColor: null}; 181 | } 182 | 183 | // Controlling the input's value 184 | updateInput(e) { 185 | this.setState({inputColor: e.target.value}); 186 | }, 187 | 188 | // Adding a color on click 189 | handleClick() { 190 | 191 | // Let's dispatch our action 192 | this.dispatch(actions.addColor, this.state.inputColor); 193 | 194 | // Resetting the input 195 | this.setState({inputColor: null}); 196 | } 197 | 198 | render() { 199 | var colors = this.state.colors; 200 | 201 | function renderItem(color) { 202 | return
  • {color}
  • ; 203 | } 204 | 205 | return ( 206 |
    207 |
      {colors.map(renderItem)}
    208 | 211 | 212 |
    213 | ); 214 | } 215 | }); 216 | 217 | module.exports = List; 218 | ``` 219 | 220 | ### Dynamically set the list's path using props 221 | 222 | Sometimes, you might find yourself needing cursors paths changing along with your component's props. 223 | 224 | For instance, given the following state: 225 | 226 | *state.js* 227 | 228 | ```js 229 | var Baobab = require('baobab'); 230 | 231 | module.exports = new Baobab({ 232 | colors: ['yellow', 'blue', 'orange'], 233 | alternativeColors: ['purple', 'orange', 'black'] 234 | }); 235 | ``` 236 | 237 | You might want to have a list rendering either one of the colors' lists. 238 | 239 | Fortunately, you can do so by passing a function taking both props and context of the components and returning a valid mapping: 240 | 241 | *list.jsx* 242 | 243 | ```jsx 244 | var React = require('react'), 245 | mixins = require('baobab-react/mixins'); 246 | 247 | var List = React.createClass({ 248 | mixins: [mixins.branch], 249 | 250 | // Using a function so that your cursors' path can use the component's props etc. 251 | cursors: function(props, context) { 252 | return { 253 | colors: [props.alternative ? 'alternativeColors' : 'colors'] 254 | }; 255 | }, 256 | 257 | render() { 258 | var colors = this.state.colors; 259 | 260 | function renderItem(color) { 261 | return
  • {color}
  • ; 262 | } 263 | 264 | return
      {colors.map(renderItem)}
    ; 265 | } 266 | }); 267 | 268 | module.exports = List; 269 | ``` 270 | 271 | ### Accessing the tree and cursors 272 | 273 | For convenience, and if you want a quicker way to update your tree, you can always access this one through the context: 274 | 275 | ```js 276 | var React = require('react'), 277 | mixins = require('baobab-react/mixins'); 278 | 279 | var List = React.createClass({ 280 | mixins: [mixins.branch], 281 | cursors: { 282 | colors: ['colors'] 283 | }, 284 | render: function() { 285 | 286 | // Accessing the tree 287 | this.context.tree.get(); 288 | } 289 | }); 290 | ``` 291 | 292 | ### Clever vs. dumb components 293 | 294 | Now you know everything to use a Baobab tree efficiently with React. 295 | 296 | However, the example app shown above is minimalist and should probably not be organized thusly in a real-life scenario. 297 | 298 | Indeed, whenever possible, one should try to separate "clever" components, that know about the tree's existence from "dumb" components, completely oblivious of it. 299 | 300 | Knowing when to branch/wrap a component and let some components ignore the existence of the tree is the key to a maintainable and scalable application. 301 | 302 | **Example** 303 | 304 | *Clever component* 305 | 306 | This component does know that a tree provides him with data. 307 | 308 | ```js 309 | var React = require('react'), 310 | mixins = require('baobab-react/mixins'), 311 | List = require('./list.jsx'); 312 | 313 | var ListWrapper = React.createClass({ 314 | mixins: [mixins.branch], 315 | cursors: { 316 | colors: ['colors'] 317 | } 318 | render: function() { 319 | return ; 320 | } 321 | }); 322 | ``` 323 | 324 | *Dumb component* 325 | 326 | This component should stay unaware of the tree so it can remain generic and be used elsewhere easily. 327 | 328 | ```js 329 | var React = require('react'); 330 | 331 | var List = React.createClass({ 332 | render() { 333 | 334 | function renderItem(value) { 335 | return
  • {value}
  • ; 336 | } 337 | 338 | return
      {this.props.items.map(renderItem)}
    ; 339 | } 340 | }); 341 | ``` 342 | -------------------------------------------------------------------------------- /docs/higher-order.md: -------------------------------------------------------------------------------- 1 | # Higher order components 2 | 3 | In this example, we'll build a simplistic React app showing a list of colors to see how one could integrate **Baobab** with React by using higher-order components. 4 | 5 | ### Summary 6 | 7 | * [Creating the app's state](#creating-the-apps-state) 8 | * [Rooting our top-level component](#rooting-our-top-level-component) 9 | * [Branching our list](#branching-our-list) 10 | * [Actions](#actions) 11 | * [Dynamically set the list's path using props](#dynamically-set-the-lists-path-using-props) 12 | * [Accessing the tree and cursors](#accessing-the-tree-and-cursors) 13 | * [Clever vs. dumb components](#clever-vs-dumb-components) 14 | * [Currying & Decorators](#currying-decorators) 15 | * [Dealing with refs to your wrapped components](#dealing-with-refs) 16 | 17 | ### Creating the app's state 18 | 19 | Let's create a **Baobab** tree to store our colors' list: 20 | 21 | *state.js* 22 | 23 | ```js 24 | import Baobab from 'baobab'; 25 | 26 | const tree = new Baobab({ 27 | colors: ['yellow', 'blue', 'orange'] 28 | }); 29 | 30 | export default tree; 31 | ``` 32 | 33 | ### Rooting our top-level component 34 | 35 | Now that the tree is created, we should bind our React app to it by "rooting" our top-level component. 36 | 37 | Under the hood, this component will simply propagate the tree to its descendants using React's context so that "branched" component may subscribe to updates of parts of the tree afterwards. 38 | 39 | *main.jsx* 40 | 41 | ```jsx 42 | import React, {Component} from 'react'; 43 | import {render} from 'react-dom'; 44 | import {root} from 'baobab-react/higher-order'; 45 | import tree from './state'; 46 | 47 | // We will write this component later 48 | import List from './list.jsx'; 49 | 50 | // Creating our top-level component 51 | class App extends Component { 52 | render() { 53 | return ; 54 | } 55 | } 56 | 57 | // Let's bind the component to the tree through the `root` higher-order component 58 | const RootedApp = root(tree, App); 59 | 60 | // Rendering the app 61 | render(, document.querySelector('#mount')); 62 | ``` 63 | 64 | ### Branching our list 65 | 66 | Now that we have "rooted" our top-level `App` component, let's create the component displaying our colors' list and branch it to the tree's data. 67 | 68 | *list.jsx* 69 | 70 | ```jsx 71 | import React, {Component} from 'react'; 72 | import {branch} from 'baobab-react/higher-order'; 73 | 74 | class List extends Component { 75 | render() { 76 | 77 | // Thanks to the branch, our colors will be passed as props to the component 78 | const colors = this.props.colors; 79 | 80 | function renderItem(color) { 81 | return
  • {color}
  • ; 82 | } 83 | 84 | return
      {colors.map(renderItem)}
    ; 85 | } 86 | } 87 | 88 | // Branching the component by mapping the desired data to cursors 89 | export default branch({ 90 | colors: ['colors'] 91 | }, List); 92 | ``` 93 | 94 | Our app would now render something of the kind: 95 | 96 | ```html 97 |
    98 |
      99 |
    • yellow
    • 100 |
    • blue
    • 101 |
    • orange
    • 102 |
    103 |
    104 | ``` 105 | 106 | But let's add a color to the list: 107 | 108 | ```js 109 | tree.push('colors', 'purple'); 110 | ``` 111 | 112 | And the list component will automatically update and to render the following: 113 | 114 | ```html 115 |
    116 |
      117 |
    • yellow
    • 118 |
    • blue
    • 119 |
    • orange
    • 120 |
    • purple
    • 121 |
    122 |
    123 | ``` 124 | 125 | Now you just need to add an action layer on top of that so that app's state can be updated and you've got yourself an atomic Flux! 126 | 127 | ### Actions 128 | 129 | Here is what we are trying to achieve: 130 | 131 | ``` 132 | ┌────────────────────┐ 133 | ┌──────────── │ Central State │ ◀───────────┐ 134 | │ │ (Baobab tree) │ │ 135 | │ └────────────────────┘ │ 136 | Renders Updates 137 | │ │ 138 | │ │ 139 | ▼ │ 140 | ┌────────────────────┐ ┌────────────────────┐ 141 | │ View │ │ Actions │ 142 | │ (React Components) │ ────────Triggers───────▶ │ (Functions) │ 143 | └────────────────────┘ └────────────────────┘ 144 | ``` 145 | 146 | For the time being we only have a central state stored by a Baobab tree and a view layer composed of React components. 147 | 148 | What remains to be added is a way for the user to trigger actions and update the central state. 149 | 150 | To do so `baobab-react` proposes to create simple functions as actions: 151 | 152 | *actions.js* 153 | 154 | ```js 155 | export function addColor(tree, color) { 156 | tree.push('colors', color); 157 | } 158 | ``` 159 | 160 | Now let's add a simple button so that a user may add colors: 161 | 162 | *list.jsx* 163 | 164 | ```jsx 165 | import React, {Component} from 'react'; 166 | import {branch} from 'baobab-react/higher-order'; 167 | import * as actions from './actions'; 168 | 169 | class List extends Component { 170 | constructor(props, context) { 171 | super(props, context); 172 | 173 | // Initial state 174 | this.state = {inputColor: null}; 175 | } 176 | 177 | // Controlling the input's value 178 | updateInput(e) { 179 | this.setState({inputColor: e.target.value}) 180 | } 181 | 182 | // Adding a color on click 183 | handleClick() { 184 | 185 | // A dispatcher is available through `props.dispatch` 186 | this.props.dispatch( 187 | actions.addColor, 188 | this.state.inputColor 189 | ); 190 | 191 | // Resetting the input 192 | this.setState({inputColor: null}); 193 | } 194 | 195 | render() { 196 | const colors = this.props.colors; 197 | 198 | return ( 199 |
    200 |
      {colors.map(renderItem)}
    201 | this.updateInput(e)} /> 204 | 205 |
    206 | ); 207 | } 208 | } 209 | 210 | // Subscribing to the relevant data in the tree 211 | export default branch({ 212 | colors: ['colors'] 213 | }, List); 214 | ``` 215 | 216 | ### Dynamically set the list's path using props 217 | 218 | Sometimes, you might find yourself needing cursors paths changing along with your component's props. 219 | 220 | For instance, given the following state: 221 | 222 | *state.js* 223 | 224 | ```js 225 | import Baobab from 'baobab'; 226 | 227 | const tree = new Baobab({ 228 | colors: ['yellow', 'blue', 'orange'], 229 | alternativeColors: ['purple', 'orange', 'black'] 230 | }); 231 | 232 | export default tree; 233 | ``` 234 | 235 | You might want to have a list rendering either one of the colors' lists. 236 | 237 | Fortunately, you can do so by passing a function taking both props and context of the components and returning a valid mapping: 238 | 239 | *list.jsx* 240 | 241 | ```jsx 242 | import React, {Component} from 'react'; 243 | import {branch} from 'baobab-react/higher-order'; 244 | 245 | class List extends Component { 246 | render() { 247 | const colors = this.props.colors; 248 | 249 | function renderItem(color) { 250 | return
  • {color}
  • ; 251 | } 252 | 253 | return
      {colors.map(renderItem)}
    ; 254 | } 255 | } 256 | 257 | // Using a function so that your cursors' path can use the component's props etc. 258 | export default branch((props, context) => { 259 | return { 260 | colors: [props.alternative ? 'alternativeColors' : 'colors'] 261 | }; 262 | }, List); 263 | ``` 264 | 265 | ### Accessing the tree and cursors 266 | 267 | For convenience, and if you want a quicker way to update your tree, you can always access this one through the context: 268 | 269 | ```js 270 | import React, {Component} from 'react'; 271 | import PropTypes from 'baobab-react/prop-types'; 272 | import {branch} from 'baobab-react/higher-order'; 273 | 274 | class List extends Component { 275 | render() { 276 | 277 | // Accessing the tree 278 | this.context.tree.get(); 279 | } 280 | } 281 | 282 | // To access the tree and cursors through context, 283 | // React obliges you to define `contextTypes` 284 | List.contextTypes = { 285 | tree: PropTypes.baobab 286 | }; 287 | 288 | export default branch({ 289 | colors: ['colors'] 290 | }, List); 291 | ``` 292 | 293 | ### Clever vs. dumb components 294 | 295 | Now you know everything to use a Baobab tree efficiently with React. 296 | 297 | However, the example app shown above is minimalist and should probably not be organized thusly in a real-life scenario. 298 | 299 | Indeed, whenever possible, one should try to separate "clever" components, that know about the tree's existence from "dumb" components, completely oblivious of it. 300 | 301 | Knowing when to branch/wrap a component and let some components ignore the existence of the tree is the key to a maintainable and scalable application. 302 | 303 | **Example** 304 | 305 | *Clever component* 306 | 307 | This component does know that a tree provides him with data. 308 | 309 | ```js 310 | import React, {Component} from 'react'; 311 | import {branch} from 'baobab-react/higher-order'; 312 | import List from './list.jsx'; 313 | 314 | class ListWrapper extends Component { 315 | render() { 316 | return ; 317 | } 318 | } 319 | 320 | export default branch({ 321 | colors: ['colors'] 322 | }, ListWrapper); 323 | ``` 324 | 325 | *Dumb component* 326 | 327 | This component should stay unaware of the tree so it can remain generic and be used elsewhere easily. 328 | 329 | ```js 330 | import React, {Component} from 'react'; 331 | 332 | export default class List extends Component { 333 | render() { 334 | 335 | function renderItem(value) { 336 | return
  • {value}
  • ; 337 | } 338 | 339 | return
      {this.props.items.map(renderItem)}
    ; 340 | } 341 | } 342 | ``` 343 | 344 |

    Currying & Decorators

    345 | 346 | For convenience, both `root` and `branch` are actually curried function. 347 | 348 | ```js 349 | const branchToName = branch({name: ['name']}); 350 | 351 | const BranchComponent = branchToName(Component); 352 | ``` 353 | 354 | This also means you can use them as ES7 decorators: 355 | 356 | ```js 357 | @branch({ 358 | name: ['name'] 359 | }) 360 | class Greeting extends Component { 361 | render() { 362 | return Hello {this.props.name}!; 363 | } 364 | } 365 | ``` 366 | 367 |

    Dealing with refs to your wrapped components

    368 | 369 | When wrapping a component with a higher-order component, a new component is created around the component you pass to the HOC. 370 | 371 | Due to this, when setting a `ref` prop on the decorated component, the reference will point to the wrapping component's instance, instead of pointing to the wrapped component as you probably intend to do. 372 | 373 | To solve this problem, the decorated component takes a `decoratedComponentRef` prop which is then forwarded to the wrapped component as a usual `ref` prop. 374 | 375 | In other words, if you want to obtain a ref to your wrapped component, you can do so like this: 376 | 377 | In your jsx: 378 | 379 | ```js 380 | 381 | class Greeting extends Component { 382 | render() { 383 | return Hello {this.props.name}!; 384 | } 385 | } 386 | 387 | const BranchGreeting = branch({name: ['name']}, Greeting); 388 | 389 | class App extends Component { 390 | setRef (instance) { 391 | // instance will now point to the rendered instance of Greeting 392 | } 393 | 394 | render() { 395 | return ; 396 | } 397 | } 398 | ``` 399 | --------------------------------------------------------------------------------