Using arrow functions inside JSX without memoization leading to re-renders

Loading

Arrow Functions in JSX Without Memoization

A common React performance pitfall is using inline arrow functions in JSX without proper memoization, which can cause unnecessary re-renders of child components.

The Problem

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      {/* ❌ New function created each render */}
      <ChildComponent onClick={() => setCount(count + 1)} />

      {/* ❌ Similarly problematic */}
      <ChildComponent onClick={e => handleClick(e, id)} />
    </div>
  );
}

function ChildComponent({ onClick }) {
  return <button onClick={onClick}>Click me</button>;
}

Why this is problematic:

  1. Creates a new function instance on every render
  2. Causes child components to re-render unnecessarily
  3. Can lead to performance bottlenecks
  4. Breaks shallow prop comparison optimizations

Correct Solutions

1. Use useCallback (Recommended)

function ParentComponent() {
  const [count, setCount] = useState(0);

  // ✅ Memoized callback
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []); // No dependencies = never changes

  return (
    <div>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

2. Move Handler Outside JSX

function ParentComponent() {
  const [count, setCount] = useState(0);

  // ✅ Defined outside JSX (stable reference)
  function handleClick() {
    setCount(count + 1);
  }

  return (
    <div>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

3. For Event Parameters

function ParentComponent({ items }) {
  // ✅ Memoized with parameter
  const handleItemClick = useCallback((itemId) => {
    console.log('Clicked', itemId);
  }, []);

  return (
    <div>
      {items.map(item => (
        <ChildComponent 
          key={item.id}
          onClick={() => handleItemClick(item.id)}
        />
      ))}
    </div>
  );
}

4. Class Component Alternative

class ParentComponent extends React.Component {
  // ✅ Class property (stable reference)
  handleClick = () => {
    this.setState(prev => ({ count: prev.count + 1 }));
  };

  render() {
    return <ChildComponent onClick={this.handleClick} />;
  }
}

When It’s Actually Okay

Inline arrow functions are acceptable when:

  1. The parent component is simple and rarely re-renders
  2. The child component is lightweight
  3. You’re not passing the handler through multiple layers
  4. The child component doesn’t optimize with React.memo

Performance Impact

  1. New function references trigger child re-renders
  2. Memoization overhead vs. render cost tradeoff
  3. Deep component trees suffer most from this issue
  4. Frequent updates exacerbate the problem

Best Practices

  1. Memoize with useCallback for frequently re-rendered components
  2. Use React.memo on child components
  3. Consider context for deeply passed handlers
  4. Profile first – don’t prematurely optimize
  5. For lists, extract item components:
function Item({ item, onItemClick }) {
  // Memoized component
  return <div onClick={() => onItemClick(item.id)}>{item.name}</div>;
}

const MemoizedItem = React.memo(Item);

Advanced Patterns

1. Stable Handler with Dynamic Data

function ParentComponent({ items }) {
  const itemMap = useMemo(() => {
    return new Map(items.map(item => [item.id, item]));
  }, [items]);

  const handleClick = useCallback((id) => {
    const item = itemMap.get(id);
    console.log('Clicked', item);
  }, [itemMap]);

  return items.map(item => (
    <MemoizedItem 
      key={item.id}
      item={item}
      onClick={handleClick}
    />
  ));
}

2. Event Pooling Consideration

const handleClick = useCallback((e) => {
  // Extract needed values immediately
  const { clientX, clientY } = e;
  // Use coordinates later
  console.log(clientX, clientY);
}, []);

Leave a Reply

Your email address will not be published. Required fields are marked *