Assessing options for handling propagation of changes in deeply nested state tree #2112
-
Imagine you have a deeply nested state tree. Leaf models in the tree are passed as props to React components using observer. Changes in one of these leaves may require updates to distant parts of the tree. What should you do? Option 1: Make the root available everywhere If the root is globally available, we can just call an action on the root or drill down to where we need to call an action.
Option 2: Use getParent to walk up the tree In many ways, this is not materially different from option 1. Leaves still need to know about the broader context in which they are used which causes loss of modularity / testability. Option 3: Use volatile state to store event handler functions Something like this:
Now parents that contain a
Option 4: Use MobX reactivity (such as autorun or reaction) Like this:
Option 5: Use onAction This works similarly to reaction, but with some trade-offs:
And so we come to option 6... Middleware! Middleware seems to be able to handle all of this pretty elegantly (or overcomplicatedly depending on your perspective I guess).
I'm happy to provide additional information / code / sandboxes if needed, but I tried to keep this as concise as possible. I'm hoping to have a discussion and see what other people think about these various choices, maybe find some consensus on the best way to handle this kind of updating disparate parts of a deep tree. I couldn't find anything in the docs or other discussions that dealt with this problem in this way, so I'm not sure if this is crazy and abusing middleware or actually a good idea. I'm immensely grateful for anyone who has read all this and offers a reply. 🙇 |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 3 replies
-
Hey @dbstraight - just wanted to say I've seen this, I don't have an clear thoughts yet, but I don't want this to go unacknowledged. I will put some more thought into it. FWIW, I have basically tried all of these except volatile event handlers, with varying degrees of success. I agree with most of your pros/cons, but don't have a better set of thoughts to provide. |
Beta Was this translation helpful? Give feedback.
-
Hi @dbstraight - I took some time to sleep on this, reflect, and put together my thoughts. I'm a current maintainer of MobX-State-Tree, but I hope anyone reading this will interpret this comment as personal opinions based on my own experience. I am not ready to declare any of these the "official" recommendation. Apologies if someone is looking for an official answer, but I'm just not comfortable drawing a line in the sand. That said, I hope my thoughts are helpful to you and others: Can we improve our data model to avoid this issue?
To start, I think this is a yellow flag that we can model our data better. A handful of these scenarios is usually unavoidable, but if you have so many of these scenarios, I think there's an underlying modeling problem. However, I understand many of us work on existing codebases where we can't make changes to the fundamental data model of our state management layer. So it's a good exercise to answer the rest of your question. I just wanted to address the root issue which is: can we avoid having leaf nodes interact with other parts of the tree? That would be a holistic answer to this. But if it's impossible or undesirable to change the tree structure, here's some thoughts about your proposed solution: Make the root available everywhereThis is basically how I do it in my day-to-day work. I think it's maximally flexible, and if a component wants multiple leaves, even if those leaves don't interact with one another, I can pull them from the root directly. I've experienced the cons you mentioned - testing can be hard. But if you take some time to write a good tree factory, that can improve your test suite overall anyways. I think solving the testing problems in this approach will improve your testing strategy. However, one con you can't avoid is the "god class" scenario. Again, if you need more than a handful of those god-level actions, I think an abstraction has been missed. But at the end of the day, it makes a lot of conceptual senes for me for the "root" to be able to kind of reach down wherever it wants and do stuff. Sure, "god classes" are undesirable. But if there's going to be one anyways, why not make it the root of the state tree? Use getParentOne advantage you didn't discuss here was that I think When I have taken this approach, I often wrap the Volatile stateI've never even considered this, but I kind of like it! I really like the way this decouples leaf nodes from parent actions. It does seem like it could get tricky to figure out correct bubbling and order-of-operations. I bet someone who was very good at event-driven programming would have some interesting thoughts around this. I am no expert on that, myself, but I think this approach warrants more consideration and writing. I'm going to personally do some thinking around it. Not sure what I want to do about it, other than lend credence to the thought. Very cool! MobX ReactivityThis makes sense, but every time MST users have to "drop down" into MobX land, I consider that a failing of MST. In our docs, we say that you don't need to know anything about MobX to use MST, and I hope to be able to live up to that. Still, I think this is perfectly valid. I just hope that we can provide better answers than this. I would not recommend this, except for folks who are already MobX users and feel very comfortable with it. onActionThis is definitely a good use for that hook, but as you say, it requires a good amount of understanding of how MST paths work to be useful. And it suffers from similar issues as I think I prefer this over the MobX approach, but in a similar way, it's a good way to start understanding what's going on in your tree. If it weren't for Middleware (next section), I'd find this quite compelling as well. MiddlewareMiddleware is awesome. I think that's preceisely what middleware was designed for. I like to reach for middlewares when I end up in a state where none of the above make sense. It's pretty easy to test, or avoid testing. But you're right, the typings can be kind of weird, and if you have enough of these scenarios that you have a bunch of middlewares, the routing ends up getting kind of gnarly, and you can try to smooth it over with a function, but at some point, you really just need a big if statement or switch statement to figure out which middlewares do what. I also think there's a sharp edge on middlewares: since they're always running, if you introduce some non-performant code at the entrypoint, you might slow down your whole app. My recommendationAgain, my actual recommendation is: try to model your data such that you rarely need to think about it. But if you need any of these solutions, they're all pretty great! But if I was forced to make my recommendation in order of highest preference to lowest preference:
|
Beta Was this translation helpful? Give feedback.
I really appreciate all the information. This is very helpful.
I wish I could come up with a good simplified example of the kind of state updates I'm thinking about without having to try to explain my entire app. I'm definitely going to look at other ways to model the data, but I don't think there's any getting around these kind of distant updates.
Anyway, I posted originally just trying to show the middleware option as "vanilla" as possible but I think TS can do a lot of heavy lifting to make it much more ergonomic, so I want to share that too. Basically I've got this helper function: