Introducción a Mobx. Un enfoque diferente para aplicaciones React

Hace algunas semanas descubrí por casualidad una librería que me llamó estrepitosamente la atención. Se trata de MobX y de su implementación para aplicaciones React, MobX-react.

MobX básicamente hace que la gestión del estado de nuestras aplicaciones sea mucho más simple y escalable implementando observables, un concepto muy elegante del mundo de la programación funcional-reactiva (FRP).

 

mobx-y-react-un-enfoque-diferente

 

Traduciendo las propias palabras de su página en Github, – la filosofía detrás de MobX es muy simple: Todo lo que se pueda derivar del estado de la aplicación, debe ser derivado. Automáticamente.

Partiendo de este enfoque y del enfoque que aporta React, la combinación de estas dos tecnologías es sin duda poderosa. React nos aporta esa manera tan particular y efectiva de crear las interfaces de usuario por medio de componentes y MobX los mecanismos necesarios para sincronizar de manera optima el estado de nuestra aplicación.

En este artículo intentaré explicar de que va MobX, como funciona y por supuesto veremos un ejemplo de uso. Dicho esto, al turrón!

 

Introducción a MobX

Como ya he comentado MobX se basa en un concepto bastante conocido en el mundo de la programación reactiva, los observables. Esto no es cosa nueva en JavaScript, algunas librerías como BaconJS o KnokoutJS (corregidme si me equivoco) ya implementan este concepto, que en realidad es bastante simple. Un observador se ‘suscribe’ a un observable, y dicho observador reaccionará cada vez que el observable modifique o actualize su valor.

Llevándonos esto al mundo React, podemos simplificar mucho el manejo del estado de nuestras aplicaciones ya que tan solo necesitamos que nuestros componentes observen ciertas propiedades ‘estatales’. Estas propiedades al actualizarse o modificar su valor, harán que nuestros componentes se re-renderizen con el nuevo valor. En otras palabras, nosotros tan solo nos tenemos que preocupar de definir el estado de nuestra aplicación y hacerlo observable. Así de simple.

Otra de las ventajas de MobX es que no opina sobre cómo se deben manejar los eventos del usuario para modificar las propiedades observables, eso lo deja totalmente a nuestro criterio. Puedes hacerlo al lo ‘Flux style‘ o puedes hacerlo de la manera que más te apetezca o convenga en cada caso.

MobX define los siguientes cuatro conceptos básicos que nos ayudarán a entender mejor como funciona esta librería:

 

Estado observable (Observable state)

Cualquier valor que pueda mutar y sirva como recurso para ‘pintar’ nuestra interface es un valor estatal, y MobX puede hacer que casi todos los tipos de valores como objetos, arrays, clases, primitivas, etc, puedan ser observados.

Para conseguir que nuestro estado sea observable tan solo tenemos que utilizar el decorador @observable para definir las propiedades de clase que queramos observar.

class ShoppingCartStore {
  @observable title = 'My Shopping cart'
  @observable entries = []
  @observable shipping = false
}

Valores calculados (Computed values)

Con esto se refiere a cualquier valor que pueda ser derivado de otros valores observables. Estos valores calculados son observables a si mismos, y al mutar cualquiera de los valores implicados se recomputará automáticamente.

Para utilizar esta capacidad usaremos el decorador @computed.

class ShoppingCartStore {
  @observable entries = []
  // Computed shopping cart price
  @computed get totalPrice () {
    return this.entries.reduce((sum, entry) => sum * (entry.price * entry.units), 0)
  }
}

Reacciones (Reactions)

Tal y como explican en la docu., las reacciones son el puente entre la programación reactiva y la imperativa. Son algo similar a los valores calculados, pero en lugar de producir un nuevo valor producirá un efecto, es decir, el re-renderizado de un componente, una llamada a la API o un simple log en la consola.

Para conseguir que nuestros componentes React sean reactivos debemos utilizar el decorador @observer de mobx-react.

@observer
class ShoppingCartView extends React.Component {
  render () {
    return (
      <div>
        <ul>
          {this.props.entries.map((entry) =>
            <ShoppingCartEntryView product={ entry } key={ entry } />
          )}
        </ul>
        <h3>Total price: <span>{ this.props.store.totalPrice }</span></h3>
      </div>
    )
  }
}

Acciones (Actions)

Las acciones, normalmente, son el medio por el cual se modifica el estado. Como ya he comentado antes, MobX deja a nuestro antojo esta gestión, por lo que una acción puede ser un evento desencadenado por una interacción del usuario o cualquier cosa que modifique el estado de la aplicación. Siguiendo con el ejemplo anterior, añadir un producto al carro puede ser tan sencillo como se muestra en el siguiente código:

class ProductView extends React.Component {
  render () {
    return (
      <li>
        <img src={ this.props.product.src } />
        <Button onClick={ this.addProductToCart } title="Add to cart"  />
      </li>
    )
  }

  // Add product to shopping cart
  addProductToCart = (event) => {
    ShoppingCart.entries.push(this.props.product)
  }
}

Ok, con esto ya tenemos bastante teoría, ahora vamos a picar algo de código que es lo que mola.

 

Construyendo una tienda con React y MobX

Creo que construir una pequeña tienda con su carrito de la compra puede ser un ejemplo bastante ilustrativo. Para comenzar vamos a plantear como queremos que funcione, aunque básicamente funcionará como la mayoría de tiendas de internet.

Tendremos un componente ShopView en el que mostraremos todos los productos de la tienda. Dichos productos se ‘pintarán’ en un componente ProductView el cual además de mostrar la información relativa al producto tendrá un botón para añadirlo al carrito. Por otro lado tendremos la vista del carrito, ShoppingCartView, en la cual se irán mostrando los productos que vayamos añadiendo y el precio total del carrito. Además de todo esto, en el menú principal tendremos un icono de un carrito con un badge que mostrará la cantidad total de productos que tenemos en el carro.

Para no escribir demasiado código, que además no viene al caso del artículo, mostraremos todas las vistas en una sola página como se ilustra en la imagen de abajo. Dejaré para otro post como navegar entre vistas con react-router.

 

 

Modelando los datos

Teniendo claro como se va a estructurar nuestra tienda es el momento de plantear el modelo de datos. Básicamente tendremos una tienda con productos, de los cuales, las propiedades que nos interesan son el precio y las unidades de cada uno de los productos que existen en el carro, por lo que crearemos un store que almacenará y manejará estos datos.

Por otro lado tenemos el carrito con sus respectivas entradas. Dichas entradas harán referencia a un producto, por lo que su modelo será el mismo que para los productos de la tienda. Además tendremos un store que servirá de modelo para controlar los datos necesarios para pintar el carrito como por ejemplo un Array con todos los productos, el cómputo del precio total del precio del carrito y la cantidad de productos añadidos. Este modelo también nos servirá para nutrir al badge de la toolbar que muestra el número total de productos que tenemos en el carro.

 

mobx-react-data-model

 

Definido el modelo ahora sí podemos empezar a escribir código. En primer lugar necesitamos crear los Stores que nutrirán los diferentes componentes de nuestra app. Comenzaremos por ShoppingCartStore.

class ShoppingCartStore {
  // Almacenamos las entradas del carrito de la compra
  @observable entries = []
  
  // Calculamos las unidades totales añadidas al carro
  @computed get units () {
    return this.entries.reduce((sum, entry) => {
      return sum + entry.units
    }, 0)
  }
	
  // Calculamos el precio total del carro
  @computed get totalPrice () {
    return this.entries.reduce((sum, entry) => {
      const price = sum + (entry.product.price * entry.units)
      const parsed = Math.round(price * 100) / 100
      return parsed
    }, 0)
  }
}

// Singleton
const shoppingCartStore = new ShoppingCartStore()

Como podéis observar en el código anterior tenemos una propiedad observable entries que servirá para almacenar los productos que vayamos añadiendo al carrito. A su vez tenemos un par de métodos que calculan un valor, este valor depende de la propiedad entries, por lo que necesitamos que dichos métodos sean calculados con @computed.

Ya por último, haremos un sigleton de nuestra clase para asegurarnos de que solo existe una instancia de la misma en toda nuestra aplicación.

Perfecto, ahora necesitamos el store para cada uno de los productos, ProductStore. Aunque tendremos dos vistas o componentes diferentes para ‘pintar’ los productos (uno para la tienda y otro para el carrito), el modelo será el mismo ya que los datos para cada una de las vistas son los mismos, tan solo cambiará la representación gráfica.

class ProductStore {
  // Unidades del producto añadidas al carro
  @observable units = this.units
  
  constructor (product) {
    this.product = product
    this.units = 1
  }
  
  // Precio total 
  @computed get total () {
    return this.product ? this.product.price * this.units : 0
  }
}

Como podéis ver en este caso tendremos una propiedad units que contará las unidades del producto que se van añadiendo al carro. Además tenemos una variable product en la que almacenaremos los datos relativos al propio producto como el nombre, el precio y la imagen, de esta manera tendremos estos datos disponibles en cualquiera de las vistas o componentes que consuman los datos de este store.

 

Consumiendo los datos, pintando la tienda

Bien, ya tenemos todos los stores que necesitamos para nuestra tienda, ahora vamos a crear los componentes. Asumo que ya todos conocemos más que de sobra como se crean componentes en React así que no me extenderé mucho en este punto.

// Catálogo
const catalog = [
	{ name: 'Cat 1', price: 1.55, img: 'https://s-media-cache-ak0.pinimg.com/favicons/8532a851520bce5e78fb294d0e07eac5c877655e81a9c76941f26f55.png?9e884f0299eee37aedab60fe1ed363b5' },
	{ name: 'Cat 2', price: 9.99, img: 'https://pbs.twimg.com/profile_images/551143684671291392/Nx_lx21L_400x400.jpeg' },
	{ name: 'Cat 3', price: 3.75, img: 'https://s-media-cache-ak0.pinimg.com/favicons/fa236078d19b23f4d78ec09fcf9f28c53761a7b6d24112b56cde9aa9.png?99028731b58ad617a5d407999149a159' },
	{ name: 'Cat 4', price: 7.15, img: 'http://i.kinja-img.com/gawker-media/image/upload/s--gRG2YWja--/efg4piwisx1tcco4byit.png' }
]

class App extends Component {
  constructor (props) {
    super(props);
  }

  render () {
    return(
      <div>
        <Toolbar />
        <ShopView products={ catalog } />
        <ShoppingCartView />
      </div>
    );
  }
}

ReactDOM.render(<App />, document.querySelector('.app'));

Lo primero que he hecho es simular un JSON del que consumir los datos de los productos que se mostrarán en la tienda y al que llamaremos (por el bien de la originalidad) catalog. Este JSON se lo pasaremos como una propiedad al componente ShopView que será el componente encargado de ‘pintar’ los productos.

class ShopView extends Component {
  constructor (props) {
    super(props)
  }

  render () {
    return(
      <div className="shop">
        <ul>
         { this.props.products.map((product, i) => <ProductView product={ product } key={ i } />) }
        </ul>
      </div>
    )
  }
}

Ahora en nuestro componente ShopView tenemos disponible el catálogo, así que podemos iterar por los diferentes items del Array para ir creando cada una de las vistas para los productos, ProductView. A estas vistas le pasaremos como propiedad el producto correspondiente.

class ProductView extends Component {
  constructor (props) {
    super(props)
    this.store = new ProductStore(this.props.product)
  }
  render () {
    return (
      <li className="shop-product">
      	<img src={ this.props.product.img } />
        <h2>{ this.props.product.name }</h2>
        <h4>{ this.props.product.price } €</h4>
        <button onClick={ this.addProductToCart }>Add to cart</button>
      </li>
    )
  }
}

Llegados a este punto ya pintamos el listado de productos, ahora necesitamos poder añadir productos al carrito, para ello disponemos de un botón en nuestro componente ProductView que llama a un método que, en primer lugar, ha de comprobar si el producto ya existe en el carro o no. Si el producto no existe lo añadiremos a la propiedad entries de ShoppingCartStore, que recordemos es observable, e incrementaremos su cantidad, de lo contrario tan solo incrementaremos la cantidad. veamos como queda ahora nuestro componente.

class ProductView extends Component {
  ...

  // Añadimos el producto al carrito de la compra
  addProductToCart = (event) => {
    const existingEntry = this.checkIfProductIsInCart()
    if (existingEntry) {
      existingEntry.units += 1
    } else {
      this.store.units += 1
      shoppingCartStore.entries.unshift(new ProductStore(this.props.product))
    }
  }

  // Comprobamos si el producto existe en nuestro carrito
  checkIfProductIsInCart () {
    return shoppingCartStore.entries.find((entry) => {
      if (entry.product === this.props.product)
        return entry
    }, this)
  }
}

Es importante tener en cuenta que no añadiremos al carro el producto en si, si no el modelo o store del mismo que es quien contiene las propiedades observables que pueden mutar y afectarán a nuestro interface.

Ahora le toca el turno a ShoppingCartView. Este componente observará las propiedades de ShoppingCartStore para actualizarse cada vez que estas se actualicen o modifiquen. Para ello utilizaremos el decorador @observer como ya hemos visto.

@observer
class ShoppingCartView extends Component {  
  constructor (props) {
    super(props);
    this.store = shoppingCartStore
  }

  render () {
    return(
      <div className="shopping-cart">
        <ul>
         { this.store.entries.map((product, i) => <EntryView entry={ product } key={ i } />) }
        </ul>
        <span>Total price: { this.store.totalPrice } €</span>
      </div>
    )
  }
}

Ahora le toca el turno a el componente que servirá para ‘pintar’ cada uno de los productos que se añade al carrito. Lo llamaremos CartEntryView.

class EntryView extends Component {
  render () {
    return (
      <li className="shop-product">
        <h2>{ this.props.entry.product.name }</h2>
        <h4>{ this.props.entry.product.price }</h4>
        <h4>{ this.props.entry.units } uds. - { this.props.entry.total } €</h4>
        <button onClick={ this.removeProduct }>Remove</button>
      </li>
    );
  }
  
  removeProduct = (event) => {
    this.props.entry.units = 0
    shoppingCartStore.entries.splice(shoppingCartStore.entries.indexOf(this.props.entry.product), 1)
  }
}

Como podéis observar es muy similar a nuestro componente ProductView a diferencia de el método que lo elimina del carrito. Así que poco que contar sobre el.

Ya para terminar le toca el turno a la Toolbar. En ella aparece un badge que muestra el número total de artículos que hay en el carro. Como imaginaréis nos va a resultar bastante fácil conseguir esto dado que la información que necesitamos ya la tenemos disponible en ShoppingCartStore. Sólo tenemos que hacer que nuestro componente observe las propiedades del store que necesitamos para su correcto renderizado.

@observer
class Toolbar extends Component {
  constructor (props) {
    super(props)
    this.store = shoppingCartStore
  }

  render () {
    return(
      <div className="toolbar"> 
      	{ this.store.units }
      </div>
    )
  }
}

Et voilà! Con esto ya tenemos nuestra grandiosa tienda funcionando.

He dejado el ejemplo aquí, os recomiendo que le deis unas vueltas e intentéis añadir alguna funcionalidad como por ejemplo la posibilidad de sumar y restar el numero de unidades para un producto desde el carro o la posibilidad de eliminar un producto del carro desde ShopView, veréis que sencillo resulta.

Espero que os haya resultado interesante.

Hasta la próxima!

 

 

Referencias

Documentación

Repositório en GitHub

Mobx Interview

Becoming fully reactive: an in-depth explanation of MobX

 

  • Pingback: Introducción a Mobx. Un enfoque diferente para aplicaciones React()

  • Alexon da Silva Moreira

    Olá, muito legal this tutorial, eu inicialmente he hecho una cosa simple con mobx !!

    1 – Tenho um Componente de Chamando: MenuFixo como un estado de la siguiente forma:
    This.state = {boolMenuleftOn: false};

    2 – Tenho um outro Componente chamando: MenuLeft sin ninguna relación de parentesco con MenuFixo

    3 – Necesito que: a medida que el valor de la variable: boolMenuleftOn, el cambio de estado en MenuFixo y también el problema de algunas modificaciones en MenuLeft

    4 – ¿Cómo hacer esto con mobx?

  • Te ENe Te

    Muy bueno, aunque creo que te saltaste el metodo “actions” para actualizar los estados en vez de actualizar los estados de manera directa; https://mobx.js.org/refguide/action.html

    • Cuando escribí este post Mobx no disponía de ‘actions’. Es posible que en breve escriba nuevamente sobre el tema.

      Saludo!