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

Question: Post data through API with RecoilJS #462

Closed
PhanDungTri opened this issue Jul 10, 2020 · 9 comments
Closed

Question: Post data through API with RecoilJS #462

PhanDungTri opened this issue Jul 10, 2020 · 9 comments
Assignees
Labels
question Further information is requested

Comments

@PhanDungTri
Copy link

In RecoilJS docs, there is an example how to handle asynchronous data queries, but it's only about get data.

Let say I have a simple state:

const accountState = atom({
  key: "accountState",
  default: {
    id: "",
    name: "",
  }
});

And a component which is a register form:

const RegisterForm = () => {
  return (
    <form>
      <input type="text" name="username" />
      <button type="submit">Register</button>
    </form>
  )
}

The posted data is in FormData. After successfully created new account, the server will send a response that contains id and name of the account.

{
  "id": "abcdef123456",
  "name": "example"
}

This response data will be set as a new state of accountState.

How can I handle the process in RecoilJS?

@adrianbw
Copy link

Here's how we accomplished it:

First, our atom, which has an postData property:

const PostState = atom<State>({
  key: "post",
  default: {
    postData: null
  },
});

Next a selector to set that state:

export const postRequestState = selector<PostData | null>({
  key: `getPostRequest`,
  get: ({ get }) => {
    return get(PostState).postData;
  },
  set: ({ set }, postData): void => {
    set(PostState, (oldState: State) => ({
      ...oldState,
      postData,
    }));
  },
});

And then finally an async selector to do the post, as long as postData isn't null:

export const postResponseState = selector({
  key: `$getPostResponse`,
  get: async ({ get }) => {
    const exportStoreData = get(PostState);
    if (PostState.exportRequest) {
      return await // async POST here
    } else {
      return false;
    }
  },
});

This way, as long as you don't have a post ready, the selector does nothing. However, if you update postData, the selector will immediately fire off your selector.

As I'm thinking about this, another approach would be to have a component responsible for doing the fetching using an async selectorFamily. Every time you wanted to post, you'd render out a new one of these with the relevant post data. That might be a better approach. Something like this:

const MakePost = selectorFamily({
  get: async () => // async POST here
});

type Props = {
  postData: PostData;
}

export const makePost: React.FunctionComponent<Props> = (props: Props) => {
  const post = useRecoilValueLoadable(MakePost(props.postData));
  React.useEffect(() => {
    if (post.state === "hasValue") {
      // do something with the server response here
    };
  }, [post]);
}

@adrianbw
Copy link

adrianbw commented Jul 10, 2020

A followup to say that I'd love to see an async setter so that we could do something much more simple like this:

const postSelector = selectorFamily({
  get: // whatever
  set: async (params) => ({ set }) => {
    const response = await postQuery(params);
    set(YourAtom, // etc);
  }
});

@drarmstr
Copy link
Contributor

A followup to say that I'd love to see an async setter so that we could do something much more simple like this:

@adrianbw - Instead of an async post selector set, consider just using a setter for YourAtom from useSetRecoilState() or useRecoilCallback() to provide a callback which can asynchronously set the atom with the response.

const yourPostQuery = useRecoilCallback(({set}) => async params => {
  const response = await postQuery(params);
  set(yourAtom, response.data);
});

@PhanDungTri
Copy link
Author

PhanDungTri commented Jul 11, 2020

A followup to say that I'd love to see an async setter so that we could do something much more simple like this:

const postSelector = selectorFamily({
  get: // whatever
  set: async (params) => ({ set }) => {
    const response = await postQuery(params);
    set(YourAtom, // etc);
  }
});

I tried this approach, but got this error.

Argument of type 'FormData' is not assignable to parameter of type 'SerializableParam'.
  Type 'FormData' is not assignable to type '{ [key: string]: SerializableParam; }'.
    Index signature is missing in type 'FormData'

As I said, my data is a FormData.

@adrianbw
Copy link

We're definitely still struggling with good patterns here.

A couple scenarios:

  1. We want to get a loadable for a POST. We can follow the pattern I put above, but is there any way to get a loadable out of useRecoilCallback?
  2. We want to make an async request and add the resulting items to an array. useRecoilCallback can overwrite the contents of the array, or we could access the snapshot and add to it, but that risks us not being in the correct state (if something else has updates it). What's the right way to do something like this?

@drarmstr
Copy link
Contributor

  • We want to get a loadable for a POST. We can follow the pattern I put above, but is there any way to get a loadable out of useRecoilCallback?

Yes, useRecoilCallback() provides a snapshot with a getLoadable() accessor.

  • We want to make an async request and add the resulting items to an array. useRecoilCallback can overwrite the contents of the array, or we could access the snapshot and add to it, but that risks us not being in the correct state (if something else has updates it). What's the right way to do something like this?

Use the updater form when setting:

  set(yourAtom, currentArray => [...currentArray, newEntry]);

@drarmstr drarmstr added the question Further information is requested label Jul 14, 2020
@drarmstr drarmstr self-assigned this Jul 14, 2020
@adrianbw
Copy link

So I'm sure I'm being absolutely dense, but let's say I do this:

const doPost =  useRecoilCallback(({set}) => async params => {
  const response = await postQuery(params);
  set(yourAtom, response.data);
});

return (<button onClick={doPost(parms)} />);

What do I do to get a loadable that I can use to watch the status of that post when I click the button?

@drarmstr
Copy link
Contributor

drarmstr commented Jul 14, 2020

  const loadable = useRecoilValueLoadable(yourAtom);

useRecoilValueLoadable() subscribes the component to re-render when the state changes and provide a Loadable with the current state. But, note that in this case you are explicitly setting the atom to the new value with doPost(), so the atom will maintain the old value until the post completes and then take the new value when done, it will not be in a pending state. So, for this case it would probably be easier to just use useRecoilValue().

We're looking into the ability to be able to set an atom to a pending promise to take advantage of React Suspense. But, in the meantime, if you want the atom in a pending state during the query you could manually provide this by storing an object which contains the state and data, basically your own loadable type, and update the atom to a loading state before the postQuery() and then updating again after it completes.

@drarmstr
Copy link
Contributor

Updated documentation for syncing Recoil state with external storage in #680

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants