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

Mutations #28

Closed
thomasdashney opened this issue Mar 3, 2020 · 1 comment · Fixed by #48
Closed

Mutations #28

thomasdashney opened this issue Mar 3, 2020 · 1 comment · Fixed by #48

Comments

@thomasdashney
Copy link
Contributor

thomasdashney commented Mar 3, 2020

Summary

Mutations are a common feature in data-fetching libraries:

While I don't find this is fully necessary in many cases, since you can call api.request() directly, there is lots of value in building a transaction / rollback strategy, for example:

  1. Managing a loading state to return from the hook and use in the UI
  2. Optimistically updating the cache value
  3. Making multiple API calls which are all required for a successful completion
  • If one API call fails, we want to roll back to the original value
  • In-between the API requests, we don't want the cache to be optimistically updated multiple times. While this could be jarring for the user, it may also introduce bugs. For example, if you're incrementing a counter really quickly with a button, we could optimistically show the last-pressed button, but the API will keep resetting it after each successive API call completes.

An explicit mutations API which supports an optimistic response could solve the above issues.

Additionally, the optimistic setter should accept a "setter" function in addition to just passing the new response directly. This would help to facilitate updating the response relative to the previous response in a deterministic way, similar to react#useState's function setter options.


Goals

  • Allow a minimal mutation to be performed without much boilerplate
  • Allow various API methods to be accessed, such as request, writeCachedResponse, readCachedResponse, buildUrl, etc. that may be useful during the mutation. Event handlers such as onError and onCachedUpdate should not be accessible during the mutation.
  • Should manage a loading flag to be returned in the hook while the mutation is occurring
  • There should be an optimistic mutation option which allows for setting the cache values in a transaction, so the cached response can be updated for the mutation query or any other query and can be rolled back if the request(s) fails
  • Concurrent requests should be allowed, with the most recent optimistic response being respected. Also, the loading flag should remain true while any requests are going on.
  • Default fetch policy of the mutation request method should only be no-cache, and should be customizable via ApiProvider.

Usage Examples

Optimistic update pattern with key-value cache:

import {useApiMutation, useApiQuery} from 'fairlight'

const {showSnackbar} = useSnackbar()
const {handleUnexpectedError} = useErrorHandler()
const [users, usersQueryActions] = useApiQuery(UserEndpoints.list())
const [user, userQueryActions] = useApiQuery(UserEndpoints.findById(props.id))
const [updateUser, {loading: updatingUser}] = useApiMutation({
  mutation: (values, formikBag) => async ({request, setOptimisticResponse}) => {
    // transform data into request body payload
    const updatePayload = transformDataForUpdate(values)

    // optimistically update the UI
    const commitUser = setOptimisticResponse(
      UserEndpoints.findById(1),
      (prev) => ({...prev, ...updatePayload})
    )
    const commitUsers = setOptimisticResponse(UserEndpoints.list(), (prev) =>
      prev.map((prevUser) => {
        return prevUser.id === prop.id ? {...prevUser, updatePayload} : prevUser
      })
    )

    // make request(s)
    // - `deduplicate` is always `false`
    // - `fetchPolicy` is always `fetch-first` or `no-cache`
    const updatedUser = await request(
      UserEndpoints.partialUpdate(props.id, updatedUser)
    )

    // now we need to replace the cached versions with
    // the correct versions
    commitUser(updatedUser)
    commitUsers((prev) =>
      prev.map((prevUser) => {
        return prevUser.id === prop.id ? updatedUser : prevUser
      })
    )

    // we may even want to refetch these related queries instead
    usersQueryActions.refetch()
    userQueryActions.refetch()

    showSnackbar({
      variant: 'Success',
      message: `User '${updatedUser.name}' has been updated.`
    })
  },

  onError(error, {args}) {
    handleFormError(error, args[1], 'An error occurred')
  }
})

Potential future-version if we start supporting a normalized cache:

import {useApiMutation, useApiQuery} from 'fairlight'

const {showSnackbar} = useSnackbar()
const {handleUnexpectedError} = useErrorHandler()
const [users, usersQueryActions] = useApiQuery(UserEndpoints.list())
const [user, userQueryActions] = useApiQuery(UserEndpoints.findById(props.id))
const [updateUser, {loading: updatingUser}] = useApiMutation({
  mutation: (values, formikBag) => async ({request}) => {
    // transform data into request body payload
    const updatePayload = transformDataForUpdate(values)

    const updatedUser = await request(
      UserEndpoints.partialUpdate(props.id, updatePayload),
      {
        // must be set directly on the `request` because
        // it doesn't have a way to match up request body
        // to identify the correct request (ie. running multiple `POST` requests)
        optimisticResponse: {
          ...user.data,
          ...updatePayload
        }
      }
    )

    // we may even want to refetch these related queries
    userQueryActions.refetch()

    showSnackbar({
      variant: 'Success',
      message: `User '${updatedUser.name}' has been updated.`
    })
  },

  onError(error, {args}) {
    handleFormError(error, args[1], 'An error occurred')
  }
})
@azmenak azmenak added this to To do in Development Mar 4, 2020
@thomasdashney thomasdashney changed the title Mutations & Optimistic UI Mutations Mar 9, 2020
@thomasdashney
Copy link
Contributor Author

@azmenak As discussed offline, this draft API is ready for feedback!

@thomasdashney thomasdashney moved this from To do to In progress in Development Mar 17, 2020
Development automation moved this from In progress to Done Apr 13, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Development
  
Done
Development

Successfully merging a pull request may close this issue.

1 participant