# 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