{{announcement.body}}
{{announcement.title}}

State Management in Corvid

DZone 's Guide to

State Management in Corvid

In this article, we discuss how we can use Redux and MobX with Corvid to better manage the state of components within the frontend of an application.

· Web Dev Zone ·
Free Resource

Corvid loves Redux & MobX

When using Wix, when working with Corvid you don’t need to deal with HTML/CSS when developing UI. Instead, you get a full-blown WYSIWYG editor, where you can create the UI for your application. Then, all that’s left to do is write the application logic, which is really what we want to focus on when developing applications.

Most examples you’ll see today that connect application logic to the view are similar to old-style jQuery applications. You bind an event handler to UI elements, and in response to those events, you run some logic that updates other UI elements with the result.

You may also like:  Build, Manage, Deploy, and Scale Your Next Web Project With Corvid.

jQuery Style

Let’s look at an example. Say you have a view with a text element ( #counter), which displays a counter and two button elements ( #increment and  #decrement) that increment or decrement that counter.

The Corvid code which implements that logic will be something like:

let counter = 0;
$w.onReady(function() {
  $w('#counter').text = `${counter}`;
  $w('#increment').onClick(function() {
    counter++;
    $w('#counter').text = `${counter}`;
  });
  $w('#decrement').onClick(function() {
    counter--;
    $w('#counter').text = `${counter}`;
  });
});


This is a coding style that professional frontend developers cringe, since it reminds them of the old jQuery days when we explicitly updated the UI with the effect of each interaction.

In order to understand why this is a bad pattern, let’s try to complicate the example a bit. Let’s say we also have another text element (#counter2) with its own increment/decrement buttons. This time, we will also use the opportunity to do a small refactor. The logic implementation for this will be something like:

let counter = 0, counter2 = 0;
function renderCounter() {
  $w('#counter').text = `${counter}`;
}
function renderCounter2() {
  $w('#counter2').text = `${counter2}`;
}
$w.onReady(function() {
  renderCounter();
  renderCounter2();
  $w('#increment').onClick(function() {
    counter++;
    renderCounter();
  });
  $w('#decrement').onClick(function() {
    counter--;
    renderCounter();
  });
  $w('#increment2').onClick(function() {
    counter2++;
    renderCounter2();
  });
  $w('#decrement2').onClick(function() {
    counter2--;
    renderCounter2();
  });
});


As you can see, in this pattern, after we update some state ( countercounter2), and then we go and update the relevant UI component(s) that should be affected by that state change. So, if for example, we add an additional text item calculating the sum of the two counters, we update it in all places:


let counter = 0, counter2 = 0;
function renderCounter() {
  $w('#counter').text = `${counter}`;
}
function renderCounter2() {
  $w('#counter2').text = `${counter2}`;
}
function renderSum() {
  $w('#sum').text = `${counter + counter2}`;
}
$w.onReady(function() {
  renderCounter();
  renderCounter2();
  renderSum();
  $w('#increment').onClick(function() {
    counter++;
    renderCounter();
    renderSum();
  });
  $w('#decrement').onClick(function() {
    counter--;
    renderCounter();
    renderSum();
  });
  $w('#increment2').onClick(function() {
    counter2++;
    renderCounter2();
  });
  $w('#decrement2').onClick(function() {
    counter2--;
    renderCounter2();
    renderSum();
  });
});


Whoops! Did you see the bug? I forgot to call renderSum() when #increment2  was clicked. Well, this is what happens when you wire UI and state manually. This only gets worse as the application gets more complicated and as the behavior of the application change as you add more features.

See for example this todo list application, written in Corvid using the jQuery pattern: https://shahartalmi36.wixsite.com/corvid-jquery.

We have two bugs there:

  1. If you check the checkbox next to the todo item that marks it as done, the todo description gets strikethrough decoration, which is the expected behavior. But if you check the top checkbox that marks all todo items as done, we forgot to update the todo item description with the strikethrough.
  2. If you check the checkbox next to the todo item which marks it as done, the “items left” counter at the left bottom is updated with the number of remaining items. But, if we delete an uncompleted todo item using the delete button, we forget to update the “items left” counter.

Those are annoying and hard to catch bugs. Every jQuery application was full of them, and it made maintaining the code base of jQuery applications complete hell.

You can also play with the real app. Just open the corvid-jquery example in the Wix editor (turn on dev mode from the top menu in order to see the code).

And Then Came Data Binding

Data binding is a pretty neat concept where you no longer need to explicitly update the UI when the state changes. Instead, you define what piece of state goes in each UI element using some framework. Then, when you update the state in that framework, it automatically updates the UI with the relevant changes. Since the framework knows what UI element needs what piece of state, it can make sure to update only the UI elements that care about the part of state that was updated.

So what are those framework? Actually there are tons of them. The first widely used framework was actually Angular, but Angular was much more than just a data binding or state management framework. It was coupled with many more concerns, most importantly with how the UI is rendered, which is obviously a problem for us, since we want to render the UI with Corvid.

But then something nice happened. React came out and put on its flag to only be opinionated about how UI is rendered; the rest was open for extensibility. Soon, a new generation of state management frameworks appeared, which only gave you a method to manage your state and only needed small adapters to bind the state into the React-based application view. Later versions of Angular also allowed users to easily use such state management instead of the built-in state management that came with Angular.

This was great since essentially, you can write almost all of your application logic without really caring what framework you would use for rendering the UI. It means that if you used such a state management framework, you could pretty easily move your application logic from React to Angular or even some other future framework that might come out (like Corvid!), and the only thing that would change is the wiring of the state to the UI.

In this article, we will look into my two favorite state management frameworks (Redux and MobX) and see how you can easily connect them to a Corvid application. This also means you can easily take any Redux or MobX-based application and migrate it from React to Corvid!

In order to use Redux and MobX, you’ll need to install these external libraries and an additional library called corvid-redux in your Corvid application.

Redux

In Redux, the main concept is that your state is managed by a reducer and updated by dispatching actions. Basically, that means that every time you want to update the state, you dispatch an action with the needed update. Then, the reducer (which is basically just a function with two arguments) is called with the current state, and the action and is supposed to return the new state.

It sounds complicated, but it is really simple. Let’s see the counter example:

import { createStore } from 'redux';

const initialState = { counter: 0 };

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { counter: state.counter + 1 };
    case 'DECREMENT':
      return { counter: state.counter - 1 };
    default:
      return state;
  }
}

const store = createStore(reducer);

$w.onReady(function() {
  store.subscribe(() => $w('#counter').text = `${store.getState().counter}`);

  $w('#increment').onClick(() => store.dispatch({ type: 'INCREMENT'}));
  $w('#decrement').onClick(() => store.dispatch({ type: 'DECREMENT'}));
});


All that changed here is that instead of incrementing or decrementing the counter ourselves when the buttons are clicked, we dispatch an INCREMENT or DECREMENT action. Redux will then call the reducer with the current state, and the dispatched action and the reducer will return the new state with the incremented or decremented counter according to what action was processed.

The most interesting line to focus on now is:

 store.subscribe(() => $w('#counter').text = `${store.getState().counter}`); 

In this, we subscribe to changes on the store, and when the state updates, Redux will call our callback, and we will have the opportunity to update the view with the new state.

This is nice, but it is not granular enough. Currently, we have just one counter, but in the example before, we had two counters and a sum that will look more like this:


import { createStore } from 'redux';

const initialState = { counter: 0, counter2: 0 };

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, counter: state.counter + 1 };
    case 'DECREMENT':
      return { ...state, counter: state.counter - 1 };
    case 'INCREMENT2':
      return { ...state, counter2: state.counter2 + 1 };
    case 'DECREMENT2':
      return { ...state, counter2: state.counter2 - 1 };
    default:
      return state;
  }
}

const store = createStore(reducer);

$w.onReady(function() {
  store.subscribe(() => {
    $w('#counter').text = `${store.getState().counter}`;
    $w('#counter2').text = `${store.getState().counter2}`;
    $w('#sum').text = `${store.getState().counter + store.getState().counter2}`;
  });

  $w('#increment').onClick(() => store.dispatch({ type: 'INCREMENT'}));
  $w('#decrement').onClick(() => store.dispatch({ type: 'DECREMENT'}));
  $w('#increment2').onClick(() => store.dispatch({ type: 'INCREMENT2'}));
  $w('#decrement2').onClick(() => store.dispatch({ type: 'DECREMENT2'}));
});


Basically, in the subscribe callback, we update all of the UI elements with the new state. This is not very efficient since there is no reason to update the first counter if the second counter is the one that was incremented. For this, we have to add the corvid-redux binding, which ensures just that by making the data bindings more declarative:

import { createStore } from 'redux';
import { createConnect } from 'corvid-redux';

const initialState = { counter: 0, counter2: 0 };

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, counter: state.counter + 1 };
    case 'DECREMENT':
      return { ...state, counter: state.counter - 1 };
    case 'INCREMENT2':
      return { ...state, counter2: state.counter2 + 1 };
    case 'DECREMENT2':
      return { ...state, counter2: state.counter2 - 1 };
    default:
      return state;
  }
}

const store = createStore(reducer);
const {connect, pageConnect} = createConnect(store);

pageConnect(() => {
  connect(state => ({ text: `${state.counter}` }))($w('#counter'));
  connect(state => ({ text: `${state.counter2}` }))($w('#counter2'));
  connect(state => ({ text: `${state.counter + state.counter2}` }))($w('#sum'));

  $w('#increment').onClick(() => store.dispatch({ type: 'INCREMENT'}));
  $w('#decrement').onClick(() => store.dispatch({ type: 'DECREMENT'}));
  $w('#increment2').onClick(() => store.dispatch({ type: 'INCREMENT2'}));
  $w('#decrement2').onClick(() => store.dispatch({ type: 'DECREMENT2'}));
});


What we basically say here is that we bind the text property of the respective UI element with the value of the counter/sum. Now, instead of updating all of the UI on state update, the UI will be updated only if the bound value is changed. In order to make this work, all we need to do is use the  createConnect method from corvid-redux, which gives us two helpers:  pageConnect, which is basically a small wrapper on top of $w.onReady, which we used up until now, and  connect, which we use to bind to element properties.

Pretty simple, right? Let’s complicate it a bit. Let’s make a simplified todo list. Now, we have a text input (#input) and an add button (#add), and we have a repeater (#repeater), which will display the items that we added. For each item in the repeater, we’ll have a text element displaying the description (#description) and a delete button to remove it from the list (#remove).

This will look something like this:

import { createStore } from 'redux';
import { createConnect } from 'corvid-redux';

let nextId = 0;

const initialState = [
  { _id: `${++nextId}`, description: 'Task 1' },
  { _id: `${++nextId}`, description: 'Task 2' },
  { _id: `${++nextId}`, description: 'Task 3' }
];

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'ADD_TODO':
      return [...state, {_id: action.id, description: action.description}];
    case 'REMOVE_TODO':
      return state.filter(todo => todo._id !== action.id);
    default:
      return state;
  }
}

const store = createStore(reducer);
const {connect, pageConnect, repeaterConnect} = createConnect(store);

pageConnect(() => {
  connect(state => ({data: state}))($w('#repeater'));

  $w('#add').onClick(() => {
    store.dispatch({type: 'ADD_TODO', id: `${++nextId}`, description: $w('#input').value});
    $w('#input').value = '';
  });

  repeaterConnect($w('#repeater'), ($item, _id) => {
    connect(state => ({text: state.find(todo => todo._id === _id).description}))($item('#description'));

    $item('#remove').onClick(() => store.dispatch({type: 'REMOVE_TODO', id: _id}));
  });
});


