# React Component Lifecycle Understanding React component lifecycle is crucial for building robust React applications. This guide covers both class component lifecycle methods and functional component lifecycle patterns using hooks. ## Introduction React components go through different phases during their lifetime: 1. **Mounting** - Component is created and inserted into the DOM 2. **Updating** - Component re-renders due to state or prop changes 3. **Unmounting** - Component is removed from the DOM ## Class Component Lifecycle ### Mounting Phase #### constructor() Called before the component is mounted. Used for initializing state and binding methods. ```javascript class Counter extends Component { constructor(props) { super(props) this.state = { count: 0 } this.handleClick = this.handleClick.bind(this) } } ``` #### `componentDidMount()` Called after the component is mounted to the DOM. Perfect for side effects like API calls, setting up subscriptions, or DOM manipulation. ```javascript class ChatRoom extends Component { componentDidMount() { this.setupConnection() this.logVisit() } setupConnection() { this.connection = createConnection(this.props.roomId) this.connection.connect() } } ``` ### Updating Phase #### `componentDidUpdate(prevProps, prevState)` Called after the component updates. Use for side effects that depend on prop or state changes. ```javascript class ChatRoom extends Component { componentDidUpdate(prevProps, prevState) { if (this.props.roomId !== prevProps.roomId) { this.destroyConnection() this.setupConnection() } } } ``` #### `shouldComponentUpdate(nextProps, nextState)` Controls whether the component should re-render. Return `false` to prevent unnecessary re-renders. ```javascript class Rectangle extends Component { shouldComponentUpdate(nextProps, nextState) { if ( nextProps.position.x === this.props.position.x && nextProps.position.y === this.props.position.y && nextState.isHovered === this.state.isHovered ) { return false // No changes, skip re-render } return true } } ``` ### Unmounting Phase #### `componentWillUnmount()` Called before the component is unmounted. Use for cleanup like canceling timers, removing event listeners, or cleaning up subscriptions. ```javascript class ChatRoom extends Component { componentWillUnmount() { this.destroyConnection() } destroyConnection() { if (this.connection) { this.connection.disconnect() this.connection = null } } } ``` ## Functional Component Lifecycle ### `useEffect` Hook The `useEffect` hook replaces most lifecycle methods in functional components. #### Mount and Unmount (componentDidMount + componentWillUnmount) ```javascript function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId) connection.connect() return () => { connection.disconnect() } }, []) // Empty dependency array = run once on mount } ``` #### Effect with Dependencies (componentDidUpdate) ```javascript function ChatRoom({ roomId }) { useEffect(() => { const connection = createConnection(serverUrl, roomId) connection.connect() return () => { connection.disconnect() } }, [roomId]) // Re-run when roomId changes } ``` #### Multiple Effects ```javascript function ChatRoom({ roomId }) { // Separate concerns into different effects useEffect(() => { logVisit(roomId) }, [roomId]) useEffect(() => { const connection = createConnection(serverUrl, roomId) connection.connect() return () => { connection.disconnect() } }, [roomId]) } ``` ## Lifecycle Phases ### Component Lifecycle Diagram ```mermaid graph TD A[Component Created] --> B[Constructor] B --> C[Render] C --> D[componentDidMount] D --> E[Component Mounted] E --> F[Props/State Change] F --> G[shouldComponentUpdate] G -->|true| H[Render] G -->|false| E H --> I[componentDidUpdate] I --> E E --> J[Component Unmounting] J --> K[componentWillUnmount] K --> L[Component Unmounted] style A fill:#e1f5fe style E fill:#c8e6c9 style L fill:#ffcdd2 ``` ### Functional Component Lifecycle Diagram ```mermaid graph TD A[Function Called] --> B[useState/useEffect Setup] B --> C[Render] C --> D[useEffect Runs] D --> E[Component Mounted] E --> F[Props/State Change] F --> G[Function Called Again] G --> H[useEffect Dependencies Check] H -->|Dependencies Changed| I[Cleanup Previous Effect] H -->|No Changes| J[Skip Effect] I --> K[Run New Effect] J --> L[Render] K --> L L --> E E --> M[Component Unmounting] M --> N[useEffect Cleanup] N --> O[Component Unmounted] style A fill:#e1f5fe style E fill:#c8e6c9 style O fill:#ffcdd2 ``` ## Real-World Examples ### Chat Application with Connection Management ```javascript // Class Component Version class ChatRoom extends Component { state = { serverUrl: 'https://localhost:1234', messages: [] } componentDidMount() { this.setupConnection() } componentDidUpdate(prevProps, prevState) { if ( this.props.roomId !== prevProps.roomId || this.state.serverUrl !== prevState.serverUrl ) { this.destroyConnection() this.setupConnection() } } componentWillUnmount() { this.destroyConnection() } setupConnection() { this.connection = createConnection(this.state.serverUrl, this.props.roomId) this.connection.connect() } destroyConnection() { if (this.connection) { this.connection.disconnect() this.connection = null } } render() { return ( <> <label> Server URL:{' '} <input value={this.state.serverUrl} onChange={(e) => { this.setState({ serverUrl: e.target.value }) }} /> </label> <h1>Welcome to the {this.props.roomId} room!</h1> </> ) } } ``` ```javascript // Functional Component Version function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234') useEffect(() => { const connection = createConnection(serverUrl, roomId) connection.connect() return () => { connection.disconnect() } }, [roomId, serverUrl]) return ( <> <label> Server URL:{' '} <input value={serverUrl} onChange={(e) => setServerUrl(e.target.value)} /> </label> <h1>Welcome to the {roomId} room!</h1> </> ) } ``` ### Performance Optimization with shouldComponentUpdate ```javascript class ExpensiveComponent extends Component { shouldComponentUpdate(nextProps, nextState) { // Only re-render if specific props change return ( nextProps.id !== this.props.id || nextProps.name !== this.props.name || nextState.isVisible !== this.state.isVisible ) } render() { return ( <div> <h2>{this.props.name}</h2> <p>ID: {this.props.id}</p> {this.state.isVisible && <p>This is expensive to render!</p>} </div> ) } } ``` ### Timer Component with Cleanup ```javascript // Class Component class Timer extends Component { state = { seconds: 0 } componentDidMount() { this.interval = setInterval(() => { this.setState((prevState) => ({ seconds: prevState.seconds + 1 })) }, 1000) } componentWillUnmount() { clearInterval(this.interval) } render() { return <div>Seconds: {this.state.seconds}</div> } } // Functional Component function Timer() { const [seconds, setSeconds] = useState(0) useEffect(() => { const interval = setInterval(() => { setSeconds((prev) => prev + 1) }, 1000) return () => clearInterval(interval) }, []) return <div>Seconds: {seconds}</div> } ``` ### Data Fetching with Loading States ```javascript // Class Component class UserProfile extends Component { state = { user: null, loading: true, error: null } async componentDidMount() { try { const response = await fetch(`/api/users/${this.props.userId}`) const user = await response.json() this.setState({ user, loading: false }) } catch (error) { this.setState({ error: error.message, loading: false }) } } render() { if (this.state.loading) return <div>Loading...</div> if (this.state.error) return <div>Error: {this.state.error}</div> return <div>Hello, {this.state.user.name}!</div> } } // Functional Component function UserProfile({ userId }) { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { async function fetchUser() { try { const response = await fetch(`/api/users/${userId}`) const userData = await response.json() setUser(userData) setLoading(false) } catch (err) { setError(err.message) setLoading(false) } } fetchUser() }, [userId]) if (loading) return <div>Loading...</div> if (error) return <div>Error: {error}</div> return <div>Hello, {user.name}!</div> } ``` ## Best Practices ### 1. Use Functional Components with Hooks Modern React development favors functional components with hooks over class components: ```javascript // Preferred: Functional component function MyComponent({ prop }) { const [state, setState] = useState(initialValue) useEffect(() => { // Side effects here return () => { // Cleanup here } }, [dependency]) return <div>{state}</div> } ``` ### 2. Separate Concerns with Multiple useEffect ```javascript function ChatRoom({ roomId }) { // Analytics effect useEffect(() => { logVisit(roomId) }, [roomId]) // Connection effect useEffect(() => { const connection = createConnection(serverUrl, roomId) connection.connect() return () => connection.disconnect() }, [roomId]) // Notification effect useEffect(() => { if (roomId === 'urgent') { showNotification('Urgent room selected') } }, [roomId]) } ``` ### 3. Always Clean Up Side Effects ```javascript function MyComponent() { useEffect(() => { const subscription = subscribe() const timer = setInterval(doSomething, 1000) return () => { subscription.unsubscribe() clearInterval(timer) } }, []) } ``` ### 4. Use Dependency Arrays Correctly ```javascript // Missing dependencies useEffect(() => { fetchData(userId, serverUrl) }, []) // Missing userId and serverUrl // Correct dependencies useEffect(() => { fetchData(userId, serverUrl) }, [userId, serverUrl]) ``` ### 5. Avoid Stale Closures ```javascript // Stale closure function MyComponent() { const [count, setCount] = useState(0) useEffect(() => { const timer = setInterval(() => { setCount(count + 1) // Always uses initial count value }, 1000) return () => clearInterval(timer) }, []) // Missing count dependency } // Correct approach function MyComponent() { const [count, setCount] = useState(0) useEffect(() => { const timer = setInterval(() => { setCount((prev) => prev + 1) // Uses current count value }, 1000) return () => clearInterval(timer) }, []) } ``` ## Migration from Class to Functional ### Step 1: Convert the Structure ```javascript // Before: Class component class Greeting extends Component { render() { return <h1>Hello, {this.props.name}!</h1> } } // After: Functional component function Greeting({ name }) { return <h1>Hello, {name}!</h1> } ``` ### Step 2: Convert State ```javascript // Before: Class component state class Counter extends Component { state = { count: 0 } handleClick = () => { this.setState({ count: this.state.count + 1 }) } } // After: Functional component with useState function Counter() { const [count, setCount] = useState(0) const handleClick = () => { setCount(count + 1) } } ``` ### Step 3: Convert Lifecycle Methods ```javascript // Before: Class component lifecycle class ChatRoom extends Component { componentDidMount() { this.setupConnection() } componentWillUnmount() { this.destroyConnection() } } // After: Functional component with useEffect function ChatRoom() { useEffect(() => { setupConnection() return () => { destroyConnection() } }, []) } ``` ### Complete Migration Example ```javascript // Before: Class component class UserProfile extends Component { state = { user: null, loading: true } componentDidMount() { this.fetchUser() } componentDidUpdate(prevProps) { if (prevProps.userId !== this.props.userId) { this.fetchUser() } } fetchUser = async () => { this.setState({ loading: true }) const user = await getUser(this.props.userId) this.setState({ user, loading: false }) } render() { if (this.state.loading) return <div>Loading...</div> return <div>{this.state.user.name}</div> } } // After: Functional component function UserProfile({ userId }) { const [user, setUser] = useState(null) const [loading, setLoading] = useState(true) useEffect(() => { async function fetchUser() { setLoading(true) const userData = await getUser(userId) setUser(userData) setLoading(false) } fetchUser() }, [userId]) if (loading) return <div>Loading...</div> return <div>{user.name}</div> } ``` ## Common Pitfalls ### 1. Missing Cleanup ```javascript // Memory leak useEffect(() => { const timer = setInterval(doSomething, 1000) // Missing cleanup! }, []) // Proper cleanup useEffect(() => { const timer = setInterval(doSomething, 1000) return () => clearInterval(timer) }, []) ``` ### 2. Incorrect Dependencies ```javascript // Missing dependencies useEffect(() => { fetchData(prop1, prop2) }, []) // Should include prop1, prop2 // Correct dependencies useEffect(() => { fetchData(prop1, prop2) }, [prop1, prop2]) ``` ### 3. Effect Running Too Often ```javascript // Runs on every render useEffect(() => { doSomething() }) // Missing dependency array // Runs only when needed useEffect(() => { doSomething() }, [dependency]) ``` ## Close it Out Understanding React component lifecycle is essential for building performant and maintainable applications. While class components provide explicit lifecycle methods, functional components with hooks offer a more modern and flexible approach. The key is to: 1. **Use functional components** with hooks for new development 2. **Separate concerns** with multiple useEffect hooks 3. **Always clean up** side effects 4. **Use dependency arrays** correctly 5. **Avoid common pitfalls** like stale closures and memory leaks ♥️, dh