question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

State management recommendations?

See original GitHub issue

Hey Cells,

This isn’t an issue, but I was wondering if we could have a discussion about state management when it comes to Cell. Perhaps I’m misunderstanding the intended architecture because of my daily work with React + Redux.

I set out to create a TodoMVC example using Cell (trying to race @devsnek 😄), and was really enjoying everything except for my total indecision about how to structure the state. The obvious place to start with this app is structuring something like this:

const TodoItem = (item) => {
  let classes;
  if (item.complete) classes = 'complete';
  return {
    $type: 'li',
    $text: item.name,
    class: classes
  }
}

var TodoList = {
  $cell: true,
  $type: 'ul',
  _items: [{ name: 'Learn Cell', complete: false, id: 42 }],
  $components: [],
  _add: function(todo) {
    this._items.push(todo);
  },
  $init: function() {
    this.$update();
  },
  $update: function() {
    this.$components = this._items.map(TodoItem);
  }
}

But this started to break down once I went to create the footer of the TodoMVC, where there’s a component that keep tracks of the remaining todos, a button to clear all completed todos, and some filtering options. How do you decide which component is the keeper of the data? Do all components need to be of the same parent and re-render down the tree when some of the data changes? One method that sort of works is to create a state object and have all the cells use that data instead.

var store = {
  items: [{ name: 'Learn Cell', complete: false, id: 42 }],
  addTodo: (todo) => {
    store.items.push(todo);
    store.update();
  }
  removeTodo: (id) => {
    const index = store.items.findIndex(i => i.id === id);
    store.items.splice(index, 1);
    store.update();
  }
  update() {
    document.querySelector('[store="true"]').$update();
  }
}

var TodoList = {
  $cell: true,
  $type: 'ul',
  store: true,
  _items: store.items,
  $components: [],
  $init: function() {
    this.$update();
  },
  $update: function() {
    this.$components = this._items.map(TodoItem);
  }
}

var TodosRemaining = {
  $cell: true,
  $type: 'span',
  store: true,
  _items: store.items,
  $init: function() {
    this.$update();
  },
  $update: function() {
    this.$text = `${this._items.length} items remaining`;
  }
}

// any cell can update the list now!
store.addTodo({ name: 'Ask for help', complete: true, id: 22 });
store.addTodo({ name: 'Figure out state', complete: false, id: 21 });

This solves the problem of needing one component to update who-knows-how-many other components that are relying on the same dataset. Now, this works, but you can agree that it’s pretty awful.

  • You have to remember a state: true property in all cells that access the store,
  • You have to remember to call the $update method on all the cells to force a re-render.
  • It makes store.items explicitly not immutable, because if you were to change store.items with a function like:
removeTodo: (id) => store.items = store.items.filter(i => i.id !== id)

It would mean that this._items on TodoList no longer references the updated store.items and even forcing a re-render through $update() wouldn’t work.

Can anyone offer me insight into the approach you’re using that you find elegant?

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:1
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

3reactions
gliechtensteincommented, Jul 18, 2017

To be honest there is no “best practice” for using cell at the moment since it’s a new and minimal library, and I myself am discovering different ways of structuring apps with Cell everytime. But I do realize how “raw” cell is in this regard and I can imagine some sort of a “centralized framework” on top of cell so that this type of centralized communication is easier to deal with. (I discuss one such idea here https://github.com/intercellular/cell/issues/143)

Also some of the ideas you suggested look like valid solutions to me.

That said, in case you’re not aware, I would like to mention one feature though. There’s this feature called “context inheritance” where you can define _variables anywhere on the tree and the node’s children will be able to access them.

Here’s the documentation https://github.com/intercellular/tutorial#b-context-inheritance-and-polymorphism and here’s an example: https://jsfiddle.net/k0knxwer/

This means in your example you don’t have to store store._items for each store node. You can keep it under a single root node and reference them from descendants. Example:

var TodoList = {
  $type: 'ul',
  store: true,
  $components: [],
  $init: function() {
    this.$update();
  },
  $update: function() {
    this.$components = this._items.map(TodoItem);
  }
}

var TodosRemaining = {
  $type: 'span',
  store: true,
  $init: function() {
    this.$update();
  },
  $update: function() {
    this.$text = `${this._items.length} items remaining`;
  }
}

var TodoApp = {
  $cell: true,
  _items: store.items,
  $components: [TodoList, TodosRemaining]
}

Note that:

  1. the $cell: true is at the top level only.
  2. store.items is only attached to TodoApp._items
  3. The _items attributes are gone from TodosRemaining and TodoList.
  4. When you refer to this._items from TodosRemaining and TodoList, they utilize context inheritance to propagate up until it finds a node with _items attribute, and utilizes that.

Also, I see a lot of $update() calls inside $init() but these are not desirable, you should directly instantiate them instead. For example instead of:

var TodoList = {
  ...
  _items: store.items,
  $init: function() {
    this.$update();
  },
  $update: function() {
    this.$components = this._items.map(TodoItem);
  }
  ...
}

It’s better if you use the below code because you don’t have to wait for an $init callback:

var TodoList = {
  ...
  _items: store.items,
  $components: store.items.map(TodoItem),
  $update: function() {
    this.$components = this._items.map(TodoItem);
  }
}

Lastly,

It makes store.items explicitly not immutable, because if you were to change store.items with a function like:

removeTodo: (id) => store.items = store.items.filter(i => i.id !== id)

It would mean that this._items on TodoList no longer references the updated store.items and even forcing a re-render through $update() wouldn’t work.

I think you can solve this if you attach an _items array at the root node as a single source of truth and refer to it everywhere, as I mentioned above. Just make sure to prefix it with “_” so the auto-trigger kicks in.

Here’s an example that may lead you to a solution https://play.celljs.org/items/O9Rf6y/edit

Basically, since cell auto-triggers $update() every time a _variable’s value changes (doesn’t matter if it’s the same reference or not, cell checks the value diff on UI update cycle and makes decisions based on that) you can take advantage of that and directly “mutate” the value, which will trigger a UI update.

I hope this helps! I think there can be many different ways of building a TODOMVC with Cell and think ANY approach is cool really. There is no right or wrong answer at this point and I think what matters is there are multiple ways of building the same thing . So please share your result once you get it working, would appreciate it.

Also feel free to ask further questions if something wasn’t clear!

2reactions
gliechtensteincommented, Jul 20, 2017

First of all, all apps–no matter how complex they are–can be built with a single cell variable theoretically. It would basically look something like this:

var app = {
  $cell: true,
  $type: "body",
  $components: [{
    ...
    $components: [{
      ...
      $components: [{
        ...

But this is not practical because you will want to make it modular, which is why you may end up creating multiple global variables.

But you probably want to only use these global variables by plugging them into cells as an attribute instead of using them everywhere. Here’s an example of a bad usage:

var doSomething = function() {
  console.log("Do something!");
}
var app = {
  $cell: true,
  $init: function() {
    doSomething()
  }
}

This is a great example of why people say “globals are bad” because if you keep doing this you will end up with a spaghetti code where you can’t keep track of what’s going on anymore.

But take the same app and do something like this:

var doSomething() {
  console.log(this._caption);
}
var app = {
  $cell: true,
  _caption: "Do something!",
  $init: doSomething
}

And you end up with a perfectly modular app. In fact the doSomething() function becomes really powerful because it is:

  1. Completely stateless on its own (it doesn’t depend on any particular object)
  2. But plugs into its parent node seamlessly to make use of the node’s attributes (this._caption can mean different things depending on where this function gets plugged into)

Here’s another example: https://play.celljs.org/items/12Dfay/edit

If you look at the Github variable:

Github = function(input){
  fetch("https://api.github.com/search/repositories?q=" + input.value).then(function(res){
    return res.json()
  }).then(function(res){
    input._done(res);
  })
}

It assumes that it will communicate with its whichever parent it’s plugged into via a _done() function, so if you scroll down to where Github is actually being used, you’ll see:

Input = {
  ...
  onkeyup: function(e){
    if(e.keyCode === 13){
      Github(this);
      this.value = "";
    }
  },
  _done: function(res){
    document.querySelector(".container")._set(res.items.map(function(item){
      return {
        image: item.owner.avatar_url,
        title: item.full_name,
        stars: item.watchers_count,
        content: item.description ? item.description : ""
      }
    }))
  }
  ...
};

Lastly, in practice your app may become more complex and you may still want to organize them based on functionality. In this case, you can take advantage of the stateless functional components I mentioned above and simply create a “module” by wrapping these functions and variables with a parent object. Example:

var Widgets = {
  Input: {
    $type: "input",
    type: "text",
    ...
  },
  List: {
    $type: "li",
    $components: [...]
    ...
  },
  Selector: {
    ...
  }
}
var app = {
  $cell: true,
  $components: [Widgets.Input, Widgets.List, Widgets.Selector]
}

This is similar to how cell.js itself is structured

If you have any other ideas or come across an interesting usage pattern please feel free to share 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

ASP.NET State Management Recommendations
State management is the process by which you maintain state and ... NET, and provides recommendations about when you should use each option....
Read more >
List of state management approaches - Flutter documentation
A list of different approaches to managing state. ... A recommended approach. Simple app state management, the previous page in this section ...
Read more >
React State Management for Enterprise Applications - Toptal
React's useState is the best option for local state management. If you need a global state solution, the most popular ones are Redux,...
Read more >
Effective State Management in React: Comprehensive guide
In this post, I'll outline some tips and pointers for effective state management in React that I picked up over the years. I...
Read more >
React State Management Libraries and How to Choose
An overview of the best state management libraries and how to choose the right state management strategy for your app.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found