Recoil: `set` from `initializeState` does not trigger hooks

Created on 26 Jun 2020  路  5Comments  路  Source: facebookexperimental/Recoil

When reinitializing state using initializeState on RecoilRoot it appears that calling set does not trigger hooks, so my app is not updated.

In the image below you can see:

  1. initializeState being run first ("Init state")
  2. The first value read from useRecoilValue - this is the default value
  3. set is called inside initializeState but useRecoilValue isn't triggered again

Screenshot 2020-06-26 at 11 48 22

I'm using Recoil with React Native based on #114 - perhaps this is related to that?

This is the code I use to persist my atoms:

export default function createRecoilPersistor(
  atoms: Array<RecoilState<any>>
): Persistors {
  function initializeState({ set }: Initializer): void {
    console.log('Init state');
    for (const currentAtom of atoms) {
      const { key } = currentAtom;
      AsyncStorage.getItem(key).then(value => {
        if (value !== null) {
          console.log('Set value for ' + currentAtom.key, { value });
          set(currentAtom, JSON.parse(value));
        }
      });
    }
  }

  function RecoilPersistor(): null {
    useTransactionObservation_UNSTABLE(
      ({ atomValues, modifiedAtoms }: Observation): void => {
        for (const modifiedAtom of modifiedAtoms) {
          const matchingAtom = atoms.find(({ key }) => key === modifiedAtom);
          if (matchingAtom) {
            AsyncStorage.setItem(
              modifiedAtom,
              JSON.stringify(atomValues.get(modifiedAtom))
            );
          }
        }
      }
    );
    return null;
  }

  return {
    initializeState,
    RecoilPersistor
  };
}

Then in my App.js:

const { RecoilPersistor, initializeState } = createRecoilPersistor([
  darkModeAtom,
  userAuthAtom,
  userDetailsAtom
]);

function useTheme() {
  const darkMode = useRecoilValue(darkModeAtom);
  console.log({ initDarkMode: darkMode });
  const baseTheme = darkMode ? eva.dark : eva.light;
  const statusBarStyle = darkMode ? 'light-content' : 'default';
  return [{ ...baseTheme, ...customTheme }, statusBarStyle];
}

function App(): React.ReactElement {
  const [theme, statusBarStyle] = useTheme();
  return (
    <ApplicationProvider {...eva} theme={theme}>
      <StatusBar barStyle={statusBarStyle} />
      <NavigationRoot />
    </ApplicationProvider>
  );
}

export default function AppRoot(): React.ReactElement {
  return (
    <>
      <IconRegistry icons={EvaIconsPack} />
      <SafeAreaProvider>
        <RecoilRoot initializeState={initializeState}>
          <RecoilPersistor />
          <App />
        </RecoilRoot>
      </SafeAreaProvider>
    </>
  );
}
question

Most helpful comment

The initializeState prop is only intended to setup the initial state before the initial render. This is useful for supporting things like server-side rendering where it is critical the state is hydrated for that initial render. It is not intended for async updates to state. Use the existing Recoil hooks for updating state asynchronously, such as useSetRecoilState() or useRecoilCallback()

All 5 comments

For anyone else waiting for the finished API, you can gate your application while you initialise your atoms manually:

export function useRecoilPersistStorage({
  atom,
  storage
}: RecoilPersistStorageHook): [boolean, any] {
  const [loaded, setLoaded] = useState(false);
  const [atomValue, setAtomValue] = useRecoilState(atom);

  useEffect(() => {
    storage.get().then(value => {
      setLoaded(true);

      if (value !== null) {
        setAtomValue(value);
      }
    });
  }, []);

  return [loaded, atomValue];
}

export function RecoilStorageGate({
  atom,
  storage,
  children
}: RecoilStorageGateProps): React.ReactElement {
  const [loaded] = useRecoilPersistStorage({ atom, storage });

  if (!loaded) {
    return (
      <View style={style.splash}>
        <Text>Splash screen</Text>
      </View>
    );
  }

  return <>{children}</>;
}

I'm too stucked with this issue. initializeState used to work with verion 0.0.8 but with version 0.0.10, it has stopped working

The initializeState prop is only intended to setup the initial state before the initial render. This is useful for supporting things like server-side rendering where it is critical the state is hydrated for that initial render. It is not intended for async updates to state. Use the existing Recoil hooks for updating state asynchronously, such as useSetRecoilState() or useRecoilCallback()

Thanks for the clarification and quick update to the docs 馃憤

Are there plans to allow for async initialisation in future?

I don't know that we'd want to complicate the core interface with this, but some helper wrapper util to support async initialization may make sense. Haven't tested the below, but maybe something like this... Let us know if it works for you.

function AsyncInitRecoilRoot({children, Fallback, initializeStateAsync}) {
  const [isLoaded, setLoaded] = useState(false);

  function InitializeState() {
    const snapshot = useRecoilSnapshot();
    const gotoSnapshot = useGotoRecoilSnapshot();

    useEffect(async () => {
      const initializedSnapshot = await snapshot.mapAsync(initializeStateAsync);
      gotoSnapshot(initializedSnapshot);
      setLoaded(true);
    }, []);

    return null;
  }

  return (
    <RecoilRoot>
      {isLoaded
        ? children
        : <>
           <InitializeState />
           <Fallback />
        </>
      }
    </RecoilRoot>
  );
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Sawtaytoes picture Sawtaytoes  路  4Comments

jamiewinder picture jamiewinder  路  3Comments

yuantongkang picture yuantongkang  路  3Comments

pesterhazy picture pesterhazy  路  4Comments

art1373 picture art1373  路  4Comments