Passing new object references in props leading to re-renders

Loading

A common performance pitfall in React is accidentally passing new object references as props, which triggers unnecessary re-renders of child components. Here’s how to identify and fix these issues:

The Core Problem

When you pass objects, arrays, or functions as props, React uses referential equality to determine if props changed. Even if the content is identical, a new reference forces a re-render:

// This triggers re-render every time, even if values are same
<ChildComponent style={{ color: 'red' }} onClick={() => {}} />

Optimization Techniques

1. Memoize Object Props with useMemo

const buttonStyle = useMemo(() => ({ 
  color: 'red',
  padding: '10px'
}), []); // Empty dependency = stable reference

<Button style={buttonStyle} />

2. Memoize Function Props with useCallback

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

<Button onClick={handleClick} />

3. Extract Static Objects Outside Components

// Outside component - single reference
const STATIC_STYLE = { color: 'red' };

function Component() {
  return <Child style={STATIC_STYLE} />;
}

4. For Arrays, Use Stable References

const items = useMemo(() => [1, 2, 3], []);

<List items={items} />

5. Custom Comparison with React.memo

const UserCard = React.memo(
  ({ user }) => <div>{user.name}</div>,
  (prevProps, nextProps) => {
    // Custom comparison logic
    return prevProps.user.id === nextProps.user.id;
  }
);

Advanced Patterns

1. Context Selectors for Object State

const user = useContextSelector(UserContext, state => ({
  name: state.name,
  avatar: state.avatar
}));

2. State Normalization

// Instead of:
setState(prev => ({ ...prev, user: { ...prev.user, name: 'New' } }));

// Normalize to flatten structure:
const [userName, setUserName] = useState('Old');

3. Immutable Update Patterns

// Using libraries like Immer for clean updates
import produce from 'immer';

setState(produce(draft => {
  draft.user.name = 'New'; // Doesn't create new references for unchanged parts
}));

Debugging Reference Changes

  1. React DevTools:
  • Highlight updates when components render
  • Inspect prop values between renders
  1. Custom Hook:
function useWhyDidYouUpdate(name, props) {
  const previousProps = useRef();

  useEffect(() => {
    if (previousProps.current) {
      const changes = {};
      Object.keys({ ...previousProps.current, ...props }).forEach(key => {
        if (previousProps.current[key] !== props[key]) {
          changes[key] = { from: previousProps.current[key], to: props[key] };
        }
      });
      if (Object.keys(changes).length) {
        console.log('[why-did-you-update]', name, changes);
      }
    }
    previousProps.current = props;
  });
}

// Usage in component:
useWhyDidYouUpdate('ComponentName', { prop1, prop2 });

Performance Impact

ScenarioReference TypeRe-renders Child?
Primitive (string/number)Same valueNo
Object/ArrayNew referenceYes
Object/ArraySame referenceNo
FunctionNew referenceYes
FunctionSame referenceNo

Best Practices

  1. For objects/arrays: Prefer useMemo when created inside component
  2. For functions: Always use useCallback for event handlers
  3. For context: Select only needed values to avoid upstream changes
  4. For lists: Ensure stable item references when possible
  5. For styles: Consider CSS-in-JS solutions that handle memoization

Leave a Reply

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