مدیریت وضعیت (State Management) در React: از useState تا Redux و Context API
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 10 دقیقه

مدیریت وضعیت (State Management) در React: از useState تا Redux و Context API

وقتی با React کار می‌کنیم، یکی از مهم‌ترین مفاهیمی که باید درک کنیم، مدیریت وضعیت یا همان State Management است.

هر کامپوننت در React می‌تواند یک وضعیت محلی (Local State) داشته باشد؛ یعنی داده‌هایی که فقط در همان کامپوننت نگه‌داری می‌شوند. اما وقتی برنامه بزرگ‌تر می‌شود و چندین کامپوننت باید داده‌های مشترک را مدیریت کنند، کار پیچیده‌تر می‌شود. برای ساده‌تر کردن این فرآیند، React ابزارهای مختلفی در اختیار ما قرار داده است: هوک‌ها (Hooks)، Context API و Redux.

ویژگی‌های کلیدی مدیریت وضعیت در React

  • وضعیت محلی (useState): برای مدیریت داده‌هایی که فقط در یک کامپوننت استفاده می‌شوند. مثال ساده: شمارنده‌ای که فقط در همان صفحه تغییر می‌کند.

  • وضعیت سراسری (Context API): وقتی چندین کامپوننت نیاز دارند به یک داده مشترک دسترسی داشته باشند، Context API کمک می‌کند بدون نیاز به ارسال داده از طریق props بین کامپوننت‌ها، وضعیت را به اشتراک بگذاریم.

  • وضعیت متمرکز (Redux): در برنامه‌های بزرگ که داده‌ها پیچیده و وابسته به بخش‌های مختلف هستند، Redux یک ذخیره‌گاه مرکزی (Global Store) فراهم می‌کند تا همه کامپوننت‌ها بتوانند وضعیت را از یک نقطه مدیریت کنند.

  • اصل تغییرناپذیری (Immutability): در React نمی‌توان وضعیت را مستقیم تغییر داد. باید از توابع مخصوص (مثل setState یا dispatch) برای به‌روزرسانی وضعیت استفاده کنیم. این کار باعث می‌شود تغییرات قابل پیش‌بینی و قابل ردیابی باشند.

  • بازرندر شدن (Re-render): هر زمان وضعیت تغییر کند، React کامپوننت مربوطه را دوباره رندر می‌کند تا رابط کاربری با داده‌های جدید هماهنگ شود.

مدیریت وضعیت با هوک‌ها (Hooks) در React

در نسخه 16.8، کتابخانه React قابلیتی به نام هوک‌ها (Hooks) معرفی کرد. این قابلیت انقلابی در شیوه‌ی کار با کامپوننت‌ها ایجاد کرد، چون به ما اجازه می‌دهد در کامپوننت‌های تابعی (Functional Components) هم وضعیت (State) و رفتارهای چرخه‌ی عمر (Lifecycle Methods) را مدیریت کنیم.

قبل از معرفی هوک‌ها، اگر می‌خواستیم وضعیت یک کامپوننت را نگه‌داری کنیم یا به رخدادهای چرخه‌ی عمر آن واکنش نشان دهیم، مجبور بودیم از کامپوننت‌های کلاسی (Class Components) استفاده کنیم. این کار هم پیچیدگی بیشتری داشت و هم کدنویسی را سنگین‌تر می‌کرد.

اما حالا با هوک‌ها، می‌توانیم همان کارها را در کامپوننت‌های تابعی انجام دهیم؛ کامپوننت‌هایی که ساده‌تر، خواناتر و قابل‌مدیریت‌تر هستند. به همین دلیل، امروزه تقریباً همه‌ی پروژه‌های React از کامپوننت‌های تابعی همراه با هوک‌ها استفاده می‌کنند و کامپوننت‌های کلاسی کمتر به کار می‌روند.

اگر تازه با React آشنا شده‌اید، کافی است بدانید که هوک‌ها ابزارهایی هستند که به شما امکان می‌دهند در کامپوننت‌های ساده‌ی تابعی، کارهایی پیشرفته مثل مدیریت وضعیت یا واکنش به تغییرات را انجام دهید؛ بدون اینکه مجبور باشید سراغ کلاس‌ها بروید.

هوک useState در React

useState پرکاربردترین هوک در React برای مدیریت وضعیت محلی (Local State) در کامپوننت‌های تابعی است. این هوک به ما اجازه می‌دهد یک مقدار وضعیت در کامپوننت تعریف کنیم و آن را با استفاده از یک تابع ویژه (Setter Function) تغییر دهیم.

سینتکس پایه

const [state, setState] = useState(<default value>);
  • useState(): هوکی برای مدیریت وضعیت در کامپوننت‌های تابعی.
  • [state, setState]:
    • state مقدار فعلی وضعیت را نگه می‌دارد.
    • setState تابعی است که برای تغییر وضعیت استفاده می‌شود.

مثال ساده: دریافت نام کاربر

 
const handleInputChange = (event) => {
    setName(event.target.value);
};

return (
    <div>
        <h1>Enter Your Name</h1>
        <input
            type="text"
            value={name}
            onChange={handleInputChange}
            placeholder="Type your name"
        />
        <p>Hello, {name ? name : 'Stranger'}!</p>
    </div>
);

توضیح کد

  • useState('') وضعیت name را با مقدار اولیه رشته‌ی خالی تعریف می‌کند.
  • تابع handleInputChange هر بار که کاربر چیزی در فیلد ورودی تایپ کند، مقدار name را به‌روزرسانی می‌کند.
  • ویژگی value={name} باعث می‌شود مقدار وضعیت در فیلد ورودی نمایش داده شود.
  • ویژگی onChange={handleInputChange} تغییرات ورودی را به وضعیت منتقل می‌کند.
  • در نهایت، کامپوننت یک پیام خوشامد چاپ می‌کند:
    • اگر کاربر نامی وارد کرده باشد: "Hello Name!"
    • اگر چیزی وارد نکرده باشد: "Hello Stranger!"

این مثال نشان می‌دهد که چگونه می‌توان با استفاده از useState یک وضعیت ساده را در کامپوننت‌های تابعی مدیریت کرد.

هوک useReducer در React

useReducer یک جایگزین قدرتمند برای useState است و زمانی بیشتر استفاده می‌شود که:

  • منطق ساخت وضعیت پیچیده باشد.
  • مقدار وضعیت جدید به مقدار قبلی وابسته باشد.
  • یا نیاز به بهینه‌سازی کامپوننت‌ها داشته باشیم.

به بیان ساده، وقتی وضعیت شما فقط یک مقدار ساده مثل عدد یا رشته نیست و شامل چندین حالت یا تغییرات وابسته به هم می‌شود، بهتر است از useReducer استفاده کنید.

سینتکس پایه

const [state, dispatch] = useReducer(reducer, initialArgs, init);
  • useReducer: برای مدیریت وضعیت‌های پیچیده در کامپوننت‌های تابعی.
  • reducer: تابعی که بر اساس اکشن‌ها (Actions) وضعیت را تغییر می‌دهد.
  • initialArgs: مقدار اولیه وضعیت.
  • init (اختیاری): تابعی برای مقداردهی اولیه تنبل (Lazy Initialization).
  • state: وضعیت فعلی.
  • dispatch: تابعی برای ارسال اکشن‌ها و به‌روزرسانی وضعیت.

اگر useState را مثل یک دفترچه یادداشت کوچک در نظر بگیریم که فقط یک مقدار ساده را نگه می‌دارد، useReducer شبیه یک سیستم مدیریت وظایف است که می‌تواند چندین تغییر مختلف را بر اساس دستورالعمل‌ها (اکشن‌ها) کنترل کند. این روش باعث می‌شود کد شما مرتب‌تر و قابل پیش‌بینی‌تر باشد، مخصوصاً در پروژه‌های بزرگ‌تر.

مدیریت وضعیت با Redux

Redux یک کتابخانه‌ی مدیریت وضعیت برای برنامه‌های جاوااسکریپت است. به زبان ساده، Redux داده‌های برنامه را در یک مکان مرکزی نگه‌داری و مدیریت می‌کند. وقتی برنامه کوچک است، معمولاً از ابزارهای ساده‌تر مثل useState یا Context API استفاده می‌کنیم. اما هرچه برنامه بزرگ‌تر و پیچیده‌تر شود، مدیریت داده‌ها سخت‌تر می‌شود. در این شرایط، Redux کمک می‌کند وضعیت و داده‌ها را به شکل سازمان‌یافته و قابل پیش‌بینی کنترل کنیم.

Redux چگونه کار می‌کند؟

  • Store (ذخیره‌گاه): محلی مرکزی که تمام وضعیت برنامه در آن نگه‌داری می‌شود. می‌توان آن را مثل یک پایگاه داده کوچک در نظر گرفت که همه‌ی کامپوننت‌ها به آن دسترسی دارند.

  • Actions (اکشن‌ها): توابع یا اشیائی که توضیح می‌دهند چه تغییری باید در وضعیت رخ دهد. مثلاً اکشن "افزایش شمارنده" یا "افزودن کار جدید".

  • Reducers (کاهنده‌ها): توابعی که اکشن‌ها را دریافت می‌کنند و بر اساس آن‌ها وضعیت را به‌روزرسانی می‌کنند. Reducer همیشه یک وضعیت جدید برمی‌گرداند و وضعیت قبلی را تغییر نمی‌دهد (اصل تغییرناپذیری).

اگر بخواهیم Redux را با زندگی روزمره مقایسه کنیم:

  • Store مثل یک دفتر مرکزی است که همه اطلاعات در آن ثبت می‌شود.
  • Actions مثل فرم‌هایی هستند که توضیح می‌دهند چه تغییری باید انجام شود.
  • Reducers مثل کارمندان دفتر هستند که فرم‌ها را بررسی کرده و اطلاعات دفتر مرکزی را به‌روزرسانی می‌کنند.

مثال ساده با Redux: شمارنده

برای اینکه ببینیم Redux چطور کار می‌کند، یک مثال خیلی ساده از شمارنده را بررسی می‌کنیم. در این مثال، می‌خواهیم دکمه‌هایی داشته باشیم که مقدار شمارنده را افزایش یا کاهش دهند و مقدار فعلی را نمایش دهند.

مراحل کار

  1. ساخت Store: ابتدا یک ذخیره‌گاه (Store) ایجاد می‌کنیم که وضعیت شمارنده را نگه دارد.

  2. تعریف Actions: اکشن‌ها مشخص می‌کنند چه تغییری باید در وضعیت رخ دهد (مثلاً افزایش یا کاهش).

  3. نوشتن Reducer: قابلیت Reducer بر اساس اکشن‌ها وضعیت را تغییر می‌دهد.

  4. اتصال به کامپوننت React: کامپوننت شمارنده وضعیت را از Store می‌گیرد و با دکمه‌ها اکشن‌ها را ارسال می‌کند.

کد‌های اپلیکیشن:

// counterReducer.js
const initialState = { count: 0 };

function counterReducer(state = initialState, action) {
    switch (action.type) {
        case 'INCREMENT':
            return { count: state.count + 1 };
        case 'DECREMENT':
            return { count: state.count - 1 };
        default:
            return state;
    }
}

export default counterReducer;
// store.js
import { createStore } from 'redux';
import counterReducer from './counterReducer';

const store = createStore(counterReducer);

export default store;
 // Counter.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';

function Counter() {
    const count = useSelector((state) => state.count);
    const dispatch = useDispatch();

    return (
        <div>
            <h1>شمارنده: {count}</h1>
            <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
            <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
        </div>
    );
}

export default Counter;
 
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import Counter from './Counter';

function App() {
    return (
        <Provider store={store}>
            <Counter />
        </Provider>
    );
}

export default App;

توضیح کد

  • counterReducer: وضعیت اولیه را با count: 0 تعریف می‌کند و بر اساس اکشن‌ها (INCREMENT یا DECREMENT) وضعیت را تغییر می‌دهد.
  • store: با استفاده از createStore ساخته می‌شود و Reducer را مدیریت می‌کند.
  • Counter کامپوننت:
    • با useSelector مقدار شمارنده را از Store می‌گیرد.
    • با useDispatch اکشن‌ها را ارسال می‌کند تا وضعیت تغییر کند.
  • App: با Provider کل برنامه را به Store متصل می‌کند تا همه کامپوننت‌ها بتوانند به وضعیت دسترسی داشته باشند.

آشنایی با Context API در React

وقتی برنامه‌های React بزرگ‌تر می‌شوند، یکی از مشکلات رایج این است که داده‌ها باید بین چندین کامپوننت رد و بدل شوند. معمولاً این کار با props انجام می‌شود؛ یعنی داده‌ها را از یک کامپوننت والد به فرزند منتقل می‌کنیم. اما اگر داده‌ای لازم باشد به چندین سطح پایین‌تر برسد، مجبوریم آن را از هر لایه عبور دهیم. این مشکل به نام Prop Drilling شناخته می‌شود و کد را پیچیده و سخت‌خوان می‌کند.

برای حل این مشکل، React قابلیتی به نام Context API معرفی کرده است.

Context API چیست؟

Context API روشی است برای اشتراک‌گذاری داده‌ها بین کامپوننت‌ها بدون نیاز به ارسال props در هر سطح. به کمک آن می‌توانیم یک «منبع داده» مرکزی تعریف کنیم و هر کامپوننتی که نیاز دارد، مستقیم به آن دسترسی داشته باشد.

اجزای اصلی Context API

  • Context Object: با استفاده از React.createContext() ساخته می‌شود و نقش منبع داده را دارد.

  • Provider: کامپوننتی که داده‌ها را در اختیار سایر کامپوننت‌ها قرار می‌دهد. هر کامپوننتی که داخل Provider قرار بگیرد، می‌تواند به داده‌ها دسترسی داشته باشد.

  • Consumer یا useContext: روشی برای دریافت داده‌ها از Context. در نسخه‌های جدید React معمولاً از هوک useContext استفاده می‌کنیم که ساده‌تر و خواناتر است.

مثال ساده

 
import React, { createContext, useContext } from 'react';

// 1. ایجاد Context
const ThemeContext = createContext();

// 2. ساخت Provider
function App() {
  return (
    <ThemeContext.Provider value="dark">
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

// 3. استفاده از useContext برای دریافت داده
function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? '#333' : '#eee' }}>Click Me</button>;
}

export default App;

توضیح کد

  • با createContext() یک Context ساخته‌ایم.
  • در کامپوننت App، Provider مقدار "dark" را به همه‌ی کامپوننت‌های داخلی می‌دهد.
  • در ThemedButton، با useContext مقدار "dark" دریافت می‌شود و بر اساس آن رنگ دکمه تغییر می‌کند.

مقایسه سه روش مدیریت وضعیت در React

در جدول زیر سه ابزار اصلی مدیریت وضعیت در React یعنی useState، Context API و Redux را از نظر کاربرد و ویژگی‌ها مقایسه کرده‌ام:

ابزار کاربرد اصلی مزایا معایب مناسب برای
useState مدیریت وضعیت محلی در یک کامپوننت ساده، سریع، یادگیری آسان فقط برای وضعیت‌های کوچک و محلی؛ در پروژه‌های بزرگ کافی نیست کامپوننت‌های کوچک یا وضعیت‌های ساده مثل شمارنده یا فرم ساده
Context API اشتراک‌گذاری وضعیت بین چندین کامپوننت بدون Prop Drilling حذف نیاز به ارسال props در چندین سطح؛ ساده‌تر از Redux مدیریت وضعیت‌های خیلی پیچیده سخت می‌شود؛ ابزارهای جانبی کمی دارد برنامه‌های متوسط که چندین کامپوننت نیاز به داده مشترک دارند (مثل تم یا زبان)
Redux مدیریت وضعیت پیچیده و متمرکز در یک Store جهانی ساختارمند، قابل پیش‌بینی، ابزارهای جانبی قوی (DevTools، Middleware) یادگیری سخت‌تر؛ کدنویسی بیشتر نسبت به useState یا Context برنامه‌های بزرگ و پیچیده با داده‌های زیاد و وابستگی‌های متعدد

جمع‌بندی مقایسه

  • اگر به تازگی با ری‌اکت آشنا شده‌اید و فقط می‌خواهید وضعیت ساده‌ای مثل شمارنده یا ورودی متن را مدیریت کنید، useState بهترین انتخاب است.
  • اگر چندین کامپوننت باید داده مشترک داشته باشند (مثل زبان، تم یا کاربر لاگین‌شده)، Context API مناسب‌تر است.
  • اگر پروژه بزرگ و پیچیده داری که نیاز به مدیریت داده‌های زیاد و قابل پیش‌بینی دارد، Redux انتخاب حرفه‌ای‌تر است.

جمع‌بندی

مدیریت وضعیت یکی از مهم‌ترین چالش‌ها در توسعه برنامه‌های React است. همان‌طور که دیدیم، ابزارهای مختلفی برای این کار وجود دارد و هرکدام مناسب شرایط خاصی هستند:

  • useState برای وضعیت‌های ساده و محلی در یک کامپوننت.
  • Context API برای اشتراک‌گذاری داده‌ها بین چندین کامپوننت و جلوگیری از Prop Drilling.
  • Redux برای مدیریت وضعیت‌های پیچیده و متمرکز در برنامه‌های بزرگ.

نکته کلیدی این است که هیچ‌کدام از این ابزارها «بهترین» در همه شرایط نیستند؛ بلکه باید با توجه به اندازه پروژه، میزان پیچیدگی داده‌ها و نیاز به اشتراک‌گذاری وضعیت تصمیم بگیریم.

اگر قصد دارید به صورت کامل با Redux و تمام ویژگی‌های آن آشنا شوید به شما پیشنهاد می‌کنم که از دوره آموزش Redux استفاده کنید.

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

/@arastoo
ارسطو عباسی
کارشناس تست نرم‌افزار و مستندات

...

دیدگاه و پرسش
برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

ارسطو عباسی

کارشناس تست نرم‌افزار و مستندات