UNPKG

baobab-react

Version:
291 lines (211 loc) 7.87 kB
# Hooks 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. ### Summary * [Hooks](#hooks) * [Summary](#summary) * [Creating the app's state](#creating-the-apps-state) * [Rooting our top-level component](#rooting-our-top-level-component) * [Branching our list](#branching-our-list) * [Actions](#actions) * [Dynamically set the list's path using props](#dynamically-set-the-lists-path-using-props) * [Clever vs. dumb components](#clever-vs-dumb-components) ### Creating the app's state Let's create a **Baobab** tree to store our colors' list: *state.js* ```js import Baobab from 'baobab'; const tree = new Baobab({ colors: ['yellow', 'blue', 'orange'] }); export default tree; ``` ### Rooting our top-level component Now that the tree is created, we should bind our React app to it by "rooting" our top-level component. 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. *main.jsx* ```jsx import React, {Component} from 'react'; import {render} from 'react-dom'; import {useRoot} from 'baobab-react/hooks'; import tree from './state'; // We will write this component later import List from './list.jsx'; // Creating our top-level component const App = function({store}) { // useRoot takes the baobab tree and provides a component bound to the tree const Root = useRoot(store); return ( <Root> <List /> <Root> ); } // Rendering the app render(<App store={tree} />, document.querySelector('#mount')); ``` ### Branching our list 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. *list.jsx* ```jsx import React, {Component} from 'react'; import {useBranch} from 'baobab-react/hooks'; const List = function() { // branch by mapping the desired data to cursors const {colors} = useBranch({ colors: ['colors'] }); function renderItem(color) { return <li key={color}>{color}</li>; } return <ul>{colors.map(renderItem)}</ul>; } export default List; ``` Our app would now render something of the kind: ```html <div> <ul> <li>yellow</li> <li>blue</li> <li>orange</li> </ul> </div> ``` But let's add a color to the list: ```js tree.push('colors', 'purple'); ``` And the list component will automatically update and to render the following: ```html <div> <ul> <li>yellow</li> <li>blue</li> <li>orange</li> <li>purple</li> </ul> </div> ``` 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! ### Actions Here is what we are trying to achieve: ``` ┌────────────────────┐ ┌──────────── │ Central State │ ◀───────────┐ │ │ (Baobab tree) │ │ │ └────────────────────┘ │ Renders Updates │ │ │ │ ▼ │ ┌────────────────────┐ ┌────────────────────┐ │ View │ │ Actions │ │ (React Components) │ ────────Triggers───────▶ │ (Functions) │ └────────────────────┘ └────────────────────┘ ``` For the time being we only have a central state stored by a Baobab tree and a view layer composed of React components. What remains to be added is a way for the user to trigger actions and update the central state. To do so `baobab-react` proposes to create simple functions as actions: *actions.js* ```js export function addColor(tree, color) { tree.push('colors', color); } ``` Now let's add a simple button so that a user may add colors: *list.jsx* ```jsx import React, {useState} from 'react'; import {useBranch} from 'baobab-react/hooks'; import * as actions from './actions'; const List = function() { const [inputColor, setColor] = useState(null); // Subscribing to the relevant data in the tree const {colors, dispatch} = useBranch({ colors: ['colors'] }); // Adding a color on click const handleClick = () => { // A dispatcher is available through `props.dispatch` dispatch( actions.addColor, inputColor ); // Resetting the input setColor(null); }; return ( <div> <ul>{colors.map(renderItem)}</ul> <input type="text" value={this.state.inputColor} onUpdate={e => setColor(e.target.value)} /> <button type="button" onClick={() => this.handleClick}>Add</button> </div> ); }; export default List; ``` ### Dynamically set the list's path using props Sometimes, you might find yourself needing cursors paths changing along with your component's props. For instance, given the following state: *state.js* ```js import Baobab from 'baobab'; const tree = new Baobab({ colors: ['yellow', 'blue', 'orange'], alternativeColors: ['purple', 'orange', 'black'] }); export default tree; ``` You might want to have a list rendering either one of the colors' lists. Fortunately, you can do so by passing a function taking both props and context of the components and returning a valid mapping: *list.jsx* ```jsx import React, {Component} from 'react'; import {useBranch} from 'baobab-react/hooks'; const List = function(props) { // Using a function so that your cursors' path can use the component's props etc. const {colors} = useBranch({ colors: [props.alternative ? 'alternativeColors' : 'colors'] }); function renderItem(color) { return <li key={color}>{color}</li>; } return <ul>{colors.map(renderItem)}</ul>; } export default List; ``` ### Clever vs. dumb components Now you know everything to use a Baobab tree efficiently with React. However, the example app shown above is minimalist and should probably not be organized thusly in a real-life scenario. Indeed, whenever possible, one should try to separate "clever" components, that know about the tree's existence from "dumb" components, completely oblivious of it. 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. **Example** *Clever component* This component does know that a tree provides him with data. ```js import React, {Component} from 'react'; import {useBranch} from 'baobab-react/hooks'; import List from './list.jsx'; class ListWrapper extends Component { const {colors} = useBranch({ colors: ['colors'] }); return <List items={this.props.colors} />; } export default ListWrapper; ``` *Dumb component* This component should stay unaware of the tree so it can remain generic and be used elsewhere easily. ```js import React, {Component} from 'react'; export default class List extends Component { render() { function renderItem(value) { return <li key={value}>{value}</li>; } return <ul>{this.props.items.map(renderItem)}</ul>; } } ```