State management recommendations?
See original GitHub issueHey 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 changestore.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:
- Created 6 years ago
- Reactions:1
- Comments:7 (3 by maintainers)
Top GitHub Comments
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:Note that:
$cell: true
is at the top level only.store.items
is only attached toTodoApp._items
_items
attributes are gone fromTodosRemaining
andTodoList
.this._items
fromTodosRemaining
andTodoList
, 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:It’s better if you use the below code because you don’t have to wait for an
$init
callback:Lastly,
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!
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:
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:
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:
And you end up with a perfectly modular app. In fact the
doSomething()
function becomes really powerful because it is: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:It assumes that it will communicate with its whichever parent it’s plugged into via a
_done()
function, so if you scroll down to whereGithub
is actually being used, you’ll see: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:
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 😃