-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Allow persist middleware to accept 'localStorage' and 'sessionStorage' for easier usage of session #248
Conversation
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 5cc14e9:
|
Yeah, I'd prefer to make persist(..., { name: 'foo', storage: typeof sessionStorage !== 'undefined' ? sessionStorage : null }) But, if it's rather common to specify a string like In either case, the documentation could be tricky. |
@dai-shi I'm not quite sure to understand why you don't want to check for the |
Just checking However, this PR resolves this concern, because it checks both. So, I'm fine with it. (Unless there is any env that has localStorage without window, in the future.) That said, this PR is acceptable to me, because we already support |
Ah, didn't though about test env. |
If it feels weird I'm not sure if it's the right approach, alternatively I could make an effort on changing the documentation to be a bit more helpful on the SSR situation for users that would prefer using sessionStorage over localStorage. Alternatively, we could export a SSR safe instance of both session as well as localStorage from the middleware, allowing users to do something like;
but then again, not sure if that's worth the trouble. A README update might just as well be enough. |
That'd be "safer" than a plain string (e.g. typing mistakes), but we'd need to provide pretty much any kind of dummy store that the user could want to use. |
(I still think it's an option to pass strings, if this is the final one.) I didn't know SSR users prefer/require sessionStorage. How are you (un)comfortable with my first suggestion? |
Sorry, totally forgot this com. It could be a nice solution. |
@RobbyUitbeijerse Would it be not too comfortable? Should we go with strings? Ping @ianstormtaylor for another nextjs user. |
FWIW, I think it would be nice to make the 90% cases for all of these not require the user having to write checks environment checks for the options. I think doing something simple like offering multiple persistence functions instead like hooks tend to do would be a nice solution. Similar to how import { useLocalStorage, useSessionStorage } from 'react-use' I think it would make sense to expose: import { create, persistLocalStorage, persistSessionStorage } from 'zustand' This would avoid the problem entirely. And a more generic Also FWIW, I think the current pattern of: import create from 'zustand'
import { persist } from 'zustand/middleware' Is an anti-pattern. It makes it hard to remember what the "proper" way to import something from the library is. And with bundlers supporting tree-shaking these days it isn't required anymore to make bundles smaller. And it can often mess up Intellisense-style autocomplete because the exports aren't as easily automatically found. |
I prefer this pattern than strings. This would be a breaking change, though. (edit) But, it's not very cute, compared to other ones. (Slightly off topic)
I'm aware of this. But, it's not only about tree-shaking and app bundle size. We want to explicitly separate it from core. I think the alternative would be: import create from 'zustand'
import { persist } from 'zustand-middleware' // or '@zustand/middleware and make it monorepo. But, maybe it's not the trend either.
I wasn't aware of this. Would adding |
@dai-shi nice, yeah in terms of separation of core, that makes sense. I think the monorepo is the way to go if you want to actually think and develop them as separate packages. And then you could even call it But in addition, maybe "what is in core" is a concept that needs to be rethought. I'd argue that… import create from 'zustand' …is kind of a "cute" attempt at simplicity, but isn't really the "truth". (I say this as someone who often falls for the cute!) For example, is I think as soon as you change the import to… import { create } from 'zustand' …you'll be able to separate the valid semantic question of "what do we want to have as part of this library's core?" from the cuteness of having a single default export. |
I personally like named imports but the cuteness would be important for this library. Good point on Intellisense-style autocomplete is still a concern to me. Let me investigate how vscode would work. OK, this is off topic from the original PR. Let me make a new issue later. |
So, what should we do with this.
|
FWIW, another thought I had... I originally wanted Local Storage, but realized soon after that I wanted to have it defaulted from the URL as well (ie. in the case of something like TypeScript Playground where the URL includes the state as well as remembering your last snippet). The existing API for persist(..., {
get: () => JSON.parse(localStorage.getItem('my-key')),
set: value => localStorage.setItem('my-key', JSON.stringify(value)),
}) This would make it infinitely flexible, and very clear for people exactly how they can augment the behavior... just write whatever they want. For example, you could imagine layering in You can still then have the more purpose-built helpers with sane defaults which most people will want to use: persistLocalStorage('my-key', ...) But that way the lowest level utility is actually as customizable as you need it to be. And as a side benefit, your purpose-built helpers can be more opinionated, like assuming JSON serialization, to keep their own APIs simpler. I say all this because I wanted to use Local Storage, but soon realized I needed something slightly different, and if you don't expose the lower level you'll be locking out these kinds of use cases. And, back to our off-topic convo... when thinking about "what's in core", the decision becomes easier:
|
My perspective is that All that said, I would like to leave @AnatoleLucet for suggestion. |
@dai-shi you get the final call, but personally, I don't think that kind of thinking leads to good library design. It will just end up frustrating users who hit the artificial limit you've imposed when they have to change their approach from importing to copy-pasting just to barely tweak some logic. I mean look at the code for My take would be… If you're truly looking to make a "recipe" then put it in a Gist, or in the docs, so that it's always copy-pasted. If instead you're looking to add a useful utility to the core of a library that aims to be small in size, make it flexible. |
Yeah, I'd admit the current version is already too complicated. |
Well, I don't personally have a strong opinion on all that, but: I don't think what @ianstormtaylor proposed here is a good idea for the persist middleware. persist(..., {
get: () => JSON.parse(localStorage.getItem('my-key')),
set: value => localStorage.setItem('my-key', JSON.stringify(value)),
})
For your discussion about the design of Zustand's core, I think this is something that should involve more people in a separated issue. |
Totally agree! It should not be called "persist" if it's that low-level/flexible—it's more of a building block. It could be called |
@AnatoleLucet Alright. I think this makes sense. |
@dai-shi surely, why not. I mean, I'm neither some kind of "persistent storage savvy" and that's why I said it'd be nice to check what others have made which fits the community's needs. |
@AnatoleLucet That's great to hear. Would you join the pmndrs discord and DM me there please? |
@AnatoleLucet is now a collaborator on the zustand repo and will take care of |
What about: persist(..., {
name: "foo",
storage: () => sessionStorage,
}) Then we can just call |
Yeah, it seems clean. Do you keep the previous behavior or make a breaking change? Either is fine from my perspective. |
Since the middleware has been introduced recently, an easy to fix breaking change like this one should be ok |
With this breaking change, is there any way to define a dynamic key? I would like to have something like this: [user_id]_key. |
@sexta13 not sure to fully understand. Do you mean a custom key which point to a storage (e.g. |
Sure. I guess I didn't make myself very clear (after reading it better). What I want/need is that the name used in the options for sessionStorage can be dynamic. The use case is: I have multiple users that use the same browser/window and they cannot share the same sessionStorage, so in order to identify which sessionStorage a user should use I have something like this: [userId]_myapp. If this is not clear enough, tell me that I'll try to post some code ;) |
I don't think this change will break your code. See, the |
Well, I guess it's me don't knowing how to do it :)
How can I pass a name to this, so that the name can be something built from outside? Or then inside this method have something to call a function that returns a name? Or even use a hook that has that info? Can it be something like this?
and then call it like |
This should work, but only if you use this store in one place... There's probably another way to do this, but as you said, it looks odd. |
Hi there @AnatoleLucet and @dai-shi !
After the middleware localStorage PR was closed I quickly ran into the situation where we actually wanted to persist state in session. I can imagine though that most developers wouldn't actually want this as the store persist in memory most of the time - but in our specific case we have multiple front-ends hosted on the same domain and the user would switch between them on multiple occasions in the same session, where we actually wanted to get back the stored data when they would come back to our frontend where the Zustand store was initially created.
Of course it's possible to pass window.sessionStorage, but you are left doing the same check that initially had to be done for localStorage in order to make it work in SSR. Allowing the user to pass 'localStorage' and 'sessionStorage' as string options and retrieving the apis from the window if it's defined solves it - but not sure if this is a welcome change since @dai-shi actually preferred not checking window.
Let me know what you think