When working with React, it's common to run into performance issues caused by unnecessary component re-renders. This usually happens when child components are being re-rendered even if their props haven't changed.

Luckily, React gives us two simple tools to optimize this: React.memo and useCallback.

In this post, I'll explain how they work and when to use them — with practical examples.


✅ The Problem: Too many re-renders

Imagine you have a parent component that updates state frequently, and a child component that receives a prop (like a callback function). Even if the child doesn't need to re-render, it will — because the function prop is re-created on every render.

Here’s a simplified version of that scenario:

import { useState } from 'react';

function MyButton({ onClick }: { onClick: () => void }) {
  console.log('Button rendered');
  return <button onClick={onClick}>Click mebutton>;
}

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

  const handleClick = () => {
    console.log('Clicked');
  };

  return (
    <div>
      <p>Count: {count}p>
      <button onClick={() => setCount(count + 1)}>Incrementbutton>
      <MyButton onClick={handleClick} />
    div>
  );
}

Even if handleClick doesn’t change, it’s recreated on every render — causing MyButton to re-render too.


💡 The Solution: React.memo + useCallback

React.memo

React.memo is a higher-order component that memoizes a component, preventing unnecessary re-renders if the props haven't changed.

const MyButton = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>Click mebutton>;
});

But this alone isn't enough — if you're passing a new function on every render, it still counts as a new prop.


useCallback

That's where useCallback comes in. It memoizes the function itself, so its reference only changes when its dependencies change.

const handleClick = useCallback(() => {
  console.log('Clicked');
}, []); // empty deps = function is stable

Together, these two tools ensure that MyButton won't re-render unless it actually needs to.


🔧 Full Example

import React from 'react';
import { useState, useCallback } from 'react';

const MyButton = React.memo(({ onClick }: { onClick: () => void }) => {
  console.log('Button rendered');
  return <button onClick={onClick}>Click mebutton>;
});

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

  const handleClick = useCallback(() => {
    console.log('Button clicked');
  }, []);

  return (
    <div>
      <p>Count: {count}p>
      <button onClick={() => setCount(count + 1)}>Incrementbutton>
      <MyButton onClick={handleClick} />
    div>
  );
}

✅ Now MyButton only renders once unless handleClick or other props change.


🧠 When to use (and when not to)

  • Use React.memo when your component:

    • Receives stable props (like functions or values that rarely change).
    • Is pure and doesn't depend on external state.
    • Is relatively expensive to render.
  • Use useCallback when:

    • You're passing functions as props to memoized children.
    • You want to keep a stable function reference between renders.

⚠️ Avoid overusing these tools — they add some complexity. Use them when profiling shows re-renders are hurting performance.


🚀 Final Thoughts

React.memo and useCallback are powerful tools to improve performance and avoid unnecessary rendering. Together, they help keep your app efficient and responsive.

Have you used React.memo or useCallback in your projects?

Let me know how — or if you’ve run into any gotchas! 👇


🔗 Follow me for more practical React + TypeScript tips!