---
title: 'TanStack Query Optimistic Updates: Rollback في'
description: 'التحديثات المتفائلة (Optimistic updates) في TanStack Query (React Query) باستخدام useMutation: الـ cancelQueries، والـ snapshot، والـ setQueryData، والتراجع التلقائي عند فشل الطلب.'
url: https://nerdleveltech.com/ar/tanstack-query-optimistic-updates-rollback-tutorial
language: ar
date: '2026-06-24'
category: frontend
tags:
  - tanstack query
  - react query
  - optimistic updates
  - usemutation
  - react
  - typescript
  - frontend
  - tutorial
markdown_url: https://nerdleveltech.com/ar/tanstack-query-optimistic-updates-rollback-tutorial.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/ar/tanstack-query-optimistic-updates-rollback-tutorial'
terms: https://nerdleveltech.com/llms.txt
---

# TanStack Query Optimistic Updates: Rollback في

تكتب **تحديثات TanStack Query المتفائلة (optimistic updates)** النتيجة المتوقعة في التخزين المؤقت (cache) في اللحظة التي يتفاعل فيها المستخدم، ثم تتم المطابقة مع الخادم عند استقرار الطفرة (mutation). تقوم بذلك في `onMutate` الخاص بـ `useMutation`: قم بإلغاء عمليات إعادة الجلب (refetches)، وأخذ لقطة (snapshot) للتخزين المؤقت، والكتابة باستخدام `setQueryData`، وإرجاع اللقطة حتى يتمكن `onError` من التراجع (roll back).

## ملخص

سنقوم ببناء تطبيق مهام (todo) صغير ومكتوب بالكامل (fully typed) مع API غير متزامن وهمي، ونضيف **تحديثات React Query المتفائلة** باستخدام `useMutation`. سترى نهج التخزين المؤقت (`onMutate` ← `setQueryData` ← التراجع)، والنهج الأبسط "عبر واجهة المستخدم" باستخدام `mutation.variables`، ومتى تستخدم كل منهما. تم فحص كل كتلة برمجية في هذا البرنامج التعليمي مقابل `@tanstack/React-query@5.101.1` وتم التحقق من سلوك التحديث المتفائل ثم التراجع في وقت التشغيل. الميزانية الزمنية حوالي 20 دقيقة.

## ما ستتعلمه

- كيفية هيكلة تطبيق Vite + React + TypeScript مع `QueryClientProvider`
- كيفية قراءة قائمة باستخدام `useQuery` و API غير متزامن وهمي
- لماذا تجعل الطفرة الساذجة القائمة تومض، وكيف يصلح `cancelQueries` هذا السباق (race condition)
- كيفية إجراء تحديث متفائل للتخزين المؤقت باستخدام `onMutate` و `setQueryData` ولقطة (snapshot)
- كيفية التراجع تلقائيًا باستخدام `onError` وإعادة المزامنة باستخدام `onSettled`
- النهج الأبسط "عبر واجهة المستخدم" باستخدام `mutation.variables` و `isPending`
- شكل رد النداء (callback) الجديد في الإصدار v5 الذي يمرر `context.client`
- متى تستخدم `onMutate` الخاص بـ TanStack Query مقابل `useOptimistic` الخاص بـ React 19

## المتطلبات الأساسية

- **Node 20.19+ أو 22.12+** (الإصدارات التي يعلن عنها Vite 8 في حقل `engines`)
- **React 19.2** و **React-dom 19.2** (يسرد TanStack Query v5 نطاق أقران `^18 || ^19`)[^peer]
- **@tanstack/React-query 5.101.1** (الأحدث وقت كتابة هذا التقرير)[^npm]
- **TypeScript 6.0** ومعرفة أساسية بـ `useQuery`

## الخطوة 1 — هيكلة المشروع وتغليفه في QueryClientProvider

أنشئ مشروع Vite React + TypeScript وأضف TanStack Query مع إصدارات محددة:

```bash
# create-vite 9.1.0 scaffolds Vite 8, React 19.2, and TypeScript 6
npm create vite@9.1.0 rq-optimistic -- --template React-ts
cd rq-optimistic
npm install
npm install @tanstack/React-query@5.101.1
```

يجب أن يقع كل مكون يستدعي استعلامًا (query) أو طفرة (mutation) تحت `QueryClientProvider`. قم بتوصيله مرة واحدة في الجذر في `src/main.tsx`:

```tsx
import { StrictMode } from 'React'
import { createRoot } from 'React-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/React-query'
import { App } from './App'

const queryClient = new QueryClient()

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </StrictMode>,
)
```

