Skip to content
This repository has been archived by the owner on Jan 1, 2025. It is now read-only.

Render based on previous state while async selector is pending. #290

Open
babichss opened this issue Jun 9, 2020 · 10 comments
Open

Render based on previous state while async selector is pending. #290

babichss opened this issue Jun 9, 2020 · 10 comments
Labels
enhancement New feature or request

Comments

@babichss
Copy link

babichss commented Jun 9, 2020

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?

@drarmstr drarmstr added the question Further information is requested label Jun 10, 2020
@drarmstr
Copy link
Contributor

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.

@babichss
Copy link
Author

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

@AjaxSolutions
Copy link

AjaxSolutions commented Jun 10, 2020 via email

@babichss
Copy link
Author

Probably, just extend Loadable type with previousContent property?

@drarmstr drarmstr added the enhancement New feature or request label Jun 10, 2020
@drarmstr
Copy link
Contributor

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

@babichss
Copy link
Author

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];
}

@drarmstr drarmstr removed the question Further information is requested label Dec 12, 2020
@drarmstr drarmstr changed the title Is it possible to get previous async selector while loading without triggering Suspense? Render based on previous state while async selector is pending. Jan 5, 2021
@koistya
Copy link

koistya commented Aug 7, 2022

Yet another solution that memoizes the last value;

export function useData() {
  const value = useRecoilValueLoadable(Data);
  const ref = React.useRef();
  
  if (value.state === "hasValue") {
    ref.current = value
  }
  
  return value.state === "loading" && ref.current?.state === "hasValue" ? ref.current : value;
}

@rkok
Copy link

rkok commented Jan 25, 2023

My own spin on @koistya's answer to show that data is loading while displaying the previous data.

export function useRecoilValueBackgroundLoadable<T>(
  data: RecoilValue<T>
): [Loadable<T>, { isLoading: boolean }] {
  const value = useRecoilValueLoadable(data);
  const ref = React.useRef<typeof value>();

  if (value.state === "hasValue") {
    ref.current = value;
  }

  return [
    value.state === "loading" && ref.current?.state === "hasValue"
      ? ref.current
      : value,
    { isLoading: value.state === "loading" },
  ];
}

const MyComponent = () => {
  const refresh = useRecoilRefresher_UNSTABLE(thingsQuery);
  const [things, { isLoading }] = useRecoilValueBackgroundLoadable(thingsQuery);

  let content;

  if (things.state === "hasError") {
    content = <div>Error loading things</div>;
  } else if (things.state === "hasValue") {
    content = <ThingsTable projects={things.getValue()} />;
  } else {
    content = <LoadingMessage />;
  }

  return (
    <>
      <button onClick={() => refresh()} disabled={isLoading}>
        Refresh
      </button>
      {content}
    </>
  );
};

@dakom
Copy link

dakom commented Jul 11, 2023

Yet another small adaptation. If there's something wrong with this, please let me know - but it seems to work perfectly to suspend only on the first load and then keep showing the "latest and greatest" without re-suspending

export function useSuspendOnce<T>(state: RecoilValueReadOnly<T>): T {
  const value = useRecoilValueLoadable(state)
  const ref = useRef<Loadable<T>>()

  if (value.state === "hasValue") {
    ref.current = value
  }

  if (value.state === "loading" && ref.current?.state === "hasValue") {
    return ref.current.contents
  }

  return value.getValue()
}

Usage:

function MyLoader() {
  return (
    <div>
      <Suspense fallback={<div>loading...</div>}>
        <MyData />
      </Suspense>
    </div>
  )
}

function MyData() {
  const data = useSuspendOnce(myDataSelector)
  return <div>{data}</div>
}

@procopenco
Copy link

procopenco commented Feb 13, 2024

https://github.com/cronocodesolutions/recoil-utils

import { useRecoilCachedValue } from '@cronocode/recoil-utils';

function Component() {
     const [data, loading] = useRecoilCachedValue(selector, defaultValue);

     // loading - use it to show spinner while new data is loading

     return <div>{data}</div>;
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

7 participants