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

Comparison with related projects #1

Closed
dai-shi opened this issue Jun 18, 2019 · 73 comments
Closed

Comparison with related projects #1

dai-shi opened this issue Jun 18, 2019 · 73 comments

Comments

@dai-shi
Copy link
Owner

dai-shi commented Jun 18, 2019

Context value Using subscriptions Optimization for rendering big object Dependencies Package size
react-tracked state-based object Yes *1 Proxy-based tracking No 1.5kB
constate state-based object No No (should use multiple contexts) No 329B
unstated-next state-based object No No (should use multiple contexts) No 362B
zustand N/A Yes Selector function No 742B
react-sweet-state state-based object Yes *3 Selector function No 4.5kB
storeon store Yes state names No 337B
react-hooks-global-state state object No *2 state names No 913B
react-redux (hooks) store Yes Selector function Redux 5.6kB
reactive-react-redux state-based object Yes *1 Proxy-based tracking Redux 1.4kB
easy-peasy store Yes Selector function Redux, immer, and so on 9.5kB
mobx-react-lite mutable state object No *4 Proxy-based tracking MobX 1.7kB
hookstate N/A Yes Proxy-based tracking No 2.6kB
  • *1 Stops context propagation by calculateChangedBits=0
  • *2 Uses observedBits
  • *3 Hack with readContext
  • *4 Mutation trapped by Proxy triggers re-render
@dai-shi
Copy link
Owner Author

dai-shi commented Jun 18, 2019

state value/object

Hm, I think context value in constate and unstated-next is state-based object as well. More precisely, output of useValue hook.

Updated.

@dai-shi
Copy link
Owner Author

dai-shi commented Jun 18, 2019

Benchmark results for react-tracked, reactive-react-redux and react-redux.
the forked repo

screenshot1

screenshot2

screenshot3

@theKashey
Copy link

Could you please add https:/atlassian/react-sweet-state to the comparison

@dai-shi
Copy link
Owner Author

dai-shi commented Jun 19, 2019

Added in the table. (Do you also mean to the benchmark?

@theKashey
Copy link

To be honest I am more concerned about API and edge cases, as long as the majority of the work I am doing is faaaaaaaaar from real time.
Performance should not be terrible. Performance should not degrade due to application size or developer mistakes.

@dai-shi
Copy link
Owner Author

dai-shi commented Jun 19, 2019

I wonder how you would evaluate edge cases...

@theKashey
Copy link

All alternate state managers were created to solve some edge cases. You may try to figure out that problem (and in which way) some library was intended to solve, and try to apply the same case to another library)
Usually:

  • cascade update progression
  • componentization (aka isolation or microfrontends)
  • verbosity
  • working in a big team
  • working with a big state
  • frequent updates
  • derived data generation
  • stability and predictability
  • learning curve
  • more to come

@dai-shi
Copy link
Owner Author

dai-shi commented Jun 30, 2019

There was a misunderstanding of mine about react-sweet-state. It uses readContext. Modified.
The README doesn't show the container. It can be used without Context.Provider because it reads the context default value.

Yeah, zustand by @drcmda is only a popular library with non-context solution. I've seen small/experimental libraries without context though.

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 1, 2019

I made a minimal example to compare actual bundle size with some libraries.

Build JS sizes
$ cd examples/counter-react-tracked
$ wc build/static/js/*.js
       1    2263  123254 build/static/js/2.0c3afdb5.chunk.js
       1      16    1066 build/static/js/main.088f65ad.chunk.js
       1      37    1502 build/static/js/runtime~main.a8a9905a.js
       3    2316  125822 total

$ cd examples/counter-constate
$ wc build/static/js/*.js
       1    2190  120150 build/static/js/2.f7ca4f1e.chunk.js
       1      16    1109 build/static/js/main.e21dfac9.chunk.js
       1      37    1502 build/static/js/runtime~main.a8a9905a.js
       3    2243  122761 total

$ cd examples/counter-zustand
$ wc build/static/js/*.js                  
       1    2222  121214 build/static/js/2.20ef84af.chunk.js
       1      18    1079 build/static/js/main.44ecf751.chunk.js
       1      37    1502 build/static/js/runtime~main.a8a9905a.js
       3    2277  123795 total

$ cd examples/counter-reactive-react-redux
$ wc build/static/js/*.js
       1    2976  148172 build/static/js/2.40846575.chunk.js
       1      18    1117 build/static/js/main.e8a6d259.chunk.js
       1      37    1502 build/static/js/runtime~main.a8a9905a.js
       3    3031  150791 total
JS file size (bytes) Diff (bytes)
react-tracked 125,822 +3,061
constate 122,761 0
zustand 123,795 +1,034
reactive-react-redux 150,791 +28,030

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 8, 2019

https:/reusablejs/reusable by @adamkleingit and @morsdyce
This is a bit different from others. In terms of its implementation, the context value is multiple stores. Looks to me like multiple constate instances + selector interface. Interesting.

@avkonst
Copy link

avkonst commented Jul 11, 2019

You may want to include https:/avkonst/react-use-state-x to your research too. It is tiny, type-safe, feature-rich useState-like React hook to manage complex state (objects, arrays, nested data, forms) and global stores (alternative to Redux/Mobx). It's size is reported here: https://bundlephobia.com/[email protected] but it includes Typescript headers too.

@avkonst
Copy link

avkonst commented Jul 11, 2019

It would be interesting to see it's performance, but I have not done much about optimising the linear performance. I was more focused on reducing number and "size' of renders on state changes.

@theKashey
Copy link

react-powerplug in hooks form 👍

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 11, 2019

@avkonst If I understand your implementation correctly, Observer triggers re-render for any state change. That’s just like the normal context. So, I might be misunderstanding something. It would be nice if there were a minimal example in codesandbox for global state.

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 11, 2019

Where can I find react-powerplug hooks form?

@avkonst
Copy link

avkonst commented Jul 11, 2019 via email

@theKashey
Copy link

So not quite “tracked” ;)

@avkonst
Copy link

avkonst commented Jul 11, 2019

For your table for react-use-state-x.
Context value in the internals of the implementation is a reference to the global state object. ValueLink returned by useStateLink, knows how to trace and unpack this reference in order to return the actual state value.
There are no dependencies.
Size is 2kb zipped, includes typescript headers, which should not be counted.
Optimisations for big object are:
Cache of nested value in the ValueLink instance.
Use multiple independent stores.
Use multiple observers deeper in the hierarchy instead of a top level single one context provider.
Use derived state link to manage local component statendependent on the global. See docs with the example about it.

@avkonst
Copy link

avkonst commented Jul 11, 2019

The change to the value in the store is done via ValueLink set API, it is not js proxy and has got other functions, eg validate state according to user's validators.

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 11, 2019

So, If I were to add an item:

Context value Using subscriptions Optimization for big state object Dependencies Pakage size
react-use-state-x mutable reference to state object (which is basically equivalent to store) Yes (observer pattern?) Multiple stores No 2.9kB

Don't worry too much about the bundle size. Mine also includes unused functions. If you are interested, create a simple counter example and compare.

As far as I understand, your library focuses on mutating state in a good way.
My comparison table is about re-renders for state updates. (no mutation in mind)


That said, I'm getting to understand your lib. useStateObject and useStateArray are easier to get.
Whereas useStateLink looks a bit complex to me, maybe because I'm not very used to observer.
BTW, if you are OK with using Proxy, link.value can do the same job of link.nested, something like immer but different. (Ah, is it like MobX?)

@avkonst
Copy link

avkonst commented Jul 11, 2019 via email

@avkonst
Copy link

avkonst commented Jul 11, 2019 via email

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 11, 2019

Will add it to the comparison table.
The optimization column is about rendering state (not clearly stated though),
so your point should be in another column, but I don't create a new column at the moment.
(Maybe, I should limit the table for Redux-inspired/reducer-oriented libraries.)

I think I understand your point. As you may know my another library react-hooks-global-state has type-safe mutation but just for a (shallow) object, so it's like your useStateObject. And, you are basically doing it recursively for a nested object. That could be tough for typing.

Speaking of types, I found typing in immer works well. I didn't realize Proxy helps typing mutation operations.
Speaking of immer, have you looked into easy-peasy? It might be close to your mind set. (But, it's action based, so slightly different.)

I have an interesting idea, If you expose a custom hook for local state, it can work with react-tracked nicely. Wait a minute, you already have one useLocalStateLink. Let me try creating an example.

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 11, 2019

https://codesandbox.io/s/react-tracked-example-trb02
There you go. Enjoy.


If you understand what's going on, here's my suggestion: if you focus on creating a custom hook for local state that has handy setter (with typing), you can turn into global state with react-tracked, constate or anything you like. As I guess typed setters are the strength of your library, let the job for global state out.

@avkonst
Copy link

avkonst commented Jul 11, 2019

Thanks. I will have a look. I do not understand now how usetracked hooks into ValueLink :)
Regarding optimisations, I tend to use multiple stores instead of multiple contexts. These are two different optimisations with different effect.

@avkonst
Copy link

avkonst commented Jul 11, 2019

I understand your lib uses undocumented internal API. I am a bit concerned that it maybe gone in the future... What do you think?

@dai-shi
Copy link
Owner Author

dai-shi commented Jul 12, 2019

I'd expect when they obsolete the unstable API, a new proper API will be provided.
If that's not the case, still I could implement without context and only with subscriptions (like zustand), but not sure it will be concurrent mode friendly.

multiple stores / multiple contexts

If I understand correctly, if one uses multiple stores, they will use context provider for each store (in this case a Observer is a provider). I guess we are talking about the same thing. There are rendering optimization and mutation optimization. The comparison table is only about the former.

You could put multiple stores in a context (see reusable), but it doesn't seem to be so in your lib.

@avkonst
Copy link

avkonst commented Jul 12, 2019

OK. I see what you are doing in your library. Tracking of used within a component props matched with detected changes to force update for the affected subscribers. I think tracking has got the potential.

I have tried the approach with subscribers in the past (just subscribers, not the tracking) and got performance issues on the use case like this (it is artificial to demonstrate what happens when nested components rerender too):

const ComponentA = (props: { depth: number }) => {
    const state = useTrackedState();
    if (props.depth > 100) {
        return <></>;
    }
    return <><p>{props.depth}</p><ComponentA depth={props.depth + 1} /></>
}

I found that plain rerender all down to from the context provider works faster in this case.

I have tried observedBits feature of react to learn what it does. I see that observedBits do not make any effect, because the root level state change causes full tree rerender.

  • Could you please help me to understand why observedBits do not make any effect? (playground is here: https://codesandbox.io/s/react-tracked-example-j4o93 - I think it does the same as yours react-hooks-global-state except no name tracking, just hard coded bits).
  • How should I use context provider and useContext to trigger rerender of only components which use the context but not of the full tree?

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 15, 2019

It will not loose typing.

Oh, yes. You are totally right. That's my bad.

But it will have other disadvantages.

Would you look into MobX and see how they solve the issues, or not?

@avkonst
Copy link

avkonst commented Aug 15, 2019 via email

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 15, 2019

I personally didn't write any benchmark code for MobX.
I tried mobx-react-lite and knew it would only trigger render for the parts changed.

But, the point I'm raising is not about the render part, but the mutation part.

What do you mean? What would you like me to look at?

I didn't yet fully understand all "disadvantages" you mentioned.
Wondered if it's solved in MobX or not.
If it's solved, you learn from it. If not, that's the point your lib beats MobX, right?
(And, if you already have an answer, no need to look at.)

@avkonst
Copy link

avkonst commented Aug 16, 2019

"... all "disadvantages" you mentioned. Wondered if it's solved in MobX or not."
It is not solved in MobX, they just do not have some of the features of the Hookstate, it is not their scope, eg. valuelink pattern for state 'leaves', various features like validation, etc.. For example, I have no idea how to put Performance metrics counter with Mobx similar to the one I put for Large Table performance demo. This is maybe where Hookstate beats Mobx.
However, I guess the main point where Hookstate beats Mobx is the following. As far as I understand, Mobx optimizes the rendering only when it is used like this by multiple consuming components. That is all clear, implementation is based on components putting global state subscribers. As far as I understand Mobx does not provide the analogy of this one or this one. It will rerender the whole upper lever state consuming component. Maybe I am missing something and Mobx can do this too, but I have not found how to achieve the same with Mobx. So, I asked if you have got some benchmarks.

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 16, 2019

Thanks for the explanation.
Unfortunately again, I don't have any benchmarks.

@avkonst
Copy link

avkonst commented Aug 16, 2019

They tell here https://mobx-react.js.org/observer-hook:
Despite using useObserver hook in the middle of the component, it will re-render a whole component on change of the observable. If you want to micro-manage renders, feel free to use or useForceUpdate option (for advanced users).
But I can not understand how a client can achieve it and how not to mess up with optimization, which could result in missed rerendering.

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 16, 2019

The reason I thought the benchmark target of your lib is MobX, is about mutations not renders.
But, it seems that it's not the case, so you might not need to compare with MobX.

@avkonst
Copy link

avkonst commented Aug 16, 2019

I have thought about putting the same large table performance demo using Mobx to compare, but I have not figured out how to achieve it with Mobx :) It seems like each table cell should be a consumer of the global state to make sure only the affected cells are rerendered, or there is another way?

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 16, 2019

I'm willing to help to compare with react-tracked. 😄
Honestly, I'm not very familiar with MobX and I did have a misconception before, so I can't be of much help.

@avkonst
Copy link

avkonst commented Aug 16, 2019

"I'm willing to help to compare with react-tracked. 😄"
Yes, I will be glad to see the results! And the benchmark code base.

@avkonst
Copy link

avkonst commented Aug 27, 2019

Hi @dai-shi Hookstate now supports IE11 via @hookstate/proxy-polyfill plugin. Example project is here. Could you please let people know, who were interested in ES5 compatible state management system with usage tracking support. Thanks.

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 27, 2019

I don’t read the code as I’m on vacation now, but do you deal with non-get proxy handlers?
I don’t know anybody who would be interested in your lib with proxy-polyfill. Sorry about that.

@avkonst
Copy link

avkonst commented Aug 27, 2019 via email

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 27, 2019

Interesting. So, you don’t use/need google lab’s proxy-polyfill.

There was a general discussion whether proxy can be polyfilled for IE11 in the Redux community.

@avkonst
Copy link

avkonst commented Aug 27, 2019 via email

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 27, 2019

I don’t have the link right now, but it’s the long discussion about hooks.

By the way, if you can implement your lib without proxies that easy, you wouldn’t need proxies from the start, would you? That might be simpler.

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 27, 2019

Let’s continue the discussion in avkonst/hookstate#6

I will close this issue later as it becomes very long mostly discussing about hookstate.

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 31, 2019

Copied the table in README.

@dai-shi dai-shi closed this as completed Aug 31, 2019
@bySabi
Copy link

bySabi commented Oct 23, 2019

@dai-shi can you add hookleton and garfio to the comparison table?

author here.

@dai-shi
Copy link
Owner Author

dai-shi commented Oct 24, 2019

@bySabi Thanks for coming here.
Unfortunately, if I were to add your libs, there would be so many other libs I would need to consider.

If there's a huge demand on a comparison table, I should probably start a new repo. Hm, that would not be a bad idea. I could add more columns to compare. Adding benchmarks would also be nice.


BTW, your implementation of

const forceUpdate = s => ~s;

might have an issue with batched updates.

@bySabi
Copy link

bySabi commented Oct 24, 2019

@dai-shi thanks for take the time for look at the code. Please ping me when the new comparison repo is done.

I also thought that s => ~ s might have problems with the batch update but in practice I haven't had any.
I have not looked at the React code to know how the algorithm works in detail.
In my case the state changes using the sequence "infinite" -1 0 -1 0 -1 ... I don't think React risks ignoring such a simple state change.
The other solutions out there like: !BOOLEAN and even increasing a counter could have the same problem of ignoring changes of state.
What solution do you propose?

@dai-shi
Copy link
Owner Author

dai-shi commented Oct 24, 2019

I used to use s => !s but there was an issue reported. If you update twice in a single batch, React "bails out" rendering and users won't see the update. This issue is easily reproducible. Just update 2x times in a batched callback.

Now, I use s => s + 1 in my libs. This is not infinite as you may imply.
More precisely, it should be s => s === Number.MAX_SAFE_INTEGER ? Number.MIN_SAFE_INTEGER : s + 1, maybe?

@bySabi
Copy link

bySabi commented Oct 24, 2019

I imagine you don't have the link to the issue at hand :-)? ... I would like to try an issue repo

s => s === Number.MAX_SAFE_INTEGER? Number.MIN_SAFE_INTEGER: s + 1
It would be the most purist but the least performant :-)

Now that I am looking at how to add Typescript support, I will probably update forceUpdate

@dai-shi
Copy link
Owner Author

dai-shi commented Oct 24, 2019

I imagine you don't have the link to the issue at hand :-)?

Let me see...

dai-shi/reactive-react-redux#20 (comment)
dai-shi/reactive-react-redux@c9132a9

Here you go.

@bySabi
Copy link

bySabi commented Oct 24, 2019

Thanks for the link, they have been useful to understand the problem.

Well, it seems that in my case the 'forceUpdater' s => ~ s is not a problem because it is called from a useMemo that "observes" the changes in the user's provided hook output so React is in charge of tracking, and Batching is desirable. https:/bySabi/hookleton/blob/master/src/index.js#L51

Using s => s + 1 does not change anything, in my case. Just as in this example:

import React from 'react';

const App = () => {
  const [s, forceUpdate] = React.useReducer(s => ~s);
  //const [s, forceUpdate] = React.useReducer(s => s + 1, 0);

  console.log('render: ', s); // should be out: "render 0"

  React.useMemo(() => {
     forceUpdate(); // should be out: "render 1"  -- NOT RENDERED --
  }, []);
  React.useMemo(() => {
     forceUpdate(); //should be out: "render 2"
  }, []);

  return null;
}

export default App;

When I create hookleton I try to make it completely user's hook output transparent and let the React "hook" engine do all the work.

Although surely there is something that escapes me. The code needs other hawk eyes :-)

Thanks for your time.

@dai-shi
Copy link
Owner Author

dai-shi commented Nov 21, 2019

@bySabi Hi, I created a new repo. https:/dai-shi/lets-compare-global-state-with-react-hooks

@RayaneNekena
Copy link

@dai-shi It's a really interesting benchmark. I'm a React developer but I'm a beginner in terms of measuring performance. How did you make them ? How do you know which features to compute and how did you calculate them ? I want to know how to make a performance evaluation of a library or tools like that.

Benchmark results for react-tracked, reactive-react-redux and react-redux. the forked repo

screenshot1 screenshot2 screenshot3

@dai-shi
Copy link
Owner Author

dai-shi commented Aug 23, 2023

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

5 participants