توسعه فرانت‌اند

React Context API: مدیریت حالت بدون کتابخانه

تفاوت Context با ذخیره State و Cache شبکه، محدوده Provider و تغییر value؛ نمونه کامل Reducer و دو Context برای یادداشت محلی.

1405/01/05 4 دقیقه مطالعه 550 بازدید

React Context راهی برای رساندن یک مقدار به بخش‌های پایین‌تر درخت است، بدون اینکه همان مقدار از همه Componentهای میانی عبور کند. خود Context محل ذخیره State یا کتابخانه کامل مدیریت داده نیست. مقدار می‌تواند از useState، useReducer یا منبع دیگری بیاید و Provider آن را ارائه کند. این تفاوت برای طراحی مهم است: مشکل انتقال Props، مشکل تغییر State و مشکل Cache داده Server سه مسئله مرتبط اما مستقل هستند.

برای مثال فرضی، چند بخش یک صفحه به یادداشت‌های مشترک نیاز دارند و کنترل‌های دیگر فرمان افزودن یا حذف را می‌فرستند. Reducer می‌تواند قواعد تغییر فهرست را نگه دارد و Context مسیر دسترسی را فراهم کند. لازم نیست State متن در حال تایپ هم به همان محدوده برود؛ آن متن می‌تواند در فرم محلی بماند. محدود کردن دامنه اشتراک، از تبدیل هر تغییر کوچک به تغییر داده عمومی صفحه جلوگیری می‌کند و خواندن مسئولیت‌ها را ساده‌تر می‌کند.

Context را برای مشکل انتقال انتخاب کنید

مرجع useContext خواندن مقدار از Provider نزدیک در درخت را توضیح می‌دهد. Context در سطح ماژول تعریف می‌شود و مصرف‌کننده باید همان object Context را استفاده کند. Default Value وقتی Provider وجود ندارد به کار می‌آید و منبع زنده داده نیست. گذاشتن یک object پیش‌فرض دارای تابع بی‌اثر ممکن است نبود Provider را پنهان کند. برای قراردادی که به Provider نیاز دارد، خطای روشن در Hook مصرف‌کننده می‌تواند مفیدتر باشد.

قبل از اضافه کردن Context، Composition و Props مستقیم را بررسی کنید. اگر فقط دو سطح و یک مصرف‌کننده دارید، مسیر صریح ممکن است خواناتر باشد. Context زمانی مفید است که بخش‌های مرتبط واقعاً به داده مشترک نیاز دارند و عبور آن از Componentهای بی‌ارتباط هزینه می‌سازد. نام‌گذاری باید دامنه را روشن کند؛ AppContext دارای همه داده‌ها پیدا کردن مسئولیت را سخت می‌کند. Theme، انتخاب جاری و داده حساب لزوماً یک چرخه تغییر ندارند.

مالک State و طول عمر را مشخص کنید

Provider را در محدوده‌ای قرار دهید که باید داده را نگه دارد. اگر با تغییر Route آن محدوده Unmount شود، State داخلی هم از بین می‌رود. این رفتار ممکن است برای فرم موقت مناسب و برای انتخابی که باید میان صفحات بماند نامناسب باشد. قرار دادن همه Providerها در ریشه، راه عمومی حل مسئله نیست؛ عمر بیش از نیاز و وابستگی گسترده می‌سازد. داده پایدار باید منبع و روش ذخیره مشخص داشته باشد، نه فقط جایگاه بالاتر در JSX.

Context Client، داده را پس از Refresh خودکار حفظ نمی‌کند و میان تب‌ها یا کاربران مشترک نمی‌سازد. اگر ذخیره محلی یا Server لازم است، قرارداد جدا بنویسید. برای داده حساب، Context فقط وضعیت نمایشی برنامه است و مجوز Backend را تعیین نمی‌کند. مخفی کردن دکمه بر اساس نقش Client، حق انجام عملیات را تضمین نمی‌کند. مقاله طراحی API درباره هویت و مجوز همان عملیات توضیح دارد.

تغییر value روی مصرف‌کنندگان اثر دارد

React مقدار قبلی و تازه Provider را با Object.is مقایسه می‌کند و مصرف‌کنندگان مربوط از مقدار تازه استفاده می‌کنند. این را با «هر تغییر در برنامه، همه درخت را دوباره می‌کشد» توضیح ندهید. Componentی که آن Context را نمی‌خواند، فقط به دلیل نام Context مصرف‌کننده محسوب نمی‌شود؛ البته مسیر Render والد و Props می‌تواند جدا اثر داشته باشد. memo نیز دریافت مقدار تازه Context را برای مصرف‌کننده مسدود نمی‌کند. رابطه دقیق را با درخت واقعی بررسی کنید.

object تازه در هر Render ممکن است value تازه ایجاد کند، حتی وقتی فیلدهایش از نظر محتوا تغییر نکرده‌اند. نگه داشتن مرجع مناسب می‌تواند بخشی از Update غیرضروری را کم کند، اما Cache نباید درستی برنامه را نگه دارد. همچنین یک object شامل State و فرمان‌ها، همه مصرف‌کنندگانش را به همان تغییر مرتبط می‌کند. جدا کردن Context داده و Context فرمان می‌تواند دامنه Subscription را روشن‌تر کند. این کار تضمین عدم Render از همه مسیرهای دیگر نیست.

Reducer قواعد تغییر را کنار هم نگه می‌دارد

راهنمای Reducer و Context در React ترکیب این دو را برای هماهنگی داده توضیح می‌دهد. Reducer باید از State و Action، State بعدی بسازد و اثر جانبی داخل آن قرار نگیرد. دریافت شبکه، ساخت شناسه تصادفی یا نوشتن Storage در Reducer می‌تواند تکرارپذیری را خراب کند. داده لازم Action پیش از dispatch آماده شود. این تفکیک به Code Review کمک می‌کند بفهمد کدام بخش فرمان را می‌سازد و کدام بخش تغییر معتبر را اعمال می‌کند.

نام Action باید معنی تغییر را نشان دهد. addNote و removeNote روشن‌تر از setEverything هستند، به شرط اینکه Reducer همان قرارداد را اجرا کند. ورودی Action در مرز مناسب بررسی شود و حذف با شناسه پایدار انجام شود. index آرایه هنگام تغییر ترتیب شناسه مطمئن نیست. مقاله توابع آرایه درباره تبدیل بدون تغییر ورودی زمینه دارد. ساخت State تازه باید داده قبلی را بی‌دلیل Mutate نکند تا رابطه تغییر برای مصرف‌کننده روشن بماند.

نمونه کامل دو Context برای یادداشت محلی

سه فایل زیر پروژه آموزشی مستقلی می‌سازند. Node.js سازگار با Vite 7 و npm لازم‌اند. package.json و index.html در ریشه و main.jsx در پوشه src قرار می‌گیرند. npm install و npm run dev را در همان ریشه اجرا و نشانی اعلام‌شده Vite را باز کنید. داده در حافظه همین برنامه است و پس از Refresh به مقدار اولیه برمی‌گردد. نمونه به API، ورود حساب یا Storage بیرونی نیاز ندارد و ثبت پایدار یادداشت را پیاده نمی‌کند.

{"name":"context-notes-demo","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^19.0.0","react-dom":"^19.0.0"},"devDependencies":{"vite":"^7.0.0"}}
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Context یادداشت</title><style>body{font:1rem/1.8 Tahoma,sans-serif;max-width:40rem;margin:2rem auto;padding:1rem}input,button{font:inherit;padding:.5rem;margin:.3rem}input:focus-visible,button:focus-visible{outline:3px solid #111;outline-offset:3px}</style></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>

NotesProvider، State را در Reducer نگه می‌دارد و دو Provider ارائه می‌کند. Form فقط dispatch را می‌گیرد و متن در حال تایپ را محلی نگه می‌دارد. List داده یادداشت‌ها و فرمان حذف را مصرف می‌کند. شناسه تازه در Handler ارسال با crypto.randomUUID ساخته می‌شود، نه داخل Reducer. این API در مرورگر مناسب و محیط قابل اعتماد مانند localhost قابل استفاده است. اگر محیط هدف پشتیبانی ندارد، روش شناسه‌سازی باید با آن محیط هماهنگ شود.

import React,{createContext,useContext,useReducer,useState} from 'react';
import {createRoot} from 'react-dom/client';
const NotesContext=createContext(null),DispatchContext=createContext(null);
function reducer(notes,action){switch(action.type){case 'add':return [...notes,action.note];case 'remove':return notes.filter(note=>note.id!==action.id);default:throw new Error('Unknown action')}}
function NotesProvider({children}){const [notes,dispatch]=useReducer(reducer,[{id:'demo-1',text:'یادداشت اولیه آموزشی'}]);return <NotesContext.Provider value={notes}><DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider></NotesContext.Provider>}
function useNotes(){const value=useContext(NotesContext);if(value===null)throw new Error('NotesProvider required');return value}
function useNotesDispatch(){const value=useContext(DispatchContext);if(value===null)throw new Error('NotesProvider required');return value}
function Form(){const dispatch=useNotesDispatch();const [text,setText]=useState('');function submit(event){event.preventDefault();const value=text.trim();if(!value||value.length>200)return;dispatch({type:'add',note:{id:crypto.randomUUID(),text:value}});setText('')}return <form onSubmit={submit}><label htmlFor="note">یادداشت محلی تا ۲۰۰ کاراکتر</label><input id="note" value={text} maxLength={200} required onChange={event=>setText(event.currentTarget.value)}/><button>افزودن</button></form>}
function List(){const notes=useNotes(),dispatch=useNotesDispatch();return <section aria-label="یادداشت‌های محلی"><p role="status">تعداد: {notes.length}</p><ul>{notes.map(note=><li key={note.id}>{note.text}<button type="button" aria-label={`حذف ${note.text}`} onClick={()=>dispatch({type:'remove',id:note.id})}>حذف</button></li>)}</ul></section>}
function App(){return <main><h1>Reducer و Context</h1><p>داده در حافظه است و با Refresh به مقدار اولیه برمی‌گردد.</p><NotesProvider><Form/><List/></NotesProvider></main>}
createRoot(document.getElementById('root')).render(<App/>);

Hookهای مصرف، نبود Provider را با خطای مشخص اعلام می‌کنند. مقدار dispatch در useReducer مرجع پایدار دارد؛ جدا بودن Context فرمان باعث می‌شود تغییر آرایه یادداشت‌ها به عنوان تغییر همان value فرمان معرفی نشود. این تفکیک برای فهم قرارداد است و به تنهایی درباره همه Renderهای فرم حکم نمی‌دهد. Reducer برای add آرایه تازه و برای remove نتیجه filter می‌سازد. متن غیرخالی تا دویست کاراکتر پیش از ساخت Action بررسی می‌شود و داده Server در این نمونه وجود ندارد.

محدودیت را با بسامد تغییر تنها توضیح ندهید

گفتن اینکه Context فقط برای داده کم‌تغییر است بیش از حد کلی است. اندازه درخت، تعداد مصرف‌کنندگان، هزینه Render و شکل value اهمیت دارند. یک مقدار پرتغییر در محدوده کوچک ممکن است مناسب باشد؛ یک object بزرگ کم‌تغییر هم می‌تواند وابستگی دشوار بسازد. برای نیازهای خاص، منبع خارجی دارای Subscription یا Selector قابل بررسی است. تصمیم باید با نیاز و شواهد همان برنامه انجام شود، نه با حکم اینکه هر State پیچیده الزاماً به یک کتابخانه نام‌دار نیاز دارد.

برای داده Server، وضعیت Fetch، تازگی، Retry و Cache مسئولیت‌های بیشتری دارند. قرار دادن پاسخ API در Context این سیاست‌ها را خودکار فراهم نمی‌کند. مقاله بارگذاری افزایشی نمونه‌ای از این پیچیدگی‌ها را توضیح می‌دهد. اگر ابزار مدیریت Query انتخاب می‌شود، مرز آن با State رابط روشن باشد. متن فیلتر موقت، انتخاب محلی و Cache پاسخ شبکه همیشه یک مالک یا یک طول عمر ندارند.

قرارداد را برای تیم قابل استفاده نگه دارید

Provider، Hookهای مصرف و نوع Actionها را در محدوده نام‌دار نگه دارید. لازم نیست مصرف‌کننده جزئیات ذخیره State را بداند، اما باید بداند داده پایدار است یا موقت و کدام فرمان‌ها مجازند. مقاله Component Patterns درباره مالکیت State و TypeScript با React درباره شکل قرارداد کمک می‌کنند. Generic یا Context عمومی بیش از حد باز می‌تواند همان ابهامی را ایجاد کند که قصد داشتید با حذف Prop Drilling کم کنید.

در تحویل، محدوده Provider، رفتار Refresh، سیاست خطا و منبع داده را ثبت کنید. اگر کندی وجود دارد، ابتدا مسیر واقعی Update را با ابزار مناسب بشناسید؛ مقاله عملکرد React برای این بررسی مفید است. شروع مناسب یک دامنه کوچک با مصرف‌کنندگان مشخص است. State را نزد مالک معتبر نگه دارید و Context را برای رساندن همان قرارداد به بخش‌های مرتبط به کار ببرید؛ اشتراک همه داده‌های برنامه هدف مستقلی نیست.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!