DEV Community

khaled-17
khaled-17

Posted on

Stop Unnecessary Re-Renders in React: The Real Cost of Inline Arrow Functions Inside JSX

🧠 “My component re-renders even though nothing has changed... WHY?”

If you've asked this question while working with React, you’re not alone.

In this article, we’ll walk through a real performance issue caused by using arrow functions inside JSX. You’ll learn:

  • ✅ Why it's a bad practice
  • ✅ How it affects React.memo, useCallback, and re-renders
  • ✅ A working demo that proves the impact
  • ✅ Clean code patterns that make you look like a React pro

🔍 The Problem: New Function on Every Render

When you define an arrow function directly inside JSX, like this:

<button onClick={() => console.log("Clicked")}>Click</button>
Enter fullscreen mode Exit fullscreen mode

You're actually creating a brand new function every time the component re-renders.

Even if the logic inside is exactly the same — the function identity is different.
That means React.memo can’t help you.
That means your child components might re-render unnecessarily.
That means your app is doing more work than it should.


🔬 The Test Case: Counter + Child Component

Let’s build a small project to visualize this problem.

👇 The Setup:

  • A parent with a counter (useState)
  • A child that receives a handleClick function
  • We'll test with React.memo to see if the child re-renders

💣 Problematic Code:

import React, { useState, memo } from "react";

const Child = memo(({ onClick }) => {
  console.log("🔄 Child rendered");
  return <button onClick={onClick}>Click Me</button>;
});

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Count: {count}</h1>

      <button onClick={() => setCount(count + 1)}>Increase</button>

      {/* ❌ This function is re-created on every render */}
      <Child onClick={() => console.log("Child Clicked")} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🧨 Every time the "Increase" button is clicked, the Child component re-renders... even though its content didn’t change.


✅ The Clean Fix: Define Handlers Outside JSX

Here’s the correct pattern:

import React, { useState, memo } from "react";

const Child = memo(({ onClick }) => {
  console.log("🔄 Child rendered");
  return <button onClick={onClick}>Click Me</button>;
});

export default function App() {
  const [count, setCount] = useState(0);

  // ✅ Defined outside JSX → Stable function reference
  const handleChildClick = () => {
    console.log("Child Clicked");
  };

  return (
    <div>
      <h1>Count: {count}</h1>

      <button onClick={() => setCount(count + 1)}>Increase</button>

      {/* ✅ Now Child won't re-render unnecessarily */}
      <Child onClick={handleChildClick} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🎯 Console Output:

  • Now, when clicking “Increase”, the child does not re-render ✅

🧠 What’s Actually Happening?

Pattern Function Identity React.memo Impact Performance
() => console.log() inside JSX 🆕 New every time ❌ Breaks memo 🔻 Bad
const fn = () => {} outside JSX ✅ Stable ✅ Memo works 🚀 Good

🛡️ Bonus: useCallback for Dynamic Handlers

Sometimes you can't avoid inline functions (e.g., if the handler depends on props).
Use useCallback to keep the reference stable:

const handleClick = useCallback(() => {
  console.log("Clicked");
}, []);
Enter fullscreen mode Exit fullscreen mode

This helps avoid unnecessary renders when passing handlers to deeply nested children.


🔥 Final Thoughts

React is fast — but it can’t optimize what you write inefficiently.

Defining arrow functions inside JSX might look clean, but behind the scenes, it’s causing:

  • 🌀 New functions every render
  • 📉 React.memo being bypassed
  • ⚠️ Performance loss in large trees

✅ The fix is simple: move the function outside JSX, or use useCallback when necessary.

Learn this now, and your future React self will thank you.





⚡ "ليه الـ Component بتاعي بيعمل Re-render رغم إن مفيش حاجة اتغيرت؟"

🧠 "My component re-renders even though nothing has changed... WHY?"

لو سألت نفسك السؤال ده وأنت شغال بـ React، فأنت مش لوحدك 👀

في المقال ده، هنتكلم عن مشكلة أداء حقيقية بتحصل بسبب إنك بتستخدم arrow function جوه JSX.
وهنعرف:

  • ✅ ليه دي ممارسة سيئة
  • ✅ إزاي بتأثر على React.memo، و useCallback، والـ re-renders
  • ✅ مثال عملي يوضح المشكلة
  • ✅ أنماط كتابة أنضف (Clean Patterns) هتخليك تكتب زي المحترفين

🔍 المشكلة: كل Render بيولد Function جديدة

لما تكتب arrow function مباشرة داخل JSX، زي كده:

<button onClick={() => console.log("Clicked")}>Click</button>
Enter fullscreen mode Exit fullscreen mode

ده معناه إنك بتولد function جديدة كل مرة الـ component بيعمل render.

حتى لو نفس الكود بالضبط – الـ identity بتاع الـ function مختلفة.
وده معناه إن React.memo مش هيقدر يمنع إعادة الرسم.
وده معناه إن ممكن مكونات الأبناء تعمل re-render من غير سبب.
وده معناه إن الأبليكيشن بيعمل شغل أكتر من اللازم.


🔬 الحالة التجريبية: Counter + Child Component

تعالى نعمل مشروع صغير يوضح المشكلة بشكل عملي.

👇 مكونات التجربة:

  • مكون أب (Parent) فيه عداد (باستخدام useState)
  • مكون طفل (Child) بياخد منه دالة handleClick
  • هنختبر باستخدام React.memo هل الطفل فعلاً بيعيد الرسم ولا لأ

💣 كود فيه مشكلة:

import React, { useState, memo } from "react";

const Child = memo(({ onClick }) => {
  console.log("🔄 Child rendered");
  return <button onClick={onClick}>Click Me</button>;
});

export default function App() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <h1>Count: {count}</h1>

      <button onClick={() => setCount(count + 1)}>Increase</button>

      {/* ❌ الـ function دي بتتولد جديدة في كل render */}
      <Child onClick={() => console.log("Child Clicked")} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🧨 كل مرة تدوس على زر "Increase"، المكون الطفل بيعمل re-render… رغم إن مفيش أي تغيير فعلي في الـ props.


✅ الحل النظيف: عرف الدوال خارج JSX

وده الشكل الصح للكود:

import React, { useState, memo } from "react";

const Child = memo(({ onClick }) => {
  console.log("🔄 Child rendered");
  return <button onClick={onClick}>Click Me</button>;
});

export default function App() {
  const [count, setCount] = useState(0);

  // ✅ معرف بره JSX → ثابت في كل render
  const handleChildClick = () => {
    console.log("Child Clicked");
  };

  return (
    <div>
      <h1>Count: {count}</h1>

      <button onClick={() => setCount(count + 1)}>Increase</button>

      {/* ✅ كده Child مش هيعمل re-render من غير داعي */}
      <Child onClick={handleChildClick} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

🎯 النتيجة في الـ Console:

  • لما تدوس "Increase"، الطفل مش هيعمل re-render ✅

🧠 إيه اللي بيحصل فعليًا؟

الحالة هوية الـ Function تأثيرها على React.memo الأداء
() => console.log() جوه JSX 🆕 جديدة كل مرة ❌ بتكسر memo 🔻 ضعيف
const fn = () => {} بره JSX ✅ ثابتة ✅ memo بيشتغل 🚀 ممتاز

🛡️ مكافأة: استخدام useCallback مع دوال ديناميكية

في بعض الحالات، بتضطر تستخدم دالة جوه JSX (مثلاً لو بتعتمد على props).
هنا تقدر تستخدم useCallback عشان تحافظ على ثبات الـ reference:

const handleClick = useCallback(() => {
  console.log("Clicked");
}, []);
Enter fullscreen mode Exit fullscreen mode

وده بيمنع إعادة الرسم غير الضرورية لما تبعت الدالة لأطفال بعيدين في الشجرة (deep children).


🔥 خلاصة

React سريع، لكن مش هيقدر يعمل Optimizations لو الكود نفسه مش مكتوب بكفاءة.

كتابة arrow functions داخل JSX شكلها نظيف، لكن بتسبب:

  • 🌀 دوال جديدة في كل render
  • 📉 تعطيل React.memo
  • ⚠️ مشاكل أداء حقيقية في المشاريع الكبيرة

✅ الحل بسيط: حط الدالة خارج JSX، أو استخدم useCallback لما يكون ضروري.

اتعلم النقطة دي دلوقتي، ومستقبلك في React هيشكرك عليها 🙌


Top comments (0)