react-query v4 공식문서 : Mutations

👾·2022년 8월 12일
0

react-query

목록 보기
18/27
post-thumbnail

공식문서 : https://tanstack.com/query/v4/docs/guides/mutations

Mutations

쿼리와 달리, mutation은 일반적으로 데이터를 생성/업데이트/삭제하거나 서버 side-effect를 수행하는데 사용된다. 이를 위해, React Query는 useMutation hook를 내보낸다.

다음은 서버에 새로운 todo를 추가하는 mutation의 예이다 :

function App() {
  const mutation = useMutation(newTodo => {
    return axios.post('/todos', newTodo)
  })

  return (
    <div>
      {mutation.isLoading ? (
        'Adding todo...'
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}

          {mutation.isSuccess ? <div>Todo added!</div> : null}

          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: 'Do Laundry' })
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  )
}

mutation은 주어진 순간에 다음과 같은 state 중 하나에만 있을 수 있다.

  • isIdle 또는 status === 'idle' - mutation이 현재 idle이거나 fresh/reset state이다.
  • isLoading 또는 status === 'loading' - mutation이 현재 running
  • isError 또는 status === 'error' - mutation에 error 발생
  • isSuccess 또는 status === 'success' - mutation이 성공했으며 mutation data를 사용할 수 있다.

이러한 기본 state들 외에도, mutation의 state에 따라 더 많은 정보를 사용할 수 있다.

  • error - mutation이 error state에 있는 경우 error 속성을 통해 error를 사용할 수 있다.
  • data - mutation이 success state에 있는 경우 data 속성을 통해 data를 사용할 수 있다.

위의 예제에서, 단일 변수 또는 객체로 mutate 함수를 호출하여 mutation function에 변수를 전달할 수도 있다.

변수만 있어도, mutation이 그렇게 특별한건 아니지만, onSuccess 옵션, Query Client의 invalidateQueries 메서드 및 Query Client의 setQueryData 메서드와 함께 사용하면 mutation은 매우 강력한 도구가 된다.

중요 : mutation 함수는 비동기 함수이므로, React 16 및 이전 버전의 event callback에서 직접 사용할 수 없다. onSubmit시 event에 접근해야하는 경우 다른 함수로 mutate를 감싸야한다. 이는 React의 event pooling 때문이다.

// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation(event => {
    event.preventDefault()
    return fetch('/api', new FormData(event.target))
  })

  return <form onSubmit={mutation.mutate}>...</form>
}

// This will work
const CreateTodo = () => {
  const mutation = useMutation(formData => {
    return fetch('/api', formData)
  })
  const onSubmit = event => {
    event.preventDefault()
    mutation.mutate(new FormData(event.target))
  }

  return <form onSubmit={onSubmit}>...</form>
}

Resetting Mutation State

mutation 요청의 dataerror를 지워야하는 경우가 있다. reset 함수를 사용하여 이를 처리할 수 있다 :

const CreateTodo = () => {
  const [title, setTitle] = useState('')
  const mutation = useMutation(createTodo)

  const onCreateTodo = e => {
    e.preventDefault()
    mutation.mutate({ title })
  }

  return (
    <form onSubmit={onCreateTodo}>
      {mutation.error && (
        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={e => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  )
}

Mutation Side Effects

useMutation은 mutation의 lifecycle동안 모든 단계에서 빠르고 쉬운 side-effect를 허용하는 helper 옵션과 함께 제공된다. 이들은 mutation 이후 쿼리를 무효화하고 refetch하는데 유용하며 심지어는 optimistic update에도 유용하다.

useMutation(addTodo, {
  onMutate: variables => {
    // A mutation is about to happen!

    // Optionally return a context containing data to use when for example rolling back
    return { id: 1 }
  },
  onError: (error, variables, context) => {
    // An error happened!
    console.log(`rolling back optimistic update with id ${context.id}`)
  },
  onSuccess: (data, variables, context) => {
    // Boom baby!
  },
  onSettled: (data, error, variables, context) => {
    // Error or success... doesn't matter!
  },
})

콜백함수에서 promise를 반환할 때 다음 콜백이 호출되기 전에 먼저 기다린다 :

useMutation(addTodo, {
  onSuccess: async () => {
    console.log("I'm first!")
  },
  onSettled: async () => {
    console.log("I'm second!")
  },
})

mutate를 호출할때 useMutation에 정의된 콜백 이외의 추가 콜백들을 트리거하고 싶을 수 있다. 이는 component-specific side effect들을 트리거하는데 사용할 수 있다. 그렇게 하기 위해 mutation 변수 뒤에 있는 mutate function에 동일한 콜백 옵션을 제공할 수 있다. 지원되는 재정의에는 : onSuccess, onErroronSettled가 있다. mutation이 완료되기 에 컴포넌트가 unmount되면 추가 콜백이 실행되지 않음을 명심하라.

useMutation(addTodo, {
  onSuccess: (data, variables, context) => {
    // I will fire first
  },
  onError: (error, variables, context) => {
    // I will fire first
  },
  onSettled: (data, error, variables, context) => {
    // I will fire first
  },
})

mutate(todo, {
  onSuccess: (data, variables, context) => {
    // I will fire second!
  },
  onError: (error, variables, context) => {
    // I will fire second!
  },
  onSettled: (data, error, variables, context) => {
    // I will fire second!
  },
})

Consecutive mutations

연속적인 mutation의 경우 onSuccess, onErroronSettled 콜백에 대한 처리는 약간의 차이가 있다. mutate 함수에 전달되면, 컴포넌트가 여전히 mount된 경우에만 단 한번만 실행된다. 이는 mutate 함수가 호출될 때마다 mutation observer가 제거되고 resubscribe되기 때문이다. 반대로, useMutation 핸들러는 각 mutate 호출에 대해 실행된다.

useMutation에 전달되는 mutationFn은 대부분 비동기이다. 이 경우 mutation들이 실행되는 순서는 mutate function 호출의 순서와 다를 수 있다.

useMutation(addTodo, {
  onSuccess: (data, error, variables, context) => {
    // Will be called 3 times
  },
})

['Todo 1', 'Todo 2', 'Todo 3'].forEach((todo) => {
  mutate(todo, {
    onSuccess: (data, error, variables, context) => {
      // Will execute only once, for the last mutation (Todo 3),
      // regardless which mutation resolves first
    },
  })
})

Promises

success시 resolve하거나 error를 throw하는 promise를 얻기 위해서는 mutate 대신 mutateAsync를 사용해라. 이는 예시로 side effect들을 구성하는데 사용할 수 있다.

const mutation = useMutation(addTodo)

try {
  const todo = await mutation.mutateAsync(todo)
  console.log(todo)
} catch (error) {
  console.error(error)
} finally {
  console.log('done')
}

Retry

기본적으로 React Query는 error시 mutation을 retry하지 않지만, retry 옵션을 사용하면 가능하다 :

const mutation = useMutation(addTodo, {
  retry: 3,
})

디바이스가 오프라인이라 mutation이 실패하면 디바이스가 재연결될때와 동일한 순서로 retry된다.

Persist mutations

mutation들은 필요한 경우 storage에 유지되고 나중에 재시작할 수 있다. 이는 hydration 함수를 사용하여 수행할 수 있다.

const queryClient = new QueryClient()

// Define the "addTodo" mutation
queryClient.setMutationDefaults(['addTodo'], {
  mutationFn: addTodo,
  onMutate: async (variables) => {
    // Cancel current queries for the todos list
    await queryClient.cancelQueries(['todos'])

    // Create optimistic todo
    const optimisticTodo = { id: uuid(), title: variables.title }

    // Add optimistic todo to todos list
    queryClient.setQueryData(['todos'], old => [...old, optimisticTodo])

    // Return context with the optimistic todo
    return { optimisticTodo }
  },
  onSuccess: (result, variables, context) => {
    // Replace optimistic todo in the todos list with the result
    queryClient.setQueryData(['todos'], old => old.map(todo => todo.id === context.optimisticTodo.id ? result : todo))
  },
  onError: (error, variables, context) => {
    // Remove optimistic todo from the todos list
    queryClient.setQueryData(['todos'], old => old.filter(todo => todo.id !== context.optimisticTodo.id))
  },
  retry: 3,
})

// Start mutation in some component:
const mutation = useMutation(['addTodo'])
mutation.mutate({ title: 'title' })

// If the mutation has been paused because the device is for example offline,
// Then the paused mutation can be dehydrated when the application quits:
const state = dehydrate(queryClient)

// The mutation can then be hydrated again when the application is started:
hydrate(queryClient, state)

// Resume the paused mutations:
queryClient.resumePausedMutations()

Persisting Offline mutations

만약 persistQueryClient 플러그인을 사용하여 오프라인 mutation을 지속한다면, 기본 mutation 함수를 제공하지 않는 한 페이지가 재로드될때 mutation을 재개할 수 없다.

이는 기술적인 한계이다. 외부 스토리지에 유지하는 경우, 함수들을 직렬화할 수 없으므로 mutation의 state만 지속된다. hydration 이후, mutation을 트리거하는 컴포넌트가 mount되지 않을 수 있으므로, resumePausedMutations을 호출하면 No mutationFn found 오류가 발생할 수 있다.

const persister = createSyncStoragePersister({
  storage: window.localStorage,
})
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      cacheTime: 1000 * 60 * 60 * 24, // 24 hours
    },
  },
})

// we need a default mutation function so that paused mutations can resume after a page reload
queryClient.setMutationDefaults(['todos'], {
  mutationFn: ({ id, data }) => {
    return api.upateTodo(id, data)
  },
})

export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // resume mutations after initial restore from localStorage was successful
        queryClient.resumePausedMutations()
      }}
    >
      <RestOfTheApp />
    </PersistQueryClientProvider>
  )
}

또한 쿼리와 mutation을 모두 다루는 광범위한 오프라인 예제가 있다.

Further reading

mutation에 대한 자세한 내용은 Community Resources에서 제공하는 #12:Mastering Mutations in React Query를 참고하라.

0개의 댓글