-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
What are the disadvantages of storing all your state in a single immutable atom? #1385
Comments
That’s a great question. I’ll try to write a comprehensive answer tomorrow but I’d like to hear from some of our users first. It would be nice to summarize this discussion as an official doc page later. We didn’t do this at first because we did not know the drawbacks. |
Thank you very much! I've really been wanting to hear about this. |
By the way, I encourage everyone who wants to contribute to this discussion to also give an idea of what they were building with Redux so it is easier to understand the scope of the projects. |
Its sort of a tough question b/c projects like OM and redux and other single state atom libs, actively mitigate the downsides. Without the the immutability constraint, and gated, controlled access a single state atom is not at all unlike attaching all your data to the Specific to the Redux approach though, the biggest downside for us is that single state atoms is sort of an all or nothing thing. The benefits of easy hydration, snapshots, time travel, etc only work if there is no other place important state lives. In the context of React this means you need to store state, that properly belongs to components in the Store, or lose a bunch of benefits. If you do want to put everything in Redux it often ends up being burdensome and verbose, and adds an annoy level of indirection. None of these downsides have been particularly prohibitive for us though :) |
So, I've been eyeballing Redux, and this style of architecture, to model game states; I have an interest in making Civ style simulated worlds, and fully recorded board game histories more simply, in a browser context, which is perhaps an unusual use case to some, but I doubt anyone would want me to stop, either. In that context, I'm interested in efforts to manage the size of the history data structure, to move parts of it to the server or disk to save it, restoring specific segments, etc. The thing I've struggled with, as a new user the most is trying to overcome both the extreme (imho) coding style change in Redux, and the conceptual changes at the same time; it remains a bit daunting even now, a month and change after trying to deep dive into it; I'm looking into https://github.com/ngrx/store next to get at the concepts but with a more familiar style/syntax and more of the rest of the framework implied/provided. I understand that for some, a framework is a crutch; but some of us need those; few people will be at the top of their game enough to have useful strong opinions, so a frameworks' are better than fumbling in the dark, you know? So that's a point from me, a mid level, middle aged programmer new to the concepts :) Basically, Redux and your videos taught me how to do it in raw javascript, but being a less experienced programmer, I still have no idea how to deliver a product without the further guidance, so until I see that example of a finished thing, I just kinda stand around like: Not your fault, but still a problem I'd love to help solve :) |
I think the biggest question I still have at this point is, where should any non-serializable items like functions, instances, or promises go? Was wondering about this over in Reactiflux the other night and didn't get any good responses. Also just saw someone post http://stackoverflow.com/questions/35325195/should-i-store-function-references-in-redux-store . |
Use case for multiple stores (keep in mind, this is the best one I could think of): An app that combines multiple sub-apps together. Think of something like the My Yahoo or another customizable home page product. Each widget will need to maintain its own state without overlap between the two. A product team is likely assigned to each widget, so they might not know the effects of others' code on the environment. Gathered, you can likely achieve this in Redux by violating a few rules and being careful about propagating those stores on something like React's context. But it might be easier with a framework that handles multiple state atoms by default. |
@gaearon we are building trading platform. I've already explained to you once why one store doesn't fit for us (after React.js meetup in Saint-Petersburg). I'll try to explain it again in details (in blog post on medium?) but I probably need help due to my English skills :) Is it ok if I'll send it to you or someone else here for review in Twitter direct messages when it will be done? I can't name the exact date though but I'll try to write this post soon-ish (most likely end of this month). |
And yes we are still using Redux with multiple stores by violating some rules from docs as @timdorr said (in cost of that we can't use |
Redux is basically event-sourcing where there is a single projection to consume. In distributed systems, there is generally one event log (like Kafka), and multiple consumers that can project/reduce this log into multiple databases/stores that are hosted on different servers (typically, a DB replica is actually a reducer). So in a distributed world the load and memory usage is... distributed, while in Redux if you have hundreds of widgets that all have their local state, all this run in a single browser, and every state change has some overhead due to immutable data updates. In most cases this overhead is not a big deal, however when mounting text inputs into a not so good state shape, and typing fast on a slow mobile device, it's not always performant enough. Rendering that state from the very top is, in my experience, is not convenient and also not always performant enough (at least with React which is probably not the fastest VDom implementation). Redux solves this with Also persistent data structures like ImmutableJS are not always offering the best performances on some operations, like adding an item at a random index in a big list (see my experience rendering big lists here) Redux includes both the event log and the projection because it is convenient and is ok for most usecases, but it could be possible to maintain an event-log outside redux, and project it into 2 or more redux stores, add all these redux stores to react context under different keys, and have less overhead by specifying which store we want to connect to. This is absolutly possible, however this would make api and devtools harder to implement as now you need a clear distinction bettween the store and the event log. I also agree with @jquense
Mounting any state to Redux store requires more boilerplate. Elm architecture probably solves this in a more elegant way but also requires a lot of boilerplate. But it is also not possible or performant to make all state controlled. Sometimes we use existing libraries for which it is hard to build a declarative interface. Some state is also hard to mount to redux store in an efficient way, including:
|
This question I just saw on SO: Echos some confusion I've seen about managing UI state, and whether the UI state should belong to the component or go into the store. #1287 offers a good answer to this question, but it's not so initially apparent and can be up for debate. Additionally, this could be a hindrance if you're trying to implement something like this https://github.com/ericelliott/react-pure-component-starter in which every component is pure and does not have a say in its own state. |
I found it difficult to use redux's single state tree when I needed to manage multiple sessions' data. I had an arbitrary number of branches with an identical shape and each had multiple unique identifiers (depending on where the data came from, you'd use a different key). The simplicity of the reducer functions and reselect functions quickly disappeared when faced with those requirements - having to write custom getters / setters to target a specific branch felt overly complex and flat in an otherwise simple and compossible environment. I don't know if multiple stores is the best option to address that requirement, but some tooling around managing sessions (or any other identically-shaped data) into a single state tree would be nice. |
Increased probability of state keys collisions between reducers. |
Let me preface with my particular use-case: I'm using Redux with virtual-dom, where all of my UI components are purely functional and there's no way to have local state for various components. With that said, definitely the hardest part is tying in animation state. The following examples are with animation state in mind, but much of this can be generalized to UI state. Some reasons why animation state is awkward for Redux:
On the other hand, there are definitely challenges to building the animation state entirely outside of the Redux state tree. If you try to do animations yourself by manipulating the DOM, you have to watch out for stuff like this:
And if you try to let virtual-dom take care of all the DOM manipulation (which you should!) but without keeping animation state in Redux, you end up with these tough problems like these:
There are currently awesome projects like react-motion that make great strides forward in solving this problem for React, but Redux is not exclusive to React. Nor should it be, I feel—a lot of folks are bringing their own view layer and trying to integrate with Redux. For anybody who's curious, the best solution I've found for Redux + virtual-dom is to keep two state atoms: Redux keeps the core application state, holds the core logic to manipulate that state in response to actions, etc. The other state is a mutable object that holds animation state (I use mobservable). The trick then is to subscribe to both Redux state changes and animation state changes, and to render the UI as a function of both: /* Patch h for jsx/vdom to convert <App /> to App() */
import h from './h'
import { diff, patch, create } from 'virtual-dom'
import { createStore } from 'redux'
import { observable, autorun } from 'mobservable'
import TWEEN from 'tween.js'
import rootReducer from './reducers'
import { addCard } from './actions'
import App from './containers/App'
// Redux state
const store = createStore()
// Create vdom tree
let tree = render(store.getState())
let rootNode = create(tree)
document.body.appendChild(rootNode)
// Animation observable
let animationState = observable({
opacity: 0
})
// Update document when Redux state
store.subscribe(function () {
// ... anything you need to do in response to Redux state changes
update()
})
// Update document when animation state changes
autorun(update)
// Perform document update with current state
function update () {
const state = store.getState()
let newTree = render(state, animationState)
let patches = diff(tree, newTree)
rootNode = patch(rootNode, patches)
tree = newTree
}
// UI is a function of current state (and animation!)
function render (state, animation = {}) {
return (
<App {...state} animation={animationState} />
)
}
// Do some animations
function animationLoop (time) {
window.requestAnimationFrame(animationLoop)
TWEEN.update(time)
}
animationLoop()
new TWEEN.Tween(animationState)
.to({ opacity: 100 }, 300)
.start()
// Or when you dispatch an action, also kick off some animation changes...
store.dispatch(addCard())
/* etc... */ |
Thank you all for great responses! Keep them coming. One thing to note is that we don’t intend Redux to be used for all state. Just whatever seems significant to the app. I would argue inputs and animation state should be handled by React (or another ephemeral state abstraction). Redux works better for things like fetched data and locally modified models. |
Would you mind creating an issue describing your use case in more detail? It might be that there is a simple way to organize the state shape differently that you are missing. In general multiple branches with same state shape but managed by different reducers is an anti-pattern. |
Would you mind explaining how this happens in more detail? Normally we suggest you to only run a single reducer on any state slice. How can collisions happen? Are you doing multiple passes over the state? If so, why? |
Yeah, this is a good point even if this is not what @istarkov meant. (I don’t know.) In general libraries should offer a way to mount reducer anywhere in the tree but I know that some libraries don’t allow that. |
I'd be happy to! I'll do that when I get a moment. I think its definitely regarded as an antipattern, although I have a single, shared, reducer to manage those branches. Nonetheless, I feel like the paradigm that redux provides is not far from being a perfect use case for serializing / deserializing identical branches as immutable state. I don't see why it would be against the ethos of redux to build something like reselect with some assumed logic for targeting specific branches by some key. I'd be happy to chat about it off-thread, I could very well be wrong. |
@taggartbg : speaking entirely without knowing what your code looks like here. Are you talking about trying to deal with state that looks like this? { groupedData : { first : {a : 1, b : 2}, second : {a : 3, b : 4}, third : {a : 5, b, 6} } Seems like you could deal with that on the reducer side by having a single reducer function that takes the per-group ID key as part of each action. In fact, have you looked at something like https://github.com/erikras/multireducer, https://github.com/lapanoid/redux-delegator, https://github.com/reducks/redux-multiplex, or https://github.com/dexbol/redux-register? Also, on the reselect side, the fact that React-Redux now supports per-component selectors might help, since that improves scenarios where you're doing selection based on component props. |
A React developer reading the docs and tutorial already has this ephemeral state abstraction at their disposal, so it's possible this is already in mind when considering where Redux makes sense for a project. However, from the perspective of a developer with little or no experience with React that wants to take a functional approach to UI, it may not be obvious which state belongs in Redux. I've done several projects now where at first Redux and virtual-dom were sufficient, but as the application grew in complexity this other "ephemeral state abstraction" became necessary. This isn't always apparent the first time you add in an animation reducer with some basic animation flags, but later on becomes quite a pain. It might be nice for the docs to mention which state is right for Redux and which state is better solved by other tooling. It may be redundant for React developers, but could be very beneficial for other devs to have in mind when looking at Redux and planning their application architecture. EDIT: the title of the issue is "What are the disadvantages of storing all your state in a single immutable atom?" This "all your state in a single immutable atom" is exactly the kind of wording that ends up getting a lot of the wrong kind of state in the Redux tree. The documentation could provide some explicit examples to help developers avoid this kind of trap. |
@gaearon We had an interesting conversation over on Cycle.js about the architectural differences between a single atom state vs pipping. HERE. I know this isn't 100% Redux related but wanted to give a different perspective from an implementation that uses Observable streams as a data-flow around an app (for Cycle the app being a set of pure functions). Because everything in Cycle is an Observable, I was having difficulties getting state to move from one route change to another. This was due to the state Observable not having a subscriber once a route was changed and thought why not implement a single atom state, so any time state changed in my app it would report back to the top level store, bypassing any piping (view terrible drawings here). With Cycle because side effects happen in Drivers you usually have to make that loop from top level driver to the component and back, so I wanted to just skip that and go straight back up and out to the components listening. Was taking Redux as a source of inspiration. It's not a case that either is right or wrong but now I do piping and we have figured out a state driver, having the ability to pipe my state to different components as I need to is really flexible for refactoring, prototyping, and having strong understanding each source of state, each consumer and how they got to each other. Also newcomers to the application (if they understand Cycle of course), can easily connect the dots and very quickly draw a visual representation in there mind of how state is handled and piped and all this from the code. Sorry in advance if this is totally out of context, wanted to demonstrate how a different paradigm had a similar conversation 😃 |
I think it's better when widget has it's own state (maybe even it's own (redux?) store) so it can work standalone in any (mashup-)app and receive only some of properties it. Think about weather widget. It can fetch and show data by itself and receive only properties like city, height and width. Another example is Twitter widget. |
@chicoxyzzy Related: reduxjs/react-redux#278 |
Great discussion! Mainly, I find it inconvenient to combine multiple apps/components/plugins. I can't just take a module I build and throw it in another module/app, as I'll need to separately import its reducers into the store of the app, and I'll have to put it under a specific key that my module knows about. This also limits me from duplicating the module if I use For example: I'm building a messenger that looks like iMessage. It has a redux state of I want to include this Now, if I want to have two Also, let's say the containing app has a certain action name that exists on the A few random/crazy ideas: 1
@connect(state => ({ messengers: state.messengers }))
class App extends Component {
render() {
return (
<div>
{this.props.messengers.map(messenger =>
<ProvideSlice slice={{messenger: messenger}}><Messenger /></ProvideSlice>
}
</div>
)
}
} There's a problem when using global normalized entities, Another issue is how to bind actions fired by components in 2Combine |
For what it's worth, I had an idea of creating "branches". It totally defies the single store principle, but it seemed like a good compromise. |
@jedwards1211 I think that as apps grow in complexity there's eventually the need to break out certain types of data and certain types of updates so that certain stores aren't handling the full load. Let's say that there's some core state that you want sync'd with the backend, there's some animation state so that your app UI components are displaying correctly according to the current actions being performed, and maybe you have some debug data that you're tracking separately. In this contrived example, it's certainly possibly to build all of this out of a single Redux store, but depending on how you architect it, there'll be some blurry lines between concerns of various branches of the state tree, and maybe lack of clarity regarding which state a given action is intended to interact with. I think it's perfectly reasonable to use different state management strategies according to how you want state to be scoped and what performance characteristics you expect out of it. Redux is suitably geared towards state in which you may want to rollback or replay actions, or serialize and return to later. For animation state, you can use a mutable store or a reactive store if you want—there will be less overhead that way and you won't be polluting your application state with this transient (but still necessary) interaction state. Real quick, I want to make a point using the upcoming React fiber architecture. Here's a link, apologies if my understanding is a bit out of date. Roughly, the fiber architecture acknowledges that there are different kind of updates that will propagate through a React component tree, and there's different expectations regarding when and how these updates will perform. You want animation updates to be fast, responsive, and reliable, and you don't want big interaction updates to introduce jank in your animations and such. So the fiber architecture breaks down updates into work packets and schedules them according to a priority based on the work being performed. Animations are high priority—so that they're not interrupted, and slower mechanical updates have a lower priority. I've skipped over a lot of details about React fibers and probably gotten some things wrong in the process, but my point is that this is the kind of granular approach I think is necessary for your data store with different types of data. If I was building a complex app today, I'd start with a Redux-like top-level store class backed by a few different stores. Roughly:
This story seems pretty close to a complete state solution based on Redux. Redux is suited to data you want to view after a sequence of actions, and there's a lot of other state out there that needs careful handling as well. |
@jsonnull I've never had any need to store animation state in a store -- local state has always been ideal for my animation use cases. I'd be curious to know if there are some use cases that local animation state is completely unsuited for. It sounds like they have great ideas for the new architecture though. |
@jedwards1211 There's a couple cases I can think of where local animation state doesn't work. I'll grant you, they're not cases you'll run into with every application, but I think they come up often enough. In one case you're using a library other than Redux where you don't have local state. (Ok, yes, I know I'm cheating a little here.) If you're using a very lean hyperscript approach, no virtual dom, pure functional components, then instead of local state you're going to have to pass some animation state to the root node or whichever node you're re-rendering. In this case, having a state tree with a few animation details set will allow you to work around lack of local state so that you can trigger animations, have the animations run for a duration, and more. The key thing here is that there's two ways to do these animations—do them as part of your render and keep the "UI as a function of state" metaphor intact, or mutate the DOM separately. Almost always the better answer is to just re-render instead of mutating. Now for an example where you do have the ability to keep some animation state local—building a browser game with a React-based UI on top of it.
In this game example, what you don't want to do is increment your ticks as part of a Now you have separated the global animation state from the interaction state, and if you want to do a "replay" using only the Redux state or by replaying actions, you can, and you're not burdened by incrementing ticks as part of your state tree. Generally speaking, it's also much better to coordinate animations by passing start/stop times through Redux and letting components maintain the rest of the state locally. This does not only apply to games. Any extended sequence of animations might go through this, such as if you wanted to do a series of long-running animations on a site using React. |
@jsonnull cool, that's a great example, thanks for sharing that. |
I would simply say - in Redux you just can't store you state objects with links to each other. For example - user can have many posts and post can have many comments and I want to store those objects in a way:
Redux doesn't allow usage of object hierarchies with circular references. In Mobx (or Cellx) you just can simply have one-to-many and many-to-many references with objects and it simplified business logic many times |
@bgnorlov I don't think anything in the You could also model relationships in your redux state by using foreign keys, though I understand this is not as convenient as using ORM-like object references: var user = {id: 'user1', postIds: [], commentIds: []}
var post = {id: 'post1', userId: user.id, commentIds: []}
user.postIds.push(post.id);
var comment = {id: 'comment1', postId: post.id, userId: user.id}
post.commentIds.push(comment.id)
user.commentIds.push(comment.id)
appState.userId = user.id
appState.posts = {[post.id]: post}
appState.comments = {[comment.id]: comment}
// then join things like so:
var postsWithComments = _.map(appState.posts, post => ({
...post,
comments: post.commentIds.map(id => appState.comments[id]),
}) |
@jedwards1211 to clone state with circular references in redux, reducer must return every new copy of an object which affected by changes. If reference to object changes the related object also need to be a new copy and this will be repeated in а recursive way and it will generate whole new state on every change. Immutable.js can't store circular references.
or with selectors
and this boilerplate turns code into mess compare to mobx version when I can simply write
|
@jedwards1211 , @bgnorlov : FWIW, this is one of the reasons why I like Redux-ORM. It allows you to keep your Redux store normalized, but makes doing those relational updates and lookups simpler. In fact, that last example is basically identical with Redux-ORM. I just wrote a couple blog posts describing Redux-ORM basics, as well as core concepts and advanced usage. |
@markerikson cool, thanks for the tip! |
I found this comment to be so incredibly useful. I know I'm late to the party, but maybe that's part of my point. Over a year since that comment was posted and this is still the only place I've seen this idea expressed. Coming from an angular and decidedly non-flux/redux background it is very hard to formulate that idea on your own. Especially when a lot of examples still create actions for every keyup in a text box. I wish somebody would put that quote in 50px text on the top of every Redux documentation page. |
@leff Agreed. I'd synthesized that exact idea some time back, but it doesn't get called out enough. Even if you're using Redux for history management, there's often a lot of ephemeral state that doesn't need to burden your Redux-style state management. That won't necessarily be a surprise to folks who've had the opportunity to work on, e.g. a mature desktop app that does rich undo/redo. But to the oceans of newcomers coming to these ideas via Redux, it'll be a revelation. |
@bgnorlov that's a good point that it's impossible to store circular references with Immutable.js, I never thought about that! I think it's a nice feature though. |
These days we do tend to store almost everything in Redux, even if it's transient view-specific state that we never use outside its context (e.g. state for a I'd say the most important thing to keep in mind is that putting all normalized state in Redux (i.e. anything that needs to be joined together to render views) will make it easiest to use. State that doesn't need to be joined with anything can probably live outside of Redux without causing you difficulty, but it usually doesn't hurt to put it in Redux either. |
FWIW, the docs do point out that not everything has to go into Redux, per http://redux.js.org/docs/faq/OrganizingState.html#organizing-state-only-redux-state . There's been quite a bit of recent chatter online about what Redux is "appropriate for". Some people see benefits from putting literally everything into Redux, others find that to be too much hassle and only want to store data retrieved from a server. So, there's definitely no fixed rule here. As always, if people have ideas for improving the docs, PRs are totally welcome :) |
If you ever need to reuse reducers and be able to allocate "sub-states" in your redux state I developed a plugin to do it effortlessly (with React integration) |
@eloytoro IMO if you have trouble with some 3rd-party reducer having hardcoded action types or location in the state, you should open an issue in the project...before long it will become unacceptable design practice as people learn the reusuable reducer/action creator pattern. |
@eloytoro I was going to write something like that. Thank you very much for the reference! |
@jedwards1211 I think you got the wrong idea. I haven't mentioned 3rd party reducers or issues with them in any way, just trying to showcase how my snippet can solve the issue for making reducers reusable in collections with dynamic sizes @avesus hope it works for you |
@eloytoro ah, that makes more sense. |
Last year I created a fairly complex game using Redux. I have also participated in another project which doesn't use Redux, but instead a custom-built, minimalistic framework which separates everything into UI Stores and Server Stores (including inputs). Without going into too many details, my conclusions so far are the following: Single state is always better, however, do not over do it. Getting every keyDown() through the store and back to the view is just confusing and unnecessary. So transient states should be handled by local components states (such as React's). |
I think that since the redux and react inflamed the observable web, the quantity of good animation on the pages declined. |
@mib32 you may be right! Hopefully people will eventually get used to crafting good animation in React. |
I understand that this is the principle underlying all of redux, and that it comes with all these awesome benefits that are pretty well known by now.
However, I feel that one place that redux is lacking in, is that it doesn't openly describe the conceptual disadvantages of using this architecture. Perhaps this is a good choice since you want people to not shy away due to the negatives.
I'm just curious because it applies not just to redux, but in some capacity to other things like Om, datomic and that talk about turning the database inside out. I like redux, I like Om, I'm interested in datomic, and I really liked that talk.
But for all the searching I've done, it's hard to find people critical of the single immutable store. This is an example, but it just seems to have a problem more with the verbosity of redux than with the actual architecture.
The only things I can think of is that perhaps it takes more memory to keep storing copies of your state, or that it's a little harder to rapidly protoype with redux.
Because you guys have probably put a lot more thought into this than I have, could you help elucidate this for me?
The text was updated successfully, but these errors were encountered: