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

Context.write #89

Open
wants to merge 2 commits into
base: master
from

Conversation

Projects
None yet
10 participants
@acdlite
Member

acdlite commented Nov 20, 2018

This is a very early stage proposal. I'm opening this RFC as a way to explain the problem space. We're still exploring alternative solutions. If this were an ECMAScript proposal it'd be Stage 0.

Summary

Proposes an extension to the context API for updating the default context value. This would allow for React-managed state that lives outside the UI tree and is shared across roots.

Basic example

const Context = React.createContext(initialValue, contextDidUpdate);
// Update the global context value across all roots. Any context consumer that
// is not wrapped in a Provider will re-render with this value.
Context.write(newValue);
// Functional updates also supported for access to the previous value.
Context.write(prevValue => newValue);

function contextDidUpdate(newContext) {
  // Optional callback that fires whenever context changes
}

Rendered text

@acdlite acdlite force-pushed the acdlite:context-write branch from b69c699 to 2883daf Nov 20, 2018

@theKashey

This comment has been minimized.

theKashey commented Nov 20, 2018

For each context, React would have to maintain a separate version per root, as well as a separate queue of updates.

I was just thinking about it - per-root changeable defaultValue would allow "automatic Provider injection" effect, making some stuff(like React-Helmet, side-effect, etc) easier.

@gaearon

This comment has been minimized.

Member

gaearon commented Nov 20, 2018

We could also use this to store latest versions of component implementations in hot reloading, and instead of forceUpdate we'd update the context.

## How to handle multiple roots
The trickiest question how to deal with multiple roots. React does not guarantee consistency across roots; each root has its own commit phase, and suspending inside one root has no effect on the others. This isn't observable in synchronous mode, since React will block the main thread (including paint) until every root's commit phase has finished. In concurrent mode, however, React may yield in between each commit. With Suspense, the time between each commit phase could vary by many seconds.

This comment has been minimized.

@gaearon

gaearon Nov 20, 2018

Member

"The trickiest question is how"

@acdlite acdlite force-pushed the acdlite:context-write branch 2 times, most recently from 7d96361 to d3dd7be Nov 20, 2018

@acdlite acdlite changed the title from [wip] Context.write proposal to Context.write Nov 20, 2018

@acdlite acdlite force-pushed the acdlite:context-write branch from d3dd7be to 3bd121a Nov 21, 2018

Still, although caching in component state isn't ideal, it's arguably "good enough" for many use cases today.
Not so with Suspense. The Suspense model is that the first time React renders an IO-bound component, an exception is thrown when attempting to read the data. While React is waiting for the data to load, it continues rendering the rest of the tree, but it doesn't commit the result; the partially completed tree is _discarded_. Once the data has resolved, React attempts to render the entire tree over again from scratch. It may, as an optimiztion, reuse parts of the previous attempt. But it's not a guarantee. That means using local component state to cache data won't work, because that state will most likely not be persisted. The underlying priciple here is that you can't cache the intermediate results of a computation (server data) on the output of the computation itself (the React tree).

This comment has been minimized.

@tf

tf Nov 21, 2018

"The underlying principle"

@tf

This comment has been minimized.

tf commented Nov 21, 2018

In particular, consumer components could still be used with a provided context to replace the global default context with a fixture in tests, right?

@jfo84

This comment has been minimized.

jfo84 commented Nov 21, 2018

Great job @acdlite :)

An alternative model is to treat all the roots as siblings and commit them at the same time. This could be viable for single page React apps (where there aren't that many roots, anyway). It doesn't work so well in cases where React is embedded inside another framework (e.g. progressive enhancement of a server rendered app), where temporary inconsistencies may be desirable. For example, the opt-in API for concurrent mode relies on roots committing separately so that you can upgrade some roots to concurrent mode without upgrading the entire app. In an app with mixed synchronous and concurrent roots, a unified commit would mean that every call to `Context.write` has to be synchronous, which probably makes this option a non-starter.
In either of these models, React would need to track a global list of all roots in order to schedule updates on them. This isn't something we do currently, and it means roots would no longer be automatically garbage collected; discarded roots would need to be explicitly unmounted to remove them from the global list.

This comment has been minimized.

@hikire

hikire Nov 22, 2018

If by "discarded roots" you mean those which are just removed from DOM without calling unmountComponentAtNode(), why not use MutationObserver?

This comment has been minimized.

@acdlite

acdlite Nov 27, 2018

Member

How would that work? AFAIK MutationObserver only works for immediate children. It won't tell you if a tree is disconnected by an ancestor node.

### Immutable store (like React Redux)
Implementing immutable stores is straightfoward. Actions are applied in the render phase using the functional form of `Context.write`.

This comment has been minimized.

@mfrachet

mfrachet Nov 22, 2018

There's a point I'm misunderstanding. What does it mean concretely that

Actions are applied in the render phase using the functional form of Context.write?

This comment has been minimized.

@acdlite

acdlite Nov 27, 2018

Member

Explanation of render phase versus commit phase: https://twitter.com/acdlite/status/977291318324948992

This doesn't address the multiple root problem, nor does it work with React's server renderer, which does not have updates.
## Use Hooks

This comment has been minimized.

@timoxley

timoxley Nov 22, 2018

+1 for more symmetry between component state and context. Ideally, lifting component state & logic up to into a shared context wouldn't require rewriting the whole thing.

This comment has been minimized.

@diegohaz

diegohaz Nov 23, 2018

Agreed. I've been doing stuff on this direction on https://github.com/diegohaz/constate

Ideally, we should be writing local state until we really need to lift it up. Then, refactoring it into global/shared/contextual state should be something really simple.

@acdlite acdlite force-pushed the acdlite:context-write branch from 1f0a0a0 to 67a9fbb Nov 27, 2018

@acdlite acdlite force-pushed the acdlite:context-write branch from 67a9fbb to 1cc318c Nov 28, 2018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment