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

How is jotai different from zustand? #13

Closed
dai-shi opened this issue Sep 8, 2020 · 38 comments · Fixed by #94
Closed

How is jotai different from zustand? #13

dai-shi opened this issue Sep 8, 2020 · 38 comments · Fixed by #94

Comments

@dai-shi
Copy link
Member

dai-shi commented Sep 8, 2020

Name

Jotai means "state" in Japanese.
Zustand means "state" in German.

Analogy

Jotai is close to Recoil.
Zustand is close to Redux.

Where state resides

Jotai state is within React component tree.
Zustand state is in the store outside React.

How to structure state

Jotai state consists of atoms (i.e. bottom-up).
Zustand state is one object (i.e. top-down).

Technical difference

The major difference is the state model. Zustand is basically a single store (you could create multiple stores, but they are separated.) Jotai is primitive atoms and composing them. In this sense, it's the matter of programming mental model.

Jotai can be seen as a replacement for useState+useContext. Instead of creating multiple contexts, atoms share one big context.

Zustand is an external store and the hook is to connect the external world to the React world.

When to use which

  • If you need a replacement for useState+useContext, jotai fits well.
  • If you want to update state outside React, zustand works better.
  • If code splitting is important, jotai should perform well.
  • If you prefer Redux devtools, zustand is good to go.
  • If you want to make use of Suspense, jotai is the one.
  • If you need a global store to bridge between two react renderers, zustand will only work. (no longer valid with jotai's provider-less mode, but zustand may work better.)
@Drakota
Copy link

Drakota commented Sep 10, 2020

Jotai looks fun to play with, thank you!

I've just realized that Zustand and Jotai were made by you guys @ react-spring, this brings the question, what kind of problems have you run into that justified the development of two different (and possibly concurrent) state management libraries? Is there any concerns of one library being cannibalized by the other one or splitting efforts?

@dai-shi
Copy link
Member Author

dai-shi commented Sep 10, 2020

While we were discussing about improving Zustand, we had two major points that we can't implement in Zustand. The first is that Zustand is an external store by design. We want a lib that only store states internally in React, which we believe supports CM better. The second is atom model that is often requested in some Zustand issues. We can't simply add the different concept into Zustand without influencing the feature complete API. So, we decided to release another lib from the same org. Each has different goal and concept. Both are more or less unopinionated and let users choose.

@sonatard
Copy link

Thanks for awesome library!!

The first is that Zustand is an external store by design. We want a lib that only store states internally in React.

What does it mean that ”extenal store” and ”store states internally in React ”?

@dai-shi
Copy link
Member Author

dai-shi commented Sep 11, 2020

@sonatard "Internal state" means React.useState, everything else is basically external (from the React point of view). Usually, we create a store object and put state in it, so I called "external store."

@merisbahti
Copy link
Collaborator

merisbahti commented Sep 18, 2020

I was curious about the statement:

Jotai state is within React component tree.

So I made a lib which looks/feels like jotai/recoil but the atoms are outside the tree:

https://github.com/merisbahti/klyva

Take a look at the tests:
https://github.com/merisbahti/klyva/blob/master/test/atom.test.ts

The atom exposes two functions: write and subscribe, so it’s pretty trivial to make a useAtom from that.

But also, for fun, I exposed a function focus, which is an addition to jotai which lets you use optics to ”slice” an atom.
(Examples here: https://github.com/merisbahti/klyva/blob/master/test/atom-perf.test.ts)

Thanks again for this library, it’s very inspiring.

ps. useAtom would look like this:

const useAtom = <T>(atom: Atom<T>) => {
  const [value, setValue] = React.useState(atom.getValue())
  React.useEffect(() => atom.subscribe(setValue), [atom])
  return [atom.getValue, atom.update]
}

@dai-shi
Copy link
Member Author

dai-shi commented Sep 18, 2020

@merisbahti Hi! Glad to see you got inspired and created something.
Yeah, it's an important distinction for me. We believe that having state in React as single source of truth can give more benefit in CM. But, I can't easily convince someone about it: a) CM is not yet ready and we don't know what it's like when it lands, b) the benefit can be trivial for most apps.
Jotai is the same as Recoil, in this sense, trying to be in React state.
Having state in external store outside React is also useful in certain cases. We have zustand for such use cases. It is technically possible (and rather easy) to add atom model in zustand, but currently we don't have such a plan.

btw, you might be interested in this issue and the discussion: #44

@merisbahti
Copy link
Collaborator

aah I was wondering why that was the case, I see, well then that explains it for me! thanks!

btw I'm the author for that linked issue ;)

@dai-shi
Copy link
Member Author

dai-shi commented Sep 18, 2020

aaah, what a shame. didn't pay much attention. (I remember your profile picture, thought it's on Twitter, looked for it...)

@ozio
Copy link

ozio commented Sep 25, 2020

I need the same article, but for Recoil.

@dai-shi
Copy link
Member Author

dai-shi commented Sep 25, 2020

You mean the comparison between Jotai and Recoil?

@ozio
Copy link

ozio commented Sep 25, 2020

You mean the comparison between Jotai and Recoil?

Yup.

I saw small explanation in the README.md, but it's too short to decide. For me it sounds like "we don't use strings as a keys - that's the main reason to use Jotai". In the other words, if I'll make a simple wrapper for Recoil to exclude string keys - what's the difference will be then?

@dai-shi
Copy link
Member Author

dai-shi commented Sep 25, 2020

Alright. First off, I'm familiar with Jotai and Zustand about their implementations, the backgrounds and goals. However, I'm not familiar with Recoil implementation at all. I grasp the Recoil docs, but don't understand it fully. Hence, it's pretty biased and maybe not accurate. Having that said, yes the biggest difference is no string keys. Recoil has it by design to allow serialization easily. Jotai avoid the string keys to simplify the API for use cases without serialization, and serialization is left for developers (we are trying to come up with best practice about serialization.) The API design is also different. While Recoil provides various functions to fulfill their requirements, Jotai's focus is to provide primitive APIs, and everything else is built on it. This will help learning the APIs in a leaner way, and creating another function based on primitives (encouraging ecosystem). The implementation should be very different, but let's not go in deep this time.

Here's the summary.


How is jotai different from recoil?

Developer

  • Jotai is developed with collective work by a few developers in Poimandres (formerly react-spring) org.
  • Recoil is developed by a team at Facebook.

Basis

  • Jotai is focusing on primitive APIs for lean learning, and it's unopinionated. (The same philosophy with Zustand)
  • Recoil is full featured for big apps with various requirements.

Technical difference

  • Jotai depends on atom object referential identities.
  • Recoil depends on atom string keys.

When to use which

  • If you want to learn something new, either should work.
  • If you want a Facebook product, choose Recoil obviously.
  • If you like Zustand, Jotai would also be pleasant.
  • If your app heavily requires state serialization (storing state in storage, server, or URL), Recoil comes with good features.
  • If you need React Context alternatives, Jotai comes with enough features.
  • If you would try to create a new library, Jotai might give good primitives.
  • Otherwise, both are pretty similar about the general goals and basic techniques, so please try both and give us a feedback.

@fjcalzado
Copy link

This kind of comparisons are great @dai-shi, I would add them to front page documentation.

@dai-shi
Copy link
Member Author

dai-shi commented Oct 4, 2020

I would add them to front page documentation.

But they are rather too big to fit in the readme. What would you suggest otherwise?
btw, we should be preparing a new web site, so that would help it sooner or later.
Maybe we'd put the comparison in docs and add links in the mean time.

@dai-shi dai-shi pinned this issue Oct 4, 2020
@fjcalzado
Copy link

Yes, sorry, I meant adding links, agree. I'd keep front page tidy & simple, with quick access to the most relevant topics. I find these kind of comparisons extremely useful as long as they are based on technical facts, as you did (instead of marketing slogans as you can see in many other libraries).

@JamesGelok
Copy link

@dai-shi what does CM stand for?

@dai-shi
Copy link
Member Author

dai-shi commented Jan 25, 2021

Concurrent Mode. Still experimental one. https://reactjs.org/docs/concurrent-mode-intro.html

@dai-shi dai-shi unpinned this issue Mar 6, 2021
@diegoarcega
Copy link

awesome explanation guys. I would be very nice to have a comparison between Zustand or Jotai and Redux.

Question: what are the differences of having the state in react (jotai) over having it externally (zustand) ? I get that the zustand store can be accessed outside of the react world, but why have it in react ?

@dai-shi
Copy link
Member Author

dai-shi commented May 21, 2021

awesome explanation guys. I would be very nice to have a comparison between Zustand or Jotai and Redux.

Do you mean a comparison between jotai and redux? What kind of comparison do you expect?

Question: what are the differences of having the state in react (jotai) over having it externally (zustand) ? I get that the zustand store can be accessed outside of the react world, but why have it in react ?

zustand (and valtio) is module state, and it's basically global in JS memory.
jotai (and recoil) is react state, and although atom definition is global, its state is not.

You can do something like this:

const countAtom = atom(0)

const Counter = () => {
  const [count, setCount] = useAtom(countAtom)
  return <div>{count} <button onClick={() => setCount((c) => c + 1)}>+1</button></div>
}

const App = () => (
  <>
    <Provider>
      <Counter />
      <Counter />
    </Provider>
    <Provider>
      <Counter />
      <Counter />
    </Provider>
  </>
)

jotai state is like useState and can't be directly accessed outside the react world.

@diegoarcega
Copy link

Do you mean a comparison between jotai and redux? What kind of comparison do you expect?

Sorry, I mean a comparison between jotai, zustand and redux, It would be more of a comparison of speed, if one has quirks (zombie child issue for example), caveats, limitations.

zustand (and valtio) is module state, and it's basically global in JS memory.
jotai (and recoil) is react state, and although atom definition is global, its state is not.

Are there performance penalties or any other caveats of picking module state over react state ?
If I am able to use the state everywhere in the app, even outside of the react world (like zustand), why would I choose to be limited to where the Provider is wrapping a component?

@dai-shi
Copy link
Member Author

dai-shi commented Jun 2, 2021

It would be more of a comparison of speed, if one has quirks (zombie child issue for example), caveats, limitations.

Speed/performance is a hard one. If it's about extra re-renders in React, all work good. For implementation efficiency, one would need to measure in a similar setting to your app. There might be differences, but technically jotai/zustand/redux all depend on immutability, so theoretical limit should be similar. valtio depends on proxies (like mobx) and can have some overhead.

Zombie child issue comes with selectors, so it's zustand and redux only.

Suspense support (technically experimental) are in jotai and valtio only.

why would I choose to be limited to where the Provider is wrapping a component?

Recently, I got a simple conclusion about it. (Apart from concurrent mode familiarity, which we don't know yet.)
If you use only one Provider in your app, module state and react state are almost the same in capability (exception: server side rendering, which shares memory across instances.)
So, react state is really necessary, if you need more than one Provider in your app.

(The same is true in useContext+useState approach. Using just one context provider in an app is almost like misusing React Context, technically speaking. This would change if useSelectedContext would become a thing.)

@redbar0n
Copy link

redbar0n commented Jun 16, 2021

@dai-shi What if one wants this?

  • atoms - to get localized bottom-up composability, since maybe some actions doesn't need to go via a global store, and also would be more performant if they didn't.
  • time-travel debugging - to not only debug easier client-side, but also to be able to transfer all actions to server, for easy bug reproducability
  • everything stored outside of the React tree - to avoid rendering concerns, and to be cross-framework compatible
  • plain JS/TS - fewest possible new concepts to deal with, plainest most straightforward JS imaginable

@dai-shi
Copy link
Member Author

dai-shi commented Jun 16, 2021

"localized" and "everything outside" sound conflicting??

@redbar0n
Copy link

yeah, that’s how it sounds.. I meant only that it would be nice to co-locate state with components, but not have the stores themselves be inside/tied to the render tree (yet somehow be able to target and trigger rendering.. maybe hooks is the best for this after all…).

So sort of like Zustand, if I’m not mistaken, but atomized design which is bottom-up composable. Maybe Jotai with hooks would be the best base for something like that, but with some centralized store allowing for time-travel debugging.

@dai-shi
Copy link
Member Author

dai-shi commented Jun 16, 2021

If I'm not mistaken, time-traveling is still possible with internal states. usetAtomDevtools does it (for each atom, but technically you could create a catch-all atom). And, we have new hooks like useAtomsSnapshot and useGotoAtomsSnapshot, and a dedicated devtool is work in progress #428.

@tonypee
Copy link

tonypee commented Jun 16, 2022

@dai-shi - Can you explain a little more how Suspense is supported (or what that gives you) for Jotai + valtio, which is missing in zustand? We are assessing between zustand & valtio which is better for us, and suspense support is stated as a thing. What does suspense support achieve?

(the stores would be 'in' the loaded module no?.. how does suspense affect them)

@dai-shi
Copy link
Member Author

dai-shi commented Jun 17, 2022

Well, it will require a series of blog posts to cover all.
Let me write a small contrived example. It's about an async action.
Suppose we have a fetch function.

const fetchData = async () => {
  // ...
  return 'hello'
}

In zustand, the store can have an action like this.

import create from 'zustand'

const useStore = create((set) => ({
  data: '',
  doFetch: async () => {
    const data = await fetchData()
    set({ data })
  },
}))

If we want to show a loading indicator, we need a property for it.

const useStore = create((set) => ({
  data: '',
  isLoading: false,
  doFetch: async () => {
    set({ isLoading: true })
    const data = await fetchData()
    set({ data, isLoading: false })
  },
}))

And use the property in your component somehow.

With valtio, on the other hand, it can be done like this:

import { proxy } from 'valtio'

const state = proxy({
  data: '',
  doFetch: () => {
    state.data = fetchData()
  },
})

And, you use React.Suspense to show a loading indicator. Or React.useTransition to show pending status.
This is what we mean by suspense support.

Jotai can do the same like valtio, and in addition, jotai supports async derived atoms which also suspend.

@looeee
Copy link

looeee commented Jun 17, 2022

When to use which

If you need a replacement for useState+useContext, jotai fits well.
If you want to update state outside React, zustand works better.
If code splitting is important, jotai should perform well.
If you prefer Redux devtools, zustand is good to go.
If you want to make use of Suspense, jotai is the one.
If you need a global store to bridge between two react renderers, zustand will only work. (no longer valid with jotai's provider-less mode, but zustand may work better.)

I think there should be one additional point here as it's the reason I had to choose Zustand over Jotai:

  • If you need transient updates, you should choose Zustand.

Although I see #610 was updated so maybe there's a workaround in Jotai now? But in any case I think it's still not officially supported in Jotai.

@dai-shi
Copy link
Member Author

dai-shi commented Jun 17, 2022

From React perspective, transient update is a hack, even with zustand, because it's essentially introduces inconsistency between state and view. It's unavoidable for game (or alike) developers though. btw, technically speaking you don't need zustand for transient update, just read from a global variable. (So, it's not a good selling point at least for me... but yeah zustand/valtio readme explicitly notes so.)

@looeee
Copy link

looeee commented Jun 17, 2022

And, you use React.Suspense to show a loading indicator. Or React.useTransition to show pending status.
This is what we mean by suspense support.

You can use Suspense with Zustand in a few ways. Here's one that I use often - handle the async operation in a component instead of in the store.

const useStore = create((set) => ({
  data: '',
  setData: (data) => set({ data }),
}))

Then you create a component wrapped in Suspense to handle the loading and call the setData when it returns.

function LoadData {
  const data = fetchDataAsync();

  const setData = useStore((state) => state.setData);

  useEffect(() => {
    setData(data);
  }, [data]);

  return null; // or something else if you want
}

function App() {
  return (
    <Suspense fallback={<h1>Loading data...</h1>}>
      <LoadData />
    </Suspense>
  );
}

@dai-shi
Copy link
Member Author

dai-shi commented Jun 17, 2022

handle the async operation in a component instead of in the store.

It works, but it's not ideal because useEffect is delayed. Suspense is essentially a method to avoid such a delay.

@looeee
Copy link

looeee commented Jun 17, 2022

For many uses, such as removing a loading screen, a one frame delay is not an issue. But there are other cases where would be a problem, I agree.

Can you use useLayoutEffect to avoid the delay?

@looeee
Copy link

looeee commented Jun 17, 2022

technically speaking you don't need zustand for transient update, just read from a global variable. (So, it's not a good selling point at least for me... but yeah zustand/valtio readme explicitly notes so.)

Yeah makes sense.

However for me, dealing with three.js, I often don't know what I need to treat transiently or not in advance - especially as something that starts as a simple component grows in complexity over time .

With Zustand it's very quick to switch if I am finding slowdown caused by state updates in a component. I can just switch to a useStore.subscribe in about one minute and see if that fixes the problem (and often, it does).

@dai-shi
Copy link
Member Author

dai-shi commented Jun 17, 2022

Can you use useLayoutEffect to avoid the delay?

No, it's mostly the same thing.

For many uses, such as removing a loading screen, a one frame delay is not an issue.

From app developer perspective, I agree. Suspense is good even with fetch-on-render.

dealing with three.js

This makes me understand where you are from.

@looeee
Copy link

looeee commented Jun 18, 2022

Can you use useLayoutEffect to avoid the delay?

No, it's mostly the same thing.

OK I think I understand that. You still need to pass the updated state to the store and then wait for other components to receive it, and it doesn't matter if you do this at the start of the cycle (synchronously) with useLayoutEffect or later with useEffect. You still need to wait for other components to receive it on the next cycle.

Suspense is essentially a method to avoid such a delay.

I have not heard it stated this way before, that's interesting. I guess my suggestion to do the following negates some of the benefit of Suspense in that case? Although you still get the clean code style and you hide the implementation in the LoadData component so it's still worth it for this use case, IMO.

  const data = fetchDataAsync();

  const setData = useStore((state) => state.setData);

  useEffect(() => {
    setData(data);
  }, [data]);

@dai-shi
Copy link
Member Author

dai-shi commented Jun 18, 2022

There's only an old doc at https://17.reactjs.org/docs/concurrent-mode-suspense.html.

you still get the clean code style

Yeah, better than before, but using useEffect to sync state will not be a best practice moving forward.

You might be interested in some of my other projects.
https://github.com/dai-shi/react-suspense-fetch
https://github.com/dai-shi/react-hooks-fetch

@looeee
Copy link

looeee commented Jun 18, 2022

Thanks for the info, I will take a look at those :)

@AjaxSolutions
Copy link
Collaborator

Jotai provides a very easy way to create a truly reactive derived (computed) state. This process is seamless in Jotai and much easier than in Zustand.

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

Successfully merging a pull request may close this issue.