Storage hook `undefined` during initial render
Hey! I'm using the
useStorage
hook and noticed that during the initial render, it is undefined
(default value) and then gets populated with the value from storage. I understand why this is the case (async data store vs sync rendering), however is there any way we can make it so that all storages are loaded before the rest of the application shows (i.e. no initial render with default values)?35 Replies
Doc: https://docs.plasmo.com/framework/storage#hook-api-for-react-components
Source: https://github.com/PlasmoHQ/storage/blob/main/src/hook.ts
Jotai has this idea of a preloader: https://jotai.org/docs/guides/persistence#with-suspense but I'm not sure how we can make it work with
useStorage
You can do
value ? <Loading> : <ActualStuff>
And chain them, fair enough. Lemme give that a shot!
This is a bit trickier than I thought.
useStorage('favoriteNumber', (value) => (value === undefined ? 10 : value));
. Initially this will render (and persist) the value 10
. If the user then modifies the value in the settings to 20
and opens up the extension again, they will see a flash of 10
before 20
. And we can't really do a null
check because theres a default value
I suppose I could try useStorage('favoriteNumber')
and useEffect
where if(!favoriteNumber) setStorage(10)
but I'm not sure if that would cause issuesinteresting
maybe we can add something to the 3rd return value of the hook
so the first 2 values model after useState
the 3rd value provides more than that
maybe we can add a
isHydrated
bool in the 3rd one
and then only if the value is hydrated would we set that to true
what do you think?
bc useStorage tries to be an API-replica of useState, the concept of rehydration doesn't quite sync up (since useState hydration is supposedly synced with the runtime and renderer).Sorry just got back, that’s exactly the kind of solution I was thinking!
I was toying around with suspense but couldn’t figure it out
But that would also be an alternative cool solution
Ah I’m too slow, I see you already implemented it
Thank you! I’ll let you know if I find any problems
oh yeah I actually
just released it lol
try out the latest version and lmk how it goes xD
Is there a way to return this promise as well? https://github.com/PlasmoHQ/storage/blob/main/src/hook.ts#L61
GitHub
storage/hook.ts at main · PlasmoHQ/storage
💾 Safely store data and share it across your extension - storage/hook.ts at main · PlasmoHQ/storage
I ask because I was messing around to see if I can implement suspense so I dont have to check for
isHydrated
in every location I use the store
https://www.youtube.com/watch?v=PtoQpVOQ5Ew
Seems like you need to throw a promise and react re-renders when the promise resolvesPR welcome :p
Let me try 🙂
in term of is there a way, I wonder
we can store that promise in a ref and expose it in the 3rd arg?
I wonder if its as simple as passing
suspense = true
as a prop to the hook and then if (suspense) throw
on that line
Ah its in the useEffect, nvmI ask because I was messing around to see if I can implement suspense so I dont have to check for isHydrated in every location I use the storeOne way to solve this is to use a context/provider
Can I clone only the storage and link it or do I have to do the whole suite?
Yeah but suspense is cooler 😛
I wonder if it works with parcel tho
since suspense is dynamic import
and the current bundler atm is actually pretty shoddy at dynamic resolving atm, more so for extension since they usually expects a single bundle code
(this is something we will need to improve along the line and think about - is there a way to do React server component/suspense in extension? <- this is a new frontier for next year imo)
Even in NextJS, they had to make their own
dynamic
function to handle dynamic import since the default async import just sucks and doesn't work in their runtime/isomorphic
so very likely Plasmo will have to adopt a similar strategy if it wanted to support suspense/dynamic importWell I already did my own little suspense experiment inside of plasmo without an issue, but i also dont understand a lot of this stuff. I think youre right that a context is probably an easier solution
ooh noice
I had no idea suspense works with Plasmo lol
(and was expecting it to not work at all xD)
but actually interesting to hear it's working
I suspect, Parcel is faking suspense - when you do suspense, do you see 2 or more script tags in your popup.html when looking at it from the inspector?
or is there just a single script
Re: cloning - I think you might need to clone the whole suite, since we link some of the global dependencies xD
and the storage repo has no lockfile
yeah
I just checked the package.json
it's using the workspace protocol, so you will have to work with it from the monorepo
Ahhh kk gotcha, will give it a shot when I get some spare time!
Hey, update here. I found a solution that works with suspense, caching and all that jazz.
I essentially used
react-query
to handle all of itFascinating, how deep is your query btw
(est. expected depth)
I've been playing around with storage to implement custom history caching, so either hashing with namespace, or using query key like in your solution are some potential solution I've been looking at
By deep do you mean in my react tree?
yeah
or like the longest query you're expecting
I go pretty deep, I wanted to organize my code so that any component can access the stores without handling loading states. I also needed multiple instances of
useBeautifulDashboardStorage
to be cached so that I dont have to worry about each component that uses the hook to trigger the suspense loading state
Here is some more code. Inside of any component I do this:
useBuildHooks
is the code in my message above
Last thing you need to do is to tell react-query to cache the queries to localStorage
At the end of the day I think Im going to go with RxDB for my storage. Unfortunately this means that I'd have to use IndexedDB instead of the chrome storage since it looks like writing an RXDB adapter would be a pain: https://github.com/pubkey/rxdb/blob/master/src/plugins/storage-dexie/rx-storage-dexie.ts
GitHub
rxdb/rx-storage-dexie.ts at master · pubkey/rxdb
A fast, offline-first, reactive database for JavaScript Applications - rxdb/rx-storage-dexie.ts at master · pubkey/rxdb
I think the reason why Id want to go with RXDB is because it provides:
- Migrations, ensuring the future builds wont break the data structure I have stored
- Validation of the stored data, so having a number instead of string doesnt throw an accidentally reset my whole application
In terms of syncing, they do have some paid options but I dont forsee allowing syncing anyways
I'd probably stick with my react-query hook above to actually load the data (and trigger a suspense) though
Happy to hear your thoughts if you think thats a bad/good idea
@nahtnam has reached level 8. GG!
Huh
I switched over and initial boot time is 2x
*my measurement included me building both for prod, then opening a new tab while recording the screen and counting frames
18 frames vs 32 fames. Not the most accurate but I also felt it which is why I did that in the first place
Ill stick to plasmo storage and figure out a migration structure
2x faster or slower?
If you want to ensure data structure for future build, ensure a consistent schema via a plain text format (Prisma's schema or trpc schema or even JSON schema). - i.e, use the tool for the job
*slower
If rxdb has a good schema structure I'd go for it
Validation of the stored data, so having a number instead of string doesnt throw an accidentally reset my whole applicationThis part generally doesn't matter much unless you're dealing with crucial financial infra OR nuke launch OR robotic supplychain IMO and if you need data validation at field level, I'd highly recommend do not try to do it on client-side, let a database do it at the engine level (postgresql), and vaildate at schema level If an error resets your whole application, I'd simply try/catch the area where that code happens and have a graceful runtime mitigation
Well the main problem is my chrome extension reads from the storage and then renders the application. Id have to validate the data, and if its not valid id have to throw the value away (no way I can guess whats wrong without putting a lot of effort) and reset the values to default. This would feel like a broken experience. How would this happen? Me accidentally updating the schema to change a value from number to string on accident. I know not to do that, but im human haha
As a new tab page, I cant really afford the data loading state to be 2x slower, i love how snappy my extension feels right now so Id have to go for a custom solution. I'll probably follow the same migration format thats used by redux-persist and rxdb but implement it by hand to not incur the overhead
hmm, is there a way to redesign your UX to introduce more smoke and mirror/warmup UI etc...
most app that load "fast" uses a couple illusion like fade-in, loading spinner, and prefetch data on hover
I actually show a skeleton with suspense but at the end of the day it's noticeable. Feels wrong to fake it plus I have some good ideas on how to implement the data store
By UX I mean is there a way to load the smallest amount of data needed for the first fold, then rehydrate the rest as needed? Or is the 18 frames of loading already entail smallest amount of data you need to show signs of life?
Actually I have no idea if querying for 1 key in storage vs querying 10 keys in storage would tampers with perf
I think the main difference is initializing all of indexed db and whatever startup costs that are associated with using the library, versus doing 2-3 localstroage reads
https://rxdb.info/rx-storage.html In fact they have a performance chart at the bottom