كل واجهة أمامية غير بسيطة تغرق في النهاية في المشكلة نفسها: بيانات تُجلب من عشرات نقاط النهاية، متناثرة بين حالة React والـ context والذاكرة المؤقتة، وكلها تُعيد العرض أكثر بكثير مما ينبغي. تُضيف الـ memoization، وترفع الحالة للأعلى، وتُدخل مخزنًا عامًا، ومع ذلك يظل التطبيق بطيئًا لحظة أن تتجاوز القائمة بضعة آلاف من الصفوف.
تأتي TanStack DB كإجابة الفريق على هذه الفوضى. إنها مخزن تفاعلي في المتصفح يجلس فوق TanStack Query، لا بديلًا عنها. تستمر Query في فعل ما تجيده — الجلب والتخزين المؤقت وإعادة التحقق من الخادم — وتُغذّي تلك البيانات إلى قاعدة بيانات محلية قابلة للاستعلام بعلاقات حقيقية، واستعلامات حيّة، وكتابات متفائلة. الرقم اللافت: تعديل صف واحد في مجموعة مرتّبة من 100,000 عنصر يكتمل في نحو 0.7 ميلي ثانية على معالج M1 Pro. هذه سرعة تكفي لأن تتوقف الواجهة المتفائلة عن كونها حيلة وتصبح الوضع الافتراضي.
يستعرض هذا الدليل المفاهيم الأساسية والـ API الذي ستستخدمه فعليًا.
المبادئ الثلاثة: المجموعات، والاستعلامات الحيّة، والمعاملات
بُنيت TanStack DB من ثلاث أفكار تتكامل بنظافة:
- المجموعات (Collections) هي مجموعات مكتوبة من الكائنات — قوائم
todosأوissuesأوusersلديك. لكل عنصر مفتاح ثابت. وتُغذَّى المجموعة من مصدر داعم: نقطة نهاية في TanStack Query، أو شكل ElectricSQL، أو التخزين المحلي، أو دالة مزامنة مخصّصة بالكامل. - الاستعلامات الحيّة (Live queries) تقرأ من مجموعة أو أكثر عبر باني استعلام علائقي. وهي تفاعلية: حين تتغيّر البيانات الأساسية بطريقة تؤثّر في النتيجة، يُعاد حساب الجزء المتأثّر فقط منها ويُعاد عرضه.
- المعاملات (Transactions) تُغلّف التعديلات. تُغيّر البيانات بتفاؤل في المجموعة المحلية، فتتحدّث الواجهة فورًا، بينما يثبّت معالِجٌ التغيير في خلفية النظام على خادمك — مع التراجع تلقائيًا إذا فشلت الكتابة.
والسبب في أن هذا أكثر من مجرد ذاكرة مؤقتة أنيقة هو المحرّك الكامن تحت الاستعلامات الحيّة.
التدفق التفاضلي: لماذا الاستعلامات أسرع من ميلي ثانية
معظم مكتبات الحالة في المتصفح تُعيد تشغيل المُحدِّد أو المُرشِّح من الصفر كلما تغيّر أي شيء. أما TanStack DB فتستخدم التدفق التفاضلي (differential dataflow)، المُنفَّذ بلغة TypeScript عبر مكتبة اسمها d2ts. فبدلًا من إعادة حساب الربط أو الترشيح أو التجميع على كامل البيانات، تُعيد حساب الفارق فقط — أي الصفوف التي تغيّرت فعلًا.
الأثر العملي: استعلام معقّد يربط مجموعتين ويُرتّب 100,000 صف يتحدّث تدريجيًا في أقل من ميلي ثانية حين يتغيّر صف واحد. يمكنك التعبير عن المنطق العلائقي الذي تحتاجه تطبيقات المنتجات في النهاية — روابط بين المجموعات، وتجميعات، وترتيب — دون دفع ضريبة إعادة العرض المعتادة. هذا هو الفارق الذي يجعل الرسوم البيانية المحلية الضخمة تبدو فورية.
إعداد مجموعة
تحتاج المجموعة المدعومة بـ TanStack Query إلى مفتاح استعلام، ودالة جلب، ومُستخرِج مفتاح، ومعالِجات تعديل اختيارية. إليك مجموعة todos موصولة بواجهة REST:
import { createCollection } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
return response.json()
},
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
const { modified: newTodo } = transaction.mutations[0]
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify(newTodo),
})
},
onUpdate: async ({ transaction }) => {
const { original, modified } = transaction.mutations[0]
await fetch(`/api/todos/${original.id}`, {
method: 'PUT',
body: JSON.stringify(modified),
})
},
onDelete: async ({ transaction }) => {
const { original } = transaction.mutations[0]
await fetch(`/api/todos/${original.id}`, { method: 'DELETE' })
},
})
)لاحظ أن onInsert وonUpdate وonDelete تتلقّى معاملة تحتوي تعديلًا واحدًا أو أكثر. تطبّق المجموعة التغيير محليًا أولًا، ثم يتحدّث المعالِج مع الخادم. في المجموعة المدعومة باستعلام، يجب ألّا يكتمل المعالِج قبل أن تُزامَن حالة الخادم رجوعًا — إذ تُعيد TanStack DB الجلب تلقائيًا بعد اكتمال المعالِج، مُوفِّقةً بين الحالة المتفائلة والحقيقة.
قراءة البيانات عبر useLiveQuery
تستعلم من المجموعات عبر باني استعلام بدل توابع المصفوفات الخام. يعكس الباني دلالات SQL — from وwhere وjoin وselect وorderBy — مع بقاء الأمان النوعي كاملًا من الطرف إلى الطرف. أبسط استعلام حيّ يُرشِّح مجموعة واحدة:
import { useLiveQuery, eq } from '@tanstack/react-db'
function ActiveTodos() {
const { data, isLoading } = useLiveQuery((q) =>
q.from({ todos: todoCollection })
.where(({ todos }) => eq(todos.completed, false))
.select(({ todos }) => ({ id: todos.id, text: todos.text }))
)
if (isLoading) return <p>جارٍ التحميل…</p>
return <ul>{data.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
}يُعيد المكوّن العرض فقط حين يُضاف عنصر مطابق للمرشّح أو يُحذف أو يتغيّر — لا مع كل كتابة في الذاكرة المؤقتة. وتُقرأ الروابط بنظافة جملة SQL، فتحلّ العلاقات عبر المجموعات دون تطبيع يدوي:
const { data } = useLiveQuery((q) =>
q.from({ issues: issueCollection })
.join({ persons: personCollection }, ({ issues, persons }) =>
eq(issues.userId, persons.id)
)
.select(({ issues, persons }) => ({
id: issues.id,
title: issues.title,
userName: persons.name,
}))
)تُعاد الاستعلامات الحيّة أيضًا حين تتغيّر تبعية تفاعلية. مرّر التبعية فيقرأ الباني قيمتها الحالية — مثل عتبة أولوية يتحكّم بها جزء من حالة المكوّن:
const { data } = useLiveQuery(
(q) => q.from({ todos: todoCollection })
.where(({ todos }) => gt(todos.priority, minPriority))
)تعديلات متفائلة تبدو فورية
لأن المجموعة المحلية هي مصدر الحقيقة للواجهة، تحدث الكتابات فورًا. واستدعاء insert أو update أو delete على مجموعة يُرجِع معاملة يمكنك انتظار تثبيتها:
// إدراج — تتحدّث الواجهة فورًا، وتجري مزامنة الخادم في المعالِج
const tx = todoCollection.insert({
id: crypto.randomUUID(),
text: 'إطلاق الميزة',
completed: false,
})
await tx.isPersisted.promise // يكتمل بمجرد تأكيد الخلفيةالتدفّق هو: طبّق محليًا، اعرض، ثبّت، وفّق. وإذا فشل طلب الخلفية، تتراجع المعاملة عن التغيير المتفائل تلقائيًا، فلا يترك استدعاء شبكي فاشل صفًّا وهميًا على الشاشة. لهذا لا تحتاج الواجهة المتفائلة في TanStack DB إلى أي جراحة يدوية للذاكرة المؤقتة — فالتراجع وإعادة المزامنة جزء من النموذج.
اختيار محرّك المزامنة
تجريد المجموعة محايد عمدًا تجاه الخلفية. تبدأ بما لديك بالفعل وتُبدِّل المصدر لاحقًا دون إعادة كتابة مكوّناتك أو استعلاماتك:
- queryCollectionOptions — أي نقطة نهاية REST أو GraphQL أو tRPC، عبر TanStack Query.
- electricCollectionOptions — مزامنة لحظية من Postgres عبر أشكال ElectricSQL.
- localOnlyCollectionOptions — حالة في المتصفح بحتة دون خلفية.
- localStorageCollectionOptions — محفوظة في التخزين المحلي للمتصفح عبر عمليات إعادة التحميل.
ولأن باني الاستعلام وواجهة useLiveQuery متطابقان في كلٍّ منها، يمكنك بناء نموذج أوّلي على نقطة REST بسيطة ثم الترقّي إلى محرّك مزامنة متدفّق حين تحتاج تعاونًا لحظيًا حقيقيًا — دون لمس جانب القراءة في تطبيقك.
أين تناسب، وملاحظة لفِرَق منطقة الشرق الأوسط وشمال أفريقيا
TanStack DB في مرحلة بيتا وقت كتابة هذا المقال، فعامِلها بوصفها واعدة للإنتاج لا مُختبَرة للإنتاج، وثبّت إصداراتك. تتألّق في الأسطح المنتَجة الكثيفة بالبيانات والتفاعلية — لوحات المعلومات، ومتتبّعات المهام، وصناديق الوارد، ولوحات الإدارة — حيث لديك كيانات كثيرة وتحديثات متكرّرة وحاجة إلى روابط في المتصفح. أما لموقع تسويقي ثابت في معظمه فهي مبالغة؛ وتكفي TanStack Query وحدها.
للفِرَق التي تبني لأسواق المنطقة، يستحق منحى الأولوية المحلية الوزن. تُبقي مجموعة localOnly أو localStorage حالة العمل على الجهاز، ما قد يقلّل الرحلات الترددية على الاتصالات المتقطّعة ويُبقي مزيدًا من بيانات المستخدم في المتصفح — رافعة مفيدة حين تفكّر في توقّعات معالجة البيانات لدى الهيئة الوطنية لحماية المعطيات الشخصية في تونس (INPDP) أو نظام حماية البيانات السعودي (PDPL). وحين تلجأ إلى محرّك مزامنة مثل ElectricSQL، يصبح سؤال أين يقيم Postgres قرار إقامة تتحكّم فيه، لا افتراضًا ترثه.
الخلاصة
تُعيد TanStack DB صياغة حالة المتصفح كقاعدة بيانات علائقية صغيرة وسريعة بدل كومة من الـ hooks والمُحدِّدات المُذكَّرة. تُطبِّع المجموعات بياناتك، ويجعل التدفّق التفاضلي الاستعلامات الحيّة شبه مجانية، وتجعل المعاملات الكتابات المتفائلة مسار المقاومة الأقل. إن كانت واجهة تطبيقك قد تجاوزت التخزين المؤقت المرتجل، فهي الإجابة الأكثر تماسكًا التي أطلقتها منظومة TanStack — وتندمج إلى جانب استدعاءات useQuery التي لديك بالفعل.