Skip to content

Add ability to delete, or choose not to rehydrate the store in onRehydrateStorage #405

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

Closed
mirshko opened this issue Jun 1, 2021 · 13 comments · Fixed by #1647
Closed

Add ability to delete, or choose not to rehydrate the store in onRehydrateStorage #405

mirshko opened this issue Jun 1, 2021 · 13 comments · Fixed by #1647
Labels
enhancement New feature or request middleware/persist This issue is about the persist middleware

Comments

@mirshko
Copy link

mirshko commented Jun 1, 2021

This might have overlap with the issue #267, however this isn't fully explained in that.

I notice there isn't a great way to handle conditional persisted state rehydration.

In the onRehydrateStorage it would be nice to be able to either return state or throw an Error / return directly to not rehydrate the storage.

A little example; here I want to check if a cookie is still present, a JWT that expires after a week, and then choose not to rehydrate, the only option I found is below, to delete the localStorage key.

onRehydrateStorage: () => {
  if (!hasJWT()) {
    console.log("No JWT Present, Delete State");

    localStorage.removeItem("web3-store");
  }
},

An ideal API would be,

onRehydrateStorage: (state) => {
  if (hasJWT()) {
    return state
  }
  
  return false
}

Let me know if I'm missing something!

@AnatoleLucet AnatoleLucet added enhancement New feature or request middleware/persist This issue is about the persist middleware labels Jun 1, 2021
@gabimoncha
Copy link

Looking forward to it!
#814

@gabimoncha
Copy link

gabimoncha commented Feb 21, 2022

@mirshko @barbogast @AnatoleLucet

How I managed to make zustand persist accept asynchronous storage.

First of all, I want to acknowledge the simplicity with which this library was created, otherwise I wouldn't have the confidence to look into its source code and try a workaround. Big up team! 🙌🏼
My hopes are that this is not just a hack, but serves as an inspiration to others or maybe even a good fix for this amazing library.

Problem

As I said earlier, my issue was that zustand's persist middleware was overwriting the storage on hydrate, because there was no "initial" way of making it to or check for a condition before overwriting the storage or wait for a trigger to start hydration, before getting the storage - in case one has an asynchronous storage ( the edge case of an encrypted storage which is available after the user successfully enters the PIN / Password / biometrics ( most cases happen in React Native).

Solution

  1. Because I am using the onRehydrateStorage method to know when my hydration ends, I configured my persist store with empty options in order to block any storage overwrite or to solve any type error that comes if my getStorage is an async function.
      onRehydrateStorage: () => async () => {},
      getStorage: undefined,
  1. Then, after I get my storage initialised, I call api.persist.setOptions overwriting in the process the empty getStorage and onRehydrateStorage with the methods that I need, and then I trigger a rehydration with api.persist.rehydrate().

Caveats

In order for this workaround to be successful, I had to take the unorthodox way and patch the library. Initial storage hydration is stopped, but at the same time makes any persist api methods unavailable as these are set later right before hydration, after the guard case where the middleware checks if the storage is set

In the patch, I'm moving the storage guard case after the api.persist property is set and before hydrate is called. Of course, this throws TypeError: Cannot read property 'setItem' of undefined because now persist will continue to call setItem on initialisation and for this I had to add an additional guard case in my patch to check if the storage exists there.

diff --git a/node_modules/zustand/middleware.js b/node_modules/zustand/middleware.js
index 4cb6011..21e052f 100644
--- a/node_modules/zustand/middleware.js
+++ b/node_modules/zustand/middleware.js
@@ -317,18 +317,11 @@ var persist = function persist(config, baseOptions) {
       storage = options.getStorage();
     } catch (e) {}
 
-    if (!storage) {
-      return config(function () {
-        console.warn("[zustand persist middleware] Unable to update item '" + options.name + "', the given storage is currently unavailable.");
-        set.apply(void 0, arguments);
-      }, get, api);
-    } else if (!storage.removeItem) {
-      console.warn("[zustand persist middleware] The given storage for item '" + options.name + "' does not contain a 'removeItem' method, which will be required in v4.");
-    }
 
     var thenableSerialize = toThenable(options.serialize);
 
     var setItem = function setItem() {
+      if (!storage) return;
       var state = options.partialize(_extends({}, get()));
 
       if (options.whitelist) {
@@ -445,6 +438,14 @@ var persist = function persist(config, baseOptions) {
         };
       }
     };
+    if (!storage) {
+      return config(function () {
+        console.warn("[zustand persist middleware] Unable to update item '" + options.name + "', the given storage is currently unavailable.");
+        set.apply(void 0, arguments);
+      }, get, api);
+    } else if (!storage.removeItem) {
+      console.warn("[zustand persist middleware] The given storage for item '" + options.name + "' does not contain a 'removeItem' method, which will be required in v4.");
+    }
     hydrate();
     return stateFromStorage || configResult;
   };

@sbland
Copy link

sbland commented May 9, 2022

Another way to do this would be to make getStorage async. Currently the below will work to clear localStorage if a check is not passed.

getStorage: () => {
   const state: IAppState = JSON.parse(localStorage.getItem("STORAGENAME"))?.state;
   const timedout = Date.now() - state._timestamp > TIMEOUT;
   if (timedout) localStorage.removeItem("STORAGENAME");
   return localStorage;
},

@Toshinaki
Copy link

Toshinaki commented Sep 26, 2022

It's been over a year. Any progress on this?

Is removeItem still the only way to achieve this?

I want to disable the auto rehydrate completely, and manually rehydrate after. So removeItem won't work for my use case.

@gabimoncha
Copy link

gabimoncha commented Nov 17, 2022

@dai-shi @AnatoleLucet having this kind option would probably fix this issue. I'm not very in tune with the library's ins and outs and not sure if this thing is possible.
Delaying the hydration or making getStorage asynchronous in order to wait for a trigger or action to happen would be of great help for mobile devices or encrypted persist storages especially

@dai-shi
Copy link
Member

dai-shi commented Nov 17, 2022

Wonder how @AnatoleLucet would like to do with these open issues.

@dai-shi
Copy link
Member

dai-shi commented Dec 5, 2022

Can anyone help understanding what should be implemented for this issue?

@gmanninglive
Copy link
Contributor

gmanninglive commented Jan 4, 2023

I'm not sure if this is 100% the same issue, but I had issues working on a SSR app where I need to control when the persist middleware triggers to respect the frameworks own rendering / hydration life cycle.

It feels similar to the OP problem just different application.

I've made a fork with a very simple change that will skip the initial hydration, allowing you to decide when the hydration is triggered.

It's pretty self explanatory but a rough example with react context provider:

const createBasketStore = ({ initialItems }: BasketStoreProps) => {
  const store = createStore<BasketState>()(
    persist(
      (set, get) => ({
        items: initialItems || [],
        address: null,
        ...
      }),
      { name: "basket", manualHydration: true }
    )
  );
  return store;
};

export function BasketProvider({ children }: { children: ReactNode }) {
  const storeRef = useRef<BasketStore>();

  if (!storeRef.current) {
    storeRef.current = createBasketStore({});
  }

  // Hydrate from local storage onMount
  useEffect(() => {
    if (storeRef.current) {
      storeRef.current.persist.rehydrate();
    }
  }, []);

  return (
    <BasketContext.Provider value={storeRef.current}>
      {children}
    </BasketContext.Provider>
  );
}

I'd be happy to contribute more on this if you think it's a valid feature.

@dai-shi
Copy link
Member

dai-shi commented Jan 5, 2023

@gmanninglive
Okay, I understand what you are doing.
I think it's a valid feature. Can you open a PR?
Some notes:

  • Please only change newImpl.
  • Let's call it skipHydration.
  • We also need tests and updating docs.

Look forward to it!

@gmanninglive
Copy link
Contributor

Great I'll get a PR sorted with tests and docs 😄

@vissharm
Copy link

Is this getting resolved or nobody looking at it ?

@dai-shi
Copy link
Member

dai-shi commented Feb 20, 2023

A PR would be welcome for this #405 and also #1537.

We need someone who maintains the persist middleware.

@gmanninglive
Copy link
Contributor

Is this getting resolved or nobody looking at it ?

Sorry i started a PR then work commitments got in the way. I'll have one out for review this eve, just need to add the docs

dai-shi added a commit that referenced this issue Mar 31, 2023

Unverified

This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
* add manual hydration option to persist middleware

* rename variable, update jsdoc, remove from oldImpl

* add tests for persist skipHydration

* add docs

* Update docs/integrations/persisting-store-data.md

Co-authored-by: Blazej Sewera <code@sewera.dev>

* Update docs/integrations/persisting-store-data.md

Co-authored-by: Blazej Sewera <code@sewera.dev>

* Update src/middleware/persist.ts

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
Co-authored-by: Blazej Sewera <code@sewera.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request middleware/persist This issue is about the persist middleware
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants