Baobab

Human Talks @ Nantes, mardi 12 janvier 2016

Alexis Jacomy

(twitter|github|slides).com/jacomyal

Flux, en bref

Le contexte

"Applications web monopage"

Des données dynamiques

Pleins de vues

Le MVC

Modèle

Vue

Contrôleur

Action

Modèle

Modèle

Vue

Vue

d'après Facebook

Leur solution : Flux

Store

Vue

Dispatcher

Action

Action

Action

Les bénéfices

Pas un framework mais

une méthode

Pousse les devs web à s'ouvrir

à d'autres paradigmes

Plusieurs outils concurrents

disponibles

Un pas plus loin avec Baobab

développé par

Guillaume Plique

publié en MIT

pour ne plus gérer de dépendances entre les stores

Un arbre​

pour stocker l'état

import Baobab from 'baobab';

const state = new Baobab({
  some: {
    nested: {
      objects: [
        1, 2, 3
      ]
    }
  }
});

state.get();
// { some: { nested: { objects: [ 1, 2, 3 ] } } }

state.get('some', 'nested');
// { objects: [ 1, 2, 3 ] }

state.set(['some', 'more', 'data'], 42);
// Renvoie la valeur : 42

state.get();
// { some: { nested: { objects: [ 1, 2, 3 ] }, more: { data: 42 } } }

Un arbre​

pour stocker l'état

pour éviter les effets de bords

Un état immuable

import Baobab from 'baobab';

const state = new Baobab({
  some: {
    data: [
      1, 2, 3
    ]
  }
});

const value = state.get();
// { some: { nested: { objects: [ 1, 2, 3 ] } } }

Object.isFrozen(value);
// true

value.toto = 42;
// - Sans "use strict", renvoie la valeur 42
// - Avec "use strict", lance une erreur :
//   "TypeError: Can't add property toto, object is not extensible"

value.toto;
// undefined

Un état immuable

avec délégation

des événements

grâce aux curseurs

Un état observable

import Baobab from 'baobab';

const state = new Baobab({
  some: {
    data: [
      1, 2, 3
    ]
  }
});

// Création d'un curseur :
const data = state.select('some', 'data');

// Écoute des updates :
data.on(
  'update',
  () => console.log(data.get())
);

// Mise à jour des données :
data.push(4);
// Log : [ 1, 2, 3, 4 ]

Un état observable

Gérer les vues

avec React

via une mixin ou

un composant à hériter

des helpers pour aider à écouter

des curseurs depuis des composants

import React from 'react';
import { branch } from 'baobab-react/mixins';

export default Checkbox = React.createComponent({
  mixins: [ branch ],
  cursors: {
    flag: [ 'some', 'path' ]
  },

  handleToggle(e) {
    this.cursors.flag.set(e.target.value);
  },

  render() {
    return (
      <input type="checkbox"
             value={ this.state.flag }
             onChange={ this.handleChange } />
    );
  }
});

Checkbox.jsx

import React from 'react';
import { root } from 'baobab-react/mixins';

import Checkbox from './Checkbox.jsx';

export default App = React.createComponent({
  mixins: [ root ],

  render() {
    return (
      <div>
        <Checkbox />
      </div>
    );
  }
});

App.jsx

import React from 'react';
import ReactDOM from 'react-dom';
import Baobab from 'baobab';

import App from './App.jsx';

const state = new Baobab({
  some: {
    path: {
      flag: false
    }
  }
});

ReactDOM.render(
  <App tree={ state } />,
  document.getElementById('root'),
);

index.js

Exemple :

La TodoList

import Baobab from 'baobab';

export default new Baobab({
  _id: 0,
  tasks: [],
  message: ''
});

state.js

import state from './state.js';

export default {
  setMessage(message) {
    state.set('message', message);
  },

  addTask(message) {
    const id = '' + state.set('_id', state.get('_id') + 1);
    state.select('tasks')
         .push({ id, message });
    state.set('message', '');
  },

  removeTask(id) {
    state.select('tasks')
         .unset({ id });
  }
};

actions.js

import React from 'react';
import { branch } from 'baobab-react/mixins';
import actions from './actions.js';

export default React.createClass({
  mixins: [ branch ],
  cursors: { tasks: [ 'tasks' ] },

  handleClick(e) {
    actions.removeTask(e.target.getAttribute('data-id'));
  },

  render() {
    return (
      <ul>{
        (this.state.tasks || []).map(task => (
          <li key={ task.id }
              data-id={ task.id }
              onClick={ this.handleClick }>{
            task.message
          }</li>
        ))
      }</ul>
    );
  }
});

List.jsx

import React from 'react';
import { branch } from 'baobab-react/mixins';
import actions from './actions.js';

export default React.createClass({
  mixins: [ branch ],
  cursors: { message: [ 'message' ] },

  handleSubmit(e) {
    e.preventDefault();
    actions.addTask(e.target.message.value);
  },
  handleChange(e) {
    actions.setMessage(e.target.value);
  },

  render() {
    return (
      <form onSubmit={ this.handleSubmit }>
        <input type="text"
               name="message"
               onChange={ this.handleChange }
               value={ this.state.message } />
        <button type="submit">Submit</button>
      </form>
    );
  }
});

Form.jsx

import React from 'react';
import { root } from 'baobab-react/mixins';

import Form from './Form.jsx';
import List from './List.jsx';

export default React.createClass({
  mixins: [ root ],

  render() {
    return (
      <div>
        <Form />
        <List />
      </div>
    );
  }
});

TodoList.jsx

import React from 'react';
import ReactDOM from 'react-dom';

import state from './state.js';
import TodoList from './TodoList.jsx';

ReactDOM.render(
  <TodoList tree={ state } />,
  document.getElementById('todolist')
);

export default {
  state
};

index.js

Pour aller plus loin

Les Monkeys

Cacher dynamiquement des vues sur les données

Des petits plus

Validation de l'arbre

Historique des modifications

Hot Module Replacement

Merci beaucoup !

slides disponibles à l'adresse :

slides.com/jacomyal/baobab

Crédits :