Skip to content
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

Open
sebmarkbage opened this issue Aug 10, 2015 · 40 comments
Open

Externalize the State Tree (or alternatives) #4595

sebmarkbage opened this issue Aug 10, 2015 · 40 comments
Labels
React Core Team Opened by a member of the React Core Team Type: Big Picture

Comments

@sebmarkbage
Copy link
Collaborator

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

@sebmarkbage sebmarkbage changed the title Umbrella: Externalize State (or alternatives) Umbrella: Externalize the State Tree (or alternatives) Aug 10, 2015
@jimfb
Copy link
Contributor

jimfb commented Aug 10, 2015

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 input). Serializability is decided by the implementor of ChildView.

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.

@sebmarkbage
Copy link
Collaborator Author

@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.

@jimfb
Copy link
Contributor

jimfb commented Aug 10, 2015

@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).

@mjackson
Copy link
Contributor

This is not something that should be 100% in user space because then components won't be able to integrate well with each other

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.

@gaearon
Copy link
Collaborator

gaearon commented Aug 11, 2015

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:

  1. Keep state shape normalized and like a database. Don't worry if it doesn't match the view hierarchy, use memoized computed data in view layer if I need it.
  2. Action log that lets me understand what and why happened in the app, and potentially implement that "lifted state" approach I described above for time travel.

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 dispatch function. It doesn't care if it's provided by Redux or something else. It doesn't care where the state is mounted, as long as it's mounted somewhere and dispatch changes it. In fact it could work just fine if dispatch was async and talked to server and the state would be transferred over the wire.

Not sure if it helps, but these are my thoughts. I'm not sure how to fit this into React.

@sebmarkbage
Copy link
Collaborator Author

@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?

@brandonbloom
Copy link
Contributor

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. get and put are pretty self-explanatory. init is like put, but the value (potentially a thunk of a value) is ignored if the key already exists. del is subject to the potential recursive nature discussed above (given path style keys). All of these methods can (should?) take collections, rather than operate on individual keys to enable atomicity and batch update efficiencies (eg using a transient data structure). There are possible variations on init/get/put like a unified update, but that's a whole separate topic.

Rollback/undo/etc can be optionally achieved by extending the store interface with two methods: capture():object and restore(object). The object can be completely opaque and offer only immutability of the object as a guarantee.

@sebmarkbage
Copy link
Collaborator Author

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.)

@brandonbloom
Copy link
Contributor

there is only really one reasonable way to implement it

That's exactly what I want. There shouldn't be a lot of these things and normal users shouldn't be writing them.

what would optional data structures for the state store try to solve? Better language interop?

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.

@jlongster
Copy link
Contributor

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.

@Aetet
Copy link

Aetet commented Aug 17, 2015

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.

@ryanflorence
Copy link
Contributor

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?

@sebmarkbage
Copy link
Collaborator Author

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.

On Sep 15, 2015, at 10:27 AM, Ryan Florence [email protected] wrote:

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(, el);

let tree = React.getStateTreeAtNode(el);

React.onStateTreeChangeAtNode(el, fn);

React.renderWithStateTree(, tree, el);
That would be enough to build some time-travel debugging tools, yeah?


Reply to this email directly or view it on GitHub.

@natew
Copy link

natew commented Sep 15, 2015

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 this.setState, or other React-specific abstractions. An example would be:

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!

@briandipalma
Copy link

what you really want which is: "change a variable, views update automatically".

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?

@threepointone
Copy link
Contributor

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)
state = ƒ(props, user, time)

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?

@jackmoore
Copy link

@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.

@jimfb
Copy link
Contributor

jimfb commented Sep 28, 2015

@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.

@dantman
Copy link
Contributor

dantman commented Sep 28, 2015

@jimfb How does that model fit in with re-rendering components when state has changed?

@jimfb
Copy link
Contributor

jimfb commented Sep 28, 2015

@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 forceUpdate() (or friends, like empty setState) after updating the current state object.

Not sure I completely understand the question, but let me know if the above didn't answer it :).

@dantman
Copy link
Contributor

dantman commented Sep 28, 2015

@jimfb Got it. I was talking about components being re-rendered when they change their state. Like what happens when you call setState.

Though I'm not sure I like the idea of calling forceUpdate or dummy setState calls whenever component state changes. That feels more like a dirty hack than it should be.

@jimfb
Copy link
Contributor

jimfb commented Sep 28, 2015

@dantman Well, that "hack" would/could be solved by #3920 but that's a whole other discussion :P.
But regardless, that's why forceUpdate exists (to trigger an update when state isn't tracked by React).

@gaearon
Copy link
Collaborator

gaearon commented Oct 4, 2015

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!)

screen shot 2015-10-04 at 11 32 06

All of this is abstracted away—you just drop <Subdivide DefaultComponent={MyContents} /> and it renders <MyContents /> in every nested part of the UI which you can drag to create or destroy.

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 value and onChange and pass opaque data structure there. This works for simple things (inputs) but I don't think this works for something as complex as Subdivide.

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 <Subdivide> as a “reducer-uncontrolled” component. In this case it manages its own state. If the consuming app is using Redux, we let you mount the reducer somewhere in your reducer tree, and connect <Subdivide> to this slice of state. <Subdivide> would then notice it's being connected (because it receives dispatch prop), and instead of changing its internal state, it will dispatch() actions that make sense to it. Then, the parent app can choose to serialize its state, re-hydrate it, ignore some of the actions, even react to them from other components, etc. Total freedom.

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 onChange. This is also the approach that Elm Architecture takes: any component exports both view and a computation (“update”).

@sebmarkbage
Copy link
Collaborator Author

serialize its state, re-hydrate it, ignore some of the actions, even react to them from other components, etc. Total freedom.

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?

On Oct 4, 2015, at 1:48 AM, Dan Abramov [email protected] wrote:

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 and it renders in every nested part of the UI which you can drag to create or destroy.

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 value and onChange and pass opaque data structure there. This works for simple things (inputs) but I don't think this works for something as complex as Subdivide.

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 as a “reducer-uncontrolled” component. In this case it manages its own state. If the consuming app is using Redux, we let you mount the reducer somewhere in your reducer tree, and connect to this slice of state. would then notice it's being connected (because it receives dispatch prop), and instead of changing its internal state, it will dispatch() actions that make sense to it. Then, the parent app can choose to serialize its state, re-hydrate it, ignore some of the actions, even react to them from other components, etc. Total freedom.

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 onChange. This is also the approach that Elm Architecture takes: any component exports both view and a computation (“update”).


Reply to this email directly or view it on GitHub.

@gaearon
Copy link
Collaborator

gaearon commented Oct 4, 2015

Is this the essence of your point? I.e. is seems like freedom to do those things is what is the goal.

Yes, this is the goal.

However, there is nothing that needs to change from the React programming model is there? Or is it too opaque?

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.

@gaearon
Copy link
Collaborator

gaearon commented Oct 4, 2015

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 setState() and I find it more powerful. If it was as easy to use as setState() that would be marvelous—but maybe I'm completely missing the point here, this stuff just hurts my brain so much.)

@ccorcos
Copy link

ccorcos commented Oct 12, 2015

I just want to leave a few thoughts here:

  1. remembering state between mounts: if you have a web version of a UITabBarController, then when you switch between tabs, you'd expect the component state (and scroll position!) to be the same as when you left them. Currently, the best way to do this is to use redux and save/restore the scroll position when the component unmounts/mounts.

  2. ui component styleguide: if you build react ui properly, then it should be trivial to build a ui styleguide demonstrating every component in every state. Currently this isn't a breeze unless you're using Redux due to component's internal state.

@gaearon
Copy link
Collaborator

gaearon commented Oct 12, 2015

  1. ui component styleguide: if you build react ui properly, then it should be trivial to build a ui styleguide demonstrating every component in every state. Currently this isn't a breeze unless you're using Redux due to component's internal state.

@skidding might have thoughts here, as he built https://github.com/skidding/cosmos

@ccorcos
Copy link

ccorcos commented Oct 13, 2015

what an awesome project! I'm definitely going to use that.

@ovidiuch
Copy link

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:

  • serialize – Generate a snapshot with the props and state of a component combined, including the state of all nested child components.
  • render – Render a component and reproduce a state snapshot by recursively injecting the nested state into the component tree it generates.

There's also an injectState method that isn't documented, which is pretty much the same as the render function but targeting an existing component instance, without touching its props. This is useful when you only track the state in a subtree of your app. E.g.

var whereUserLeftOff = localStorage.get('componentSnapshot');
ComponentTree.injectState(this.refs.dynamicComponent, whereUserLeftOff)

This being said, I'll touch some of the comments in this thread.

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?

@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.

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.

@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.

Yes but the key issue is: Is this a dev mode debugging tool or also a production API?

@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 serialize method is harmless, but the render and injectState ones basically call setState from outside component, which—even though it never happened—I'm afraid it might interfere lifecycle methods, since state should be private under normal circumstances.

Thoughts?

@jimfb
Copy link
Contributor

jimfb commented Nov 3, 2015

Good read: https://www.safaribooksonline.com/blog/2015/10/29/react-local-component-state/

@ccorcos
Copy link

ccorcos commented Nov 3, 2015

@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.

@threepointone
Copy link
Contributor

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} />);

@mheiber
Copy link
Contributor

mheiber commented Jan 23, 2016

@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?

@jquense
Copy link
Contributor

jquense commented Feb 23, 2016

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:

  • It a single (serializable?) state tree/atom allows for most of the use cases folks want to see.
  • State continues to be exposed as "local" to components (not changing any of the current component API)
  • There is disagreement on what the API should be to expose that state tree to a user

@natew
Copy link

natew commented Jul 28, 2018

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.

@sebmarkbage
Copy link
Collaborator Author

@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.

@natew
Copy link

natew commented Jul 28, 2018

@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.

@sebmarkbage sebmarkbage changed the title Umbrella: Externalize the State Tree (or alternatives) Externalize the State Tree (or alternatives) Sep 14, 2018
@necolas necolas added the React Core Team Opened by a member of the React Core Team label Jan 8, 2020
@bgergen
Copy link

bgergen commented Feb 3, 2020

Have there been any further thoughts regarding this over the past year and a half?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
React Core Team Opened by a member of the React Core Team Type: Big Picture
Projects
None yet
Development

No branches or pull requests