شرح بناء تطبيق CRUD لإدارة الكتب باستخدام React من الصفر

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

مقدمة: لماذا نبني تطبيق إدارة كتب باستخدام React؟

يُعد تطبيق إدارة الكتب مثالًا عمليًا ممتازًا لتعلّم أساسيات تطوير الواجهات الحديثة باستخدام React. فمن خلال هذا المشروع ستتعرّف على كيفية تنفيذ عمليات CRUD، والتنقّل بين الصفحات عبر React Router، وتخزين البيانات محليًا داخل المتصفح باستخدام localStorage، إلى جانب تنظيم الحالة ومشاركتها بين المكوّنات بطريقة نظيفة وقابلة للتوسع.

في هذا الدليل سنبني التطبيق من الصفر بصورة احترافية، مع شرح واضح للأفكار التقنية المهمة، بحيث ينتج لديك مشروع عملي مفيد وقابل للتطوير لاحقًا.

واجهة توضيحية لتطبيق إدارة الكتب المبني باستخدام React مع دعم عمليات CRUD

ما الذي ستتعلمه من هذا المشروع؟

  • تنفيذ عمليات Create وRead وUpdate وDelete.
  • التنقّل بين المسارات باستخدام React Router.
  • إنشاء Custom Hook مخصص للتعامل مع localStorage.
  • الاحتفاظ بالبيانات بعد تحديث الصفحة.
  • إعادة استخدام نموذج واحد للإضافة والتعديل.
  • استخدام Context API لتقليل تمرير الخصائص بين المكوّنات.

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

ابدأ بإنشاء مشروع جديد عبر الأداة create-react-app:

npx create-react-app book-management-app

بعد إنشاء المشروع، احذف الملفات الموجودة داخل مجلد src ثم أنشئ الملفات والمجلدات التالية:

  • src/index.js
  • src/styles.scss
  • src/components
  • src/context
  • src/hooks
  • src/router

بعد ذلك ثبّت الاعتماديات المطلوبة:

yarn add bootstrap@4.6.0 lodash@4.17.21 react-bootstrap@1.5.2 node-sass@4.14.1 react-router-dom@5.2.0 uuid@8.3.2

هذه الحزم ستساعدك في:

  • تنسيق الواجهة باستخدام bootstrap وreact-bootstrap.
  • استخدام بعض الأدوات المفيدة عبر lodash.
  • إدارة التنقّل عبر react-router-dom.
  • توليد معرفات فريدة باستخدام uuid.
  • كتابة الأنماط بصيغة SCSS عبر node-sass.

إنشاء الصفحات الأساسية للتطبيق

مكوّن الترويسة Header

أنشئ الملف Header.js داخل مجلد components وأضف الكود التالي:

import React from 'react';
import { NavLink } from 'react-router-dom';

const Header = () => {
  return (
    <header>
      <h1>Book Management App</h1>
      <hr />
      <div className="links">
        <NavLink to="/" className="link" activeClassName="active" exact>
          Books List
        </NavLink>
        <NavLink to="/add" className="link" activeClassName="active">
          Add Book
        </NavLink>
      </div>
    </header>
  );
};

export default Header;

يستخدم هذا المكوّن العنصر NavLink بدلًا من الوسم التقليدي <a> حتى يتم التنقّل داخل التطبيق دون إعادة تحميل الصفحة بالكامل، وهو سلوك أساسي في تطبيقات SPA.

مكوّن قائمة الكتب BooksList

import React from 'react';

const BooksList = () => {
  return <h2>List of books</h2>;
};

export default BooksList;

مكوّن إضافة كتاب AddBook

import React from 'react';
import BookForm from './BookForm';

const AddBook = () => {
  const handleOnSubmit = (book) => {
    console.log(book);
  };

  return (
    <React.Fragment>
      <BookForm handleOnSubmit={handleOnSubmit} />
    </React.Fragment>
  );
};

export default AddBook;

هنا نعرض مكوّن BookForm ونمرّر له الدالة handleOnSubmit حتى نستخدم البيانات المُرسلة لاحقًا عند حفظ الكتاب.

بناء نموذج الكتاب القابل لإعادة الاستخدام

أنشئ الملف BookForm.js داخل مجلد components:

import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
import { v4 as uuidv4 } from 'uuid';

const BookForm = (props) => {
  const [book, setBook] = useState({
    bookname: props.book ? props.book.bookname : '',
    author: props.book ? props.book.author : '',
    quantity: props.book ? props.book.quantity : '',
    price: props.book ? props.book.price : '',
    date: props.book ? props.book.date : ''
  });

  const [errorMsg, setErrorMsg] = useState('');
  const { bookname, author, price, quantity } = book;

  const handleOnSubmit = (event) => {
    event.preventDefault();
    const values = [bookname, author, price, quantity];
    let errorMsg = '';

    const allFieldsFilled = values.every((field) => {
      const value = `${field}`.trim();
      return value !== '' && value !== '0';
    });

    if (allFieldsFilled) {
      const book = {
        id: uuidv4(),
        bookname,
        author,
        price,
        quantity,
        date: new Date()
      };
      props.handleOnSubmit(book);
    } else {
      errorMsg = 'Please fill out all the fields.';
    }

    setErrorMsg(errorMsg);
  };

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

    switch (name) {
      case 'quantity':
        if (value === '' || parseInt(value) === +value) {
          setBook((prevState) => ({ ...prevState, [name]: value }));
        }
        break;
      case 'price':
        if (value === '' || value.match(/^\d{1,}(\.\d{0,2})?$/)) {
          setBook((prevState) => ({ ...prevState, [name]: value }));
        }
        break;
      default:
        setBook((prevState) => ({ ...prevState, [name]: value }));
    }
  };

  return (
    <div className="main-form">
      {errorMsg && <p className="errorMsg">{errorMsg}</p>}
      <Form onSubmit={handleOnSubmit}>
        <Form.Group controlId="name">
          <Form.Label>Book Name</Form.Label>
          <Form.Control
            className="input-control"
            type="text"
            name="bookname"
            value={bookname}
            placeholder="Enter name of book"
            onChange={handleInputChange}
          />
        </Form.Group>

        <Form.Group controlId="author">
          <Form.Label>Book Author</Form.Label>
          <Form.Control
            className="input-control"
            type="text"
            name="author"
            value={author}
            placeholder="Enter name of author"
            onChange={handleInputChange}
          />
        </Form.Group>

        <Form.Group controlId="quantity">
          <Form.Label>Quantity</Form.Label>
          <Form.Control
            className="input-control"
            type="number"
            name="quantity"
            value={quantity}
            placeholder="Enter available quantity"
            onChange={handleInputChange}
          />
        </Form.Group>

        <Form.Group controlId="price">
          <Form.Label>Book Price</Form.Label>
          <Form.Control
            className="input-control"
            type="text"
            name="price"
            value={price}
            placeholder="Enter price of book"
            onChange={handleInputChange}
          />
        </Form.Group>

        <Button variant="primary" type="submit" className="submit-btn">
          Submit
        </Button>
      </Form>
    </div>
  );
};

export default BookForm;

كيف يعمل هذا النموذج؟

يعتمد المكوّن على الخطاف useState لتخزين بيانات الكتاب داخل كائن واحد. وهذا مفيد لأننا نتعامل مع عدة حقول مرتبطة بالكيان نفسه.

كما أن المكوّن مصمم ليعمل في حالتين:

  • إضافة كتاب جديد.
  • تعديل كتاب موجود.

ولهذا نتحقق أولًا من وجود الخاصية props.book. إذا كانت موجودة نملأ الحقول بقيمها، وإلا نبدأ بقيم فارغة.

التحقق من صحة المدخلات

داخل الدالة handleInputChange توجد معالجة دقيقة لبعض الحقول:

  • حقل quantity يقبل أعدادًا صحيحة فقط.
  • حقل price يقبل أرقامًا عشرية حتى منزلتين فقط بعد الفاصلة.

هذا يرفع جودة البيانات المخزنة ويمنع إدخال قيم غير مناسبة.

أما عند الإرسال، فتستخدم الدالة every() للتأكد من أن جميع الحقول الأساسية مملوءة، وأن القيمة ليست فارغة أو مساوية لـ 0.

إعداد نظام التوجيه باستخدام React Router

أنشئ الملف AppRouter.js داخل مجلد router:

import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';

const AppRouter = () => {
  return (
    <BrowserRouter>
      <div>
        <Header />
        <div className="main-content">
          <Switch>
            <Route component={BooksList} path="/" exact={true} />
            <Route component={AddBook} path="/add" />
          </Switch>
        </div>
      </div>
    </BrowserRouter>
  );
};

export default AppRouter;

بهذا أصبح لدينا مساران أساسيان:

  • / لعرض قائمة الكتب.
  • /add لإضافة كتاب جديد.

ملف التشغيل الرئيسي index.js

import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './router/AppRouter';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.scss';

ReactDOM.render(<AppRouter />, document.getElementById('root'));

ثم شغّل المشروع:

yarn start

الشاشة الأولية لتطبيق إدارة الكتب بعد تشغيل مشروع React لأول مرةتجربة إدخال كتاب جديد داخل نموذج تطبيق إدارة الكتب باستخدام React

لماذا نستخدم localStorage في هذا التطبيق؟

عند تخزين البيانات داخل الحالة فقط، فإنها تختفي بمجرد تحديث الصفحة. أما باستخدام localStorage فتظل البيانات محفوظة في المتصفح حتى بعد إعادة التحميل أو إغلاق الصفحة.

هذه الطريقة مناسبة للمشاريع التعليمية، ولوحات التحكم البسيطة، وبعض حالات التخزين المحلي المؤقت.

إضافة قيمة إلى localStorage تتم عبر:

localStorage.setItem(key, value)

وبما أن التخزين يقبل نصوصًا فقط، فإننا نحول الكائنات إلى نص بصيغة JSON باستخدام JSON.stringify()، ثم نستعيدها عبر JSON.parse().

إنشاء Custom Hook للتعامل مع التخزين المحلي

أنشئ الملف useLocalStorage.js داخل مجلد hooks:

import { useState, useEffect } from 'react';

const useLocalStorage = (key, initialValue) => {
  const [value, setValue] = useState(() => {
    try {
      const localValue = window.localStorage.getItem(key);
      return localValue ? JSON.parse(localValue) : initialValue;
    } catch (error) {
      return initialValue;
    }
  });

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  return [value, setValue];
};

export default useLocalStorage;

ما فائدة هذا الخطاف المخصص؟

بدلًا من تكرار منطق القراءة والكتابة من localStorage داخل أكثر من مكوّن، نقوم بعزله داخل خطاف واحد قابل لإعادة الاستخدام.

كما أن استخدام التهيئة الكسولة داخل useState يعني أن القراءة من التخزين المحلي تتم مرة واحدة عند تحميل المكوّن، وهو تحسين بسيط لكنه مهم في التطبيقات الأكبر.

ربط التطبيق بالتخزين المحلي

عدّل الملف AppRouter.js لاستخدام الخطاف useLocalStorage:

import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
import useLocalStorage from '../hooks/useLocalStorage';

const AppRouter = () => {
  const [books, setBooks] = useLocalStorage('books', []);

  return (
    <BrowserRouter>
      <div>
        <Header />
        <div className="main-content">
          <Switch>
            <Route component={BooksList} path="/" exact={true} />
            <Route
              render={(props) => (
                <AddBook {...props} books={books} setBooks={setBooks} />
              )}
              path="/add"
            />
          </Switch>
        </div>
      </div>
    </BrowserRouter>
  );
};

export default AppRouter;

ثم حدّث الملف AddBook.js:

import React from 'react';
import BookForm from './BookForm';

const AddBook = ({ history, books, setBooks }) => {
  const handleOnSubmit = (book) => {
    setBooks([book, ...books]);
    history.push('/');
  };

  return (
    <React.Fragment>
      <BookForm handleOnSubmit={handleOnSubmit} />
    </React.Fragment>
  );
};

export default AddBook;

هنا نقوم بإضافة الكتاب الجديد في مقدمة المصفوفة باستخدام معامل الانتشار ...، ثم نعيد المستخدم إلى الصفحة الرئيسية عبر history.push('/').

حفظ بيانات الكتب داخل localStorage في تطبيق React وإعادة التوجيه إلى القائمة

عرض الكتب المخزنة داخل واجهة المستخدم

الخطوة التالية هي تمرير البيانات إلى مكوّن عرض القائمة.

عدّل AppRouter.js بالشكل التالي:

import React from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
import useLocalStorage from '../hooks/useLocalStorage';

const AppRouter = () => {
  const [books, setBooks] = useLocalStorage('books', []);

  return (
    <BrowserRouter>
      <div>
        <Header />
        <div className="main-content">
          <Switch>
            <Route
              render={(props) => (
                <BooksList {...props} books={books} setBooks={setBooks} />
              )}
              path="/"
              exact={true}
            />
            <Route
              render={(props) => (
                <AddBook {...props} books={books} setBooks={setBooks} />
              )}
              path="/add"
            />
          </Switch>
        </div>
      </div>
    </BrowserRouter>
  );
};

export default AppRouter;

مكوّن عرض الكتاب Book

import React from 'react';
import { Button, Card } from 'react-bootstrap';

const Book = ({ id, bookname, author, price, quantity, date, handleRemoveBook }) => {
  return (
    <Card style={{ width: '18rem' }} className="book">
      <Card.Body>
        <Card.Title className="book-title">{bookname}</Card.Title>
        <div className="book-details">
          <div>Author: {author}</div>
          <div>Quantity: {quantity}</div>
          <div>Price: {price}</div>
          <div>Date: {new Date(date).toDateString()}</div>
        </div>
        <Button variant="primary">Edit</Button>{' '}
        <Button variant="danger" onClick={() => handleRemoveBook(id)}>
          Delete
        </Button>
      </Card.Body>
    </Card>
  );
};

export default Book;

تحديث مكوّن القائمة BooksList

import React from 'react';
import _ from 'lodash';
import Book from './Book';

const BooksList = ({ books, setBooks }) => {
  const handleRemoveBook = (id) => {
    setBooks(books.filter((book) => book.id !== id));
  };

  return (
    <React.Fragment>
      <div className="book-list">
        {!_.isEmpty(books) ? (
          books.map((book) => (
            <Book key={book.id} {...book} handleRemoveBook={handleRemoveBook} />
          ))
        ) : (
          <p className="message">No books available. Please add some books.</p>
        )}
      </div>
    </React.Fragment>
  );
};

export default BooksList;

تُستخدم الدالة filter() هنا لحذف الكتاب المطلوب عبر الاحتفاظ بكل العناصر التي لا تتطابق مع المعرّف id المحدد.

عرض قائمة الكتب المخزنة داخل واجهة تطبيق إدارة الكتب باستخدام Reactإضافة وحذف الكتب من واجهة تطبيق React مع تحديث localStorage مباشرة

إضافة ميزة تعديل الكتب

حتى تكتمل دورة CRUD، نحتاج إلى تعديل السجلات الموجودة.

تحديث زر التعديل داخل Book.js

import React from 'react';
import { Button, Card } from 'react-bootstrap';
import { useHistory } from 'react-router-dom';

const Book = ({ id, bookname, author, price, quantity, date, handleRemoveBook }) => {
  const history = useHistory();

  return (
    <Card style={{ width: '18rem' }} className="book">
      <Card.Body>
        <Card.Title className="book-title">{bookname}</Card.Title>
        <div className="book-details">
          <div>Author: {author}</div>
          <div>Quantity: {quantity}</div>
          <div>Price: {price}</div>
          <div>Date: {new Date(date).toDateString()}</div>
        </div>
        <Button variant="primary" onClick={() => history.push(`/edit/${id}`)}>
          Edit
        </Button>{' '}
        <Button variant="danger" onClick={() => handleRemoveBook(id)}>
          Delete
        </Button>
      </Card.Body>
    </Card>
  );
};

export default Book;

استخدمنا الخطاف useHistory للوصول إلى كائن history داخل مكوّن غير مُعرّف مباشرة داخل Route.

إنشاء مكوّن التعديل EditBook

import React from 'react';
import BookForm from './BookForm';
import { useParams } from 'react-router-dom';

const EditBook = ({ history, books, setBooks }) => {
  const { id } = useParams();
  const bookToEdit = books.find((book) => book.id === id);

  const handleOnSubmit = (book) => {
    const filteredBooks = books.filter((book) => book.id !== id);
    setBooks([book, ...filteredBooks]);
    history.push('/');
  };

  return (
    <div>
      <BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
    </div>
  );
};

export default EditBook;

إضافة مسار التعديل داخل AppRouter.js

import React from 'react';
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
import EditBook from '../components/EditBook';
import useLocalStorage from '../hooks/useLocalStorage';

const AppRouter = () => {
  const [books, setBooks] = useLocalStorage('books', []);

  return (
    <BrowserRouter>
      <div>
        <Header />
        <div className="main-content">
          <Switch>
            <Route
              render={(props) => (
                <BooksList {...props} books={books} setBooks={setBooks} />
              )}
              path="/"
              exact={true}
            />
            <Route
              render={(props) => (
                <AddBook {...props} books={books} setBooks={setBooks} />
              )}
              path="/add"
            />
            <Route
              render={(props) => (
                <EditBook {...props} books={books} setBooks={setBooks} />
              )}
              path="/edit/:id"
            />
            <Route component={() => <Redirect to="/" />} />
          </Switch>
        </div>
      </div>
    </BrowserRouter>
  );
};

export default AppRouter;

تعديل بيانات كتاب داخل تطبيق إدارة الكتب باستخدام React Router ومسار ديناميكي

كيف تعمل عملية التعديل؟

  1. عند الضغط على زر Edit يتم الانتقال إلى المسار /edit/:id.
  2. يُقرأ المعرّف من الرابط باستخدام useParams().
  3. يتم البحث عن الكتاب المطابق داخل مصفوفة الكتب عبر find().
  4. تُمرر البيانات إلى مكوّن BookForm ليُعرض النموذج بالقيم الحالية.
  5. بعد الحفظ، تُستبدل النسخة القديمة ببيانات الكتاب المعدلة.

تحسين الأداء باستخدام التهيئة الكسولة في useState

يمكن تحسين تعريف الحالة داخل BookForm بدلًا من تمرير الكائن مباشرة، من خلال استخدام دالة تُنفّذ مرة واحدة فقط عند تحميل المكوّن:

const [book, setBook] = useState(() => {
  return {
    bookname: props.book ? props.book.bookname : '',
    author: props.book ? props.book.author : '',
    quantity: props.book ? props.book.quantity : '',
    price: props.book ? props.book.price : '',
    date: props.book ? props.book.date : ''
  };
});

هذا الأسلوب مفيد عندما تكون عملية التهيئة غير بسيطة أو عند الحاجة لتجنب إعادة الحساب في كل عملية إعادة رسم re-render.

تبسيط تمرير البيانات باستخدام React Context API

مع توسع التطبيق، يصبح تمرير الخصائص من الموجّه إلى كل مكوّن أمرًا مرهقًا. وهنا يظهر دور Context API لتوفير طريقة مركزية لمشاركة البيانات بين المكوّنات.

إنشاء السياق BooksContext

import React from 'react';

const BooksContext = React.createContext();

export default BooksContext;

تغليف المكوّنات داخل Provider

import React from 'react';
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
import Header from '../components/Header';
import AddBook from '../components/AddBook';
import BooksList from '../components/BooksList';
import EditBook from '../components/EditBook';
import useLocalStorage from '../hooks/useLocalStorage';
import BooksContext from '../context/BooksContext';

const AppRouter = () => {
  const [books, setBooks] = useLocalStorage('books', []);

  return (
    <BrowserRouter>
      <div>
        <Header />
        <div className="main-content">
          <BooksContext.Provider value={{ books, setBooks }}>
            <Switch>
              <Route component={BooksList} path="/" exact={true} />
              <Route component={AddBook} path="/add" />
              <Route component={EditBook} path="/edit/:id" />
              <Route component={() => <Redirect to="/" />} />
            </Switch>
          </BooksContext.Provider>
        </div>
      </div>
    </BrowserRouter>
  );
};

export default AppRouter;

استخدام useContext داخل BooksList

import React, { useContext } from 'react';
import _ from 'lodash';
import Book from './Book';
import BooksContext from '../context/BooksContext';

const BooksList = () => {
  const { books, setBooks } = useContext(BooksContext);

  const handleRemoveBook = (id) => {
    setBooks(books.filter((book) => book.id !== id));
  };

  return (
    <React.Fragment>
      <div className="book-list">
        {!_.isEmpty(books) ? (
          books.map((book) => (
            <Book key={book.id} {...book} handleRemoveBook={handleRemoveBook} />
          ))
        ) : (
          <p className="message">No books available. Please add some books.</p>
        )}
      </div>
    </React.Fragment>
  );
};

export default BooksList;

استخدام useContext داخل AddBook

import React, { useContext } from 'react';
import BookForm from './BookForm';
import BooksContext from '../context/BooksContext';

const AddBook = ({ history }) => {
  const { books, setBooks } = useContext(BooksContext);

  const handleOnSubmit = (book) => {
    setBooks([book, ...books]);
    history.push('/');
  };

  return (
    <React.Fragment>
      <BookForm handleOnSubmit={handleOnSubmit} />
    </React.Fragment>
  );
};

export default AddBook;

استخدام useContext داخل EditBook

import React, { useContext } from 'react';
import BookForm from './BookForm';
import { useParams } from 'react-router-dom';
import BooksContext from '../context/BooksContext';

const EditBook = ({ history }) => {
  const { books, setBooks } = useContext(BooksContext);
  const { id } = useParams();
  const bookToEdit = books.find((book) => book.id === id);

  const handleOnSubmit = (book) => {
    const filteredBooks = books.filter((book) => book.id !== id);
    setBooks([book, ...filteredBooks]);
    history.push('/');
  };

  return (
    <div>
      <BookForm book={bookToEdit} handleOnSubmit={handleOnSubmit} />
    </div>
  );
};

export default EditBook;

بهذه الخطوة أصبح الكود أنظف وأكثر قابلية للصيانة، خصوصًا إذا زاد عدد الصفحات أو المكوّنات في المستقبل.

تدفق كامل لإضافة وتعديل وحذف الكتب باستخدام React Context API وlocalStorage

أفضل الممارسات التقنية في هذا المشروع

  • إعادة استخدام مكوّن BookForm للإضافة والتعديل بدل تكرار الكود.
  • عزل منطق التخزين المحلي داخل الخطاف useLocalStorage.
  • الاعتماد على Context API عندما تتكرر الحاجة إلى تمرير البيانات.
  • التحقق من صحة المدخلات قبل الحفظ لتحسين جودة البيانات.
  • استخدام معرفات فريدة عبر uuidv4() لتجنّب التعارض.

متى يكون هذا الأسلوب مناسبًا؟

هذا النمط مناسب جدًا عند بناء:

  • مشروعات تدريبية لتعلّم React.
  • تطبيقات صغيرة لا تحتاج إلى خادم خلفي في البداية.
  • نماذج أولية Prototype سريعة.
  • واجهات إدارة بسيطة تعتمد على التخزين المحلي.

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

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

هذا المشروع يقدّم مسارًا عمليًا متكاملًا لتعلّم بناء تطبيق CRUD في React بطريقة واضحة وقابلة للتطوير. الأهم من مجرد كتابة الكود هو فهم سبب استخدام كل أداة: فـReact Router ينظم التنقل، وlocalStorage يحافظ على البيانات، وCustom Hooks تقلل التكرار، وContext API تجعل البنية أنظف. إذا أتقنت هذا النمط، فستكون جاهزًا للانتقال إلى تطبيقات أكبر تستخدم APIs حقيقية وإدارة حالة أكثر تقدمًا.

اترك تعليقاً

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