zustand-persist icon indicating copy to clipboard operation
zustand-persist copied to clipboard

Usage with expo

Open GaspardC opened this issue 3 years ago • 28 comments

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

GaspardC avatar Oct 07 '20 12:10 GaspardC

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

roadmanfong avatar Oct 18 '20 12:10 roadmanfong

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 avatar Oct 19 '20 12:10 Trashpants

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

Trashpants avatar Oct 24 '20 18:10 Trashpants

I'm having the same problem without Expo.

alexandersandberg avatar Oct 24 '20 20:10 alexandersandberg

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

Trashpants avatar Oct 27 '20 12:10 Trashpants

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.

callmetwan avatar Nov 02 '20 21:11 callmetwan

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

Trashpants avatar Nov 02 '20 22:11 Trashpants

@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.

callmetwan avatar Nov 03 '20 00:11 callmetwan

@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

Trashpants avatar Nov 03 '20 08:11 Trashpants

@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.

callmetwan avatar Nov 03 '20 15:11 callmetwan

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>

Trashpants avatar Nov 03 '20 18:11 Trashpants

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.

callmetwan avatar Nov 03 '20 19:11 callmetwan

const { data } = useStore();

within App.tsx

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

alexandersandberg avatar Nov 04 '20 18:11 alexandersandberg

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)

Trashpants avatar Nov 04 '20 18:11 Trashpants

iOS 14.1, simulator, not Expo.

PersistGate seems to open fine when debug mode is enabled.

alexandersandberg avatar Nov 04 '20 18:11 alexandersandberg

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

Trashpants avatar Nov 09 '20 17:11 Trashpants

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.

callmetwan avatar Nov 09 '20 17:11 callmetwan

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 avatar Nov 09 '20 17:11 Trashpants

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?

Trashpants avatar Nov 09 '20 18:11 Trashpants

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

callmetwan avatar Nov 09 '20 19:11 callmetwan

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 avatar Nov 09 '20 20:11 Trashpants

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?

Trashpants avatar Nov 09 '20 21:11 Trashpants

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

saschageyer avatar Apr 10 '21 13:04 saschageyer

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.

ghacosta avatar May 18 '21 19:05 ghacosta

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?

Trashpants avatar May 18 '21 19:05 Trashpants

+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.

smallsaucepan avatar Nov 02 '21 02:11 smallsaucepan

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

roadmanfong avatar Nov 03 '21 10:11 roadmanfong

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

smallsaucepan avatar Nov 03 '21 13:11 smallsaucepan