Now, this is interesting. Here, we bind an array to the data property of the repeater element (#repeater). As you can see, the initial state of that array has three todo items; each item has an _id property and a  description property. The _id is a unique identifier mandatory for any repeater item, as described in .. The second interesting thing here is  repeaterConnect, which we use in order to bind elements inside the repeater to our state.

repeaterConnect gets two parameters: the repeater element we want to bind into and a callback. This callback is called for each new item in the repeater (including the initial three items of course) in order to allow it to bind the internal elements of this repeater item to the state. You don’t need to worry about unbinding when items are removed since corvid-redux takes care of that automatically.

As you can see, the callback simply receives the $item selector function, which you can use to select internal elements of the item and the _id, which you can use to access the appropriate item in the state. Binding inside repeaterConnect is done using connect, exactly as you would have performed binding outside a repeater.

Note that, as always with Redux, we must remember that state is immutable, which means we are not allowed to save references to an item inside the array since that reference will never contain later changes to the state. Instead, we must find the correct item in the array inside the  connect callback like so:

connect(state => ({ 
  text: state.find(todo => todo._id === _id).description
}))

($item('#description')); 


Since usually we will have multiple such connect calls inside a repeaterConnect probably we can do a small refactor and extract the find part to a function and then the connect part will be somewhat shorter:

 const find = state => state.find(todo => todo._id === _id); connect(state => ({  text: find(state).description}))($item('#description')) 

const find = state => state.find(todo => todo._id === _id); 
connect(state => ({  
  text: find(state).description
}))

($item('#description'));


In general, as we learned here, keeping all logic of the state far away from the binding code is always good practice since it allows you to replace view frameworks in the future with small changes to your logic code. I highly recommend reading Redux docs about derived data in order to learn more patterns regarding how to extract data from state.

This is actually all we need to know in order to use Redux in Corvid. There are many interesting things to learn about Redux, such as how to add middleware to your store and how to handle asynchronous operations, but the nice thing is that it is all just Redux and isn’t specific to the view technology you use, which in our case just happens to be Corvid. You can read all about those in the Redux docs about advanced topics and try to apply them in your Corvid application.

For a live example of a more complicated todo list open the corvid-redux example in the Wix editor (turn on dev mode from the top menu in order to see the code).

MobX

To be honest, I’m not a big fan of Redux. The boilerplate some patterns introduce are sometimes just too much, and in general I think that for most use cases, the disadvantages of immutability are bigger than the advantages. That’s not to say that I never use Redux; it can sometimes be very helpful. Let’s examine an alternative state management solution, which I mostly prefer to use — MobX.

In MobX, the main concept is that you bind to some derived state, and MobX knows to automatically identify your state dependencies and automatically create an observable for it. It then runs your binding again only when the state that you depend on changes. Let’s see MobX in action with our two counters and a sum example:

import { autorun, observable } from 'mobx';

const state = observable({
  counter: 0,
  counter2: 0,
  get sum() {
    return this.counter + this.counter2;
  }
});

$w.onReady(() => {
  autorun(() => $w('#counter').text = `${state.counter}`);
  autorun(() => $w('#counter2').text = `${state.counter2}`);
  autorun(() => $w('#sum').text = `${state.sum}`);

  $w('#increment').onClick(() => state.counter++);
  $w('#decrement').onClick(() => state.counter--);
  $w('#increment2').onClick(() => state.counter2++);
  $w('#decrement2').onClick(() => state.counter2--);
});


That’s it. The cool thing about MobX is that it will run the autorun methods only when the state that they use has changed. So, when we update counter, the second autorun doesn’t run, and when we update counter2, the first  autorun doesn’t run. The third  autorun will run in both cases since it depends on sum, which depends both on counter and counter2.

Note that instead of having that sum getter function in the observable, we could have done something like:

autorun(() => $w('#sum').text = `${state.counter+state.counter2}`); 


This would have worked just the same, and we wouldn’t have needed the getter. However, as I mentioned before, it is wise to separate the logic from the view because then you can easily reuse your state when changing the view technology. I recommend to always do all calculations in the observable state using getters and leave the view as simple as possible.

Let’s try to see how MobX holds up when we try the simplified todo list example. Just to remind you: We have a text input (#input), an add button (#add), and we have a repeater (#repeater), which will display the items that we added. For each item in the repeater, we’ll have a text element displaying the description (#description) and a delete button to remove it from the list (#remove).

import { autorun, observable } from 'mobx';

let nextId = 0;

const state = observable({
  tasks: [
    { _id: `${++nextId}`, description: 'Task 1', completed: false },
    { _id: `${++nextId}`, description: 'Task 2', completed: true },
    { _id: `${++nextId}`, description: 'Task 3', completed: false }
  ],
  addTodo(description) {
    this.tasks.push({_id: `${++nextId}`, description, completed: false})
  },
  removeTodo(id) {
    this.tasks.remove(this.tasks.find(todo => todo._id === id));
  }
});

$w.onReady(() => {
  autorun(() => $w('#repeater').data = state.tasks);

  $w('#add').onClick(() => {
    state.addTodo($w('#input').value);
    $w('#input').value = '';
  });

  const destroyers = {};
  $w('#repeater').onItemReady(($item, {_id}) => {
    destroyers[_id] = [
      autorun(() => $item('#description').value = state.tasks.find(todo => todo._id === _id))
    ];

    $item('#remove').onClick(() => state.removeTodo(_id));
  });
  $w('#repeater').onItemRemoved(({_id}) => destroyers[_id].forEach(f => f()));
});


All we needed to do, just like with Redux, is to bind an array to the data property of the repeater element (#repeater). Now, onItemReady will be called for any new item and onItemRemoved for any removed item. Inside onItemReady, we do all of the binding necessary using the $item selector.

Notice one special thing, which is very important to understand: the returned values from all of the calls to autorun must be saved as an array in the destroyers map, which is later invoked for each return value when the item is removed. The reason for this is that the return value of an autorun call is actually an unsubscribe method for that specific autorun. In corvid-redux, this is done automatically for us, but in MobX, we must call those unsubscribe methods when the item is removed.

One place where MobX is much more comfortable is when you want to do some side effect in the binding of some value (e.g. hiding and showing an element). Let’s say you have some boolean state that specifies if some element should be visible or not. Since visibility in Corvid is controlled through show/hide functions, it is a bit tricky in Redux. In MobX, you would simply do:

autorun(() => state.shouldShow ? $w('#element').show() : $w('#element').hide()); 


Whereas in Redux, corvid-redux needs to supply you with a magical visible property, which behind the scene calls show/hide:

For a live example of a more complicated todo list open the corvid-mobx example in the Wix editor (turn on dev mode from the top menu in order to see the code).

connect(state => ({visible: state.shouldShow}))($w('#element')); 


This post originally appeared on Medium.


Further Reading

Topics:
state management ,web dev ,corvid ,spa ,redux ,javascript ,frontend ,tutorial

Published at DZone with permission of Shahar Talmi . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}