;
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
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 |
--------------------------------------------------------------------------------