roadmanfong / zustand-persist

Persist and rehydrate state
MIT License
119 stars 13 forks source link

Usage with expo #4

Open GaspardC opened 4 years ago

GaspardC commented 4 years ago

Hi, is it meant to work with Expo ?

I'm trying with the last Expo (sdk 39) but the app is stuck on the Persist Gate which only display the loading component.

thanks

roadmanfong commented 4 years ago

Hi, could you create a basic repository so that I can investigate it?

Trashpants commented 4 years ago

So I'm also getting this its very weird - when I run expo and remotely debug its totally fine, but when I disable remote debugging it stops working and refuses to load.


Will Attempt to set up a repo at some point today

Trashpants commented 4 years ago

So I've scratched up a https://github.com/Trashpants/zustand-demo-expo Hopefully this helps get to the bottom of this.

alexandersandberg commented 4 years ago

I'm having the same problem without Expo.

Trashpants commented 4 years ago

@roadmanfong Anything we can do to help getting this resolved?

callmetwan commented 4 years ago

It appears that the white screen is actually a stuck loading screen. You can test by providing <PersistGate> with a loading prop.

<PersistGate
  loading={
    <View>
        <BaseText>Loading</BaseText>
    </View>
}
>
// App content
</PersistGate>
);

I haven't dug further, I don't know that I have the time to.

Trashpants commented 4 years ago

Yeah it seems to not get past the loading, which points to the fact it’s not rehyrdating or not completing the rehydration process.

callmetwan commented 4 years ago

@Trashpants I think I got it to work. A key part of the hydration happening is calling the store within your main App structure. Below I have a partial from my App.tsx file and my store.ts. In store.ts I'm instantiating a zustand store utilizing configurePersist zustand-persist. Then in App.tsx I call the hook.

//store.ts
const { persist, purge } = configurePersist({
    storage: AsyncStorage,
    rootKey: 'root'
});

export const useStore = create(
    persist(
        {
            key: 'data'
        },
        set => ({
            count: 0,
            method: () => set((state: any) => ({ count: state.count + 1 }))
        })
    )
);

//App.tsx
const App: () => JSX.Element = () => {
    const { data } = useStore();
    return (
        <PersistGate
            loading={
                <View>
                    <BaseText>Loading</BaseText>
                </View>
            }
        >
                     // App content
        </PersistGate>
    );
};

I was very confused when installing the library that no configuration parameters were passed to PersistGate. This answers why.

Trashpants commented 4 years ago

@callmetwan 's solution was exactly what was needed - making a call to some kind of state in the same screen as <PersistGate> is what was needed

For anyone looking at this or confused the magic line in @callmetwan solution is:

const { data } = useStore();

within App.tsx

callmetwan commented 4 years ago

@Trashpants While not strictly related to this topic it is worth noting that you cannot use any non-stringable types in your store if using this zustand-persist. The reason being that localStorage and AsyncStorage serialize their values (require them to be strings). This may or may not have practical limitations on how you build your store.

For instance, any actions you create must exist during bootstrapping. If you tried to add them using useStore.setState({newAction: () => console.log('hi')}) they will exist while the app is in memory but they will not be persisted. This also means you cannot use types like Map or Set.

I was already intending to use whichever state solution I decided on in the with AsyncStorage so I'm not losing much, but others reading should be aware. Just to be clear, this isn't a limitation of this library, this is a limitation of localStorage and AsyncStorage.

Trashpants commented 4 years ago

hmmm I've just got around to trying with iOS and again I'm hitting the thing I had before which is very weird - unless im debugging it doesn't want to get past loading - what's super strange is that I can update the store within the loading section:

/**
 * font loading stuff
 */

  const { onboardingComplete, toggleOnboardingComplete } = useSettingsStore();
  return (
    <PersistGate
      loading={
        <View
          style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
        >
          <Text
            onPress={() => {
              toggleOnboardingComplete();
            }}
          >
            APP HAS LOADED: {onboardingComplete + ''}, fonts loaded:
            {loaded + ''},
          </Text>
        </View>
      }
    >
      {loaded && onboardingComplete && <RootNavigator />}
    </PersistGate>
callmetwan commented 4 years ago

Very weird. For a moment that happened to me, but then I stopped/restarted the metro bundler and it all cleared up. I'm a bit busy today but I'd be interested in looking into this with you; I'm planning on using this library for a project and once I make a decision I won't have time to reverse it, so discovering and solving problems like these is important to me.

alexandersandberg commented 4 years ago

const { data } = useStore();

within App.tsx

Hmm, this does nothing for me. I'm still having the same issue.

Trashpants commented 4 years ago

Out of interest, iOS or Android?

const { data } = useStore(); within App.tsx

Hmm, this does nothing for me. I'm still having the same issue.

what happens when you do remote debugging (assuming you're using expo)? Thats what's really leaving me scratching my head - when I have it enabled things work as expected (on iOS, android seems to be totally fine with and without remote debugging)

alexandersandberg commented 4 years ago

iOS 14.1, simulator, not Expo.

PersistGate seems to open fine when debug mode is enabled.

Trashpants commented 4 years ago

Anyone have any ideas where to start digging to fix this issue - I've been asked to use zustand in a project as other devs are comfortable using it, but I definitely need a persistence layer for RN.

especially as its impossible to actually console.log bits of data out for this anyone got any ideas of where to begin poking?

Obviously the isReady value inside the PersistGate file is always returning false, which makes sense but I'm struggling to get past that

callmetwan commented 4 years ago

I don’t have an immediate suggestion for that, but what I did to test was copy the source files to my project and import them from there instead of the npm package. This will allow you to debug where the failure is happening.

Trashpants commented 4 years ago

okay so looking directly in the source code in node_modules:

index.js line 130

changing it from

return React__default['default'].createElement(React__default['default'].Fragment, null, isReady ? children : loading);

to

return React__default['default'].createElement(React__default['default'].Fragment, null, isReady ? children : loading(getLoadManager().onAllLoadedCallback()));

Gets me around it, which points me to thinking that the function just isn't being re-run as values change and firing it again (presumably after the data is updated) forces that re-render?

Trashpants commented 4 years ago

okay sorry to spam up the thread but it looks like the onAllLoaded function just flat out doesn't run. I'm not sure why it doesn't as the LoadManager.setLoaded function works as its possible to watch loadStatusRecord get updated.

Im not sure what happens but it appears that onAllLoaded is entirely failing to run or the method acts like its empty?

callmetwan commented 4 years ago

Not spamming, this is helpful! I'll try to take a look at it tonight to see if I can add anything useful.

Trashpants commented 4 years ago

I've done what you have suggested and copied the code into my project directly. Now the project never loads which seems weird but what I think is happening is as follows:

configurePersist creates a new LoadManager when trying to hydrate and once its complete it SHOULD then do an onAllLoadedCallback, however that's not set within the instance because.....

PersistGate is trying to get access to the SAME LoadManager instance where it should instantly set the onAllLoadedCallback. However that's not happening.

in LoadManager

update line 6 to be

this.onAllLoadedCallback = () => {
      console.log('INITIAL VERSION OF CALLBACK');
 };

And it will only be called once where from what I understand it should be called twice - technically once with the default callback and then once again once the onAllLoaded has been set within PersistGate

edit:

Ignore this comment im being a dope - made a typo in my app so it wasn't actually using the persist gate properly

Trashpants commented 4 years ago

okay so, back to the original point I had, adding this inside the main body of PersistGate fixes the issue - ie forces it to run again.

(add it in at line 15)

  useEffect(() => {
    getLoadManager().setLoaded('');
  }, []);

the whole PeristGate file looks like this:

import React, { useEffect, useState } from 'react';

import { getLoadManager } from './LoadManager';

export interface PersistGateProps {
  children?: React.ReactNode;
  loading?: React.ReactNode;
  onBeforeLift?: () => void;
}

export function PersistGate(props: PersistGateProps): JSX.Element {
  const { children, loading = false, onBeforeLift } = props;
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    getLoadManager().setLoaded('');
  }, []);

getLoadManager().onAllLoaded(() => {
    onBeforeLift && onBeforeLift();
    setIsReady(true);
  });

  return <React.Fragment>{isReady ? children : loading}</React.Fragment>;
}

I think this points to it being a race condition, I assume when we're debugging PeristGate loads first, then when the hydrate runs it fires the setLoaded up, when running without debugging I assume the other way around


edit:

Im pretty much stuck at this point, I know adding this in forces it to work for me. However it definitely feels like a total hack. @roadmanfong any ideas why re-running setLoaded that would get around the issue?

saschageyer commented 3 years ago

Hi, is there any progress on this issue? We are also stuck at this point..

ghacosta commented 3 years ago

hi everybody! Do you have any news about this issue? I really want to use zustand but without Persist layer on React Native I'd go with redux and redux-persist.

Trashpants commented 3 years ago

As it currently stands I’m still running with that hack above and I’ve not come across any other issues with it.

has anyone else tried that out and seen if it solves the issue for them?

smallsaucepan commented 3 years ago

+1 for the useEffect workaround working for me. @roadmanfong would you be open to a PR even though this would be a workaround? Would make the library feel more predictable.

roadmanfong commented 3 years ago

Any pr are welcome here, I'm afraid I don't have time to implemented right away.

smallsaucepan commented 3 years ago

I think this might effectively be a duplicate of #9 as well. Will take a look at both and see what can be done.