دليل شامل: استخدام Redux في تطبيقات React TypeScript بكفاءة

دقائق القراءة: 9

دليل شامل: استخدام Redux في تطبيقات React TypeScript بكفاءة

يُعد Redux حاوية حالة (state container) يمكن التنبؤ بها لتطبيقات JavaScript، ويُعرف بكونه مكتبة شائعة لإدارة الحالة في تطبيقات React. عندما يُستخدم Redux جنبًا إلى جنب مع TypeScript، فإنه يوفر تجربة تطوير محسّنة بشكل ملحوظ. TypeScript هو مجموعة فائقة من JavaScript يقوم بفحص الأنواع (type-checks) لضمان قوة الكود وسهولة فهمه.

في هذا الدليل، سأوضح لك كيفية استخدام Redux في مشروع React TypeScript الخاص بك من خلال بناء تطبيق يسمح لك بإضافة المقالات وحذفها وعرضها. دعنا نتعمق في التفاصيل.

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

يفترض هذا الدليل أن لديك فهمًا أساسيًا لـ React و Redux و TypeScript. إذا لم تكن على دراية بهذه التقنيات، فننصحك أولاً بقراءة دليل عملي لـ TypeScript أو هذا البرنامج التعليمي لـ React Redux. بخلاف ذلك، لنبدأ.

إعداد المشروع

لاستخدام Redux و TypeScript، نحتاج إلى إنشاء تطبيق React جديد. للقيام بذلك، افتح واجهة سطر الأوامر (CLI) ونفذ هذا الأمر:

npx create-react-app my-app --template typescript

بعد ذلك، دعنا ننظم بنية المشروع على النحو التالي:

├── src
|   ├── components
|   |   ├── AddArticle.tsx
|   |   └── Article.tsx
|   ├── store
|   |   ├── actionCreators.ts
|   |   ├── actionTypes.ts
|   |   └── reducer.ts
|   ├── type.d.ts
|   ├── App.test.tsx
|   ├── App.tsx
|   ├── index.css
|   ├── index.tsx
|   ├── react-app-env.d.ts
|   └── setupTests.ts
├── tsconfig.json
├── package.json
└── yarn.lock

تُعد بنية ملفات المشروع بسيطة للغاية. ومع ذلك، هناك نقطتان مهمتان يجب ملاحظتهما:

  • المجلد store الذي يحتوي على الملفات المتعلقة بـ React Redux.
  • الملف type.d.ts الذي يحمل أنواع TypeScript، والتي يمكن استخدامها الآن في ملفات أخرى دون الحاجة إلى استيرادها.

بعد ذلك، يمكننا الآن تثبيت Redux وإنشاء أول مخزن (store) لنا. لذا، افتح المشروع وشغل الأمر التالي:

yarn add redux react-redux redux-thunk

أو عند استخدام npm:

npm install redux react-redux redux-thunk

يجب علينا أيضًا تثبيت أنواعها (types) كاعتمادات تطوير (development dependencies) لمساعدة TypeScript على فهم المكتبات. لذا، دعنا ننفذ هذا الأمر مرة أخرى في واجهة سطر الأوامر (CLI).

yarn add -D @types/redux @types/react-redux @types/redux-thunk

أو لـ npm:

npm install -D @types/redux @types/react-redux @types/redux-thunk

رائع! مع هذه الخطوة، يمكننا الآن إنشاء أنواع TypeScript للمشروع في القسم التالي.

إنشاء الأنواع (Types)

تسمح لك أنواع TypeScript بتعيين أنواع لمتغيراتك، ومعاملات الدوال (function parameters)، وما إلى ذلك. في ملف type.d.ts، سنقوم بتعريف الأنواع التالية:

type.d.ts

interface IArticle {
  id: number
  title: string
  body: string
}

type ArticleState = {
  articles: IArticle[]
}

type ArticleAction = {
  type: string
  article: IArticle
}

type DispatchType = (args: ArticleAction) => ArticleAction

هنا، نبدأ بإعلان الواجهة IArticle التي تعكس شكل المقال المحدد. ثم لدينا ArticleState و ArticleAction و DispatchType التي ستعمل كأنواع على التوالي لكائن الحالة (state object)، ومنشئي الإجراءات (action creators)، ودالة الإرسال (dispatch function) التي يوفرها Redux. بعد ذلك، أصبح لدينا الآن الأنواع الضرورية لبدء استخدام React Redux. دعنا ننشئ أنواع الإجراءات (action types).

إنشاء أنواع الإجراءات (Action Types)

نحتاج إلى نوعين من الإجراءات لمخزن Redux. أحدهما لإضافة المقالات والآخر لحذفها. سنقوم بتعريفها في ملف store/actionTypes.ts.

store/actionTypes.ts

export const ADD_ARTICLE = "ADD_ARTICLE"
export const REMOVE_ARTICLE = "REMOVE_ARTICLE"

إنشاء منشئي الإجراءات (Action Creators)

منشئو الإجراءات هي دوال تقوم بإنشاء الإجراءات التي يتم إرسالها إلى المخزن لتغيير الحالة. سنقوم بإنشاء دوال لإضافة وحذف المقالات، بالإضافة إلى دالة لمحاكاة طلب HTTP.

store/actionCreators.ts

import * as actionTypes from "./actionTypes"

export function addArticle(article: IArticle) {
  const action: ArticleAction = {
    type: actionTypes.ADD_ARTICLE,
    article,
  }
  return simulateHttpRequest(action)
}

export function removeArticle(article: IArticle) {
  const action: ArticleAction = {
    type: actionTypes.REMOVE_ARTICLE,
    article,
  }
  return simulateHttpRequest(action)
}

export function simulateHttpRequest(action: ArticleAction) {
  return (dispatch: DispatchType) => {
    setTimeout(() => {
      dispatch(action)
    }, 500)
  }
}

في هذا الدليل، سنقوم بمحاكاة طلب HTTP عن طريق تأخيره لمدة 0.5 ثانية. ولكن، لا تتردد في استخدام خادم حقيقي إذا كنت ترغب في ذلك. هنا، ستقوم الدالة addArticle بإرسال إجراء لإضافة مقال جديد، وستقوم الدالة removeArticle بالعكس، أي حذف الكائن الذي تم تمريره كوسيط.

إنشاء المُخفض (Reducer)

المُخفض (reducer) هو دالة نقية (pure function) تستقبل حالة المخزن (store state) وإجراءً (action) كمعاملات، ثم تُعيد الحالة المحدثة. سنقوم بتعريف الحالة الأولية للمقالات وكيفية تحديثها بناءً على الإجراءات المختلفة.

store/reducer.ts

import * as actionTypes from "./actionTypes"

const initialState: ArticleState = {
  articles: [
    {
      id: 1,
      title: "post 1",
      body: "Quisque cursus, metus vitae pharetra Nam libero tempore, cum soluta nobis est eligendi",
    },
    {
      id: 2,
      title: "post 2",
      body: "Harum quidem rerum facilis est et expedita distinctio quas molestias excepturi sint",
    },
  ],
}

كما ترى هنا، نعلن عن حالة أولية (initial state) ليكون لدينا بعض المقالات لعرضها عند تحميل الصفحة. يجب أن يتطابق كائن الحالة مع النوع ArticleState – وإلا، سيقوم TypeScript بإطلاق خطأ. بعد ذلك، سنقوم بتعريف دالة المُخفض الرئيسية:

const reducer = (
  state: ArticleState = initialState,
  action: ArticleAction
): ArticleState => {
  switch (action.type) {
    case actionTypes.ADD_ARTICLE:
      const newArticle: IArticle = {
        id: Math.random(), // ليس فريدًا حقًا
        title: action.article.title,
        body: action.article.body,
      }
      return {
        ...state,
        articles: state.articles.concat(newArticle),
      }
    case actionTypes.REMOVE_ARTICLE:
      const updatedArticles: IArticle[] = state.articles.filter(
        article => article.id !== action.article.id
      )
      return {
        ...state,
        articles: updatedArticles,
      }
  }
  return state
}

export default reducer

بعد ذلك، لدينا دالة المُخفض reducer التي تتوقع الحالة السابقة (previous state) وإجراءً (action) لتتمكن من تحديث المخزن. هنا، لدينا إجراءان: أحدهما للإضافة والآخر للحذف. مع هذا الترتيب، يمكننا الآن التعامل مع الحالة باستخدام المُخفض. دعنا الآن ننشئ مخزنًا للمشروع.

إنشاء المخزن (Store)

مخزن Redux هو المكان الذي تعيش فيه حالة تطبيقك. سنقوم بإنشائه في ملف index.tsx، وهو نقطة الدخول الرئيسية لتطبيق React.

index.tsx

import * as React from "react"
import { render } from "react-dom"
import { createStore, applyMiddleware, Store } from "redux"
import { Provider } from "react-redux"
import thunk from "redux-thunk"

import App from "./App"
import reducer from "./store/reducer"

const store: Store<ArticleState, ArticleAction> & { dispatch: DispatchType } = createStore(reducer, applyMiddleware(thunk))

const rootElement = document.getElementById("root")

render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
)

كما ترى، نقوم باستيراد دالة المُخفض reducer ثم نمررها كوسيط إلى الدالة createStore لإنشاء مخزن Redux جديد. يجب أيضًا معالجة وسيط redux-thunk كمعامل ثانٍ للدالة ليكون قادرًا على التعامل مع الكود غير المتزامن (asynchronous code).

بعد ذلك، نربط React بـ Redux عن طريق توفير كائن المخزن store كخصائص (props) للمكون Provider. يمكننا الآن استخدام Redux في هذا المشروع والوصول إلى المخزن. لذا، دعنا ننشئ المكونات للحصول على البيانات ومعالجتها.

إنشاء المكونات (Components)

في هذا القسم، سنقوم بإنشاء المكونات اللازمة لعرض المقالات وإضافة مقالات جديدة.

مكون إضافة المقالات (AddArticle.tsx)

هذا المكون سيحتوي على نموذج لإضافة مقالات جديدة إلى المخزن.

components/AddArticle.tsx

import * as React from "react"

type Props = {
  saveArticle: (article: IArticle | any) => void
}

export const AddArticle: React.FC<Props> = ({ saveArticle }) => {
  const [article, setArticle] = React.useState<IArticle | {}>()

  const handleArticleData = (e: React.FormEvent<HTMLInputElement>) => {
    setArticle({
      ...article,
      [e.currentTarget.id]: e.currentTarget.value,
    })
  }

  const addNewArticle = (e: React.FormEvent) => {
    e.preventDefault()
    saveArticle(article)
  }

  return (
    <form onSubmit={addNewArticle} className="Add-article">
      <input type="text" id="title" placeholder="Title" onChange={handleArticleData} />
      <input type="text" id="body" placeholder="Description" onChange={handleArticleData} />
      <button disabled={article === undefined ? true : false}>
        Add article
      </button>
    </form>
  )
}

لإضافة مقال جديد، سنستخدم مكون النموذج هذا. يستقبل الدالة saveArticle كمعامل، والتي تسمح بإضافة مقال جديد إلى المخزن. يجب أن يتبع كائن المقال النوع IArticle لجعل TypeScript راضيًا.

مكون المقال (Article.tsx)

هذا المكون سيعرض تفاصيل مقال واحد مع خيار حذفه.

components/Article.tsx

import * as React from "react"
import { Dispatch } from "redux"
import { useDispatch } from "react-redux"

type Props = {
  article: IArticle
  removeArticle: (article: IArticle) => void
}

export const Article: React.FC<Props> = ({ article, removeArticle }) => {
  const dispatch: Dispatch<any> = useDispatch()

  const deleteArticle = React.useCallback(
    (article: IArticle) => dispatch(removeArticle(article)),
    [dispatch, removeArticle]
  )

  return (
    <div className="Article">
      <div>
        <h1>{article.title}</h1>
        <p>{article.body}</p>
      </div>
      <button onClick={() => deleteArticle(article)}>Delete</button>
    </div>
  )
}

يعرض المكون Article كائن مقال. يجب أن تقوم الدالة removeArticle بالإرسال (dispatch) للوصول إلى المخزن وبالتالي حذف مقال معين. هذا هو السبب في أننا نستخدم الخطاف useDispatch هنا، والذي يسمح لـ Redux بإكمال إجراء الحذف. بعد ذلك، يساعد استخدام useCallback على تجنب إعادة العرض غير الضرورية عن طريق حفظ القيم كاعتمادات (dependencies).

لدينا أخيرًا المكونات التي نحتاجها لإضافة وعرض المقالات. دعنا الآن نضيف القطعة الأخيرة إلى اللغز باستخدامها في ملف App.tsx.

المكون الرئيسي (App.tsx)

هذا المكون سيجمع كل المكونات الأخرى ويعرض قائمة المقالات.

App.tsx

import * as React from "react"
import { useSelector, shallowEqual, useDispatch } from "react-redux"
import "./styles.css"
import { Article } from "./components/Article"
import { AddArticle } from "./components/AddArticle"
import { addArticle, removeArticle } from "./store/actionCreators"
import { Dispatch } from "redux"

const App: React.FC = () => {
  const articles: readonly IArticle[] = useSelector(
    (state: ArticleState) => state.articles,
    shallowEqual
  )

  const dispatch: Dispatch<any> = useDispatch()

  const saveArticle = React.useCallback(
    (article: IArticle) => dispatch(addArticle(article)),
    [dispatch]
  )

  return (
    <main>
      <h1>My Articles</h1>
      <AddArticle saveArticle={saveArticle} />
      {articles.map((article: IArticle) => (
        <Article key={article.id} article={article} removeArticle={removeArticle} />
      ))}
    </main>
  )
}

export default App

يتيح الخطاف useSelector الوصول إلى حالة المخزن. هنا، نمرر shallowEqual كوسيط ثانٍ للدالة لإخبار Redux باستخدام المساواة السطحية (shallow equality) عند التحقق من التغييرات. بعد ذلك، نعتمد على useDispatch لإرسال إجراء لإضافة المقالات في المخزن. أخيرًا، نكرر عبر مصفوفة المقالات ونمرر كل مقال إلى مكون Article لعرضه.

تشغيل التطبيق

مع كل ما سبق، يمكننا الآن الانتقال إلى جذر المشروع ثم تنفيذ هذا الأمر:

yarn start

أو لـ npm:

npm start

إذا فتحت http://localhost:3000/ في المتصفح، يجب أن ترى هذا:

لقطة شاشة لتطبيق React TypeScript يستخدم Redux لعرض قائمة بالمقالات مع نموذج لإضافة مقالات جديدة.

رائع! يبدو تطبيقنا جيدًا. بهذا، نكون قد انتهينا الآن من استخدام Redux في تطبيق React TypeScript. يمكنك العثور على المشروع النهائي في CodeSandbox هذا.

الخلاصة التقنية

يُظهر هذا الدليل بوضوح كيف يمكن لـ Redux و TypeScript العمل معًا بسلاسة لإنشاء تطبيقات React قوية وقابلة للصيانة. يوفر Redux بنية واضحة لإدارة الحالة المعقدة، بينما يضيف TypeScript طبقة من الأمان من خلال التحقق من الأنواع، مما يقلل من الأخطاء المحتملة ويحسن من تجربة المطور. إن استخدام الخطافات (hooks) مثل useSelector و useDispatch يبسط التفاعل مع مخزن Redux في المكونات الوظيفية لـ React. هذه التركيبة لا تعزز فقط قابلية القراءة والفهم للكود، بل تضمن أيضًا قابلية التوسع والمرونة اللازمة لتطوير تطبيقات الويب الحديثة بكفاءة عالية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *