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

RFC: Context selectors #119

Open
wants to merge 2 commits into
base: master
from

Conversation

@gnoff
Copy link

commented Jul 8, 2019

View Rendered Text

This RFC describes a new API, currently envisioned as a new hook, for allowing users to make a selection from a context value instead of the value itself. If a context value changes but the selection does not the host component would not update.

This is a new API and would likely remove the need for observedBits, an as-of-yet unreleased bailout mechanism for existing readContext APIs

For performance and consistency reasons this API would rely on changes to context propagation to make it lazier. See RFC for lazy context propagation

Motivation

In summary, adding this useContextSelector API would solve external state subscription incompatibilities with Concurrent React, eliminate a lot of complexity and code size in userland libraries, make almost any Context-using app the same speed or faster, and provide users with a more ergonomic alternative to the observedBits bailout optimization.

Addendum

Example: https://codesandbox.io/s/react-context-selectors-xzj5v
Implementation: https://github.com/gnoff/react/pull/3/files

gnoff added some commits Jun 28, 2019

@theKashey

This comment has been minimized.

Copy link

commented Jul 8, 2019

@dantman

This comment has been minimized.

Copy link

commented Jul 17, 2019

Hello @gnoff, I posted an alternative API idea awhile ago but never got around to writing an RFC for it and I'd like to get your opinion on it.

The idea was instead of adding a hooks specific selector was to allow creating "slices" of the contexts, which can then be used with any context api. The basic api was MyContext.slice(selector), i.e. contexts would have a new .slice method which would return a new context object. The original idea was in this comment.
Some examples:

// Static usage (possibly allows for extra internal optimizations?)
const EmailContext = UserContext.slice(user => user.email);
let MyComponent() {
  const email = useContext(EmailContext);
  // ...
}

// Dynamic usage with hooks
const KeyContext = useMemo(() => MyContext.slice(value => value[key]), [key])
const keyValue = useContext(KeyContext);

// Static usage with contextType
const EmailContext = UserContext.slice(user => user.email);
class MyComponent extends Component {
  static contextType = EmailContext;
  // ...
}

// Deep slices (imagine this is split up between different layers of an app)
const UserContext = AppState.slice(appState => appState.currentUser);
const EmailContext = UserContext.slice(user => user.email);
let MyComponent() {
  const email = useContext(EmailContext);
  // ...
}

I will admit there is one small flaw in my idea I didn't realize before. I thought this could be used universally, i.e. MyContext.slice could provide a selectable version of context that could be used across useContext, contextType, and <Consumer> and only contextType would have the limitation of being static-only. So I originally had some examples of using this with the consumer API. However I just realized that the slice api cannot be used dynamically with the consumer API. Because when you change the selector and get a new context back <SlicedContext.Consumer> will be a different component and as a result you'll get a full remount of everything inside it.

@j-f1

This comment has been minimized.

Copy link

commented Jul 17, 2019

You could work around the issue with <Consumer /> by making a custom component that uses useContext to create a HOC matching Context.Consumer’s API.

@dantman

This comment has been minimized.

Copy link

commented Jul 17, 2019

@j-f1 Interesting idea, though rather than HOC I presume you mean render function.

let Consumer = ({context, children}) => {
  const data = useContext(context);
  return children(data);
};

const KeyContext = SomeContext.slice(value => value[key]);
render(<Consumer context={KeyContext}>{keyValue => <span>{keyValue}</span>}</Consumer>);

You know, I'd almost think of recommending using something like that everywhere even in non-sliced contexts. The context API is really strange now that you think about it. When context was released <Context.Consumer> was the only way to consume a context and the best practice was to export just the consumer so you could control the provider. But now useContext and contextType instead take the context object directly. Which makes using a consumer off the context, it would make much more sense if you passed the context to the consumer instead of using a consumer from a context.

@gnoff

This comment has been minimized.

Copy link
Author

commented Jul 19, 2019

@dantman Interesting idea for sure. I think the way context is currently implemented internally would make some of the really dynamic stuff hard but the static stuff could probably be implemented in userland with some clever user of providers and consumers

One of the things that I don't think can be done in userland without changing the context propagation to run a bit lazier is to safely use props inside selectors for context. The issue is that during an update work may be done to change the prop so if a selector runs too early it will do so with the current prop and not the next one. this can lead to extra work being done but also errors in complicated cases where a context update is the thing itself that would change the prop.

The focus of my rfc and implementation PR is on hooks because it is the most dynamic use case. It would be relatively straight forward to add selector support for Consumer and contextType as well

As for your slice API I think you can also more or less create that on top of this without some of the challenges of creating dynamic contexts just by layering selectors and passing the result into new contexts as values

One more thing to point out, in one of my Alternatives I mention something that I think is genuinely a bit novel

// take in multiple context values, only re-render when the selected value changes
// in this case only when one of the three contexts is falsey vs when they are all truthy)
useContexts([MyContext1, MyContext2, MyContext3], (v1, v2, v3) => Boolean(v1 && v2 && v3))

Every other context optimization I've seen can only work on single context evaluations, and while I've not implemented the above api it is readily within grasp given how useContextSelector is implemented

It's almost more like a useComputedValue but by virtue of how react manages updates it can only reasonably react to changes in context values and not values in general

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants
You can’t perform that action at this time.