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

fix(docs): Issue 1120 docs guides #1244

Merged
merged 16 commits into from
Aug 30, 2022

Conversation

Exalted100
Copy link
Contributor

Related Issues

Fixes #.

Summary

Reviewed the following documentation:

  • maps and sets usage
  • practice with no store actions
  • typescript
  • updating nested state object values

Check List

  • yarn run prettier for formatting code and docs

@codesandbox-ci
Copy link

codesandbox-ci bot commented Aug 30, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit df820bf:

Sandbox Source
React Configuration
React Typescript Configuration
React Browserify Configuration
React Snowpack Configuration
React Parcel Configuration

Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other than those, it looks good.


<br/>

**TLDR**: Because state generic `T` is invariant.

Consider this minimal version `create`...
Consider this minimal version `create...`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not "create:"?

@@ -132,23 +132,23 @@ const useBearStore = create(
```

<details>
<summary>But be a little careful...</summary>
<summary>Be careful</summary>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you leave "little" or something? This describes very rare case and most people don't need to read. So, maybe "careful" is misleading. cc @devanshj

@dai-shi dai-shi changed the title Issue 1120 docs guides fix(docs): Issue 1120 docs guides Aug 30, 2022
@Exalted100
Copy link
Contributor Author

@dai-shi I've made changes based on your comments.

@Exalted100 Exalted100 requested a review from dai-shi August 30, 2022 22:45
Copy link
Member

@dai-shi dai-shi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@dai-shi dai-shi merged commit 476a381 into pmndrs:main Aug 30, 2022
Copy link
Contributor

@devanshj devanshj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the edit. I've left my two cents. Most of them are trying to preserve and improve the expression and intent as the original author. But of course I do understand I don't own the document haha so no pressure. Other than that...

  • I like the punctuations, makes it more readable.
  • I also like the more simplified phrasing at places.
  • I personally don't like the use of non-contractions ie "do not" instead of "don't". Reading "do not" feels too passive to me. But I'm fine it others are okay with it.
  • I also personally prefer "..." instead of ":" but again I'm fine as long as we're consistent.

cc @dai-shi

@@ -5,7 +5,7 @@ nav: 9

## Basic usage

The difference when using TypeScript is instead of writing `create(...)`, you have to write `create<T>()(...)` where `T` would be type of the state so as to annotate it. Example...
The difference when using TypeScript is that instead of typing `create(...)`, you have to type `create<T>()(...)`. `T` would have a type of the state to annotate it. For example:
Copy link
Contributor

@devanshj devanshj Aug 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of typing create(...), you have to type create<T>()(...)

I'm not sure what do you mean by "typing" here. If by "typing" you mean TypeScript-typing then we're not only typing per se when we write the code create<T>()(...).

T would have a type of the state to annotate it.

T does not "have" the type of state, it "is" the type of state.

Suggested change
The difference when using TypeScript is that instead of typing `create(...)`, you have to type `create<T>()(...)`. `T` would have a type of the state to annotate it. For example:
The difference when using TypeScript is that instead of writing `create(...)`, you have to write `create<T>()(...)` where `T` is type of the state so as to annotate it. For example:

If you want something simpler then the following would do too...

Suggested change
The difference when using TypeScript is that instead of typing `create(...)`, you have to type `create<T>()(...)`. `T` would have a type of the state to annotate it. For example:
The difference when using TypeScript is that instead of writing `create(...)`, you have to write `create<T>()(...)` where `T` is the type of the state. For example:

@@ -44,20 +44,20 @@ const x = create((get) => ({
// }
```

Here if you look at the type of `f` in `create` ie `(get: () => T) => T` it "gives" `T` as it returns `T` but then it also "takes" `T` via `get` so where does `T` come from TypeScript thinks... It's a like that chicken or egg problem. At the end TypeScript gives up and infers `T` as `unknown`.
Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it returns `T`. However, it also "takes" `T` via `get`. Typescript wonders where `T` comes from, like that chicken or egg problem. At the end TypeScript, gives up and infers `T` as `unknown`.
Copy link
Contributor

@devanshj devanshj Aug 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it returns T

It's important to keep the original phrase with "gives" in quotes. Here "gives" implies covariance and "takes" implies contravariance. It's possible to "give" without returning, for example, in type F<T> = (g: (x: T) => void) => void, F is giving T because when you write the implementation of F you are the one that will pass x (ie T) to g and hence "give"/"send" it outside.

Made some other punctuations corrections to my original copy to express my intention better.

Suggested change
Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it returns `T`. However, it also "takes" `T` via `get`. Typescript wonders where `T` comes from, like that chicken or egg problem. At the end TypeScript, gives up and infers `T` as `unknown`.
Here, if you look at the type of `f` in `create`, i.e. `(get: () => T) => T`, it "gives" `T` via returning (making it covariant), but it also "takes" `T` via `get` (making it contravariant). "So where does `T` come from?" TypeScript wonders. It's like that chicken or egg problem. At the end TypeScript gives up and infers `T` as `unknown`.


So as long as the generic to be inferred is invariant TypeScript won't be able to infer it. Another simple example would be this...
So, as long as the generic to be inferred is invariant, TypeScript will be unable to infer it. Another simple example would be this:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explaining invariant too like I explained covariant and contravariant above.

Suggested change
So, as long as the generic to be inferred is invariant, TypeScript will be unable to infer it. Another simple example would be this:
So, as long as the generic to be inferred is invariant (i.e. both covariant and contravariant), TypeScript will be unable to infer it. Another simple example would be this:


Now one can argue it's impossible to write an implementation for `createFoo`, and that's true. But then it's also impossible to write Zustand's `create`... Wait but Zustand exists? So what do I mean by that?
One could argue that it is impossible to write an implementation for `createFoo`, and that would be true. But then it is also impossible to write Zustand's `create(...)`. However, Zustand exists.
Copy link
Contributor

@devanshj devanshj Aug 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I mean it's impossible to write Zustand's create I mean it's impossible to write a value of the type of typeof create. So it's not create(...) but create. (I'm not talking about create(...) vs create<T>()(...) but create itself).

The original copy may seem have a bit non-formal tone but it's important to capture the absurdity of the statement "impossible to write Zustand's create".

If I want to explain what I'm trying to explain I'll have to write a lot more words and delve into a lot more type theory. Which would be diverging here, hence to keep it succinct I've written it like so.

Suggested change
One could argue that it is impossible to write an implementation for `createFoo`, and that would be true. But then it is also impossible to write Zustand's `create(...)`. However, Zustand exists.
One could argue that it is impossible to write an implementation for `createFoo`, and that would be true. But then it is also impossible to write Zustand's `create`... Wait but Zustand exists? So what do we mean by that?

@@ -67,9 +67,9 @@ const useBoundStore = create<{ foo: number }>()((_, get) => ({
}))
```

This code compiles, but guess what happens when you run it? You'll get an exception "Uncaught TypeError: Cannot read properties of undefined (reading 'foo') because after all `get` would return `undefined` before the initial state is created (hence kids don't call `get` when creating the initial state). But the types tell that get is `() => { foo: number }` which is exactly the lie I was taking about, `get` is that eventually but first it's `() => undefined`.
This code compiles. However, you will get an exception when you run it: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". This is because `get` would return `undefined` before the initial state is created (hence you should not call `get` when creating the initial state). But the types tell that get is `() => { foo: number }`, which is exactly the lie I was taking about. That value is eventually `get`, but first it is `() => undefined`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the ending of my original copy a bit more clear.

Suggested change
This code compiles. However, you will get an exception when you run it: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". This is because `get` would return `undefined` before the initial state is created (hence you should not call `get` when creating the initial state). But the types tell that get is `() => { foo: number }`, which is exactly the lie I was taking about. That value is eventually `get`, but first it is `() => undefined`.
This code compiles. However, you will get an exception when you run it: "Uncaught TypeError: Cannot read properties of undefined (reading 'foo')". This is because `get` would return `undefined` before the initial state is created (hence you should not call `get` when creating the initial state). But the types tell that `get` is of type `() => { foo: number }`, which is exactly the lie I was taking about. The type of `get` is is eventually that, but first it is `() => undefined`.


Okay we're quite deep in the rabbit hole haha, long story short zustand has a bit crazy runtime behavior that can't be typed in a sound way and inferrable way. We could make it inferrable with the right TypeScript features that don't exist today. And hey that tiny bit of unsoundness is not a problem.
Long story short, Zustand has a strange runtime behavior that can not be typed in a sound and inferrable way. We could make it inferrable with the right TypeScript features. However, those do not exist yet. And the strange behaviour is not a problem.
Copy link
Contributor

@devanshj devanshj Aug 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not "strange" it's "crazy". It's crazy in context of type-theory, ie you won't be able to write such code in a more strict statically typed language, maybe "wild" is a more decent way to put it.

Also made the original statement simpler.

Suggested change
Long story short, Zustand has a strange runtime behavior that can not be typed in a sound and inferrable way. We could make it inferrable with the right TypeScript features. However, those do not exist yet. And the strange behaviour is not a problem.
Long story short, Zustand has a bit type-theoretically wild runtime behavior, which can not be typed in a sound and inferrable way with the currently available TypeScript features. To workaround the lack of inferrence we provide the state type via a type parameter, and the tiny bit of unsoundness is not a problem.


<br/>

We achieve the inference by lying a little in the types of `set`, `get` and `store` that you receive as parameters. The lie is that they're typed in a way as if the state is the first parameter only when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. So for example `get` from the second parameter has type `() => { bears: number }` and that's a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type, ie for example `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`.
We achieve the inference by lying a little in the types of `set`, `get`, and `store` that you receive as parameters. The lie is that they are typed as if the state is the first parameter. In fact, the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"when in fact" is a single phrase, and it's good to keep the lie and the truth in one sentence.

Suggested change
We achieve the inference by lying a little in the types of `set`, `get`, and `store` that you receive as parameters. The lie is that they are typed as if the state is the first parameter. In fact, the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`.
We achieve the inference by lying a little in the types of `set`, `get`, and `store` that you receive as parameters. The lie is that they are typed as if the state is the first parameter, when in fact the state is the shallow-merge (`{ ...a, ...b }`) of both first parameter and the second parameter's return. For example, `get` from the second parameter has type `() => { bears: number }` and that is a lie as it should be `() => { bears: number, increase: (by: number) => void }`. And `useBearStore` still has the correct type; for example, `useBearStore.getState` is typed as `() => { bears: number, increase: (by: number) => void }`.


It's not a lie lie because `{ bears: number }` is still a subtype `{ bears: number, increase: (by: number) => void }`, so in most cases there won't be a problem. Just you have to be careful while using replace. For eg `set({ bears: 0 }, true)` would compile but will be unsound as it'll delete the `increase` function. (If you set from "outside" ie `useBearStore.setState({ bears: 0 }, true)` then it won't compile because the "outside" store knows that `increase` is missing.) Another instance where you should be careful you're doing `Object.keys`, `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]` (the return type of `get` can make you fall for this).
It is technically not lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be incorrect as it will delete the `increase` function. If you set from "outside" i.e. `useBearStore.setState({ bears: 0 }, true)` then it will not compile because the "outside" store knows that `increase` is missing. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]` (the return type of `get` can make you fall for this).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you set from "outside" i.e. useBearStore.setState({ bears: 0 }, true) then it will not compile because the "outside" store knows that increase is missing

It'd still compile, I wrote this when we had strict replace but then we removed it. (Reference for background: #944)

And it is "unsound" ie it can produce a runtime exception.

Suggested change
It is technically not lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be incorrect as it will delete the `increase` function. If you set from "outside" i.e. `useBearStore.setState({ bears: 0 }, true)` then it will not compile because the "outside" store knows that `increase` is missing. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]` (the return type of `get` can make you fall for this).
It is not really lie because `{ bears: number }` is still a subtype of `{ bears: number, increase: (by: number) => void }`. Therefore, there will be no problem in most cases. You should just be careful while using replace. For example, `set({ bears: 0 }, true)` would compile but will be unsound as it will delete the `increase` function. Another instance where you should be careful is if you use `Object.keys`. `Object.keys(get())` will return `["bears", "increase"]` and not `["bears"]`. The return type of `get` can make you fall for these mistakes.

@@ -190,11 +190,11 @@ const useBearStore = create<BearState>()(
)
```

Also it's recommended to use `devtools` middleware as last as possible, in particular after `immer` middleware, ie it should be `immer(devtools(...))` and not `devtools(immer(...))`. The reason being that `devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) mutate `setState` before `devtools`.
Also, we recommend using `devtools` middleware as an argument, rather than passing arguments to it. For example, when you use it with `immer` as a middleware, it should be `immer(devtools(...))` and not `devtools(immer(...))`. This is because`devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) mutates `setState` before `devtools`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we recommend using devtools middleware as an argument, rather than passing arguments to it

This sounds a bit wrong, even if you use devtools at the end you're still passing an argument to it namely your state creator.

I think the original wording is better, I'm fine if someone can come up with a better one but the one is PR isn't really making the cut imo.

Also "mutate" was correct because it's "middlewares".

Suggested change
Also, we recommend using `devtools` middleware as an argument, rather than passing arguments to it. For example, when you use it with `immer` as a middleware, it should be `immer(devtools(...))` and not `devtools(immer(...))`. This is because`devtools` mutates the `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) mutates `setState` before `devtools`.
Also, we recommend using `devtools` middleware as last as possible. For example, when you use it with `immer`, it should be `immer(devtools(...))` and not `devtools(immer(...))`. This is because `devtools` mutates `setState` and adds a type parameter on it, which could get lost if other middlewares (like `immer`) also mutate `setState` before `devtools`.


For an usual statically typed language this is impossible, but thanks to TypeScript, Zustand has something called an "higher kinded mutator" that makes this possible. If you're dealing with complex type problems like typing a middleware or using the `StateCreator` type, then you'll have to understand this implementation detail, for that check out [#710](https://github.com/pmndrs/zustand/issues/710).
For a usual statically typed language, this is impossible. However, with TypeScript, Zustand has something called a "higher kind mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original phrasing "thanks to" is better as it implies it's a TypeScript feature more significantly than a mere "with".

And it's called "higher kinded" and not "higher kind". Putting a hyphen to make it more correct.

Suggested change
For a usual statically typed language, this is impossible. However, with TypeScript, Zustand has something called a "higher kind mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710).
For a usual statically typed language, this is impossible. But thanks to TypeScript, Zustand has something called a "higher-kinded mutator" that makes this possible. If you are dealing with complex type problems, like typing a middleware or using the `StateCreator` type, you will have to understand this implementation detail. For this, you can [check out #710](https://github.com/pmndrs/zustand/issues/710).

@dai-shi
Copy link
Member

dai-shi commented Aug 31, 2022

@Exalted100 Can you please take the above comments into account?

@devanshj devanshj mentioned this pull request Sep 1, 2022
1 task
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 this pull request may close these issues.

4 participants