添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement . We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I've seen many posts detailing how to use a selector for async get requests, these show whether request has loaded or if it has content and they work great. But they don't store to state.

I've also seen explanations on how to store async state in atoms using useEffect etc. But they don't show the current status of the request.

I haven't been able to find out whether you can:

  • Make an async call
  • Perform operations on the data
  • Store the data to state
  • Check whether data is 'loading', 'hasError' or 'hasContent'
  • And potentially store the error to state.
  • changed the title How to store data to state asynchronously And know if request is 'loading', 'hasContent', 'hasError' How to store data to state asynchronously. And know if request is 'loading', 'hasContent', 'hasError' Jun 2, 2020

    You can useRecoilValueLoadable and useRecoilStateLoadable to return a loadable which lets you manually deal with the results.

    For doing this inside the async function you can use noWait and waitForAny / waitForNone to convert them into loadables which let you manually handle the statuses. With this though, setting that state isn't going to work, as selectors can only read values. You might be able to get around this part by wiring some stuff up so that when setting a selector it does this request and sets the necessary values that you would then read from, kinda like a "WriteOnly" selector (you probably shouldn't do this?).

    @Shmew Is this not a pretty common situation where you want to be able to set async data to state but also display loading screens while async data is being loaded into state and possibly handle errors if there are any. It seems like right now you can either handle loading and get requests - but not store in state. Or store data in state, but not be able to handle or observe loading status or errs.

    Oh yeah absolutely, you can just create an atom/selector with a promise inside. It will throw it to the nearest suspense element. You can have your selector return a type that can represent a successful or failed state and handle it that way, or without suspense you can use a loadable and check if it's still loading/successful/encountered an error.

    See this and this .

    Oh yeah absolutely, you can just create an atom/selector with a promise inside. It will throw it to the nearest suspense element. You can have your selector return a type that can represent a successful or failed state and handle it that way, or without suspense you can use a loadable and check if it's still loading/successful/encountered an error.

    See this and this .

    But this doesn't allow you to simply store the state in an atom you can only use a selector with 'get' which gets cached. Other than that I can't find a way to store the state via async actions and also observe the current status of the promise that's updating state. I have read both of those links, the first only describes 'get' selectors (these don't update state) and the second (correct me if i'm wrong) is about subscribing to backend events (for notifications etc).

    Do you have an example?

    currently I have an atom, which I am updating in useeffect with the result of a query to my backend.
    I want this to also be able to display the status so I can render loading pages.

    What you want is a selectorFamily . It's the same as the read-only selectors, but it lets you pass in the query.

    Another option is creating an atom that is fetched by a read-only selector that then performs the async action.

    I have examples of both of these scenarios, but I write my code in F#, so I'm not sure how much it will help you (although the API is followed pretty closely for both react and recoil):

  • selectorFamily
  • atom and read-only selector
  • Why do you want to store the result to state? If you go through the problem you're trying to solve at a high level I may be able to suggest a way to model it using Recoil's concepts.

    @davidmccabe

    What about the general Idea of getting/posting data to/from the server, updating data and displaying updated responses. Getting and setting new data. With all of this you'd also want to have errors stored to display any errors for toasters or if they effect other pages, and you'd need loading to show loading screens and you'd need be able to be able to asynchronously set data. And store this to state so you can do background work elsewhere if things are loading.

    I don't have a high level example as I initially tried to use selectors for setting and async stuff this way but I couldn't get them to work. I've just been using atoms for everything now. But here is some pseudo code of my problem:

    import React from 'react';
    import axios from 'axios';
    import { atom, selector, useRecoilValueLoadable, useRecoilValue } from 'recoil';
    // our questions
    const questionsState = atom({
      key: 'questionsState',
      default: [{ rating: 0 }],
    });
    // errors state for displaying error messages on other screens or for showing visual feedback on current screen
    const errorsState = atom({
      key: 'errorsState',
      default: {},
    });
    const lastQuestionState = atom({
      key: 'lastQuestionState',
      default: '',
    });
    // nice lil derivation of state not a problem
    const ratedFiveStarQuestions = selector({
      key: 'myDoubleCount',
      get: ({ get }) => get(questionsState).filter(q => q.rating >= 5),
    });
    const questionsSelector = selector({
      key: 'questionsSelector',
      get: async () => {
        // get data for caching... but what do you do if the cache needs to be updated?
        // what about if the cache is out of sync etc?
        const qs = await axios.get('/questions');
        return qs;
      set: async ({ set, get }, newQuestion) => {
        // set data to our state, simple async example
        // post new question add it to stateful questions array for use in other components
        try {
          const { questionResponse } = await axios.post('/questions', {
            data: newQuestion,
          });
          set(questionsState, [...get(questionsState), questionResponse]);
          set(lastQuestionState, questionResponse);
        } catch (err) {
          set(errorsState, err);
    });
    const App = () => {
      // access loading to be able to display loading wheels
      const lastQuestion = useRecoilValue(lastQuestionState);
      const questions = useRecoilValue(questionsState);
      const questionsLoadable = useRecoilValueLoadable(questionsSelector);
      switch (questionsLoadable.state) {
        case 'hasValue':
          return <div>{questionsLoadable.contents}</div>;
        case 'loading':
          return <div>Loading...</div>;
        case 'hasError':
          throw questionsLoadable.contents;
      return (
        <div className='App'>
          <div>Your Last Question</div>
          <div>{lastQuestion.title}</div>
          <div>{lastQuestion.desc}</div>
          <div>All Questions</div>
            {questions.map(q => (
              <div>{q.title}</div>
            ))}
          </div>
        </div>
    export default App;

    @stephanoparaskeva - It sounds like what you're trying to do is use remote mutable state with a local atom state as a write-through cache?

    For now, can you use an atom to represent the local cache of the state? The atom could contain an object which contains the value and/or if it is an error or pending. This loses the automatic support for Suspense, but you can check for the async status directly and render as desired. Or, if you only care about loading state/error for the initial render, instead of also for subsequent updates, then using a promise in the atom default may be sufficient. Then, in a component you could use useEffect() s to subscribe to atom changes for a side-effect of updating the remote storage and subscribing for changes from the remote storage to update the atom state. We're considering and working on ways to better support this pattern, but would this work for now?

    Maybe I am missing something fundamental too because I am wondering the EXACT same thing.

    The very first line in the docs for Atoms is:
    "Atoms contain the source of truth for our application state."

    How is it the source of truth if we cannot load data to it from the server? I am using a hook, then setting state with useRecoilState(myAtom) -- but this way I have to handle my own isLoading, etc...

    How I thought it would work.

    1.) Get data from server
    2.) Store it in an atom so that all components can see the state
    3.) Simply change from useRecoilState to useRecoilStateLoadable to utilize the status of fetching/error/hasValue - then change consumers to utilize the state and contents props since its now a 'Loadable'.

    <Header> {myAtom.content?.userName} </Header> <Content> myAtom.hasValue && <div> {myAtom.content.userName} </div> <div> { myAtom.content.favoriteColor } </div> myAtom.loading && <div> loading... </div> </Content>

    This loses the automatic support for Suspense, but you can check for the async status directly and render as desired.

    @drarmstr

    The Suspense support is one of my favorite things about Recoil, though it's not losing automatic Suspense support that bums me out. Managing fetch lifecycles for each asynchronous resource is tedious and cumbersome; encapsulating it in Recoil's Loadable is a huge improvement. I followed your advice and made some hooks to encapsulate Loadable behavior into an atom.

    import type { RecoilState, Loadable, LoadablePromise, Snapshot } from 'recoil';
    import { atom, useRecoilSnapshot, useRecoilState } from 'recoil';
    import { useCallback } from 'react';
    function memoize<R, T>(func: (state: RecoilState<T>, snapshot: Snapshot) => R) {
      const cache: Map<RecoilState<T>, R> = new Map();
      return function memoized(state: RecoilState<T>, snapshot: Snapshot) {
        if (!cache.has(state)) {
          cache.set(state, func(state, snapshot));
        return cache.get(state)!;
    const loadableAtomFamily = memoize(<T>(state: RecoilState<T>, snapshot: Snapshot) =>
      atom<Loadable<T>>({
        key: `loadable_${state.key}`,
        default: snapshot.getLoadable(state)
    type AsyncSetter<T> = Promise<T> | (<TArgs extends any[]>(...args: TArgs) => Promise<T>);
    function useAtomLoadable<T>(atom: RecoilState<T>): [Loadable<T>, (arg: AsyncSetter<T>) => void] {
      const snapshot = useRecoilSnapshot();
      const [value, setValue] = useRecoilState(loadableAtomFamily(atom, snapshot));
      const set = useCallback(
        (arg: AsyncSetter<T>) => {
          (async () => {
            const promise: LoadablePromise<T> = new Promise((resolve) =>
              (typeof arg === 'function' ? arg() : arg).then((result) => resolve({ value: result }))
            setValue({
              state: 'loading',
              contents: promise
            });
            try {
              setValue({
                state: 'hasValue',
                contents: (await promise).value
              });
            } catch (error) {
              setValue({
                state: 'hasError',
                contents: error
              });
          })(); // wrapping this so the return type is void, not Promise<void>
        [setValue]
      return [value, set];
    

    And some convenience hooks to add Suspense support and analogs for useSetRecoilState and useRecoilValue.

    export function useAtomAsync<T>(atom: RecoilState<T>): [T, (arg: AsyncSetter<T>) => void] {
      const [value, setValue] = useAtomLoadable(atom);
      if (value.state === 'hasValue') {
        return [value.contents, setValue];
      throw value.contents;
    export function useSetAtomAsync<T>(atom: RecoilState<T>): (arg: AsyncSetter<T>) => void {
      const [, setValue] = useAtomLoadable(atom);
      return setValue;
    export function useAtomAsyncValue<T>(atom: RecoilState<T>): T {
      const [value] = useAtomLoadable(atom);
      if (value.state === 'hasValue') {
        return value.contents;
      throw value.contents;
    export default useAtomLoadable;

    Finally I made an atom whose default value is an arbitrary Promise. I feel like this is jank but I don't know what Promise to pass in as the default. Please advise?

    export const fooState = atom<Foo>({
      key: 'state',
      default: Promise.resolve() as any as Promise<Foo>
    

    So now I have a setter for my atom that accepts a promise or a promise callback. In my case I'm using using useEffect to sync a route param with the recoil state and an async resource.

    const setFoo = useSetAtomAsync(currentState);
    const { id } = useParams<{ id?: string }>();
    useEffect(() => {
      if (id) {
        // promise
        setFoo(RemoteAPI.fetchFoo(id));
    }, [id, setFoo]);
    // or push updates to an API and sync the result
    function handleSubmit(data: Foo) {
      // promise callback
      setFoo(async () => {
        const value = await RemoteAPI.updateFoo(data);
        return value;
      });
    

    It feels like the problem I'm trying to solve is very fundamental but I had to do a bunch of stuff to get there. Am I missing something? Does my solution make sense?

    @justintoman - Yup, I absolutely agree that this pattern is something we want to support better with Recoil. We're exploring an API to help manage the "global" async state subscription and atom sync. But, we're still considering alternatives and validating it with some libraries, so we aren't ready to publish it yet. We're also looking at the ability to explicitly set Atoms to async values so you can get the benefit of Suspense with Atoms beyond the default. But, that requires defining some subtle API semantics and API changes.

    changed the title How to store data to state asynchronously. And know if request is 'loading', 'hasContent', 'hasError' Ability to set an Atom to a Promise / pending async value. Aug 31, 2020

    hey there, couple years later, but I stumbled upon this issue to do something similar. I realized what I needed was actually to have a POST request I could call, not when the component rendered but upon user action, and still get the loading/value/error state, similar to things being discussed on this topic. Also, I needed it to be identified by it's id not the params

    This is my solution:

    type LazySelector<T, P> = {
      get: RecoilValueReadOnly<T | undefined>
      call: RecoilValueReadOnly<(params: P) => void>
    export const lazySelectorFamily = <T, P>(props: {
      key: string
      get: (id: string) => (params: P) => Promise<T>
    }): ((id: string | undefined) => LazySelector<T, P>) => {
      const requests: RecoilState<{ [key: string]: Promise<T> }> = atom({
        key: props.key,
        default: {} as { [key: string]: Promise<T> },
      const getter = selectorFamily({
        key: `${props.key}Getter`,
        get:
          (id) =>
          async ({ get }) => {
            return id ? await get(requests)[id.toString()] : undefined
      const caller = selectorFamily({
        key: `${props.key}Caller`,
        get:
          (id) =>
          async ({ get, getCallback }) => {
            const currentRequests = get(requests)
            return getCallback(({ set }) => (params: P) => {
              if (!id) return
              const newValue = props.get(id.toString())(params)
              set(requests, {
                ...currentRequests,
                [id.toString()]: newValue,
      return (id: string | undefined) => ({ get: getter(id), call: caller(id) })
    export const useRecoilLazyLoadable = <T, P>(lazySelector: LazySelector<T, P>): [Loadable<T | undefined>, (params: P) => void] => {
      const get = useRecoilValueLoadable(lazySelector.get)
      const call = useRecoilValue(lazySelector.call)
      return [get, call]
    

    Basically I store the promises in an atom, and use a selector to wrap around it to return the current promise to be able to be used with suspense/loadable, or undefined if the request was not triggered yet, then another selector which uses getCallback to be able to have a caller function with a nice type signature for the desired params

    Then you can define the selector like that:

    export const generateImageRequest = lazySelectorFamily({
      key: "generateImageRequest",
      get: (_id) => async (params: { frame: GenerationFrame; editor: Editor }) => {
        return await generateImage(params)
    

    And use it like this:

    const [imageRequest, generateImage] = useRecoilLazyLoadable(generateImageRequest(popup?.target.id))
    const onClick = useCallback(async () => {
      generateImage({
        frame: popup.target,
        editor: editor,
    }, [popup, generateImage, editor])
    return (
         <Button size="compact" onClick={onClick}>
            Generate
         </Button>
         {imageRequest.state == "loading" && "Loading..."}
         {imageRequest.state == "hasValue" && imageRequest.getValue() && <img src={imageRequest.getValue().image} />}
      </div>
    

    This makes it very easy to both trigger the call whenever you want (not on component render), and render according to the state of the promise

    I have a similar (same?) problem.

    The default property for an atom already accepts a Promise and that makes using Suspense work nicely. In the example below, I would like to not load the remote list of APIs eagerly, but only after the user clicks a button. How can this be accomplished without a selector, hacking Snapshots or dozens of lines of utility code–if at all?

    I feel like I a missing something simple. I did read the docs multiple times... 😅

    import { Suspense } from 'react'
    import { atom, useRecoilValue } from 'recoil'
    const publicApisAtom = atom<ApiInfoList>({
      key: 'PublicApis',
      default: fetch('https://api.publicapis.org/entries') // from https://apipheny.io/free-api/
        .then((result) => result.json() as Promise<PublicApis>)
        .then((publicApis) => publicApis.entries)
    function ApiList() {
      const publicApis = useRecoilValue(publicApisAtom)
      return <>{publicApis.map((it) => <p key={it.API}>{it.API}: {it.Link}</p>)}</>
    function App() {
      return <>
        <Suspense fallback={<p>Public APIs loading ...</p>}>
          <p>Public APIs:</p>
          <ApiList />
        </Suspense>
    type PublicApis = { readonly count: number, readonly entries: ApiInfoList }
    type ApiInfoList = ReadonlyArray<ApiInfo>
    type ApiInfo = {
      readonly API: string, readonly Description: string, readonly Auth: string,
      readonly HTTPS: boolean, readonly Cors: string, readonly Link: string,
      readonly Category: string