Recoil: Render based on previous state while async selector is pending.

Created on 9 Jun 2020  Â·  6Comments  Â·  Source: facebookexperimental/Recoil

My case is very simple.

I want to be able to read old data while new data is fetching with async selector which is depending on some changing value.

Is it possible?

enhancement

Most helpful comment

Implemented temporary solution for this problem, in case someonr has same issues.

import { useEffect, useState } from 'react';
import { RecoilValue, useRecoilValueLoadable } from 'recoil';

export function useLoadable<T>(
  defaultValue: Partial<T>,
  recoilLoadable: RecoilValue<T>,
  pick?: [keyof T],
): [T, 'loading' | 'hasValue' | 'hasError'];
export function useLoadable<T>(
  defaultValue: Partial<T>,
  recoilLoadable: RecoilValue<T>,
  pick?: [keyof T],
): [Partial<T>, 'loading' | 'hasValue' | 'hasError'] {
  const [value, setValue] = useState(defaultValue);
  const recoilValue = useRecoilValueLoadable<T>(recoilLoadable);

  let returnValue: Partial<T> = defaultValue;

  useEffect(() => {
    if (recoilValue.state === 'hasValue' && recoilValue.contents !== value) {
      setValue(recoilValue.contents);
    }
  }, [recoilValue.contents, recoilValue.state, value]);

  if (recoilValue.state !== 'hasValue' && value) {
    if (pick) {
      returnValue = pick.reduce((res, key) => ({ ...res, [key]: value[key] }), {});
    } else {
      returnValue = value;
    }
  }

  if (recoilValue.state === 'hasValue') {
    returnValue = recoilValue.contents;
  }

  return [returnValue, recoilValue.state];
}

All 6 comments

Async selectors need to be "pure", that is always return the same results for a given set of input dependency values. However, if you are fetching data based on state from dependent atoms/selectors, or using a pattern such as selectorFamily to pass in data query parameters, then you may wish to show old state while loading.

Internally, we have situations we want to show a spinner overlay on a chart with old data, while the new data is loading. We played with some React component abstraction that will take asynchronous dependencies, use useRecoilValueLoadable() to get and save the current state, which knows if they are loading. If so, show the old state with the overlay until new data is loaded, then update the state we store. However, this is dangerous and unsafe! Rendering the content based on the previous saved state may reference other atoms/selectors which could then be inconsistent with the remembered state and lead to errors or invalid data.

However, a proper solution is in the works. When we finish support for React Concurrent Mode, then there is a transition hook you can use to render from the old state to the new. cc @davidmccabe to update when that's available.

@drarmstr so as for now I need to keep additional atom to keep state from async selector and check on selector state to decide which value to use, right?

Async selectors need to be "pure", that is always return the same
results for a given set of input dependency values.

I think the proper word in this context is "idempotent".

An idempotent function can cause idempotent side-effects. A pure function
cannot.

https://stackoverflow.com/questions/1077412/what-is-an-idempotent-operation

https://stackoverflow.com/questions/4801282/are-idempotent-functions-the-same-as-pure-functions

On Wed, Jun 10, 2020 at 3:45 AM Sergiy Babich notifications@github.com
wrote:

@drarmstr https://github.com/drarmstr so as for now I need to keep
additional atom to keep state from async selector and check on selector
state to decide which value to use, right?

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/facebookexperimental/Recoil/issues/290#issuecomment-641848815,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/AADBBIHWBJBYG5MMPJN4LQLRV5B2RANCNFSM4NZQWJQQ
.

Probably, just extend Loadable type with previousContent property?

@babichss - In our wrapper we used React state to store the previous Loadables from useRecoilValueLoadable. But again, remember, this is dangerous! I shouldn't even be suggesting such hacks... ;) Think of it as a workaround until Concurrent Mode support.

Implemented temporary solution for this problem, in case someonr has same issues.

import { useEffect, useState } from 'react';
import { RecoilValue, useRecoilValueLoadable } from 'recoil';

export function useLoadable<T>(
  defaultValue: Partial<T>,
  recoilLoadable: RecoilValue<T>,
  pick?: [keyof T],
): [T, 'loading' | 'hasValue' | 'hasError'];
export function useLoadable<T>(
  defaultValue: Partial<T>,
  recoilLoadable: RecoilValue<T>,
  pick?: [keyof T],
): [Partial<T>, 'loading' | 'hasValue' | 'hasError'] {
  const [value, setValue] = useState(defaultValue);
  const recoilValue = useRecoilValueLoadable<T>(recoilLoadable);

  let returnValue: Partial<T> = defaultValue;

  useEffect(() => {
    if (recoilValue.state === 'hasValue' && recoilValue.contents !== value) {
      setValue(recoilValue.contents);
    }
  }, [recoilValue.contents, recoilValue.state, value]);

  if (recoilValue.state !== 'hasValue' && value) {
    if (pick) {
      returnValue = pick.reduce((res, key) => ({ ...res, [key]: value[key] }), {});
    } else {
      returnValue = value;
    }
  }

  if (recoilValue.state === 'hasValue') {
    returnValue = recoilValue.contents;
  }

  return [returnValue, recoilValue.state];
}
Was this page helpful?
0 / 5 - 0 ratings