Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

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

How to set a new state using async API call #149

Closed
otaviobonder-deel opened this issue May 23, 2020 · 15 comments
Closed

How to set a new state using async API call #149

otaviobonder-deel opened this issue May 23, 2020 · 15 comments
Assignees
Labels
documentation Improvements or additions to documentation

Comments

@otaviobonder-deel
Copy link

I'm trying to implement basic features to a project using Recoil, just to test the library. I'm really liking what I'm seeing. However, I have a doubt regarding to updating a state based on an async API call, i.e., I created the following selectors to fetch from API:

export const teamsState = selector({
    key: "teams",
    get: async ({ get }) => {
        try {
            const response = await api.get("/teams");
            return response.data.teams;
        } catch (e) {
            throw e;
        }
    },
});

export const teamById = (id) =>
    selector({
        key: "teamById",
        get: ({ get }) => {
            const teams = get(teamsState);

            return teams.filter((team) => team._id === id)[0];
        },
    });

The teamsState fetch a list of teams from the API, and the teamById returns the selected team from the list of teams.

My doubt comes now. If I need to add a new team, how should I proceed?

I have a route to add a team in my backend, and the response is the object team I added, not a list of teams. So, how should I update the teamsState to reflect this added object? Should I get all teams again, making an API call to get the list of teams (this doesn't seem the right choice, but would work)?

Great work on bringing this new library to the React environment!

@acutmore
Copy link

Hi @otaviobps. Similar to React.useState you can pass an updater function when updating an atom/selector.

const [teams, setTeams] = useRecoilState(teamsState);

const addTeam = useCallback((team) => {
   api.addTeam(team).then(addedTeam => {
    setTeams(teams => teams.concat(addedTeam));
  });
}, [setTeams]);

@otaviobonder-deel
Copy link
Author

Hey @acutmore thanks for replying!

Well, I'm entering a recursion problem, because I don't have an atom and I'm not sure what to call on the set property:

export const teamsState = selector({
    key: "teams",
    get: async ({ get }) => {
        try {
            const response = await api.get("/teams");
            return response.data.teams;
        } catch (e) {
            throw e;
        }
    },
    set: ({ set }, newValue) => set(teamsState, newValue),
});

The documentation states that to create an async state, the way is creating a selector instead of an atom. The problem is that in the selector documentation page, the set property says that it needs the Recoil state:

set: a function used to set the values of Recoil state. The first parameter is the Recoil state and the second parameter is the new value.

But since my Recoil state was made with a selector, I'm not sure what should I pass as the first parameter. Passing the own selector creates a recursion error

@amiregypt
Copy link

تزكروني باني احبكم جدا

@amiregypt
Copy link

هااي

@drarmstr
Copy link
Contributor

@otaviobps - A few notes:

Your teamById example won't work as-is because it tries to create a new selector every time the function is called. So, it would create redundant selectors and would try to create them all with the same key. You could embed the id in the key and memoize the function to make it work. Or, use the selectorFamily helper for this pattern.

This line:

    set: ({ set }, newValue) => set(teamsState, newValue),

won't work because the selector is trying to set itself from its set function, which would be an infinite loop. the set in a selector is intended to set upstream selectors or atoms.

In general, selectors are intended to be pure functions for derived state. Given a set of inputs, a selector should always evaluate to the same result and may be cached internally. I suspect your await api.get("/teams"); may resolve to different values if the teams state changes. Selectors are great for modeling async queries for things like DB lookups, but not for async queries for state that may change.

For the situation of syncing mutable remote state with local Recoil state, consider using an atom. The atom can represent the local copy of the state and effects can be used to subscribe either remote or local changes to keep them in sync. Please refer to the example in #143 . I'll be updating the online docs to make these two use-cases of async queries more clear, please provide feedback if the example in #143 helps.

@drarmstr
Copy link
Contributor

Duplicate of #143

@drarmstr drarmstr marked this as a duplicate of #143 May 24, 2020
@drarmstr drarmstr added the documentation Improvements or additions to documentation label May 24, 2020
@drarmstr drarmstr self-assigned this May 24, 2020
@acutmore
Copy link

acutmore commented May 24, 2020

Hi @otaviobps.

Well, I'm entering a recursion problem, because I don't have an atom and I'm not sure what to call on the set property:

Good question. This is why Recoil has Atoms and Selectors. Atoms have state, but no logic. Selectors have logic but no state.

If you want async state you need to combine an Atom and Selector together. (maybe @drarmstr will be able to correct me here).

const state = atom({
    key: "state",
    default: null,
});

export const asyncState = selector({
    key: "async-state",
    async get({ get }) {
        const s = get(state);
        if (s !== null) {
            return s;
        }
        return await api.get("/default");
    },
    set({ set }, newValue) {
        set(state, newValue);
    }
});

The above pattern only works for simple use-cases. If you had two web pages open and they were both updating the server state, or Recoil was persisting state between page-loads then the client could easily get out of sync with the server. In these situations you need a way of listening to remote state changes and updating recoil to ensure they stay in sync. More information in #143.

@otaviobonder-deel
Copy link
Author

otaviobonder-deel commented May 24, 2020

Hi @acutmore and @drarmstr. Thank you for your both replies.

Your teamById example won't work as-is because it tries to create a new selector every time the function is called. So, it would create redundant selectors and would try to create them all with the same key. You could embed the id in the key and memoize the function to make it work. Or, use the selectorFamily helper for this pattern.

In fact it worked great. Maybe was luck, but I'll refactor it to use the selectorFamily.

For the situation of syncing mutable remote state with local Recoil state, consider using an atom. The atom can represent the local copy of the state and effects can be used to subscribe either remote or local changes to keep them in sync.

How exactly would I initialize the atom? The documentation only shows an async API call with selectors, and I think it's not possible to put the API call in the atom directly, isn't it right? I'm seeing the #143 and trying to understand what's happening there, but I'm not sure how to adapt it to my problem. I'll continue to investigate it.

@acutmore for what I'm reading in issues and the documentation, it's not a good practice to set a selector that has an API call, isn't that right? So maybe, instead of set the selector, forcing it to update making another API call wouldn't solve the issue? The side effect would be that I would make another API call.

Edit: Just to clarify, getTeams API call returns an array of teams. What I'm trying to achieve is manipulate these teams, by editing them and creating new ones. I created the teamsById to filter the team I'm searching for by Id. I'm not sure how to update the teams remotely and locally, keeping their status in sync

@acutmore
Copy link

acutmore commented May 24, 2020

Hi @otaviobps

How exactly would I initialize the atom

Correct you can't put an async API call inside an atom, you'll need a component to set the initial value of the atom. e.g. by fetching the state in a useEffect then setting the Atom.

If you want to delay loading Recoil until after the Atom has been initialised the <RecoilRoot> component can take a initializeState function as a prop.

function AppLoader() {
  const [defaultValue, setDefaultValue] = useState(null);

  useEffect(() => {
    fetch("/teams").then(teams => setDefaultValue(teams));
  }, []);

  if (defaultValue === null) {
    return "loading";
  }

  function initializeState({ set }) {
    set(teamsState, defaultValue);
  }

  return (
    <RecoilRoot initializeState={initializeState}>
      <App />
    </RecoilRoot>
  );
}

Keeping databases in sync over a network connection is a difficult problem. I would be more tempted to use a library that has this as a core feature. https://pouchdb.com/ for example. And then use Recoil as a way for React components to access / respond / interact with the data.

@drarmstr
Copy link
Contributor

Atom defaults can also accept selectors, if you want to initialize the value based on a query.

And yes, feel free to use standard React effects or other libraries to sync remote state and then wrap that with Recoil nodes to integrate with Recoil's data-flow graph for updating and rendering React components.

@drarmstr
Copy link
Contributor

Duplicate of #143

@acutmore
Copy link

Hi @drarmstr

Atom defaults can also accept selectors, if you want to initialize the value based on a query.

That sounds perfect. I tried this out but looks like there is a bug preventing that from working in the released version at the moment. I've raised #161

@stephanoparaskeva
Copy link

stephanoparaskeva commented Jun 2, 2020

const state = atom({
    key: "state",
    default: null,
});

export const asyncState = selector({
    key: "async-state",
    async get({ get }) {
        const s = get(state);
        if (s !== null) {
            return s;
        }
        return await api.get("/default");
    },
    set({ set }, newValue) {
        set(state, newValue);
    }
});

How do you use this to add to state?

What's the reason for having a 'get' async call when I can just make a get request without having a selector. How is this 'get' storing the items to state? if it isn't, what Is this doing?

If I need to do a simple request for an array of users, and store that in recoil state. while being able to check if data is loading or hasErrored. How do I do it?
If I need to do a simple request to POST a new user and store the response in global state along with whether it's loading and if there is an error, how am I doing that?

I can't find where it explains this in the docs, I've only found explanation related to using 'get' of the selector.

Now I know you can use an atom to store this in state within a useEffect hook. But this is basic and does not allow you to check if the thing has loaded or has err'd.

@acutmore
Copy link

acutmore commented Jun 3, 2020

Hi @stephanoparaskeva

Lots of questions, I'll do my best to answer some of them.

how is this 'get' storing the items to state

Selectors will cache the returned value. If it is re-subscribed to later a new get won't be triggered.

You can reverse around the order so the result is stored in the atom, like this:

const asyncDefault = selector({
    key: "asyncDefault",
    async get() {
        return await api.get("/default");
    }
});

export const stateWithAsyncDefault = atom({
    key: "stateWithAsyncDefault",
    default: asyncDefault,
});

@luixo
Copy link

luixo commented Nov 27, 2020

Hello,
As this issue is the first one I stumble upon every time I try to google this, I'll ask my question here.
I have a complex atom that holds an array of entities, say string. I need to get some amount of them by offset and pageSize params. When I don't find entities to be present, I fetch them from api.
I don't get how I can save fetched value in the atom.

type Pager = {
    ids: string[];
    totalAmount: number;
};
const pagerAtom = recoil.atom<Pager>({
    key: 'pagerAtom',
    default: {
        totalAmount: 999, // some value I get from initial json object
        ids: []
    }
});

type PageSelectorOptions = {
    offset: number;
    pageSize: number;
};
const pageSelector = recoil.selectorFamily<string[], PageSelectorOptions>({
    key: 'pageSelector',
    get: ({offset, pageSize}) => async ({get}) => {
        const pager = get(pagerAtom);
        const availableAmount = Math.min(pager.totalAmount - offset, pageSize);
        const ids = pager.ids.slice(offset, offset + pageSize);
        if (ids.filter(Boolean).length >= availableAmount) {
            return ids;
        }
        const remotePager = await fetchPager({offset, pageSize});
        // this is the point I need to set fetched data to pagerAtom
        // set(pagerAtom, (prev) => {
        //     const nextIds = [...prev.ids];
        //     remotePager.ids.forEach((id, index) => nextIds[offset + index] = id);
        //     return {
        //         totalAmount: remotePager.totalAmount,
        //         ids: nextIds
        //     };
        // });
        return remotePager.ids;
    }
});

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

6 participants