-
Notifications
You must be signed in to change notification settings - Fork 47k
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
Externalize the State Tree (or alternatives) #4595
Comments
I don't (yet) buy the argument that this shouldn't be solved in userland. As an alternative, I think it might be a better to teach about state hoisting, rather than trying to implement something as part of the core. Personally, I'm still a fan of the state-hoisting pattern (similar but different from #4594): class View {
state = { childStates: [ new ChildViewInternalState(), new ChildViewInternalState(), new ChildViewInternalState()] }
render() {
return <ChildView state={this.state.childStates[this.props.index]} />;
}
} Component implementations provide a black-box state object which may be retained by the parent. If the parent does not provide the component's state, the component will initialize one internally (in which case, the parent forfeits the option to preserve that child component's internal state). This also solves all three use cases described in #4594, without any special logic within the core and without increasing our API surface area. It has added benefits (for instance, allowing the parent to query a component's internal state without getting a ref to the component, if the implementor chooses to expose that internal information as part of their public API - useful for things like form If we recommend this pattern, it can become the defacto standard, and then you can preserve entire subtrees by saving the root's internal state. IMO, we should more aggressively utilize this design pattern to solve this problem. If this state-hoisting pattern becomes the defacto standard, many of these problems just evaporate. It doesn't require any universal library/framework; it only requires that components provide a way of passing in an internal state, which may be treated as a black-box by the parent. |
@jimfb I disagree that it is simple. Everyone doing this is building a library around it because of the boilerplate to deal with diffing lists etc. Edit: If this was the case, it would've been a solved problem in user space by now. |
@sebmarkbage Yeah, diffing lists sucks, but that can be solved with clever diffing libraries. The implementation remains internal to the component and doesn't need a global external state library. Components can swap out a diffing implementation without breaking other components/libraries. Various diffing libraries can coexist perfectly without stepping on each other's toes and without requiring conformance to a global data loading interface (therefore, not harming the ecosystem). We could even release an officially supported/recommended list-diffing implementation. IMHO, people are developing their own frameworks because they don't know of anything better and we haven't sufficiently taught them about data hoisting (the pattern is never mentioned in any docs). |
This is a great point, and one that we can already see taking shape in the current Flux landscape. Every Flux library is its own little ecosystem. |
What I'm interested in (and one of the reasons I started Redux) is being able to keep the state tree a computation, but let the app think it's a static value. If you look at how Redux DevTools work, we keep a "lifted state" (initial app state + debug UI + "staged" actions which will be replayed on hot reload + current state index useful for slider monitors), but the app only sees "unlifted state". (Which makes it behave normally as it's unaware that the state is backed by computation.) Another observation is that I use Flux/Redux not just as means of "keeping state alive", but rather to:
Right now it's hard to imagine how React could be solving that problem for me. I want my state logic to be composable, just like my UI is composable, but these are different trees. UI tree uses abstractions like "list view" to show paginated items, model tree may use abstractions like "paginated reducer factory" that creates reducers managing pagination data for ongoing requests. They do not map 1:1 though. Here's a new interesting thing. A form component mounted to the Redux state tree, but having no dependency on Redux per se. It's nice because it works with record/replay/DevTools/etc, has prop only interface, and only needs a "state provider" in terms of current state and a Not sure if it helps, but these are my thoughts. I'm not sure how to fit this into React. |
@gaearon We can implement hot reload and time travel for the purpose of devtools / debugging with any model. People even do that with mutation. Reducers simplifies the implementation but isn't a feature in itself. The use case is solved by #4593. To put it another way, some of the features you get is purely because you have your own framework. If you maintained React (or sent a PR :P ) you would have the same flexibility to add those features. Do you have any other examples of use cases for reducers in production environment? We see a use case for declarative programming to solve timing issues. E.g. allowing execution to be deferred based on the tree being in an inconsistent state. https://github.com/reactjs/react-future/blob/master/09%20-%20Reduce%20State/01%20-%20Declarative%20Component%20Module.js#L32 However, this allow us to defer the tree resolution (and therefore props) before invoking the dispatch. This is not a use case that is solved by state reducers per say since any transformation of props is still synchronous. So what is the use case that make them valuable? #4594 Solves the rollback scenario, I think. The big difference between Flux patterns and the React component state tree is the divergence between the two hierarchical models. They seem inherently incompatible since one is optimizing for componentization and the other is optimizing for domain modeling. Perhaps, the top of an app should be modeled as a domain and then the leaves are opaque component state somehow? Except, how do you then deal with the same "user" form being open twice in different dialogs with different state. You have to refactor your domain model to model the view concepts which loses the appeal of the domain model to begin with. So perhaps they should remain parallel? |
Forgive me for quasi-ignoring the above comments. I'd like to focus on a particular design avenue. If you're going to make this part of the public API, you shouldn't expose any concrete data types (such as immutable.js) for the state values themselves. Instead, you should specify an interface to be implemented by state stores. However, you'll probably want to expose a concrete data type for the state keys. The case against concrete state value data types is simple: There's no benefit to restricting what can be state (other than maybe immutable) as the abstract data type of "state store" doesn't need any such restrictions. Keys on the other hand, have some critical requirements that can influence how you'd implement the state store protocol. You'll need to decide if you want the keys to be opaque or to have some exposed structure to them. Opaque means that a single-level map is basically the only realistic option as the basis of an implementation. With eq/hash, the map can be a hash map. With a defined ordering, it can be a red/black tree, b-tree or similar. Transparent keys, on the other hand, can facilitate tree-shaped storage. Either by using a prefix of the key to delete a range in a sorted-map, or by structuring the store as a nested tree. Note that exposing tree path-style keys closes some other potential future avenues, such as reparenting stateful components. The path-style tradeoff may be wise for React as it exists today. Another big concern is atomic/batch updates of the store. With tree path keys, you can atomic/batch remove a subtree as I mentioned above. But a state store may still choose to iterate all deleted state to do something like decrement ref-counts or implement other such cleanup. Ultimately, the specified interface should be some variation of init/get/put/del plus potential extensions. This interface leaves all of the lifecycle management out of the store. Rollback/undo/etc can be optionally achieved by extending the store interface with two methods: |
Let me clarify a situation that I really want to avoid. I often hear arguments that goes something like: Expose A, B, C. Then let the user implement however they want. It's totally flexible. Except of course, once you only have A, B and C, there is only really one reasonable way to implement it. I think you alluded to this, @brandonbloom. One data structure becomes defacto the implementation, or you have many small ones that are effectively the same. Then the only value they provide is for those authors to understand the inner workings. We're not short on experimental implementations that can by-pass any React logic anyway. Especially with the decoupled renderers. Allowing room for experimentation isn't necessarily a primary goal. We already have that. So, what would optional data structures for the state store try to solve? Better language interop? Are there app specific perf tradeoffs? I'm leaning towards the idea that they don't really matter. I don't think we'll restrict what can be put as values into state. For user space content there is obviously a lot of different data structures that might be useful. (Although immutable and serializable would be nice.) |
That's exactly what I want. There shouldn't be a lot of these things and normal users shouldn't be writing them.
Yes: It would enable regular js objects for React.js out-of-box. immutable-js for "frameworks" built on React.js. ClojureScript data structures for Om. etc. Alternatively, you can insist on a standard, such as immutable-js, and make the conscious decision to force frameworks such as Om to either translate state values to immutable-js, or to go-outside the system to implement their on state stores. Insisting on a standard would enable global undo/snapshot/etc, but it would also be a massive breaking change, since people store plain old javascript objects in state now. At best, you could strongly encourage immutable-js and not disallow storage of stateful, unserializable objects. My proposal assumes heterogenous state storage. If this were a new framework, I'd propose just forcing immutable-js and ignore higher-level frameworks like Om. |
I haven't thought about this as much as others above, but the use case I really want want is full snapshotting (I'm sure that's obvious, but just want to say that). I don't even think undo is a strong enough use case for this, because usually when you want undo you take special care to only track the state that you want to undo, and you also need to perform an action on the server to revert the change. Usually it's fine to just use a userland library for this stuff. (of course, with an immutable database, dumb snapshotting is possible but not many people use immutable dbs) But full snapshotting is something that really begs for controlling where the entire state tree lives, even if child components don't know that their state is not actually internal. Snapshotting is really useful for debugging. Even in production, you can keep track of the last N app states, and when something goes wrong, serialize them all and send them somewhere a developer can resume and watch to see what happened. It's also just useful for showing off parts of any site statically (for example, go to http://jlongster.com, press cmd+shift+k and paste this in: https://gist.githubusercontent.com/jlongster/55b1f54f0a29ea235dc3/raw/8643c538e836a96eaa9e7b0c5e05d998d0363268/gistfile1.txt) I plan on researching this more, as I don't have much else to contribute right now. I do understand the difficulty though after implementing my own library: https://github.com/jlongster/blog/blob/master/src/lib/local-state.js. It's really bad though, and only works with a single React tree mounted. I don't even know how you are going to be able to track multiple mounted trees. |
I think base of all data structures, must be Cursor-like. Because it has simple API: get and set. And you can extend them with your own functionality. I think it must be something like https://github.com/zerkalica/immutable-di/#working-with-state . Here's an interface. And of course baobab can be provided as example. I think separation between container and cursor that navigates on data off the container can help achieve modularity. Because we can have ImmutableCursor or NativeCursor if we need them, but container stay the same. |
I get excited about redux because of time-travel debugging and eventually creating test cases out of dispatched actions ... but I really like how components encapsulate state. Trying to create a place in redux state for each component instance is cumbersome and pretty ridiculous when the component already has a place for its own state. Also, it turns out that component state is the state that the time-travel debugging is most useful! So the state I keep in redux is boring, the state I don't put in redux doesn't get time-travel. Feels weird. What I love about redux is the middleware to create tools around the app's state. I'm sure this API has 8 trillion holes in it, but just to prime the conversation what if we had stuff like: React.render(<App/>, el);
let tree = React.getStateTreeAtNode(el);
React.onStateTreeChangeAtNode(el, fn);
React.renderWithStateTree(<App/>, tree, el); That would be enough to build some time-travel debugging tools, yeah? |
Yes but the key issue is: Is this a dev mode debugging tool or also a production API? That strongly informs the implementation of this because this API limits the short cuts we can take.
|
Just ran across this thread and glad to see it's around. For the past few months I've been working on something with a friend in this direct area. Though our approach touches on a few other things, the general idea is we've added a global state store behind React, but it also works with component local state. There are also stores, which are backed by the same global store behind the scenes. The result is you have a view system that is simple to write (no reducers or other complex concepts), and still has all the upside: stores, logging, serializable, time-travel, inspectable, and can be easily optimized with immutable libraries (this is our next step). We ended up writing a compiler to power this, which gives us some other new things. Namely, with a compiler that can track which views rely on which stores so you can free the user having to use/know "actions", which are really just another abstraction over what you really want which is: "change a variable, views update automatically". Finally, and this is probably off topic, but the we went ahead and created a view macro. This lets you use "normal" variables inside views and have them backed into this global store, getting all those benefits while not requiring users to learn classes, or view Header {
let books = [{ title: 'Dune' }];
const addBook = () => books.push({ title: 'The Book' });
<div onClick={addBook}>
{books.map(book => <div>{book.title}</div>}
</div>
} Where books is now backed by the framework, and addBook would log the added book and change the view, all backed to the store. Though you have a new macro, you actually are much closer to "normal" coding! You just use variables and functions. The result is still a work in progress, but it's incredibly fun to use and almost ready for beta. If interested, feel free to reach out to me. In sum, I definitely support this idea and have been talking about it for a while! |
Only in some cases. In more complex cases it's more like "action occurred, execute business logic with new state and possibly update n components. @jimfb Do you have any documentation/gists explaining state hoisting? |
Echoing @ryanflorence, I too like the way react encapsulates state in component. We use react fairly heavily at myntra/flipkart, and keep bumping into a problem when server side rendering. To oversimplify, consider the following relations - view = ƒ(state) On the server we have access to props, and don't 'need' user inputs to generate 'first' rendered html. However, react doesn't run lifecycle/state methods on the server either (componentWillMount, setState, etc), meaning the effect of 'time' on a component is discarded on a server. This makes component lifecycle methods fairly useless for anything non-browser specific - network requests, async business logic, etc etc. Not ideal. Hence, 'frameworks' pull state and signals out of react / the components, and run them 'in-memory' and pass props down to a react component that only represents a snapshot of the tree for a given set of props. (Flux frameowrks, etc etc) What I'd really like for server side rendering is something like this - import {AsyncApp} from './app';
// ... express.js boilerplate ...
app.use('/app', (req, res) => {
var browser = new Browser('<div id=\'root\' />'); // a lightweight 'browser' that doesn't do dom events, xhr etc.
var el = <AsyncApp onDone={()=> {
// internally might have made a universal http request to load some data, etc etc
React.render(el, browser.getElementById('root'), ()=> {
res.send(browser.toHTML());
browser.destroy(); // cleanup
});
} />
}); Under the assumption that adding listeners like onClick etc are no-ops, and lifecycle methods like componentWillMount etc etc will be called. This would reduce reliance on redux etc for managing state that would be more elegantly handled inside a component. Thoughts? |
@threepointone FYI, componentWillMount is called when invoking React.renderToString. I use componentWillMount and Baobab (an immutable-like library that replaced my need for component state) to do async fetching on both server and client. But, it would be great if React could have something like this built-in so that I could use these components in any project. |
@briandipalma Just my comment here: #3653 (comment) It is a pattern well-known to the team, but one which we haven't talked about much in the docs. The basic idea/tldr is that you "hoist" the state up and out of the child component. The child component defines some sort of black-box data type, which the child (optionally) accepts from the parent. The child component stores all internal state into that object. If every component does this, then the entire state tree is effectively bubbled up to the root node. This gives the parent component full control over the state of the subtree. Parents can "reset" their children by passing new/empty state objects, can "snapshot" a child by saving a copy of the state object, can "reparent"/"clone" a child by restoring/reusing a state object, etc. Basically, it allows components to have fine-grained control of their children's internal state without breaking any abstractions. In my example at the top of this issue, I defined a view that shows on of three child views (eg. a tab view). The child views can be arbitrarily complex and retain any internal state even when the user changes tabs and then goes back to the first tab. This pattern already allows users to pull state out the state tree, and avoids introducing new APIs. IMHO, the pattern is woefully underutilized, largely because we've never documented it. The "downside" is that state is effectively managed in userland rather than being managed by React, but that's the natural/unavoidable outcome if you're externalizing the state tree. |
@jimfb How does that model fit in with re-rendering components when state has changed? |
@dantman Restoring a serialized/copied state allows you to re-render at any prior state in time. Re-using the "current" state object allows you to re-render a component with the current/latest state. Child components can trigger a re-render of themselves by calling Not sure I completely understand the question, but let me know if the above didn't answer it :). |
@jimfb Got it. I was talking about components being re-rendered when they change their state. Like what happens when you call Though I'm not sure I like the idea of calling |
Just throwing in an example I bumped into today. Here's a React component called Subdivide that has really complex internal state. Basically it lets you split a view into recursively nested views as many times as you like. (It's really cool, you should watch the video!) All of this is abstracted away—you just drop However we have another use case: persisting its state. Of course only the parent component(s) would be responsible for this. The usual React answer in this case is “make it controlled”. We could accept The reason I think it doesn't is because the data structure is too opaque, but the actions are actually not. The user may “split panes” or “unite panes” or “resize panes”. All of these might potentially make sense for the consuming application, and it might want to react to these things. What if an app might want to ignore (only) some of these actions? The way I'm proposing to solve it now is to export a reducer. If the consuming app is not using Redux (or similar reducer-based library), it can use I'm not a huge fan of not using React state—I agree it's a nice abstraction. But I think it's hard to hoist it up the tree unless you also export a computation (reducer in my case) so the caller may choose to use it in any way they like, and understand what's happening instead of receiving an opaque data structure in |
Is this the essence of your point? I.e. is seems like freedom to do those things is what is the goal. However, there is nothing that needs to change from the React programming model is there? Or is it too opaque?
|
Yes, this is the goal.
I've been thinking about it on and off for the last few months and I still have no idea. React makes some things super easy (e.g. potentially stateful children inside potentially stateful parents without any coordination between them) but as a tradeoff some things become hard (e.g. (re)hydrating the state tree, reparenting an element without destroying its state, rewiring child state changes to contribute to state changes somewhere up the tree). Whether it is a weakness of React programming model that can be fixed, or a particular set of tradeoffs that are its essence, is what I don't understand yet. |
I just published React Elmish Example as my ongoing attempt at understanding Elm Architecture niceties, tradeoffs and limits. Perhaps the examples there are convoluted—they very well may be—but I think they show the power of its state model. Parents having full control over interpreting child state updates makes implementing Undo/Redo, action log (or potentially time travel solution like I showed at React Europe) a breeze, as you can see from the source code. The downside is that writing “normal” code is way more ceremony than it is with React. Basically all state update flow is explicit, and instead of callbacks, you call static methods. If you add a stateful component as a child, you need to explicitly specify how to handle its actions. Of course there may also be big performance downsides. (Of course I implemented it on top of React so it's higher level. It's just a different higher level abstraction than |
I just want to leave a few thoughts here:
|
@skidding might have thoughts here, as he built https://github.com/skidding/cosmos |
what an awesome project! I'm definitely going to use that. |
Thank you @gaearon for the mention. While Cosmos' surface functionality addresses @ccorcos' 2nd point—a component styleguide, I feel the internal mechanics of Cosmos are more relevant to this thread. Namely, the small ComponentTree lib. It has two methods:
There's also an var whereUserLeftOff = localStorage.get('componentSnapshot');
ComponentTree.injectState(this.refs.dynamicComponent, whereUserLeftOff) This being said, I'll touch some of the comments in this thread.
@ryanflorence Pretty much, yes. ComponentTree covers get and set, you just need the onStateTreeChange event. So far I've relied on component callbacks to update the snapshots because it allowed me to control the granularity. Persisting a snapshot on EVERY state change might not be what you want, but maybe you could wrap some React lifecycle method like componentDidUpdate using a babel transform/webpack loader/etc and attach a callback that gets called whenever a component from the tree updates.
@jlongster The nice part about ComponentTree: The state is internal and the components are completely unaware of the whole they're part of. It allows you to implement state tree persistence/playback/undo-redo/etc using Stock React components.
@sebmarkbage While dev is the more popular use-case, I'd say both. See my local storage example above, I've used that pattern to persist snapshots of a subtree of a React app before and it worked great. So this is how Cosmos deals with state trees internally. But since there are core React people on this thread, it would be great to get some feedback on this. The Thoughts? |
@skidding I think its dirty having to deal with component local state. Thats the approach I took in my most recent project and I'm not sure I like it so much. @jimfb nice article -- I met Richard last week actually at the Elm meetup. I highly suggest reading through the elm architecture tutorial. It discusses a very interesting pattern similar to redux, but more generically. I played around and implemented the elm pattern in coffeescript. It definitely led me to some insights. |
I wrote a react+falcor integration recently (https://github.com/threepointone/falcro/). I wanted to represent the data fetches themselves as react components, using the child-function/ render-callback pattern to render the results. This works out fairly elegantly, with the downside that one can't statically analyze data dependencies anymore. So for server-side rendering, I worked around that by first rendering to string, while keeping a cache of falcor queries. I could then prime my falcor model with data corresponding to those queries, and render to string again. (implementation here, example here). Quite pleased, really. While doing so, I realized that I don't really want to 'see' the state tree; I really want to see the react component tree itself. Alternately, I just need a way to 'partially' render the tree, which gives me an opportunity to fetch data etc etc (using lifecycle hooks to 'register' queries, etc). Here's an example API of what I mean - // no changes in the browser
React.render(<App/>, el);
// on the server
let queries;
let tree = React.toTree(<App onPartial={q => queries = q}}/>);
// this `tree` could be completely opaque, though it would be nice
// to able to analyze it as a type/props/children structure
// preventing the need for the `onPartial` type callback
// I'd expect getInitialState, componentWillMount to trigger by this point.
// now fetch some data
let prefetched = await fetch(queries);
// then finally render to string
React.renderToString(<App data={prefetched} />); |
@brandonbloom wrote about "... interceding on all requests between the React diff engine and its state service, [to] effectively substitute our own state service. " in Local State, Global Concerns. I something like this possible using just JS? |
Does anyone feel like there is some sort of consensus on how to model something like this in React? At least enough so that ya'll would be open to a PR that explores this? What I gather from the conversation is that:
|
Reminder it's been three years and still no progress here. It would really, really help with our day to day, and I've been working on some impressively hacky and buggy workarounds, and I keep spending 1/2 days here and there patching them. With a simple key-path hook exposed by React this would all go away, and we could all finally reach the development Nirvana Brett Victor showed us 7 years ago. |
@natew Do you have anything written down about your solution and how you use it? Our experience from the test renderer and DevTools is that it’s really difficult to keep a stable api for this. |
@sebmarkbage it's a bit embarrassing but basically just using the DOM path to the view once it mounts to check for past state. Of course that gets blown away in many cases so we had it sort of re-hydrate and re-render down the tree and then have sub-component re-check and re-hydrate, etc. Still breaks in quite a few cases. We're using react-hot-loader, but essentially have a store system outside of setState. I just checked to see if I could persist the DOM key path we generate into a react-hot-loader wrapped parent component, and then check that on re-mount and use it to find the right key again. Weirdly, I don't see any state being preserved even in setState. But I need a bit more time to set up a better isolated example and work my way through. If rhl preserved state across hot reloads, we could just piggy back like that and it would probably be good enough. Edit: Actually after some more experimentation, I found that having a React import before rhl was not always logging an error, but it was causing state to be reset for many trees. I had somehow worked around that and didn't even notice as we don't use setState for much. After breaking things down and working back up to it, I discovered the import order bug. Then, patching that let me simplify and just rely on a HOC that has a simple unique ID in state that it passes down to store provider components. That seems to be working well. |
Have there been any further thoughts regarding this over the past year and a half? |
React provides the notion of implicitly allowing a child component to store state (using the
setState
functionality). However, it is not just used for business logic state. It is also used to remember DOM state, or tiny ephemeral state such as scroll position, text selection etc. It is also used for temporary state such as memoization.This is kind of a magic black box in React and the implementation details are largely hidden. People tend to reinvent the wheel because of it, and invent their own state management systems. E.g. using Flux.
There is still plenty of use cases for Flux, but not all state belongs in Flux stores.
Manually managing the adding/removing of state nodes for all of this becomes a huge burden. So, regardless you're not going to keep doing this manually, you'll end up with your own system that does something similar. We need a convenient and standard way to handle this across components. This is not something that should be 100% in user space because then components won't be able to integrate well with each other. Even if you think you're not using it, because you're not calling setState, you still are relying on the capability being there.
It undermines the ecosystem and eventually everyone will reconverge on a single external state library anyway. We should just make sure that gets baked into React.
We designed the state tree so that the state tree data structure would be opaque so that we can optimize the internals in clever ways. It blocks many anti-patterns where external users breaks through the encapsulation boundaries to touch someone else's state. That's exactly the problem React's programming model tries to address.
However, unfortunately this state tree is opaque to end users. This means that there are a bunch of legitimate use cases are not available to external libraries. E.g. undo/redo, reclaiming memory, restoring state between sessions, debugging tools, hot reloading, moving state from server to the client and more.
We could provide a standard externalized state-tree. E.g. using an immutable-js data structure. However, that might make clever optimizations and future features more difficult to adopt. It also isn't capable of fully encapsulating the true state of the tree which may include DOM state, it may be ok to treat this state differently as a heuristic but the API need to account for it. It also doesn't allow us to enforce a certain level of encapsulation between components.
Another approach is to try to add support for more use cases to React, one-by-one until the external state tree doesn't become useful anymore. I've created separate issues for the ones we we're already planning on supporting:
#4593 Debugger Hooks as Public API
#4594 Hibernating State (not the serialized form)
What else do we need?
Pinging some stake holders:
@leebyron @swannodette @gaearon @yungsters @ryanflorence
The text was updated successfully, but these errors were encountered: