4 أخطاء شائعة في React قد ترتكبها الآن وكيف تعالجها باحتراف
عند تطوير تطبيقات React، قد تبدو بعض الأنماط البرمجية صحيحة ظاهرياً، لكنها مع الوقت تؤدي إلى شيفرة أصعب في الصيانة، وأكثر عرضة للأخطاء، وأقل وضوحاً عند التوسّع. في هذا المقال سنستعرض أربعة أخطاء شائعة يقع فيها كثير من المطورين، مع توضيح السبب التقني وراء كل خطأ، ثم تقديم الطريقة الصحيحة لمعالجته بأسلوب عملي ومنظم.
الهدف هنا ليس فقط إصلاح المشكلة الحالية، بل بناء عقلية أفضل في كتابة مكونات React بطريقة نظيفة وقابلة للتطوير.

1. لا تمرر الحالة الحالية إلى setState عندما يمكنك الاعتماد على الحالة السابقة
في المثال التالي لدينا تطبيق مهام بسيط يعرض قائمة من العناصر داخل المكوّن TodoList، كما يتيح إضافة عنصر جديد من خلال المكوّن AddTodo. يتم تحديث مصفوفة todos في المكوّن الرئيسي App.
export default function App() {
const [todos, setTodos] = React.useState([]);
return (
<div>
<h1>Todo List</h1>
<TodoList todos={todos} />
<AddTodo setTodos={setTodos} todos={todos} />
</div>
);
}
function AddTodo({ setTodos, todos }) {
function handleAddTodo(event) {
event.preventDefault();
const text = event.target.elements.addTodo.value;
const todo = {
id: 4,
text,
done: false
};
const newTodos = todos.concat(todo);
setTodos(newTodos);
}
return (
<form onSubmit={handleAddTodo}>
<input name="addTodo" placeholder="Add todo" />
<button type="submit">Submit</button>
</form>
);
}
المشكلة هنا أن المكوّن AddTodo يستقبل كلاً من setTodos وtodos، رغم أن التحديث الجديد يعتمد فقط على الحالة السابقة. وهذا يعني أننا نمرر بيانات إضافية لا حاجة فعلية لها، مما يزيد الارتباط بين المكونات ويجعل الشيفرة أقل مرونة.
لماذا يُعد هذا خطأ؟
- يزيد عدد
propsبلا داعٍ. - يربط المكوّن الفرعي مباشرة ببنية الحالة الحالية.
- قد يسبب مشكلات عند تنفيذ تحديثات متزامنة أو متكررة للحالة.
الحل الأفضل هو استخدام التحديث الوظيفي داخل setTodos، حيث تحصل على الحالة السابقة مباشرة من React، ثم تبني عليها الحالة الجديدة.
export default function App() {
const [todos, setTodos] = React.useState([]);
return (
<div>
<h1>Todo List</h1>
<TodoList todos={todos} />
<AddTodo setTodos={setTodos} />
</div>
);
}
function AddTodo({ setTodos }) {
function handleAddTodo(event) {
event.preventDefault();
const text = event.target.elements.addTodo.value;
const todo = {
id: 4,
text,
done: false
};
setTodos(prevTodos => prevTodos.concat(todo));
}
return (
<form onSubmit={handleAddTodo}>
<input name="addTodo" placeholder="Add todo" />
<button type="submit">Submit</button>
</form>
);
}
الفائدة العملية من هذا الأسلوب
- تقليل تمرير البيانات غير الضرورية بين المكونات.
- جعل التحديثات أكثر أماناً عند الاعتماد على الحالة السابقة.
- تحسين قابلية إعادة استخدام المكوّن الفرعي.
2. اجعل كل مكوّن في React مسؤولاً عن مهمة واحدة فقط
من الأخطاء المنتشرة أن يقوم المكوّن الواحد بجلب البيانات من الخادم، وإدارة الحالة، ثم عرض الواجهة في الوقت نفسه. قد يعمل هذا الأسلوب في البداية، لكنه يصبح مرهقاً جداً كلما كبر التطبيق.
لننظر إلى المثال التالي:
export default function App() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((data) => {
setUsers(data);
});
}, []);
return (
<>
<h1>Users</h1>
{users.map((user) => (
<div key={user.id}>
<div>{user.name}</div>
</div>
))}
</>
);
}
المكوّن App هنا يؤدي أكثر من وظيفة:
- يجلب البيانات عبر
fetch(). - يدير الحالة باستخدام
useState. - يعرض عناصر الواجهة باستخدام
JSX.
هذا يخالف مبدأ المسؤولية الواحدة، وهو أحد أهم المبادئ التصميمية التي تساعد على بناء تطبيقات أكثر وضوحاً واعتمادية.
كيف نعيد تنظيم الشيفرة؟
يمكن فصل منطق جلب البيانات داخل custom hook مستقل، وليكن اسمه useUserData، بحيث يتولى فقط استرجاع البيانات وتخزينها في الحالة المحلية.
function useUserData() {
const [users, setUsers] = React.useState([]);
React.useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/users")
.then((res) => res.json())
.then((json) => {
setUsers(json);
});
}, []);
return users;
}
بعد ذلك يمكن إنشاء مكوّن مستقل باسم User تكون مهمته الوحيدة عرض بيانات المستخدم.
function User({ user }) {
const styles = {
container: {
margin: '0 auto',
textAlign: 'center'
}
};
return (
<div style={styles.container}>
<div>{user.name}</div>
</div>
);
}
export default function App() {
const users = useUserData();
return (
<>
<h1>Users</h1>
{users.map((user) => (
<User key={user.id} user={user} />
))}
</>
);
}
لماذا هذا التقسيم أفضل؟
- كل جزء من الشيفرة له دور واضح ومحدد.
- يسهل اختبار المكونات والخطافات
hooks. - تصبح إعادة الاستخدام أسهل في أجزاء أخرى من التطبيق.
- يصبح التعديل المستقبلي أقل تكلفة وأقل خطورة.
3. اجعل التأثيرات الجانبية Side Effects منفصلة حسب المسؤولية
كثير من المطورين يجمعون كل التأثيرات الجانبية داخل useEffect واحد. ورغم أن ذلك قد يبدو عملياً، إلا أنه يؤدي غالباً إلى استدعاءات غير ضرورية، وسلوك يصعب تتبعه لاحقاً.
في المثال التالي، يتم جلب بيانات المستخدم الموثق وبيانات المنشور عند تغيّر المسار الحالي للتطبيق:
export default function App() {
const location = useLocation();
function getAuthUser() {
// fetches authenticated user
}
function getPostData() {
// fetches post data
}
React.useEffect(() => {
getAuthUser();
getPostData();
}, [location.pathname]);
return (
<main>
<Navbar />
<Post />
</main>
);
}
هنا يتم تنفيذ getAuthUser() كلما تغيّر location.pathname، رغم أن بيانات المستخدم الموثق لا تحتاج عادة إلى إعادة جلب مع كل تغيير في الرابط.
المشكلة التقنية
- تنفيذ عمليات لا نحتاجها فعلياً.
- زيادة عدد الطلبات المرسلة إلى الخادم.
- تعقيد منطق التأثيرات الجانبية داخل المكوّن.
الحل هو فصل كل تأثير جانبي في useEffect مستقل وفق سبب تشغيله.
export default function App() {
const location = useLocation();
React.useEffect(() => {
getAuthUser();
}, []);
React.useEffect(() => {
getPostData();
}, [location.pathname]);
return (
<main>
<Navbar />
<Post />
</main>
);
}
ما الذي كسبناه بعد الفصل؟
- تحميل بيانات المستخدم مرة واحدة فقط عند تحميل المكوّن.
- تحديث بيانات المنشور فقط عند تغيّر الرابط.
- منطق أوضح وأسهل للفهم والصيانة.
القاعدة الذهبية هنا: إذا اختلف سبب تشغيل التأثير، فالأفضل غالباً أن يكون في useEffect منفصل.
4. استخدم العامل الثلاثي بدلاً من && داخل JSX عند الحاجة
من الشائع في React استخدام العامل && للعرض الشرطي، لكنه ليس آمناً دائماً، خاصة عند التعامل مع أرقام أو قيم قد تُعرض للمستخدم دون قصد.
لنأخذ المثال التالي الذي يعرض قائمة منشورات داخل المكوّن PostList:
export default function PostList({ posts }) {
return (
<div>
<ul>
{posts.length && posts.map((post) => (
<PostItem key={post.id} post={post} />
))}
</ul>
</div>
);
}
للوهلة الأولى يبدو هذا صحيحاً: إذا كانت قيمة posts.length أكبر من الصفر، يتم تنفيذ map(). لكن إذا كانت المصفوفة فارغة، فإن القيمة ستكون 0، وبما أن 0 قيمة falsy في JavaScript، فسيُرجع التعبير الرقم 0 نفسه، وقد يظهر في الواجهة.
لماذا يحدث هذا السلوك؟
العامل && لا يُرجع دائماً true أو false، بل يُرجع أحد طرفي التعبير. وعندما تكون الجهة اليسرى هي 0، فهذه هي النتيجة التي تصل إلى الواجهة.
الحل الأفضل
استخدم العامل الثلاثي ternary لتحديد ما يجب عرضه بشكل صريح. وإذا لم يتحقق الشرط، أعد القيمة null حتى لا يظهر شيء.
export default function PostList({ posts }) {
return (
<div>
<ul>
{posts.length
? posts.map((post) => (
<PostItem key={post.id} post={post} />
))
: null}
</ul>
</div>
);
}
متى يكون العامل الثلاثي أفضل؟
- عندما تريد تحكماً صريحاً في المخرجات.
- عندما تكون القيمة الشرطية رقمية مثل
length. - عندما تريد تجنب ظهور قيم غير مقصودة مثل
0.
أفضل الممارسات المستفادة من الأخطاء الأربعة
- اعتمد على الحالة السابقة داخل
setStateعندما يكون التحديث مبنياً عليها. - صمّم كل مكوّن ليؤدي وظيفة واحدة فقط.
- افصل التأثيرات الجانبية حسب سبب التنفيذ وليس حسب القرب المكاني في الشيفرة.
- كن واضحاً في العرض الشرطي داخل
JSX، ولا تفترض أن&&مناسب في كل الحالات.
الخلاصة التقنية
الفرق بين تطبيق React عادي وآخر احترافي لا يكمن فقط في النتيجة النهائية، بل في جودة القرارات الصغيرة داخل الشيفرة. تقليل تمرير props غير الضرورية، والالتزام بمبدأ المسؤولية الواحدة، وفصل side effects، واختيار أدوات العرض الشرطي المناسبة، كلها ممارسات تجعل التطبيق أكثر استقراراً وأسهل في التطوير مستقبلاً. إذا طبّقت هذه المبادئ باستمرار، فستحصل على قاعدة شيفرة أنظف، وأقل أخطاء، وأفضل أداء على المدى الطويل.