كيفية بناء تطبيق تسجيل متعدد الخطوات مع انتقالات متحركة باستخدام مكدس MERN
مقدمة: لماذا يعد نموذج التسجيل متعدد الخطوات خياراً عملياً؟
يُعد نموذج التسجيل متعدد الخطوات من أفضل الأساليب لتحسين تجربة المستخدم عند جمع بيانات كثيرة نسبياً. بدلاً من عرض جميع الحقول دفعة واحدة، يتم تقسيم العملية إلى مراحل واضحة، مما يقلل التشتت ويرفع معدل الإكمال. في هذا الدليل سنبني تطبيق تسجيل احترافي باستخدام مكدس MERN الذي يضم MongoDB وExpress وReact وNode.js، مع إضافة انتقالات متحركة سلسة وتحقيق فعلي لواجهات REST API.
خلال الشرح ستتعلم كيف تدير بيانات عدة نماذج، وكيف تحتفظ بالقيم بين المسارات، وكيف تُحمّل الدول والمناطق والمدن ديناميكياً، وكيف تربط الواجهة الأمامية بالخلفية، مع تخزين كلمات المرور بشكل مشفر داخل قاعدة البيانات.

ما الذي ستتعلمه من هذا المشروع؟
- إدارة نماذج متعددة مع التحقق من الحقول.
- الاحتفاظ ببيانات المستخدم بين الصفحات باستخدام
React Router. - عرض شريط تقدم يوضح المرحلة الحالية.
- تحميل الدول والمناطق والمدن عبر مكتبة
country-state-city. - إضافة انتقالات انزلاقية باستخدام
framer-motion. - إنشاء واجهات
APIباستخدامExpress.js. - تنفيذ التسجيل وتسجيل الدخول مع
MongoDB. - تشفير كلمة المرور باستخدام
bcryptjs.
التهيئة الأولية للمشروع
سنبدأ بإنشاء مشروع React جديد عبر الأمر التالي:
npx create-react-app multi-step-form-using-mern
بعد إنشاء المشروع، احذف الملفات داخل مجلد src ثم أنشئ الملفات والمجلدات التالية:
- ملف
index.js - ملف
styles.scss - مجلد
components - مجلد
router - مجلد
utils
بعد ذلك ثبّت الاعتماديات الأساسية:
yarn add axios@0.21.1 bootstrap@4.6.0 react-bootstrap@1.5.0 country-state-city@2.0.0 framer-motion@3.7.0 node-sass@4.14.1 react-hook-form@6.15.4 react-router-dom@5.2.0 sweetalert2@10.15.5
اعتمادنا على SCSS هنا سيسهّل كتابة الأنماط بشكل أكثر تنظيماً وقابلية للتوسع.
بناء الصفحات الأساسية للتطبيق
إنشاء المكوّن Header
import React from 'react';
const Header = () => (
<div>
<h1>Multi Step Registration</h1>
</div>
);
export default Header;
إنشاء الخطوة الأولى FirstStep
import React from 'react';
const FirstStep = () => {
return <div>First Step Form</div>;
};
export default FirstStep;
إعداد الموجه AppRouter
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import FirstStep from '../components/FirstStep';
import Header from '../components/Header';
const AppRouter = () => (
<BrowserRouter>
<div className="container">
<Header />
<Switch>
<Route component={FirstStep} path="/" exact={true} />
</Switch>
</div>
</BrowserRouter>
);
export default AppRouter;
ثم أضف نقطة الدخول في ملف src/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 وستظهر الشاشة الأولى.

إضافة شريط التقدم في رأس الصفحة
لتوضيح موضع المستخدم داخل عملية التسجيل، أنشئ مكوّناً باسم Progress.js:
import React from 'react';
const Progress = () => {
return (
<React.Fragment>
<div className="steps">
<div className="step">
<div>1</div>
<div>Step 1</div>
</div>
<div className="step">
<div>2</div>
<div>Step 2</div>
</div>
<div className="step">
<div>3</div>
<div>Step 3</div>
</div>
</div>
</React.Fragment>
);
};
export default Progress;
ثم استدعِه داخل Header.js:
import React from 'react';
import Progress from './Progress';
const Header = () => (
<div>
<h1>Multi Step Registration</h1>
<Progress />
</div>
);
export default Header;

بناء الخطوة الأولى مع التحقق من المدخلات
سنستخدم مكتبة react-hook-form لأنها تقلل الحاجة إلى إدارة حالة كل حقل يدوياً، وتوفر آلية تحقق مرنة وواضحة.
import React from 'react';
import { useForm } from 'react-hook-form';
import { Form, Button } from 'react-bootstrap';
const FirstStep = (props) => {
const { register, handleSubmit, errors } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<Form className="input-form" onSubmit={handleSubmit(onSubmit)}>
<div className="col-md-6 offset-md-3">
<Form.Group controlId="first_name">
<Form.Label>First Name</Form.Label>
<Form.Control
type="text"
name="first_name"
placeholder="Enter your first name"
autoComplete="off"
ref={register({
required: 'First name is required.',
pattern: {
value: /^[a-zA-Z]+$/,
message: 'First name should contain only characters.'
}
})}
className={`${errors.first_name ? 'input-error' : ''}`}
/>
{errors.first_name && <p className="errorMsg">{errors.first_name.message}</p>}
</Form.Group>
<Form.Group controlId="last_name">
<Form.Label>Last Name</Form.Label>
<Form.Control
type="text"
name="last_name"
placeholder="Enter your last name"
autoComplete="off"
ref={register({
required: 'Last name is required.',
pattern: {
value: /^[a-zA-Z]+$/,
message: 'Last name should contain only characters.'
}
})}
className={`${errors.last_name ? 'input-error' : ''}`}
/>
{errors.last_name && <p className="errorMsg">{errors.last_name.message}</p>}
</Form.Group>
<Button variant="primary" type="submit">Next</Button>
</div>
</Form>
);
};
export default FirstStep;
يعتمد هذا النموذج على الدالة useForm() التي توفر:
registerلربط الحقول بالنموذج.handleSubmitلمعالجة الإرسال.errorsللوصول إلى أخطاء التحقق.
عند وجود خطأ في الحقل، يتم عرضه مباشرة من الكائن errors. هذه المقاربة تمنحنا كوداً أنظف، خصوصاً عندما يكبر المشروع.

إنشاء الخطوة الثانية وإضافة التنقل البرمجي
الخطوة الثانية تجمع البريد الإلكتروني وكلمة المرور:
import React from 'react';
import { useForm } from 'react-hook-form';
import { Form, Button } from 'react-bootstrap';
const SecondStep = (props) => {
const { register, handleSubmit, errors } = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<Form className="input-form" onSubmit={handleSubmit(onSubmit)}>
<div className="col-md-6 offset-md-3">
<Form.Group controlId="first_name">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
name="user_email"
placeholder="Enter your email address"
autoComplete="off"
ref={register({
required: 'Email is required.',
pattern: {
value: /^[^@ ]+@[^@ ]+\.[^@.]{2,}$/,
message: 'Email is not valid.'
}
})}
className={`${errors.user_email ? 'input-error' : ''}`}
/>
{errors.user_email && <p className="errorMsg">{errors.user_email.message}</p>}
</Form.Group>
<Form.Group controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
name="user_password"
placeholder="Choose a password"
autoComplete="off"
ref={register({
required: 'Password is required.',
minLength: {
value: 6,
message: 'Password should have at-least 6 characters.'
}
})}
className={`${errors.user_password ? 'input-error' : ''}`}
/>
{errors.user_password && <p className="errorMsg">{errors.user_password.message}</p>}
</Form.Group>
<Button variant="primary" type="submit">Next</Button>
</div>
</Form>
);
};
export default SecondStep;
بعدها أضف المسار الجديد في AppRouter.js:
import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import FirstStep from '../components/FirstStep';
import Header from '../components/Header';
import SecondStep from '../components/SecondStep';
const AppRouter = () => (
<BrowserRouter>
<div className="container">
<Header />
<Switch>
<Route component={FirstStep} path="/" exact={true} />
<Route component={SecondStep} path="/second" />
</Switch>
</div>
</BrowserRouter>
);
export default AppRouter;

بدلاً من الانتقال يدوياً إلى المسار /second، يمكننا استخدام الكائن history الذي يوفره React Router تلقائياً:
const onSubmit = (data) => {
console.log(data);
props.history.push('/second');
};

إعداد ثابت عنوان الواجهة الخلفية
أنشئ ملف constants.js داخل المجلد utils:
export const BASE_API_URL = 'http://localhost:3030';
هذا الأسلوب يقلل التكرار ويجعل تعديل عنوان الخادم لاحقاً أسهل بكثير.
بناء الخطوة الثالثة وتحميل الدول من الواجهة البرمجية
الآن نصل إلى مرحلة أكثر تفاعلاً، حيث سنحمل الدول ثم المناطق فالمدن تبعاً لاختيار المستخدم.
import React, { useState, useEffect } from 'react';
import { Form, Button } from 'react-bootstrap';
import csc from 'country-state-city';
import axios from 'axios';
import { BASE_API_URL } from '../utils/constants';
const ThirdStep = (props) => {
const [countries, setCountries] = useState([]);
const [states, setStates] = useState([]);
const [cities, setCities] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [selectedCountry, setSelectedCountry] = useState('');
const [selectedState, setSelectedState] = useState('');
const [selectedCity, setSelectedCity] = useState('');
useEffect(() => {
const getCountries = async () => {
try {
const result = await csc.getAllCountries();
console.log(result);
} catch (error) {}
};
getCountries();
}, []);
const handleSubmit = async (event) => {
event.preventDefault();
};
return (
<Form className="input-form" onSubmit={handleSubmit}>
<div className="col-md-6 offset-md-3"></div>
</Form>
);
};
export default ThirdStep;
بعد ذلك عدّل useEffect ليقوم بتصفية البيانات المطلوبة فقط:
useEffect(() => {
const getCountries = async () => {
try {
setIsLoading(true);
const result = await csc.getAllCountries();
let allCountries = [];
allCountries = result?.map(({ isoCode, name }) => ({ isoCode, name }));
const [{ isoCode: firstCountry } = {}] = allCountries;
setCountries(allCountries);
setSelectedCountry(firstCountry);
setIsLoading(false);
} catch (error) {
setCountries([]);
setIsLoading(false);
}
};
getCountries();
}, []);
هنا استخدمنا العامل ?. المعروف باسم optional chaining حتى لا يتسبب الوصول إلى بيانات غير معرفة في ظهور خطأ وقت التنفيذ.
ثم أضف حقل اختيار الدولة:
return (
<Form className="input-form" onSubmit={handleSubmit}>
<div className="col-md-6 offset-md-3">
<Form.Group controlId="country">
{isLoading && <p className="loading">Loading countries. Please wait...</p>}
<Form.Label>Country</Form.Label>
<Form.Control
as="select"
name="country"
value={selectedCountry}
onChange={(event) => setSelectedCountry(event.target.value)}
>
{countries.map(({ isoCode, name }) => (
<option value={isoCode} key={isoCode}>{name}</option>
))}
</Form.Control>
</Form.Group>
</div>
</Form>
);

تحميل المناطق حسب الدولة المختارة
أضف تأثيراً جديداً يعتمد على تغير قيمة selectedCountry:
useEffect(() => {
const getStates = async () => {
try {
const result = await csc.getStatesOfCountry(selectedCountry);
let allStates = [];
allStates = result?.map(({ isoCode, name }) => ({ isoCode, name }));
const [{ isoCode: firstState = '' } = {}] = allStates;
setCities([]);
setSelectedCity('');
setStates(allStates);
setSelectedState(firstState);
} catch (error) {
setStates([]);
setCities([]);
setSelectedCity('');
}
};
getStates();
}, [selectedCountry]);
ثم اعرض قائمة المناطق:
<Form.Group controlId="state">
<Form.Label>State</Form.Label>
<Form.Control
as="select"
name="state"
value={selectedState}
onChange={(event) => setSelectedState(event.target.value)}
>
{states.length > 0 ? (
states.map(({ isoCode, name }) => (
<option value={isoCode} key={isoCode}>{name}</option>
))
) : (
<option value="" key="">No state found</option>
)}
</Form.Control>
</Form.Group>

تحميل المدن حسب الدولة والمنطقة
الخطوة التالية هي تحميل المدن وفق اختيار الدولة والمنطقة:
useEffect(() => {
const getCities = async () => {
try {
const result = await csc.getCitiesOfState(selectedCountry, selectedState);
let allCities = [];
allCities = result?.map(({ name }) => ({ name }));
const [{ name: firstCity = '' } = {}] = allCities;
setCities(allCities);
setSelectedCity(firstCity);
} catch (error) {
setCities([]);
}
};
getCities();
}, [selectedState]);
ثم اعرض حقل المدن:
<Form.Group controlId="city">
<Form.Label>City</Form.Label>
<Form.Control
as="select"
name="city"
value={selectedCity}
onChange={(event) => setSelectedCity(event.target.value)}
>
{cities.length > 0 ? (
cities.map(({ name }) => (
<option value={name} key={name}>{name}</option>
))
) : (
<option value="">No cities found</option>
)}
</Form.Control>
</Form.Group>
<Button variant="primary" type="submit">Register</Button>


جعل شريط التقدم متفاعلاً مع المسارات
لأن المكوّن Progress ليس مرتبطاً مباشرة بمسار داخل Route، فلن يستقبل خصائص مثل history وlocation افتراضياً. الحل هو استخدام withRouter.
import React from 'react';
import { Link, withRouter } from 'react-router-dom';
const Progress = ({ location: { pathname } }) => {
const isFirstStep = pathname === '/';
const isSecondStep = pathname === '/second';
const isThirdStep = pathname === '/third';
return (
<React.Fragment>
<div className="steps">
<div className={`${isFirstStep ? 'step active' : 'step'}`}>
<div>1</div>
<div>
{isSecondStep || isThirdStep ? <Link to="/">Step 1</Link> : 'Step 1'}
</div>
</div>
<div className={`${isSecondStep ? 'step active' : 'step'}`}>
<div>2</div>
<div>
{isThirdStep ? <Link to="/second">Step 2</Link> : 'Step 2'}
</div>
</div>
<div className={`${pathname === '/third' ? 'step active' : 'step'}`}>
<div>3</div>
<div>Step 3</div>
</div>
</div>
</React.Fragment>
);
};
export default withRouter(Progress);

الاحتفاظ ببيانات المستخدم بين الخطوات
عند الانتقال بين المسارات، يقوم React Router بإزالة المكوّن السابق وتركيب التالي من جديد، ما يؤدي إلى فقدان الحالة المحلية. أفضل مكان لتخزين البيانات المشتركة هنا هو المكوّن AppRouter.
const [user, setUser] = useState({});
const updateUser = (data) => {
setUser((prevUser) => ({ ...prevUser, ...data }));
};
const resetUser = () => {
setUser({});
};
تمتزج القيم الجديدة مع القيم السابقة عبر عامل النشر .... ثم نمرر هذه البيانات إلى المسارات باستخدام الخاصية render بدلاً من component:
<Route
render={(props) => (
<FirstStep {...props} user={user} updateUser={updateUser} />
)}
path="/"
exact={true}
/>
وعند الإرسال داخل الخطوتين الأولى والثانية:
// FirstStep.js
const onSubmit = (data) => {
props.updateUser(data);
props.history.push('/second');
};
// SecondStep.js
const onSubmit = (data) => {
props.updateUser(data);
props.history.push('/third');
};
لاسترجاع القيم تلقائياً عند العودة، استخدم defaultValues داخل useForm:
const { user } = props;
const { register, handleSubmit, errors } = useForm({
defaultValues: {
first_name: user.first_name,
last_name: user.last_name
}
});
وفي الخطوة الثانية:
const { user } = props;
const { register, handleSubmit, errors } = useForm({
defaultValues: {
user_email: user.user_email,
user_password: user.user_password
}
});

إضافة انتقالات متحركة سلسة باستخدام framer-motion
لإضفاء لمسة احترافية على تجربة الاستخدام، سنغلف الحاوية بـ motion.div:
import { motion } from 'framer-motion';
<motion.div
className="col-md-6 offset-md-3"
initial={{ x: '-100vw' }}
animate={{ x: 0 }}
transition={{ stiffness: 150 }}
>
...
</motion.div>
تعمل الخاصية initial على تحديد نقطة البداية، بينما تحدد animate الوجهة النهائية. أما transition فتتحكم في سلاسة الحركة.

إعداد الواجهة الخلفية باستخدام Node.js وExpress
أنشئ مجلداً باسم server خارج src، ثم أنشئ داخله مجلدين:
modelsrouters
ثم نفّذ:
yarn init -y
وثبّت الحزم المطلوبة:
yarn add bcryptjs@2.4.3 cors@2.8.5 express@4.17.1 mongoose@5.11.18 nodemon@2.0.7
أضف ملف .gitignore داخل server:
node_modules
الاتصال بقاعدة البيانات
const mongoose = require('mongoose');
mongoose.connect('mongodb://127.0.0.1:27017/form-user', {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
});
ملف الخادم الرئيسي
const express = require('express');
require('./db');
const app = express();
const PORT = process.env.PORT || 3030;
app.get('/', (req, res) => {
res.send('<h2>This is from index.js file</h2>');
});
app.listen(PORT, () => {
console.log(`server started on port ${PORT}`);
});
وأضف في server/package.json:
"scripts": {
"start": "nodemon index.js"
}

إنشاء نموذج المستخدم في MongoDB
const mongoose = require('mongoose');
const userSchema = mongoose.Schema({
first_name: { type: String, required: true, trim: true },
last_name: { type: String, required: true, trim: true },
user_email: {
type: String,
required: true,
trim: true,
validate(value) {
if (!value.match(/^[^@ ]+@[^@ ]+\.[^@ .]{2,}$/)) {
throw new Error('Email is not valid.');
}
}
},
user_password: { type: String, required: true, trim: true, minlength: 6 },
country: { type: String, required: true, trim: true },
state: { type: String, trim: true },
city: { type: String, trim: true }
}, { timestamps: true });
const User = mongoose.model('User', userSchema);
module.exports = User;
إنشاء واجهة التسجيل /register
const express = require('express');
const User = require('../models/user');
const bcrypt = require('bcryptjs');
const router = express.Router();
router.post('/register', async (req, res) => {
const { user_email, user_password } = req.body;
let user = await User.findOne({ user_email });
if (user) {
return res.status(400).send('User with the provided email already exist.');
}
try {
user = new User(req.body);
user.user_password = await bcrypt.hash(user_password, 8);
await user.save();
res.status(201).send();
} catch (e) {
res.status(500).send('Something went wrong. Try again later.');
}
});
module.exports = router;
في هذا المسار:
- نتحقق من وجود البريد الإلكتروني مسبقاً.
- ننشئ مستخدماً جديداً من البيانات الواردة في
req.body. - نشفّر كلمة المرور عبر
bcrypt.hash(). - نحفظ السجل داخل قاعدة البيانات.
ربط الراوتر بالخادم
const express = require('express');
const userRouter = require('./routers/user');
require('./db');
const app = express();
const PORT = process.env.PORT || 3030;
app.use(express.json());
app.use(userRouter);
app.get('/', (req, res) => {
res.send('<h2>This is from index.js file</h2>');
});
app.listen(PORT, () => {
console.log(`server started on port ${PORT}`);
});
إرسال بيانات التسجيل من الواجهة الأمامية
حدّث الدالة handleSubmit في ThirdStep.js لتستدعي واجهة التسجيل:
const handleSubmit = async (event) => {
event.preventDefault();
try {
const { user } = props;
const updatedData = {
country: countries.find((country) => country.isoCode === selectedCountry)?.name,
state: states.find((state) => state.isoCode === selectedState)?.name || '',
city: selectedCity
};
await axios.post(`${BASE_API_URL}/register`, { ...user, ...updatedData });
} catch (error) {
if (error.response) {
console.log('error', error.response.data);
}
}
};
يتم هنا تحويل isoCode إلى الاسم الحقيقي للدولة أو المنطقة قبل الإرسال، لأن تخزين الاسم المفهوم في قاعدة البيانات أنسب من تخزين الرمز فقط.
حل مشكلة CORS أثناء التطوير
بسبب تشغيل React على المنفذ 3000 والخادم على 3030، سيمنع المتصفح الطلبات عبر النطاق المختلف ما لم نفعّل CORS.
const cors = require('cors');
app.use(express.json());
app.use(cors());
app.use(userRouter);


إظهار رسائل النجاح والخطأ بعد التسجيل
لتحسين تجربة المستخدم، سنستخدم مكتبة sweetalert2 لعرض نوافذ منبثقة واضحة:
import Swal from 'sweetalert2';
const handleSubmit = async (event) => {
event.preventDefault();
try {
const { user } = props;
const updatedData = {
country: countries.find((country) => country.isoCode === selectedCountry)?.name,
state: states.find((state) => state.isoCode === selectedState)?.name || '',
city: selectedCity
};
await axios.post(`${BASE_API_URL}/register`, { ...user, ...updatedData });
Swal.fire('Awesome!', "You're successfully registered!", 'success').then((result) => {
if (result.isConfirmed || result.isDismissed) {
props.history.push('/');
}
});
} catch (error) {
if (error.response) {
Swal.fire({
icon: 'error',
title: 'Oops...',
text: error.response.data
});
}
}
};
وإذا أردت إعادة تعيين حالة المستخدم بعد نجاح التسجيل، مرّر الدالة resetUser إلى الخطوة الثالثة ثم استدعها قبل إعادة التوجيه.

إضافة وظيفة تسجيل الدخول
في الخلفية، أضف المسار /login:
router.post('/login', async (req, res) => {
try {
const user = await User.findOne({ user_email: req.body.user_email });
if (!user) {
return res.status(400).send('User with provided email does not exist.');
}
const isMatch = await bcrypt.compare(req.body.user_password, user.user_password);
if (!isMatch) {
return res.status(400).send('Invalid credentials.');
}
const { user_password, ...rest } = user.toObject();
return res.send(rest);
} catch (error) {
return res.status(500).send('Something went wrong. Try again later.');
}
});
في هذا المسار نقارن كلمة المرور المدخلة بالقيمة المشفرة عبر bcrypt.compare()، ثم نعيد بيانات المستخدم بدون الحقل user_password لأسباب أمنية.
واجهة تسجيل الدخول في React
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { Form, Button } from 'react-bootstrap';
import axios from 'axios';
import { BASE_API_URL } from '../utils/constants';
const Login = () => {
const { register, handleSubmit, errors } = useForm();
const [successMessage, setSuccessMessage] = useState('');
const [errorMessage, setErrorMessage] = useState('');
const [userDetails, setUserDetails] = useState('');
const onSubmit = async (data) => {
try {
const response = await axios.post(`${BASE_API_URL}/login`, data);
setSuccessMessage('User with the provided credentials found.');
setErrorMessage('');
setUserDetails(response.data);
} catch (error) {
if (error.response) {
setErrorMessage(error.response.data);
}
}
};
return (
<Form className="input-form" onSubmit={handleSubmit(onSubmit)}>
<div className="col-md-6 offset-md-3">
{errorMessage ? (
<p className="errorMsg login-error">{errorMessage}</p>
) : (
<div>
<p className="successMsg">{successMessage}</p>
{userDetails && (
<div className="user-details">
<p>Following are the user details:</p>
<div>First name: {userDetails.first_name}</div>
<div>Last name: {userDetails.last_name}</div>
<div>Email: {userDetails.user_email}</div>
<div>Country: {userDetails.country}</div>
<div>State: {userDetails.state}</div>
<div>City: {userDetails.city}</div>
</div>
)}
</div>
)}
<Form.Group controlId="first_name">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
name="user_email"
placeholder="Enter your email address"
ref={register({
required: 'Email is required.',
pattern: {
value: /^[^@ ]+@[^@ ]+\.[^@.]{2,}$/,
message: 'Email is not valid.'
}
})}
className={`${errors.user_email ? 'input-error' : ''}`}
/>
</Form.Group>
<Form.Group controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
name="user_password"
placeholder="Choose a password"
ref={register({
required: 'Password is required.',
minLength: {
value: 6,
message: 'Password should have at-least 6 characters.'
}
})}
className={`${errors.user_password ? 'input-error' : ''}`}
/>
</Form.Group>
<Button variant="primary" type="submit">Check Login</Button>
</div>
</Form>
);
};
export default Login;

إخفاء شريط الخطوات في صفحة تسجيل الدخول
يمكن إخفاء شريط التقدم إذا كان المسار الحالي هو /login:
const isLoginPage = pathname === '/login';
<React.Fragment>
{!isLoginPage ? (
<div className="steps">...</div>
) : (
<div></div>
)}
</React.Fragment>

معالجة المسارات غير الصحيحة
لمنع ظهور صفحة فارغة عند إدخال مسار غير موجود، أضف مساراً أخيراً يعيد التوجيه:
import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom';
<Route component={Login} path="/login" />
<Route render={() => <Redirect to="/" />} />
من المهم أن يكون هذا المسار في نهاية القائمة حتى لا يسبق المسارات الصحيحة.

تشغيل التطبيق من خادم واحد والتخلص من CORS
في بيئة الإنتاج، من الأفضل أن يقدم خادم Express ملفات React المبنية من مجلد build مباشرة:
const path = require('path');
const express = require('express');
const userRouter = require('./routers/user');
require('./db');
const app = express();
const PORT = process.env.PORT || 3030;
app.use(express.static(path.join(__dirname, '..', 'build')));
app.use(express.json());
app.use(userRouter);
app.get('/', (req, res) => {
res.send('<h2>This is from index.js file</h2>');
});
app.use((req, res, next) => {
res.sendFile(path.join(__dirname, '..', 'build', 'index.html'));
});
app.listen(PORT, () => {
console.log(`server started on port ${PORT}`);
});
ثم عدّل الملف src/utils/constants.js ليصبح:
export const BASE_API_URL = '';
بعد ذلك نفّذ أمر البناء من جذر المشروع:
yarn build
ثم شغّل الخادم من داخل مجلد server:
yarn start
بهذا يصبح التطبيق والواجهات البرمجية كلاهما متاحين من نفس الأصل origin، فلا تعود بحاجة إلى CORS في هذا السيناريو.

أفضل تحسينات مقترحة قبل النشر
- إضافة تحقق يمنع تجاوز الخطوات مباشرة عبر كتابة المسار يدوياً.
- إضافة رسائل تحميل أثناء جلب الدول والمناطق والمدن.
- ربط الواجهة بآلية مصادقة حقيقية عند الحاجة.
- تحسين رسائل الخطأ لتكون أكثر وضوحاً للمستخدم النهائي.
- إضافة اختبارات وحدات واختبارات تكامل لأهم المسارات.
الخلاصة التقنية
هذا المشروع مثال ممتاز على كيفية بناء تجربة تسجيل حديثة وقابلة للتوسع باستخدام MERN. على مستوى الواجهة الأمامية، قدم react-hook-form إدارة مرنة للنماذج، بينما ساعد framer-motion في تحسين الإحساس البصري والانتقال بين الخطوات. وعلى مستوى الخادم، وفر Express وMongoDB بنية واضحة لتخزين البيانات والتحقق منها، مع تعزيز الأمان من خلال تشفير كلمات المرور باستخدام bcryptjs. إذا أردت بناء تطبيقات تسجيل احترافية قابلة للنشر الفعلي، فهذا النمط المعماري يُعد نقطة انطلاق قوية وعملية.