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

zero update propagation Context #3

Closed
theKashey opened this issue Jul 26, 2019 · 13 comments
Closed

zero update propagation Context #3

theKashey opened this issue Jul 26, 2019 · 13 comments

Comments

@theKashey
Copy link

(Documentation only) feature request

There is a set of cases, when you need to read context only once. Read without any future update, so no subscription is needed - mount effect are a great example of this.

Some libraries already using this pattern, for example sweet-state uses readContext("a hack") to just get the data from React, in a same "no update" mode.

I think this pattern should be documented, but it seems a bit wrong to use useContext to do so, as long as there are some expectations about how useContext is working.

What do you think about useUntrackedContextSelector? The same useContextSelector, but "untracked"?

@dai-shi
Copy link
Owner

dai-shi commented Jul 27, 2019

I understand your point. (I glanced react-sweet-state code using readContext before and I saw @JoviDeCroock 's code using React.useContext for that purpose.)

My preference is something like useReadContext. (no selector)

export const useReadContext = React.useContext;

However, I'm not sure if it should be a stable feature, because even if @gnoff 's RFC is accepted, this feature is not provided. (correct?)
What do you think?

@theKashey
Copy link
Author

useReadContext might be not enough, I've really would like to have a selector. So

export const useReadContext(context, selector = I => I) => selector(React.useContext(context)

@dai-shi
Copy link
Owner

dai-shi commented Jul 27, 2019

I don’t understand why you need a selector for non-updating context. Would you elaborate your use case please?

@theKashey
Copy link
Author

To have a common interface to read or derive something from it.

@gnoff
Copy link

gnoff commented Jul 27, 2019

I hadn’t seen sweet-state until now but a few observations

  1. the readContext technique is employed because react does not support selectors (per comments in the code) so if my RFC ever landed it would eliminate this particular need for a non updating read
  2. the provided values do not appear to ever change after initial construction so I can’t see why it needed to avoid using context in the regular way (maybe I’m missing some place where this is not the case)

Ive not personally come across a use case where reading passively is essential to correct logic or performance that couldn’t be achieved by existing tools. Is the goal here a simpler API or does the passive reading actually unlock otherwise impossible capabilities?

@theKashey
Copy link
Author

The common pattern - perform an API call something onMount. You have to provide some data to component, from any source, but there is no need to ever update the component if data updates.

@dai-shi
Copy link
Owner

dai-shi commented Jul 27, 2019

My opinion is probably similar to @gnoff 's.
What I think is idiomatic is to memoize a value in a Provider and useContextSelector in a component.

So, it becomes something like this.

const Provider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const actions = useMemo(() => ({
    increment: () => dispatch({ type: 'increment' }),
  }, []);
  return (
    <MyContext.Provider value={{ state, actions }}>
      {children}
    </MyContext.Provider>
  );
};

const Component = () => {
  const { increment } = useContextSelector(MyContext, s => s.actions);
  return (
    <div><button onClick={increment}>+1</button></div>
  );
};

The point is that a provider has a control on the value equality, not consumers.

@theKashey
Copy link
Author

Let's imagine that we are storing URL information in the Context - current page, dozen arguments, language settings derived from used domain, and so on.

  • it would be nice to read only what you need, like page or language from the URL using useContextSelector. Then - if something "not yours" is changed - you will not be updated.
  • it would be nice to read something once, like matched url to use it in mount effect, and as long as mount effect is a dependency effect - never run it again
  • it would be also nice to defer props you need to read to another function.
const MyDataLoader = () => {
  const settings = useReadContext(urlContext, getAllSearchParams);
  useEffect( () => {
     loadSomeData(settings); // context data are needed only here and only once. 
   }, []);
});

So the case I am talking about is to execute some action once. Which is quite common in Redux world for example - display a mount action, and do the rest in the thunk/saga - but with pure react stuff there is that "rest".

@dai-shi
Copy link
Owner

dai-shi commented Jul 27, 2019

If you follow exhaustive-deps, it becomes:

useEffect(() => {
  loadSomeData(settings);
}, [settings]);

Oh, and this requires getAllSearchParams to be a memoized selector!
It's non-trivial to me, but I wonder if it's @theKashey 's point.


Another point of discussion (might not be directly related):
There's no such "onMount" nor "once" things in the after-hooks era.
It's all about if a value is changed from previous one.
For example, here's an example that breaks your MyDataLoader.

const Parent = () => {
  const [urlInfo, setUrlInfo] = useState({});
  useEffect(() => {
    setUrlInfo(fetchUrlInfoSomehow());
  }, []);
  return (
    <urlContext.Provider value={urlInfo}>
      <MyDataLoader />
    </urlContext.Provider>
  );
};

@theKashey
Copy link
Author

There's no such "onMount" nor "once" things in the after-hooks era. + For example, here's an example that breaks your MyDataLoader.

You don't have to use hooks for synchronization - tasks could be quite different. For example, it would be much easier to use mount hooks if you have to call one stuff multiple times - for example for pagination. One component will load data for a page, and will clean up it later - but you might have more than one page visible at the screen - it's just about abstractions.

@dai-shi
Copy link
Owner

dai-shi commented Jul 27, 2019

Hmmm, I don't fully understand it even with the pagination example, but let me assume there's a use case. My original hesitation is how it would be implemented after the RFC becomes real. I wonder if "obervedBits=0" allows implementing useReadContext.

@theKashey
Copy link
Author

Probably you are right, and I could solve the problem by creating two context - one with propagation, and another without. At least I would be able to use only built-in React functions. Or readContext hack.

@dai-shi
Copy link
Owner

dai-shi commented Jul 27, 2019

A side note:
When I was developing react-tracked, I was wondering if I should create one context or two contexts for state and dispatch.
My preference and intuition is the latter, because that should be easier to see the state in react dev tools.
But the current implementation is the former, because one context is easier to create multiple containers and people are probably used to putting things in one context.
I'm not sure which will be more popular in the react community in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants