Passing new object references in props leading to re-renders

Loading

A common performance pitfall in React is unintentionally passing new object references as props, which can trigger unnecessary re-renders even when the actual data hasn’t meaningfully changed.

The Problem

When you pass objects, arrays, or functions as props, React does a shallow comparison. If the reference changes, the component will re-render:

// This will cause child to re-render every time Parent renders
function Parent() {
  return <Child style={{ color: 'blue' }} />;
}

Solutions

1. Memoize Object Props with useMemo

function Parent() {
  const style = useMemo(() => ({ color: 'blue' }), []);
  return <Child style={style} />;
}

2. Memoize Array Props

function Parent() {
  const items = useMemo(() => ['a', 'b', 'c'], []);
  return <Child items={items} />;
}

3. Stabilize Function Props with useCallback

function Parent() {
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  return <Child onClick={handleClick} />;
}

4. Move Static Objects Outside the Component

const staticStyle = { color: 'blue' };

function Parent() {
  return <Child style={staticStyle} />;
}

5. For Context Providers, Memoize the Value

function App() {
  const [user, setUser] = useState(null);

  const contextValue = useMemo(() => ({
    user,
    login: (userData) => setUser(userData),
    logout: () => setUser(null)
  }), [user]);

  return (
    <AuthContext.Provider value={contextValue}>
      {/* children */}
    </AuthContext.Provider>
  );
}

6. Use Custom Comparison for Deep Equality

For class components, implement shouldComponentUpdate with deep comparison:

class Child extends React.Component {
  shouldComponentUpdate(nextProps) {
    return !deepEqual(this.props, nextProps);
  }
  // ...
}

Or for function components, use React.memo with a custom comparison function:

const Child = React.memo(
  function Child(props) { /* ... */ },
  (prevProps, nextProps) => {
    return prevProps.objectProp.id === nextProps.objectProp.id;
  }
);

7. Flatten Your Props

Instead of passing big objects, pass individual primitives:

// Instead of:
<Child user={user} />

// Do:
<Child userId={user.id} userName={user.name} />

8. Consider Using Immutable Data Structures

Libraries like Immer can help manage state without creating new references unnecessarily:

import produce from 'immer';

function reducer(state, action) {
  return produce(state, draft => {
    switch (action.type) {
      case 'UPDATE_NAME':
        draft.user.name = action.payload;
        break;
    }
  });
}

9. Use React’s Built-in Components for Elements

For style objects specifically, consider using the built-in style prop which React may optimize:

// This might be more efficient than passing an object
<div style={{ color: 'blue' }} />

Debugging Tips

  1. Use React DevTools to highlight re-renders
  2. Add a render counter:
   const renderCount = useRef(0);
   useEffect(() => {
     renderCount.current++;
     console.log(`Rendered ${renderCount.current} times`);
   });
  1. Check prop references:
   useEffect(() => {
     console.log('Object prop reference changed');
   }, [props.objectProp]);

By carefully managing object references in your props, you can significantly reduce unnecessary re-renders and improve your application’s performance.

Leave a Reply

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