## الخطوة 2 — API غير متزامن وهمي وقائمة مع useQuery

لا يكون للتحديثات المتفائلة معنى إلا في مواجهة زمن الانتقال (latency)، لذا فإن API الوهمي في `src/API.ts` ينتظر قبل التنفيذ ويكشف عن مفتاح `setShouldFail` حتى نتمكن من تجربة مسار التراجع عند الطلب:

```ts
export type Todo = { id: string; text: string; done: boolean }

let store: Todo[] = [{ id: '1', text: 'Learn TanStack Query', done: false }]
export let shouldFail = false
export function setShouldFail(v: boolean) { shouldFail = v }

const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

export async function fetchTodos(): Promise<Todo[]> {
  await sleep(300)
  return structuredClone(store)
}

export async function addTodo(text: string): Promise<Todo> {
  await sleep(600)
  if (shouldFail) throw new Error('Server rejected the new todo')
  const todo: Todo = { id: crypto.randomUUID(), text, done: false }
  store = [...store, todo]
  return todo
}

export async function toggleTodo(id: string): Promise<Todo> {
  await sleep(600)
  if (shouldFail) throw new Error('Server rejected the toggle')
  store = store.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
  const updated = store.find((t) => t.id === id)
  if (!updated) throw new Error('Not found')
  return updated
}
```

قراءة القائمة هي عملية `useQuery` عادية. لاحظ أنه في الإصدار v5 يتم الكشف عن حالة "لا توجد بيانات" للاستعلام كـ `isPending` (حالة `pending`)، ويتم تحديد نوع `error` بمجرد تضييق نطاق `isError`:[^mutations]

```tsx
const todosQuery = useQuery({ queryKey: ['todos'], queryFn: fetchTodos })

if (todosQuery.isPending) return <p>Loading…</p>
if (todosQuery.isError) return <p>Error: {todosQuery.error.message}</p>
```

## الخطوة 3 — لماذا يأتي cancelQueries أولاً (السباق)

إليك الفخ الذي يجعل التحديث المتفائل "الشغال" يومض. في اللحظة التي تستدعي فيها `mutate`، قد تكون عملية إعادة جلب في الخلفية لـ `['todos']` جارية بالفعل. إذا اكتملت عملية إعادة الجلب هذه *بعد* استدعاء `setQueryData` المتفائل، فإن استجابة الخادم القديمة ستمسح قيمتك المتفائلة ويختفي العنصر الجديد لفترة وجيزة.

هذا هو بالضبط السبب في أن دليل التحديث المتفائل يخبرك بضرورة استدعاء `await queryClient.cancelQueries({ queryKey })` أولاً: فهو يلغي عمليات إعادة الجلب الصادرة حتى لا تمسح كتابتك المتفائلة.[^optimistic] تعامل مع `cancelQueries` كخطوة صفرية في كل تحديث متفائل يعتمد على التخزين المؤقت — تخطيها هو سبب شائع لعدم "ثبات" التحديث المتفائل.

## الخطوة 4 — التحديث المتفائل عبر التخزين المؤقت

الآن إلى الشيء الحقيقي. في `src/App.tsx`، تقوم طفرة الإضافة بإلغاء عمليات إعادة الجلب، وأخذ لقطة للتخزين المؤقت، وكتابة مهمة متفائلة، وإرجاع اللقطة. يتم تسليم القيمة المرتجعة إلى `onError` و `onSettled` لاحقًا، وهو ما يجعل التراجع ممكنًا:[^optimistic]

```tsx
import { useState } from 'React'
import { useMutation, useQuery, useQueryClient } from '@tanstack/React-query'
import { addTodo, fetchTodos, toggleTodo, type Todo } from './API'

const todosKey = ['todos'] as const

export function App() {
  const queryClient = useQueryClient()
  const [text, setText] = useState('')

  const todosQuery = useQuery({ queryKey: todosKey, queryFn: fetchTodos })

  const addMutation = useMutation({
    mutationFn: addTodo,
    onMutate: async (newText: string) => {
      await queryClient.cancelQueries({ queryKey: todosKey })
      const previousTodos = queryClient.getQueryData<Todo[]>(todosKey)
      const optimistic: Todo = { id: `temp-${Date.now()}`, text: newText, done: false }
      queryClient.setQueryData<Todo[]>(todosKey, (old) => [...(old ?? []), optimistic])
      return { previousTodos }
    },
    onError: (_err, _newText, context) => {
      if (context?.previousTodos) {
        queryClient.setQueryData(todosKey, context.previousTodos)
      }
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: todosKey })
    },
  })
  // …render below
}
```

