Skip to main content

React Functional Component Lifecycle

Introduction

React functional components go through several phases during their lifetime. While class components have traditional lifecycle methods, functional components use Hooks to handle lifecycle events. This document explores the complete lifecycle of a React functional component.

Component Lifecycle Phases

1. Mounting Phase

When a component is being created and inserted into the DOM:

function ExampleComponent({ initialData }) {
// 1. Component initialization
const [data, setData] = useState(initialData);

// 2. First render setup
useEffect(() => {
// Runs after first render
console.log('Component mounted');

// Cleanup function (optional)
return () => {
console.log('Component will unmount');
};
}, []); // Empty dependency array = run once on mount

return <div>{data}</div>;
}

2. Updating Phase

When a component is re-rendering due to changes in props or state:

function UpdatingExample({ user }) {
const [count, setCount] = useState(0);

// Runs on specific prop/state changes
useEffect(() => {
console.log('user or count updated');
// Side effects here
}, [user, count]); // Dependency array with values to watch

// Runs on every render
useEffect(() => {
console.log('Component updated');
}); // No dependency array

return (
<div>
<p>User: {user}</p>
<p>Count: {count}</p>
</div>
);
}

3. Unmounting Phase

When a component is being removed from the DOM:

function UnmountExample() {
useEffect(() => {
// Setup phase
const subscription = setupSubscription();

// Cleanup phase
return () => {
subscription.unsubscribe();
console.log('Cleanup performed');
};
}, []);

return <div>Subscribed Component</div>;
}

Common Lifecycle Patterns

Data Fetching

function DataFetchingComponent({ id }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
let mounted = true;

async function fetchData() {
try {
setLoading(true);
const response = await fetch(`/api/data/${id}`);
const result = await response.json();

if (mounted) {
setData(result);
setLoading(false);
}
} catch (err) {
if (mounted) {
setError(err);
setLoading(false);
}
}
}

fetchData();

return () => {
mounted = false;
};
}, [id]);

if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{data}</div>;
}

Event Listeners

function EventListenerComponent() {
useEffect(() => {
const handleResize = () => {
console.log('Window resized');
};

window.addEventListener('resize', handleResize);

return () => {
window.removeEventListener('resize', handleResize);
};
}, []);

return <div>Window Size Observer</div>;
}

Best Practices

1. Cleanup Functions

Always include cleanup functions in useEffect when:

  • Setting up subscriptions
  • Adding event listeners
  • Starting timers
  • Creating WebSocket connections
function BestPracticeExample() {
useEffect(() => {
const timer = setInterval(() => {
console.log('Timer tick');
}, 1000);

return () => clearInterval(timer);
}, []);
}

2. Dependency Arrays

  • Empty array ([]) = run once on mount
  • No array = run on every render
  • Array with dependencies = run when dependencies change
function DependencyExample({ data, onUpdate }) {
// Runs on every data or onUpdate change
useEffect(() => {
onUpdate(data);
}, [data, onUpdate]);
}

3. Conditional Effects

Use conditional logic inside useEffect, not around it:

// Good
useEffect(() => {
if (condition) {
// Do something
}
}, [condition]);

// Bad - Don't do this
if (condition) {
useEffect(() => {
// Do something
}, []);
}

Common Pitfalls

1. Infinite Loops

// ❌ Wrong - Creates infinite loop
function InfiniteLoopExample() {
const [count, setCount] = useState(0);

useEffect(() => {
setCount(count + 1); // Triggers re-render, which triggers effect again
}, [count]);
}

// ✅ Correct - Using functional update
function FixedExample() {
const [count, setCount] = useState(0);

useEffect(() => {
setCount(prev => prev + 1);
}, []); // Runs once on mount
}

2. Stale Closures

// ❌ Wrong - Stale closure problem
function StaleClosureExample() {
const [count, setCount] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
console.log(count); // Will always log initial value
}, 1000);

return () => clearInterval(timer);
}, []); // Missing dependency
}

// ✅ Correct - Using functional update
function FixedStaleClosureExample() {
const [count, setCount] = useState(0);

useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // Uses latest state
}, 1000);

return () => clearInterval(timer);
}, []); // No dependencies needed
}

Performance Optimization

1. useMemo and useCallback

function OptimizedComponent({ data }) {
// Memoize expensive calculations
const processedData = useMemo(() => {
return expensiveOperation(data);
}, [data]);

// Memoize callbacks
const handleClick = useCallback(() => {
console.log(processedData);
}, [processedData]);

return <button onClick={handleClick}>Process Data</button>;
}

2. Avoiding Unnecessary Re-renders

// Use React.memo for component-level memoization
const MemoizedChild = React.memo(function Child({ data }) {
return <div>{data}</div>;
});

function Parent() {
// Memoize value to prevent unnecessary re-renders
const memoizedValue = useMemo(() => ({
complex: 'data structure'
}), []);

return <MemoizedChild data={memoizedValue} />;
}

Conclusion

Understanding the lifecycle of functional components is crucial for building efficient React applications. By properly managing effects, cleanup, and dependencies, you can create components that are both performant and maintainable. Remember to always consider the cleanup phase and carefully manage your effect dependencies to avoid common pitfalls.