React Query وContext API
في نهاية هذا الجزء، سنتناول بضع طرق أخرى مختلفة لإدارة حالة التطبيق.
لنواصل مع تطبيق الملاحظات. سنركّز على التواصل مع الخادم. لنبدأ التطبيق من الصفر. النسخة الأولى كما يلي:
const App = () => {
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.reset()
console.log(content)
}
const toggleImportance = (note) => {
console.log('toggle importance of', note.id)
}
const notes = []
return (
<div>
<h2>Notes app</h2>
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
{notes.map((note) => (
<li key={note.id}>
{note.important ? <strong>{note.content}</strong> : note.content}
<button onClick={() => toggleImportance(note.id)}>
{note.important ? 'make not important' : 'make important'}
</button>
</li>
))}
</div>
)
}
export default App
الشيفرة الأولية موجودة على GitHub في هذا المستودع، في الفرع part6-0.
إدارة البيانات على الخادم باستخدام مكتبة TanStack Query
سنستخدم الآن مكتبة TanStack Query لتخزين البيانات المسترجَعة من الخادم وإدارتها.
ثبّت المكتبة بالأمر
npm install @tanstack/react-query
نحتاج إلى بضع إضافات في الملف main.jsx لتمرير دوال المكتبة إلى التطبيق بأكمله:
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App.jsx'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
لنستخدم JSON Server كما في الأجزاء السابقة لمحاكاة الواجهة الخلفية. JSON Server مهيّأ مسبقاً في المشروع المثال، ويحتوي جذر المشروع على ملف db.json يضم افتراضياً ملاحظتين. يمكنك تشغيل الخادم بالأمر:
npm run server
يمكننا الآن استرجاع الملاحظات في مكوّن App. تتوسّع الشيفرة كما يلي:
import { useQuery } from '@tanstack/react-query'
const App = () => {
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.reset()
console.log(content)
}
const toggleImportance = (note) => {
console.log('toggle importance of', note.id)
}
const result = useQuery({
queryKey: ['notes'],
queryFn: async () => {
const response = await fetch('http://localhost:3001/notes')
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
return await response.json()
}
})
console.log(JSON.parse(JSON.stringify(result)))
if (result.isPending) {
return <div>loading data...</div>
}
const notes = result.data
return (
// ...
)
}
يتم جلب البيانات من الخادم، كما في الفصل السابق، باستخدام دالة fetch من Fetch API. غير أن استدعاء الدالة أصبح الآن ملفوفاً داخل استعلام يكوّنه خطاف useQuery. يتلقّى استدعاء useQuery كمعامل كائناً فيه الحقلان queryKey و queryFn. قيمة الحقل queryKey هي مصفوفة تحتوي على النص notes، وهي تعمل بمنزلة مفتاح للاستعلام المعرَّف، أي قائمة الملاحظات.
القيمة المُعادة من دالة useQuery هي كائن يبيّن حالة الاستعلام. ويوضّح الخرج في وحدة التحكم الوضع:

كما نرى، عند عرض المكوّن لأول مرة يظل الاستعلام في حالة الانتظار (pending)، أي أن طلب HTTP المرتبط لا يزال معلّقاً. في هذه المرحلة، لا يُعرض سوى ما يلي:
<div>loading data...</div>
غير أن طلب HTTP يكتمل بسرعة كبيرة بحيث يستحيل رؤية النص. وعند اكتمال الطلب يُعرض المكوّن مرة أخرى. ويكون الاستعلام في الحالة success عند العرض الثاني، ويحتوي الحقل data من كائن الاستعلام على البيانات التي أعادها الطلب، أي قائمة الملاحظات المعروضة على الشاشة.
إذن يسترجع التطبيق البيانات من الخادم ويعرضها على الشاشة دون استخدام خطافات React useState و useEffect المستخدمة في الفصول 2-5 على الإطلاق. أصبحت البيانات الموجودة على الخادم الآن بالكامل تحت إدارة مكتبة TanStack Query، ولم يعد التطبيق بحاجة إلى الحالة المعرَّفة بخطاف useState في React إطلاقاً!
لننقل الدالة التي تُجري طلب HTTP الفعلي إلى ملفها الخاص src/requests.js
const baseUrl = 'http://localhost:3001/notes'
export const getNotes = async () => {
const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
return await response.json()
}
أصبح مكوّن App الآن مبسّطاً قليلاً:
import { useQuery } from '@tanstack/react-query'
import { getNotes } from './requests'
const App = () => {
// ...
const result = useQuery({
queryKey: ['notes'],
queryFn: getNotes
})
// ...
}
شيفرة التطبيق الحالية موجودة على GitHub في الفرع part6-1.
مزامنة البيانات مع الخادم باستخدام TanStack Query
لقد نجحنا بالفعل في استرجاع البيانات من الخادم. بعد ذلك، سنحرص على تخزين البيانات المُضافة والمعدَّلة على الخادم. لنبدأ بإضافة ملاحظات جديدة.
لنضف دالة createNote إلى الملف requests.js لحفظ الملاحظات الجديدة:
const baseUrl = 'http://localhost:3001/notes'
export const getNotes = async () => {
const response = await fetch(baseUrl)
if (!response.ok) {
throw new Error('Failed to fetch notes')
}
return await response.json()
}
export const createNote = async (newNote) => {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newNote)
}
const response = await fetch(baseUrl, options)
if (!response.ok) {
throw new Error('Failed to create note')
}
return await response.json()
}
سيتغيّر مكوّن App كما يلي
import { useQuery, useMutation } from '@tanstack/react-query'
import { getNotes, createNote } from './requests'
const App = () => {
const newNoteMutation = useMutation({
mutationFn: createNote,
})
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.reset()
newNoteMutation.mutate({ content, important: true })
}
//
}
لإنشاء ملاحظة جديدة، نعرّف mutation باستخدام الدالة useMutation:
const newNoteMutation = useMutation({
mutationFn: createNote,
})
المعامل هو الدالة التي أضفناها إلى الملف requests.js والتي تستخدم Fetch API لإرسال ملاحظة جديدة إلى الخادم.
ينفّذ معالج الحدث addNote الـ mutation باستدعاء الدالة mutate التابعة لكائن الـ mutation وتمرير الملاحظة الجديدة كوسيط:
newNoteMutation.mutate({ content, important: true })
حلّنا جيد، إلا أنه لا يعمل. تُحفظ الملاحظة الجديدة على الخادم، لكنها لا تظهر محدَّثة على الشاشة.
لكي نعرض الملاحظة الجديدة أيضاً، علينا إخبار TanStack Query بأن النتيجة القديمة للاستعلام الذي مفتاحه النص notes ينبغي إبطالها.
لحسن الحظ، الإبطال سهل، ويمكن تنفيذه بتعريف دالة الاستدعاء onSuccess المناسبة للـ mutation:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getNotes, createNote } from './requests'
const App = () => {
const queryClient = useQueryClient()
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] })
},
})
// ...
}
الآن، بعد تنفيذ الـ mutation بنجاح، يُستدعى ما يلي
queryClient.invalidateQueries({ queryKey: ['notes'] })
وهذا بدوره يجعل TanStack Query يحدّث تلقائياً الاستعلام ذا المفتاح notes، أي يجلب الملاحظات من الخادم. ونتيجة لذلك، يعرض التطبيق الحالة الأحدث على الخادم، أي تُعرض الملاحظة المضافة أيضاً.
لننفّذ أيضاً تغيير أهمية الملاحظات. تُضاف دالة لتحديث الملاحظات إلى الملف requests.js:
export const updateNote = async (updatedNote) => {
const options = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedNote)
}
const response = await fetch(`${baseUrl}/${updatedNote.id}`, options)
if (!response.ok) {
throw new Error('Failed to update note')
}
return await response.json()
}
يتم تحديث الملاحظة أيضاً عبر mutation. يتوسّع مكوّن App كما يلي:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getNotes, createNote, updateNote } from './requests'
const App = () => {
const queryClient = useQueryClient()
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] })
}
})
const updateNoteMutation = useMutation({
mutationFn: updateNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] })
}
})
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.reset()
newNoteMutation.mutate({ content, important: true })
}
const toggleImportance = (note) => {
updateNoteMutation.mutate({...note, important: !note.important })
}
// ...
}
إذن مرة أخرى، يُبطل الـ mutation الذي أنشأناه استعلام notes كي تُعرض الملاحظة المحدَّثة بشكل صحيح. استخدام الـ mutations سهل؛ إذ تتلقّى الدالة mutate ملاحظة كمعامل، وقد غُيّرت أهميتها إلى نقيض القيمة القديمة.
شيفرة التطبيق الحالية موجودة على GitHub في الفرع part6-2.
تحسين الأداء
يعمل التطبيق جيداً، والشيفرة بسيطة نسبياً. والملفت بشكل خاص سهولة إجراء تغييرات على قائمة الملاحظات. فمثلاً، عند تغيير أهمية ملاحظة، يكفي إبطال الاستعلام notes لتُحدَّث بيانات التطبيق:
const updateNoteMutation = useMutation({
mutationFn: updateNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] }) }
})
نتيجة ذلك، بالطبع، أنه بعد طلب PUT الذي يسبّب تغيير الملاحظة، يُجري التطبيق طلب GET جديداً لاسترجاع بيانات الاستعلام من الخادم:

إذا لم تكن كمية البيانات التي يسترجعها التطبيق كبيرة، فلا يهم ذلك حقاً. فمن وجهة نظر وظائف جهة المتصفح، لا يهم حقاً إجراء طلب HTTP GET إضافي، لكنه قد يشكّل عبئاً على الخادم في بعض الحالات.
وعند الحاجة، يمكن أيضاً تحسين الأداء بتحديث حالة الاستعلام التي يديرها TanStack Query يدوياً.
التغيير الخاص بالـ mutation الذي يضيف ملاحظة جديدة كما يلي:
const App = () => {
const queryClient = useQueryClient()
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: (newNote) => {
const notes = queryClient.getQueryData(['notes'])
queryClient.setQueryData(['notes'], notes.concat(newNote))
}
})
// ...
}
أي أن كائن queryClient يقرأ أولاً في دالة الاستدعاء onSuccess حالة notes الحالية للاستعلام ويحدّثها بإضافة ملاحظة جديدة تُستلم كمعامل لدالة الاستدعاء. وقيمة المعامل هي القيمة التي تعيدها الدالة createNote المعرَّفة في الملف requests.js كما يلي:
export const createNote = async (newNote) => {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newNote)
}
const response = await fetch(baseUrl, options)
if (!response.ok) {
throw new Error('Failed to create note')
}
return await response.json()
}
سيكون من السهل نسبياً إجراء تغيير مماثل على الـ mutation الذي يغيّر أهمية الملاحظة، لكننا نتركه كتمرين اختياري.
أخيراً، لاحظ تفصيلاً مثيراً للاهتمام. يعيد TanStack Query جلب كل الملاحظات عندما ننتقل إلى تبويب آخر في المتصفح ثم نعود إلى تبويب التطبيق. ويمكن ملاحظة ذلك في تبويب Network في وحدة تحكم المطوّر:

ما الذي يحدث؟ بقراءة التوثيق، نلاحظ أن السلوك الافتراضي لاستعلامات TanStack Query هو تحديث الاستعلامات (التي تكون حالتها stale) عند تغيّر window focus. وإذا أردنا، يمكننا تعطيل هذه الوظيفة بإنشاء استعلام كما يلي:
const App = () => {
// ...
const result = useQuery({
queryKey: ['notes'],
queryFn: getNotes,
refetchOnWindowFocus: false
})
// ...
}
إذا أضفت عبارة console.log إلى الشيفرة، يمكنك أن ترى من وحدة تحكم المتصفح كم مرة يجعل TanStack Query التطبيق يُعاد عرضه. والقاعدة العامة أن إعادة العرض تحدث على الأقل كلما دعت الحاجة إليها، أي عند تغيّر حالة الاستعلام. يمكنك القراءة أكثر عن ذلك مثلاً هنا.
الخطاف المخصّص useNotes
حلّنا جيد إلى حد بعيد، لكن ما يزعج بعض الشيء هو أن كثيراً من تفاصيل تنفيذ TanStack Query وُضعت مباشرة داخل مكوّن React. لنستخرجها إلى دالة خطاف مخصّص خاصة بها:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getNotes, createNote, updateNote } from '../requests'
export const useNotes = () => {
const queryClient = useQueryClient()
const result = useQuery({
queryKey: ['notes'],
queryFn: getNotes,
refetchOnWindowFocus: false
})
const newNoteMutation = useMutation({
mutationFn: createNote,
onSuccess: (newNote) => {
const notes = queryClient.getQueryData(['notes'])
queryClient.setQueryData(['notes'], notes.concat(newNote))
}
})
const updateNoteMutation = useMutation({
mutationFn: updateNote,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['notes'] })
}
})
return {
notes: result.data,
isPending: result.isPending,
addNote: (content) => newNoteMutation.mutate({ content, important: true }),
toggleImportance: (note) => updateNoteMutation.mutate({
...note, important: !note.important
}),
}
}
تغلّف دالة الخطاف كل الشيفرة المتعلقة بـ TanStack Query: الاستعلام الخاص بجلب الملاحظات وكلا الـ mutations الخاصة بإنشاء الملاحظات وتحديثها. وتُخفى تفاصيل التنفيذ هذه عن مستخدم الخطاف، إذ تعيد الدالة كائناً بسيطاً يحتوي على
- notes: قائمة الملاحظات
- isPending: ما إذا كانت البيانات لا تزال قيد التحميل
- addNote: دالة لإضافة ملاحظة جديدة بنص المحتوى فقط
- toggleImportance: دالة لتبديل أهمية الملاحظة
يُبسَّط مكوّن App بشكل كبير:
import { useNotes } from './hooks/useNotes'
const App = () => {
const { notes, isPending, addNote: addNoteToServer, toggleImportance } = useNotes()
const addNote = async (event) => {
event.preventDefault()
const content = event.target.note.value
event.target.reset()
addNoteToServer(content)
}
if (isPending) {
return <div>loading data...</div>
}
return (
<div>
<h2>Notes app</h2>
<form onSubmit={addNote}>
<input name="note" />
<button type="submit">add</button>
</form>
{notes.map((note) => (
<li key={note.id}>
{note.important ? <strong>{note.content}</strong> : note.content}
<button onClick={() => toggleImportance(note)}>
{note.important ? 'make not important' : 'make important'}
</button>
</li>
))}
</div>
)
}
شيفرة التطبيق موجودة على GitHub في الفرع part6-3.
TanStack Query مكتبة متعددة الاستخدامات، وهي بناءً على ما رأيناه بالفعل تبسّط التطبيق. فهل تجعل TanStack Query حلول إدارة الحالة الأكثر تعقيداً مثل Zustand غير ضرورية؟ لا. يمكن لـ TanStack Query أن تحلّ محل حالة التطبيق جزئياً في بعض الحالات، لكن كما يذكر التوثيق
- TanStack Query هي مكتبة حالة الخادم (server-state library)، مسؤولة عن إدارة العمليات غير المتزامنة بين خادمك وعميلك
- أما Zustand وغيرها فهي مكتبات حالة العميل (client-state libraries) التي يمكن استخدامها لتخزين البيانات غير المتزامنة، وإن كان ذلك بكفاءة أقل مقارنة بأداة مثل TanStack Query
إذن TanStack Query مكتبة تحافظ على حالة الخادم في الواجهة الأمامية، أي تعمل كذاكرة مؤقتة لما هو مخزَّن على الخادم. تبسّط TanStack Query معالجة البيانات الموجودة على الخادم، ويمكنها في بعض الحالات إلغاء الحاجة إلى حفظ بيانات الخادم في حالة الواجهة الأمامية.
تحتاج معظم تطبيقات React ليس فقط إلى طريقة لتخزين البيانات المُقدَّمة مؤقتاً، بل أيضاً إلى حل لكيفية التعامل مع بقية حالة الواجهة الأمامية (مثل حالة النماذج أو الإشعارات).
19. استعلام anecdotes، الخطوة 1
20. استعلام anecdotes، الخطوة 2
21. استعلام anecdotes، الخطوة 3
22. استعلام anecdotes، الخطوة 4
23. استعلام anecdotes، مراجعة
Context API
لنعد إلى تطبيق العدّاد القديم الجيد. يُعرَّف التطبيق كما يلي:
import { useState } from 'react'
import Display from './components/Display'
import Controls from './components/Controls'
const App = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<Display counter={counter} />
<Controls counter={counter} setCounter={setCounter} />
</div>
)
}
يعرّف مكوّن App حالة التطبيق ويمرّرها إلى مكوّن Display الذي يعرض قيمة العدّاد:
const Display = ({ counter }) => {
return (
<div>{counter}</div>
)
}
وإلى مكوّن Controls الذي يعرض الأزرار:
const Controls = ({ counter, setCounter }) => {
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
const zero = () => setCounter(0)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}
ينمو التطبيق:

يتغيّر دور مكوّن App: فهو لا يزال يحتفظ بحالة التطبيق، لكنه لم يعد يعرض المكوّنات التي تستخدم حالة العدّاد مباشرة:
const App = () => {
const [counter, setCounter] = useState(0)
return (
<div>
<Navbar />
<Panel counter={counter} setCounter={setCounter} />
<Footer />
</div>
)
}
المكوّن الجديد Panel مسؤول عن عرض المكوّنات التي تعرض العدّاد والأزرار:
import Display from './Display'
import Controls from './Controls'
const Panel = ({ counter, setCounter }) => {
return (
<div>
<Display counter={counter} />
<Controls counter={counter} setCounter={setCounter} />
</div>
)
}
التسلسل الهرمي لمكوّنات التطبيق كما يلي:
App (state)
├── Panel
│ ├── Display
│ └── Controls
└── Footer
لا تزال حالة التطبيق في مكوّن App. ولتمكين Display و Controls من الوصول إلى حالة العدّاد، يجب تمرير الحالة ودالة تحديثها كـ props عبر مكوّن Panel، رغم أن Panel نفسه لا يحتاجهما. وينشأ هذا النوع من الحالات بسهولة عند استخدام حالة أُنشئت بخطاف useState. وتُسمّى هذه الظاهرة prop drilling.
تقدّم واجهة Context API المدمجة في React حلاً واحداً لهذه المشكلة. وسياق React (context) هو نوع من الحالة العامة للتطبيق، يتيح منح أي مكوّن وصولاً مباشراً إليه.
لننشئ الآن سياقاً في التطبيق يخزّن إدارة حالة العدّاد.
يُنشأ السياق باستخدام دالة createContext في React. لننشئ السياق في ملف src/CounterContext.jsx:
import { createContext } from 'react'
const CounterContext = createContext()
export default CounterContext
يمكن لمكوّن App الآن أن يوفّر السياق لمكوّناته الفرعية كما يلي:
import CounterContext from './components/CounterContext'
const App = () => {
const [counter, setCounter] = useState(0)
return (
<CounterContext.Provider value={{counter, setCounter}}>
<Panel />
<Footer />
</CounterContext.Provider>
)
}
يتم توفير السياق بلفّ المكوّنات الفرعية داخل مكوّن CounterContext.Provider وتعيين قيمة مناسبة للسياق.
قيمة السياق الآن كائن له الخاصيتان counter و setCounter، أي حالة العدّاد والدالة التي تحدّثها.
لاحظ أن مكوّن Panel لم يعد يتلقّى أي props متعلقة بالعدّاد، لذا يُبسَّط إلى:
const Panel = () => {
return (
<div>
<Display />
<Controls />
</div>
)
}
يمكن للمكوّنات الأخرى الآن الوصول إلى السياق باستخدام خطاف useContext. يتغيّر مكوّن Display كما يلي:
import { useContext } from 'react'
import CounterContext from './CounterContext'
const Display = () => {
const { counter } = useContext(CounterContext)
return <div>{counter}</div>
}
لم يعد مكوّن Display بحاجة إلى أي props. فهو يحصل على قيمة العدّاد باستدعاء خطاف useContext مع كائن CounterContext كمعامل له.
وبالمثل، يتغيّر مكوّن Controls إلى:
import { useContext } from 'react'
import CounterContext from './CounterContext'
const Controls = () => {
const { counter, setCounter } = useContext(CounterContext)
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
const zero = () => setCounter(0)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}
export default Controls
أصبحت المكوّنات الآن قادرة على الوصول إلى المحتوى الذي يعيّنه مزوّد السياق، أي حالة العدّاد ودالة تحديثها.
تستخرج المكوّنات الخصائص التي تحتاجها باستخدام صيغة التفكيك (destructuring) في JavaScript:
const { counter } = useContext(CounterContext)
تعريف سياق العدّاد في ملفه الخاص
لا يزال في تطبيقنا ما هو غير مستحب: إذ إن وظيفة إدارة حالة العدّاد معرَّفة داخل مكوّن App. لننقل كل الشيفرة المتعلقة بالعدّاد إلى الملف CounterContext.jsx:
import { createContext, useState } from 'react'
const CounterContext = createContext()
export default CounterContext
export const CounterContextProvider = (props) => {
const [counter, setCounter] = useState(0)
return (
<CounterContext.Provider value={{ counter, setCounter }}>
{props.children}
</CounterContext.Provider>
)
}
يصدّر الملف الآن كلاً من كائن CounterContext ومكوّن CounterContextProvider، وهو أساساً مزوّد سياق تحتوي قيمته على العدّاد ودالة تحديثه.
لنستخدم مزوّد السياق مباشرة في الملف main.jsx:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
import { CounterContextProvider } from './CounterContext'
createRoot(document.getElementById('root')).render(
<CounterContextProvider>
<App />
</CounterContextProvider>
)
أصبح السياق الذي يعرّف قيمة العدّاد ووظائفه متاحاً الآن لجميع مكوّنات التطبيق.
يُبسَّط مكوّن App إلى:
import Panel from './components/Panel'
import Footer from './components/Footer'
const App = () => {
return (
<div>
<Navbar />
<Panel />
<Footer />
</div>
)
}
export default App
لا يزال السياق يُستخدم بالطريقة نفسها، ولا حاجة إلى أي تغييرات في المكوّنات الأخرى. فمثلاً، يبقى Controls:
const Controls = () => {
const { counter, setCounter } = useContext(CounterContext)
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
const zero = () => setCounter(0)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}
الحل جيد تماماً. أصبحت حالة التطبيق بأكملها، أي قيمة العدّاد، معزولة الآن في ملف CounterContext. وتصل المكوّنات إلى الجزء الذي تحتاجه بالضبط من السياق باستخدام خطاف useContext وصيغة التفكيك في JavaScript.
لنجرِ تحسيناً صغيراً واحداً ونعرّف أيضاً دوال تحديث العدّاد increment و decrement و zero في السياق:
import { createContext, useState } from 'react'
const CounterContext = createContext()
export default CounterContext
export const CounterContextProvider = (props) => {
const [counter, setCounter] = useState(0)
const increment = () => setCounter(counter + 1)
const decrement = () => setCounter(counter - 1)
const zero = () => setCounter(0)
return (
<CounterContext.Provider value={{ counter, increment, decrement, zero }}>
{props.children}
</CounterContext.Provider>
)
}
الآن يمكننا استخدام الدوال المستحصل عليها من السياق مباشرة كمعالجات أحداث للأزرار:
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const Controls = () => {
const { increment, decrement, zero } = useContext(CounterContext)
return (
<div>
<button onClick={increment}>plus</button>
<button onClick={decrement}>minus</button>
<button onClick={zero}>zero</button>
</div>
)
}
لا يزال هناك مجال لتحسين آخر. إذا نظرنا إلى طريقة استخدام سياق العدّاد، نلاحظ أن الشيفرة المتكررة (boilerplate) نفسها تظهر في كلا المكوّنين اللذين يستهلكانه:
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const Display = () => {
const { counter } = useContext(CounterContext)
// ...
}
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const Controls = () => {
const { increment, decrement, zero } = useContext(CounterContext)
// ...
}
يمكننا أن نخطو بالحل خطوة إضافية بإنشاء خطاف مخصّص يعيد السياق مباشرة. لنضفه إلى الملف hooks/useCounter.js:
import { useContext } from 'react'
import CounterContext from '../CounterContext'
const useCounter = () => useContext(CounterContext)
export default useCounter
أصبح استخدام السياق الآن أبسط بخطوة:
import useCounter from '../hooks/useCounter'
const Display = () => {
const { counter } = useCounter()
// ...
}
import useCounter from '../hooks/useCounter'
const Controls = () => {
const { increment, decrement, zero } = useCounter()
// ...
}
نحن راضون عن الحل. فهو يعزل كل إدارة الحالة بالكامل داخل السياق. والمكوّنات التي تستخدم الحالة لا تعرف شيئاً عن كيفية تنفيذها. وبفضل الخطاف المخصّص، فهي لا تدرك حتى أن الحل قائم على Context API.
شيفرة التطبيق موجودة في مستودع GitHub https://github.com/fullstack-hy2020/context-counter.
24. استعلام anecdotes، الخطوة 5
25. استعلام anecdotes، الخطوة 6
26. استعلام anecdotes، الخطوة 7
27. استعلام anecdotes، الفحص النهائي
28. مستودع GitHub الخاص بك
أي حل لإدارة الحالة ينبغي اختياره؟
في الفصول 1-5، كانت كل إدارة الحالة في التطبيق تُعالج باستخدام خطاف useState في React. وقد تطلّبت الاستدعاءات غير المتزامنة إلى الواجهة الخلفية استخدام خطاف useEffect في بعض الحالات. ومن حيث المبدأ، لا حاجة إلى أي شيء آخر.
ثمة مشكلة دقيقة في الحلول القائمة على الحالة المُنشأة بخطاف useState وهي أنه إذا احتاجت مكوّنات متعددة إلى جزء من حالة التطبيق، وجب تمرير الحالة والدوال الخاصة بالتعامل معها عبر props إلى كل المكوّنات التي تتعامل مع تلك الحالة. وأحياناً يجب تمرير props عبر مكوّنات متعددة، وقد لا تكون المكوّنات الواقعة في الطريق مهتمة بالحالة بأي شكل. وتُسمّى هذه الظاهرة غير المستحبة بعض الشيء prop drilling.
على مرّ السنوات، طُوّرت عدة حلول بديلة لإدارة الحالة في تطبيقات React، يمكن استخدامها لتخفيف المواقف الإشكالية مثل prop drilling. ومع ذلك، لم يكن أي حل «نهائياً» — فلكل حل مزاياه وعيوبه، وتُطوَّر حلول جديدة طوال الوقت.
قد يربك هذا الوضع المبتدئ وحتى مطوّر ويب متمرس. فأي حل ينبغي استخدامه؟
بالنسبة لتطبيق بسيط، يُعدّ useState نقطة بداية جيدة بالتأكيد. وإذا كان التطبيق يتواصل مع خادم، فيمكن التعامل مع التواصل بالطريقة نفسها كما في الفصول 1-5، باستخدام حالة التطبيق الخاصة. غير أنه أصبح من الشائع حديثاً نقل التواصل وإدارة الحالة المرتبطة به، جزئياً على الأقل، إلى نطاق سيطرة TanStack Query (أو مكتبة أخرى مشابهة). وإذا كنت قلقاً بشأن useState وprop drilling الذي يستتبعه، فقد يكون استخدام السياق خياراً جيداً. وهناك أيضاً حالات قد يكون من المنطقي فيها التعامل مع جزء من الحالة بـ useState وجزء آخر بالسياقات.
لفترة طويلة، كان Redux أشهر حلول إدارة الحالة وأكثرها شمولاً، وهو طريقة لتنفيذ ما يُسمّى معمارية Flux. غير أن Redux معروف بتعقيده وكثرة الشيفرة المتكررة فيه، وقد كان ذلك دافعاً لظهور حلول إدارة حالة أحدث. في هذه المادة التعليمية، حلّت مكتبة Zustand محل Redux، وهي تقدّم وظائف مكافئة بواجهة برمجية أبسط بكثير. وأصبح Zustand خياراً شائعاً خصوصاً عندما تحتاج إلى أكثر مما يقدّمه useState، لكن جهاز Redux الكامل يبدو مبالغاً فيه. وقد أصبح بعض النقد الموجّه إلى جمود Redux قديماً بفضل Redux Toolkit، ولا يزال Redux مستخدماً على نطاق واسع، خاصة في المشاريع الأكبر.
لا يلزم استخدام Zustand ولا Redux في التطبيق بأكمله. فقد يكون من المنطقي، مثلاً، إدارة حالة النموذج خارجهما، خصوصاً في الحالات التي لا تؤثر فيها حالة النموذج على بقية التطبيق. كما أن استخدام Zustand أو Redux مع TanStack Query في التطبيق نفسه ممكن تماماً.
مسألة أي حل لإدارة الحالة ينبغي استخدامه ليست مباشرة إطلاقاً. فمن المستحيل إعطاء إجابة صحيحة واحدة، ومن المرجّح أيضاً أن يتبيّن أن الحل المختار دون المستوى الأمثل مع نمو التطبيق، مما يستلزم تغيير المقاربة حتى لو كان التطبيق قد دخل الإنتاج بالفعل.