بعض التفاصيل التي يهتم بها فاحص النوع (type-checker). يعيد `getQueryData<Todo[]>` القيمة `Todo[] | undefined`، لذا فإن محدث `setQueryData` يحمي باستخدام `old ?? []`. يستخدم العنصر المتفائل `id` مؤقتًا (`temp-…`) لأن معرف الخادم الحقيقي غير موجود بعد — تقوم عملية الإبطال (invalidation) في `onSettled` بإعادة الجلب واستبداله بالسجل المعتمد.

## الخطوة 5 — التراجع عند الخطأ، وإعادة المزامنة عند الاستقرار

تقسم ردود النداء الثلاثة العمل بوضوح:

- **`onMutate`** يتم تشغيله قبل الطلب ويعيد سياق التراجع (لقطتك).
- **`onError`** يتلقى تلك اللقطة كوسيط ثالث ويستعيدها باستخدام `setQueryData`. التسلسل الاختياري (`context?.previousTodos`) مطلوب لأن `onMutate` يمكن من الناحية النظرية ألا يعيد شيئًا.
- **`onSettled`** يتم تشغيله بعد النجاح *أو* الخطأ ويستدعي `invalidateQueries`، بحيث تتم مطابقة التخزين المؤقت مع الخادم بعد كل طفرة — تخمينك المتفائل ليس الكلمة الأخيرة أبدًا.

تتعامل نفس ردود النداء الثلاثة مع تحديث عنصر واحد. أعلن عن طفرة تبديل (toggle) بجوار طفرة الإضافة مباشرة في المكون — فهي تأخذ لقطة، وتعيد كتابة الصف المتأثر فقط، وتتراجع عن القائمة بأكملها في حالة الفشل:

```tsx
const toggleMutation = useMutation({
  mutationFn: toggleTodo,
  onMutate: async (id: string) => {
    await queryClient.cancelQueries({ queryKey: todosKey })
    const previousTodos = queryClient.getQueryData<Todo[]>(todosKey)
    queryClient.setQueryData<Todo[]>(todosKey, (old) =>
      (old ?? []).map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
    )
    return { previousTodos }
  },
  onError: (_err, _id, context) => {
    if (context?.previousTodos) {
      queryClient.setQueryData(todosKey, context.previousTodos)
    }
  },
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: todosKey })
  },
})
```

أخيرًا، قم بتوصيل النموذج والقائمة لإنهاء `App.tsx`. يستدعي كل عنصر في القائمة التبديل عند النقر:

```tsx
  if (todosQuery.isPending) return <p>Loading…</p>
  if (todosQuery.isError) return <p>Error: {todosQuery.error.message}</p>

  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault()
          if (!text.trim()) return
          addMutation.mutate(text.trim())
          setText('')
        }}
      >
        <input value={textonChange={(e) => setText(e.target.value)} />
        <button type="submit" disabled={addMutation.isPending}>Add</button>
      </form>
      <ul>
        {todosQuery.data.map((todo) => (
          <li key={todo.id} onClick={() => toggleMutation.mutate(todo.id)}>
            {todo.done ? '✓ ' : '○ '}{todo.text}
          </li>
        ))}
      </ul>
    </div>
  )
```

## الخطوة 6 — النهج الأبسط "عبر واجهة المستخدم"

إذا كان هناك مكون واحد فقط يحتاج إلى إظهار العنصر المعلق، فلست مضطرًا للمس التخزين المؤقت على الإطلاق. يحتفظ TanStack Query بالمتغيرات (`variables`) الجارية في كائن الطفرة، لذا يمكنك عرض صف مؤقت بينما تكون `isPending` صحيحة وتترك عملية الإبطال تنظفه. لا توجد `setQueryData` ولا تراجع للكتابة:[^optimistic]

```tsx
import { useMutation, useQueryClient } from '@tanstack/React-query'
import { addTodo } from './API'

export function useAddTodoViaUI() {
  const queryClient = useQueryClient()
  const mutation = useMutation({
    mutationFn: addTodo,
    // return the promise so the mutation stays pending until the refetch finishes
    onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
    mutationKey: ['addTodo'],
  })
  const { isPending, submittedAt, variables, isError, mutate } = mutation
  return { isPending, submittedAt, variables, isError, mutate }
}
```

هناك أمران يجعلان هذا النهج ممتعًا. أولاً، يجب عليك *إرجاع* وعد (promise) `invalidateQueries` من `onSettled` حتى تظل الطفرة في حالتها المعلقة حتى تكتمل عملية إعادة الجلب — وإلا سيختفي الصف المؤقت قبل وصول الصف الحقيقي. ثانيًا، لا يتم مسح `variables` عند حدوث خطأ في الطفرة، لذا يمكنك الاحتفاظ بالعنصر الفاشل على الشاشة مع زر إعادة المحاولة.[^optimistic] إذا كانت الطفرة تعيش في مكون مختلف عن القائمة، فاقرأ المتغيرات الجارية في أي مكان باستخدام `useMutationState` (هذا هو سبب تعيين `mutationKey`):

```tsx
import { useMutationState } from '@tanstack/React-query'

export function usePendingNewTodos() {
  return useMutationState<string>({
    filters: { mutationKey: ['addTodo'], status: 'pending' },
    select: (mutation) => mutation.state.variables as string,
  })
}
```

استخدم نهج واجهة المستخدم (UI approach) عندما يكون هناك موقع عرض واحد وتفضل عدم امتلاك منطق التراجع (rollback)؛ استخدم نهج ذاكرة التخزين المؤقت (cache approach) عندما تقرأ عدة مكونات نفس الاستعلام ويجب أن تعكس جميعها التغيير في وقت واحد.[^optimistic]

## الخطوة 7 — صيغة استدعاء context.client الأحدث

تقوم إصدارات v5 الحديثة بتمرير كائن `context` كمعامل أخير لكل استدعاء طفرة (mutation callback)، حيث يكون `context.client` هو الـ `QueryClient`. يتيح لك ذلك كتابة منطق متفائل دون الحاجة للإغلاق على `useQueryClient()`، كما يعيد تسمية القيمة المرجعة من `onMutate` إلى `onMutateResult` في التوقيع البرمجي:[^mutations]

```tsx
import { useMutation } from '@tanstack/React-query'
import { addTodo, type Todo } from './API'

export function useAddTodoViaContext() {
  return useMutation({
    mutationFn: addTodo,
    onMutate: async (newText: string, context) => {
      await context.client.cancelQueries({ queryKey: ['todos'] })
      const previousTodos = context.client.getQueryData<Todo[]>(['todos'])
      context.client.setQueryData<Todo[]>(['todos'], (old) => [
        ...(old ?? []),
        { id: `temp-${Date.now()}`, text: newText, done: false },
      ])
      return { previousTodos }
    },
    onError: (_err, _newText, onMutateResult, context) => {
      if (onMutateResult?.previousTodos) {
        context.client.setQueryData(['todos'], onMutateResult.previousTodos)
      }
    },
    onSettled: (_d, _e, _v, _r, context) =>
      context.client.invalidateQueries({ queryKey: ['todos'] }),
  })
}
```

كلا الصيغتين يتم تجميعهما (compile) وتعملان بشكل متطابق. نمط الإغلاق (closure pattern) من الخطوة 4 (تسمية المعامل الثالث لـ `onError` بـ `context` وقراءة الـ `QueryClient` من نطاق المكون) يستمر في العمل لأن ذلك المعامل الموضعي الثالث لا يزال هو القيمة المرجعة من `onMutate` — لذا يمكنك اعتماد `context.client` عندما يكون ذلك مناسباً دون إعادة كتابة الطفرات الموجودة.

## التحقق

فحص الأنواع والبناء:

```bash
npx tsc --noEmit   # exits 0
npx vite build     # exits 0
npm run dev        # then open the printed localhost URL
```

ماذا تتوقع عند تشغيله:

- **إضافة مهمة (todo):** يظهر الصف الجديد فوراً (قبل اكتمال الطلب الذي يستغرق 600 مللي ثانية)، ثم يقوم إبطال `onSettled` بإعادة جلب البيانات ويتم استبدال المعرف المؤقت `temp-…` بالمعرف الحقيقي من الخادم.
- **فرض فشل:** استدعِ `setShouldFail(true)` من `API.ts`، أضف مهمة، وسيظهر الصف المتفائل ثم يختفي عندما يقوم `onError` باستعادة اللقطة (snapshot). تعود القائمة تماماً إلى حالتها السابقة.

يمكنك إثبات التراجع دون متصفح عن طريق تشغيل دورة الحياة برمجياً باستخدام `QueryClient` و `MutationObserver`. في مسار النجاح، تنمو ذاكرة التخزين المؤقت من مدخل واحد إلى مدخلين؛ وفي مسار الفشل تعود إلى مدخل واحد — مما يؤكد استعادة اللقطة.

## الأخطاء الشائعة

- **العنصر المتفائل يومض ثم يختفي، ثم يظهر مرة أخرى.** اكتملت عملية إعادة جلب في الخلفية بعد `setQueryData` الخاصة بك وقامت بمسحها. لقد نسيت `await queryClient.cancelQueries({ queryKey })` في بداية `onMutate`.[^optimistic]
- **`setQueryData` يشتكي من أن `old` قد يكون غير معرف (undefined).** يمكن لـ `getQueryData`/المُحدِّث (updater) تلقي `undefined` إذا لم يتم تحميل الاستعلام بعد. قم بالحماية باستخدام `old ?? []` وحدد نوع الاستدعاء كـ `setQueryData<Todo[]>`.
- **التراجع لا يعمل أبداً.** تأكد من أن `onMutate` يقوم بالفعل بـ `return` لكائن اللقطة — إذا أرجع `undefined`، فسيكون معامل الاستدعاء الثالث `undefined` ولن يكون هناك شيء لاستعادته. حافظ على حماية `context?.previousTodos`.
- **الصف المعلق "عبر واجهة المستخدم" يختفي مبكراً جداً.** لقد نسيت *إرجاع* وعد (promise) `invalidateQueries` من `onSettled`؛ بدونه تخرج الطفرة من الحالة المعلقة قبل وصول إعادة الجلب.[^optimistic]
- **إعادة المحاولة على عنصر فاشل لا تفعل شيئاً مفيداً.** تذكر أن الـ `variables` تنجو من الخطأ، لذا فإن `mutate(variables)` تعيد تشغيل نفس المدخلات من زر إعادة المحاولة.[^optimistic]

## TanStack Query onMutate مقابل React 19 useOptimistic

هذه أدوات مختلفة تتعايش معاً في عام 2026. يوفر خطاف `useOptimistic` المدمج في React 19 واجهة مستخدم متفائلة محلية للمكون مرتبطة بالإجراءات (actions) والانتقالات (transitions): تعود طبقتها المتفائلة تلقائياً عند استقرار الإجراء، لكنها لا تدير ذاكرة تخزين مؤقت مشتركة للخادم عبر المكونات بالطريقة التي تفعلها ذاكرة تخزين TanStack Query.[^react19] الجأ إلى `useOptimistic` للصدى المحلي المدفوع بالنماذج والانتقالات (غالباً جنباً إلى جنب مع Server Actions، كما في [دليل React 19 useOptimistic](https://nerdleveltech.com/nextjs-16-server-actions-React-19-optimistic-ui-tutorial) الخاص بنا)؛ والجأ إلى TanStack Query `onMutate` عندما يجب أن ينعكس التغيير المتفائل في كل مكان يقرأ نفس الاستعلام المخزن مؤقتاً وتتم تسويته مع الخادم بعد ذلك.

## الخطوات التالية ومزيد من القراءة

- اجمع بين الطفرات المتفائلة وتحميل البيانات من جانب الخادم — راجع [الجلب المسبق لـ TanStack Query في Next.js App Router](https://nerdleveltech.com/tanstack-query-nextjs-app-router-prefetch-tutorial).
- تحقق من صحة النموذج الذي يغذي طفرتك باستخدام [TanStack Form و Zod](https://nerdleveltech.com/tanstack-form-React-zod-validation-tutorial).
- للتحديثات المتفائلة المتزامنة عبر العديد من الطفرات الجارية، اقرأ تعمق TkDodo المرتبط من الدليل الرسمي.[^optimistic]

[^peer]: `@tanstack/React-query@5.101.1` `peerDependencies` — سجل npm، تم الاسترجاع في 2026-06-24. <https://www.npmjs.com/package/@tanstack/React-query>

[^npm]: `@tanstack/React-query` dist-tags (`latest = 5.101.1`) — سجل npm، تم الاسترجاع في 2026-06-24. <https://www.npmjs.com/package/@tanstack/React-query>

[^mutations]: "Mutations" — وثائق TanStack Query v5 لـ React (حالات الطفرات، تواقيع الاستدعاء، `context.client`)، تم الاسترجاع في 2026-06-24. <https://tanstack.com/query/latest/docs/framework/React/guides/mutations>

[^optimistic]: "Optimistic Updates" — وثائق TanStack Query v5 React، تم الاسترجاع في 2026-06-24. <https://tanstack.com/query/latest/docs/framework/React/guides/optimistic-updates>

[^react19]: "useOptimistic" — وثائق React. <https://React.dev/reference/React/useOptimistic>
