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

What is trackMemo and when to use it #30

Closed
dai-shi opened this issue Nov 20, 2019 · 13 comments
Closed

What is trackMemo and when to use it #30

dai-shi opened this issue Nov 20, 2019 · 13 comments

Comments

@dai-shi
Copy link
Owner

dai-shi commented Nov 20, 2019

What is state usage tracking

Let's say we have a state like this.

const state = { a: { b: 1, c: 2 } };

We then use this state in render:

return (
  <div>{state.a.b}</div>
);

With state usage tracking, it will mark .a.b is used.
So, only when state.a.b is changed, it triggers re-render.
If only state.a.c is change, it won't.

What would be a problem

If the render function is really pure, that is, it is consistent with using state in every render,
there's no problem.
However, suppose the function uses state.a.b for the first time,
but does not use it next time, then it will forget the usage marked in the first time.
If that's intentional, it fine. But, this could happen with memoization unexpectedly.

For example:

const b = useMemo(state => state.a.b, [state.a]);
const x = state.a.c + 1;

This might cause a problem.

  1. state = { a: { b: 1, c: 2 } }
  2. usage = ['.a.b', '.a.c']
  3. state = { a: { b: 1, c: 3 } }
  4. usage = ['.a.c'] // not ['.a', '.a.c'], because we only care leaves.
  5. state = { a: { b: 4, c: 3 } }
  6. no re-render

When we should use trackMemo

If a render function uses some kind of memoization based on object identity.
We should mark the whole object as used. This is what trackMemo is for.

Its implementation seems trivial, but this is only required if render is not pure for using state.
Only such case known so far is memoization. So, it's named trackMemo.

@dai-shi
Copy link
Owner Author

dai-shi commented Nov 20, 2019

@AlexGalays This is my quick write-up. Let me see how it works to answer your question. https://twitter.com/boubiyeah/status/1196876390060179456

@AlexGalays
Copy link

@dai-shi I think I see where you're getting at, but wouldn't the problem disappear if your useMemo function used the correct dependency? i.e:

const b = useMemo(state => state.a.b, [state.a.b]);

As the dependency array would always evaluate the proper proxy key.

@dai-shi
Copy link
Owner Author

dai-shi commented Nov 20, 2019

Correct. In that case, there's no problem. I used useMemo for this example, but the real issue is with React.memo.

I faced the problem. Ref: #21

This

<TodoItem id={todo.id} todo={todo} />

and this

const TodoItem = React.memo(({ todo }) => ...);

is troublesome.

@dai-shi
Copy link
Owner Author

dai-shi commented Nov 20, 2019

@faceyspacey I guess you have the same question in Twitter, so pinging you.
(It was in the context of reactive-react-redux, but it's the same story.)

@AlexGalays
Copy link

Hummm, but if you use a reducer and the convention of only having immutable updates, I don't see how todo not changing reference can be a problem. a todo would always and only change reference if you updated one of its (possibly nested) key?

@AlexGalays
Copy link

AlexGalays commented Nov 20, 2019

Oh, is it that, when the todo didn't change, React.memo uses its cached version, and you lose the information of which dependencies are used inside the child component render? If that's the case, can't you cache the last known information, and keep that till the related component is unmounted?

@dai-shi
Copy link
Owner Author

dai-shi commented Nov 20, 2019

a todo would always and only change reference if you updated one of its (possibly nested) key?

That's correct. OK, I think the basic explanation is missing.
We have a notion of "valuables", which I learned from https://github.com/theKashey/proxyequal.

usage = ['.a.c'] // not ['.a', '.a.c'], because we only care leaves.

I assume you don't yet get what this means: "only care leaves".

Let's see simple examples. (Forget about memoization for now)

const ComponentA = () => {
  const state = { a: { b: 1, c: 2 } };
  return <div>{state.a.b}</div>;
};
// we used ['.a.b'] in ComponentA

const ComponentB = () => {
  const state = { a: { b: 1, c: 2 } };
  const a = state.a;
  const b = a.b;
  return <div>{b}</div>;
};
// we touched `state.a` and `state.a.b`, it means it used ['.a.b'] in ComponentB
// not ['.a', '.a.b'], because it's the same as ComponentA

const ComponentC = () => {
  const state = { a: { b: 1, c: 2 } };
  return <Child id={state.a.b} a={state.a} />;
};
// we touched `state.a` and `state.a.b` in ComponentC
// if Child doesn't use the prop `a` and just ignore it,
// we assume it used ['.a.b'] in ComponentC, just like ComponentA and ComponentB
// this means, even if state becomes { a { b: 1, c: 3 } }, it won't trigger re-render
// even if `state.a` is referentially changed.

Now, usually the prop a will be used in <Child> like a.c, and if it's not used at all, we don't need to re-render.
However, if Child is wrapped by React.memo, React would skip rendering, in that case, Child wouldn't touch a.c even if it's used inside. This is the problem.

So, in the case:

const Child = ({ a }) => <div>{a.c}</div>

The ComponentC will mark ['.a.b', '.a.c'] as used for the first render, but
it will mark only ['.a.b'] as used for the second render if the Child is memoized.

@dai-shi
Copy link
Owner Author

dai-shi commented Nov 20, 2019

when the todo didn't change, React.memo uses its cached version, and you lose the information of which dependencies are used inside the child component render?

yes.

can't you cache the last known information

Maybe not, unless you could know which components are cached by React.memo deep in the component tree from the component with useTrackedState.

@lishine
Copy link

lishine commented Apr 12, 2020

You mentioned proxyequal. I looked at it and amazed about the idea. So a state object can be tracked for changes and then only the needed subscriptions will be rerun!
Do you use it?
AFAIK you use proxy only for usage tracking .

@dai-shi
Copy link
Owner Author

dai-shi commented Apr 12, 2020

I originally used proxyequal (more precisely in reactive-react-redux), and then I developed the same but slimmed implementation by my own. dai-shi/reactive-react-redux#23
It's now available in a separate package https://github.com/dai-shi/proxy-compare rewritten in TypeScript, and react-tracked v2 #43 uses it.

So a state object can be tracked for changes and then only the needed subscriptions will be rerun!

That's the whole point of useTrackedState in react-tracked and r-r-r.

AFAIK you use proxy only for usage tracking .

Uh, "usage tracking" is the word for that point.
Maybe, I misused the word? What was your understanding from "usage tracking"? (and how should I improve my description to tell that it's basically the same as proxyequal?)

@lishine
Copy link

lishine commented Apr 12, 2020

I thought that the proxy intended to see state.a.b usage in a component and create appropriate subscription. (This is what I called usage)
Then as in react-context-selector, when some part of the state changes, all subscriptions being run and components rerendered.
Apparently here proxy tracks usage and as well track changes. Actually yes, logical thing to implement, because already the state wrapped in proxy, so get as well as set can be tracked.
Very cool indeed!
Actually now understand, that only immer left for integration. And the whole API will be with mutable convenience.
(I saw immer in the docs as well) actually immer could be simply used in the reducer classically in user land.

Or immer could be integrated into async-reducer Async handlers, like easy-peasy does it.
I suppose that you have some grand plane for comprehensive all in one state management. But before the grand plane you wait to synchronise with react team planes, right?

There are two libs that use mutable Mobx like proxy interface but have immutable state. I don't know how they do it.
https://github.com/ForsakenHarmony/parket
https://github.com/davidgilbertson/react-recollect
Both use component wrappers Mobx style.
I used parket and really liked it, it is innovative and small 1.5gzip AFAIR. I don't know how it does it in that small package.

I have read react-tracked docs, they are very comprehensive. Saw there also redux Saga. I am very impressed with the level of investment.

BTW I'm thinking to learn Shakuhachi Japanese flute. Because I like it's meditative sound.

@dai-shi
Copy link
Owner Author

dai-shi commented Apr 13, 2020

Actually now understand, that only immer left for integration. And the whole API will be with mutable convenience.

Yeah! Glad to hear you get the point.

actually immer could be simply used in the reducer classically in user land.

This is my preference.

But before the grand plane you wait to synchronise with react team planes, right?

Not sure what you mean by this.

parket

Looks interesting and clean. Nested seems a bit tricky.

react-recollect

More featured. Not sure its hooks interface.

I am very impressed with the level of investment.

Glad to hear that.

learn Shakuhachi

Good for you. Never tried though.

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 8, 2020

Since v1.4.0, we export memo instead of trackMemo.
memo is a simple wrapper around React.memo and the usage is the same. It internally calls trackMemo.
With react-tracked, we recommend to use memo instead of React.memo.

@dai-shi dai-shi closed this as completed Jul 8, 2020
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