Lifecycle methods are special methods that get called at different stages of a component’s life. They allow you to run code when a component mounts, updates, or unmounts.
Lifecycle methods are only available in class components. Functional components use hooks like useEffect instead.
Component lifecycle phases
A component goes through three main phases:
- Mounting - Component is created and inserted into the DOM
- Updating - Component re-renders due to prop or state changes
- Unmounting - Component is removed from the DOM
Mounting phase
These methods are called when a component is being created and inserted into the DOM.
constructor()
Called before the component is mounted. Used for initializing state and binding methods.
import { Component } from 'preact';
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
this.increment = this.increment.bind(this);
}
increment() {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.increment}>
Count: {this.state.count}
</button>
);
}
}
Always call super(props) before accessing this.props in the constructor.
componentWillMount()
Deprecated - This method is called before mounting but is deprecated. Use constructor() or componentDidMount() instead.
Called immediately before mounting occurs. It’s invoked during the diff process (src/diff/index.js:168):
if (
isClassComponent &&
newType.getDerivedStateFromProps == NULL &&
c.componentWillMount != NULL
) {
c.componentWillMount();
}
componentDidMount()
Called immediately after the component is mounted. Perfect for:
- Fetching data from APIs
- Setting up subscriptions
- Adding event listeners
- Interacting with the DOM
Implementation in Preact (src/diff/index.js:173):
if (isClassComponent && c.componentDidMount != NULL) {
c._renderCallbacks.push(c.componentDidMount);
}
Example usage
import { Component } from 'preact';
class UserProfile extends Component {
state = { user: null, loading: true };
componentDidMount() {
fetch(`/api/users/${this.props.userId}`)
.then(res => res.json())
.then(user => {
this.setState({ user, loading: false });
});
}
render() {
if (this.state.loading) {
return <div>Loading...</div>;
}
return (
<div>
<h1>{this.state.user.name}</h1>
<p>{this.state.user.email}</p>
</div>
);
}
}
Updating phase
These methods are called when a component’s props or state change.
getDerivedStateFromProps()
Static method called before rendering, both on initial mount and on updates. Returns an object to update state, or null to update nothing.
Implementation (src/diff/index.js:148):
if (isClassComponent && newType.getDerivedStateFromProps != NULL) {
if (c._nextState == c.state) {
c._nextState = assign({}, c._nextState);
}
assign(
c._nextState,
newType.getDerivedStateFromProps(newProps, c._nextState)
);
}
Example usage
import { Component } from 'preact';
class EmailInput extends Component {
state = { email: '', domain: '' };
static getDerivedStateFromProps(props, state) {
// Update domain when email changes
if (props.email !== state.email) {
const domain = props.email.split('@')[1] || '';
return { email: props.email, domain };
}
return null;
}
render() {
return (
<div>
<p>Email: {this.state.email}</p>
<p>Domain: {this.state.domain}</p>
</div>
);
}
}
getDerivedStateFromProps is static, so it doesn’t have access to this. It’s a pure function based only on props and state.
componentWillReceiveProps()
Deprecated - Called when component receives new props. Use getDerivedStateFromProps() instead.
Called when a component receives new props (src/diff/index.js:177):
if (
isClassComponent &&
newType.getDerivedStateFromProps == NULL &&
newProps !== oldProps &&
c.componentWillReceiveProps != NULL
) {
c.componentWillReceiveProps(newProps, componentContext);
}
shouldComponentUpdate()
Called before rendering when new props or state are received. Return false to skip rendering.
Implementation (src/diff/index.js:186):
if (
newVNode._original == oldVNode._original ||
(!(c._bits & COMPONENT_FORCE) &&
c.shouldComponentUpdate != NULL &&
c.shouldComponentUpdate(
newProps,
c._nextState,
componentContext
) === false)
) {
// Skip rendering
c.props = newProps;
c.state = c._nextState;
c._bits &= ~COMPONENT_DIRTY;
// ...
}
Example usage
import { Component } from 'preact';
class UserCard extends Component {
shouldComponentUpdate(nextProps, nextState) {
// Only update if user ID changed
return this.props.userId !== nextProps.userId;
}
render() {
return (
<div className="user-card">
<h3>User #{this.props.userId}</h3>
<p>{this.props.name}</p>
</div>
);
}
}
This is a powerful performance optimization. Use it to prevent unnecessary renders.
componentWillUpdate()
Deprecated - Called immediately before rendering. Use getSnapshotBeforeUpdate() instead.
Called before rendering with new props or state (src/diff/index.js:223):
if (c.componentWillUpdate != NULL) {
c.componentWillUpdate(newProps, c._nextState, componentContext);
}
getSnapshotBeforeUpdate()
Called right before DOM updates are committed. Return value is passed to componentDidUpdate().
Implementation (src/diff/index.js:270):
if (
isClassComponent &&
oldVNode._component &&
c.getSnapshotBeforeUpdate != NULL
) {
snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
}
Example usage
import { Component } from 'preact';
class ChatBox extends Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
// Capture scroll position before update
if (prevProps.messages.length < this.props.messages.length) {
const list = this.listRef;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// Restore scroll position after update
if (snapshot !== null) {
const list = this.listRef;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={el => this.listRef = el}>
{this.props.messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
}
}
componentDidUpdate()
Called immediately after updating occurs. Good for:
- Operating on the DOM after an update
- Making network requests based on prop changes
Implementation (src/diff/index.js:227):
if (isClassComponent && c.componentDidUpdate != NULL) {
c._renderCallbacks.push(() => {
c.componentDidUpdate(oldProps, oldState, snapshot);
});
}
Example usage
import { Component } from 'preact';
class UserData extends Component {
state = { data: null };
componentDidUpdate(prevProps) {
// Fetch new data if userId changed
if (this.props.userId !== prevProps.userId) {
fetch(`/api/users/${this.props.userId}`)
.then(res => res.json())
.then(data => this.setState({ data }));
}
}
render() {
return <div>{this.state.data?.name}</div>;
}
}
Always compare props/state before making side effects in componentDidUpdate() to avoid infinite loops.
Unmounting phase
componentWillUnmount()
Called immediately before a component is unmounted and destroyed. Perfect for:
- Cleaning up timers
- Canceling network requests
- Removing event listeners
- Cleaning up subscriptions
Implementation (src/diff/index.js:695):
if ((r = vnode._component) != NULL) {
if (r.componentWillUnmount) {
try {
r.componentWillUnmount();
} catch (e) {
options._catchError(e, parentVNode);
}
}
r._parentDom = NULL;
}
Example usage
import { Component } from 'preact';
class Timer extends Component {
state = { seconds: 0 };
componentDidMount() {
this.interval = setInterval(() => {
this.setState({ seconds: this.state.seconds + 1 });
}, 1000);
}
componentWillUnmount() {
// Clean up interval
clearInterval(this.interval);
}
render() {
return <div>Seconds: {this.state.seconds}</div>;
}
}
Lifecycle method order
On mount
constructor()
getDerivedStateFromProps()
render()
- DOM updates
componentDidMount()
On update
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
- DOM updates
componentDidUpdate()
On unmount
componentWillUnmount()
- Component removed from DOM
Complete lifecycle example
Here’s a real-world example demonstrating multiple lifecycle methods:
import { Component } from 'preact';
class UserDashboard extends Component {
constructor(props) {
super(props);
this.state = {
user: null,
loading: true,
error: null
};
}
static getDerivedStateFromProps(props, state) {
// Reset error when userId changes
if (props.userId !== state.prevUserId) {
return {
prevUserId: props.userId,
error: null
};
}
return null;
}
componentDidMount() {
this.fetchUser();
// Set up polling
this.pollInterval = setInterval(() => this.fetchUser(), 30000);
}
componentDidUpdate(prevProps) {
// Fetch new user data if ID changed
if (this.props.userId !== prevProps.userId) {
this.fetchUser();
}
}
componentWillUnmount() {
// Clean up polling
clearInterval(this.pollInterval);
// Cancel any pending requests
if (this.controller) {
this.controller.abort();
}
}
fetchUser = async () => {
this.setState({ loading: true });
this.controller = new AbortController();
try {
const response = await fetch(
`/api/users/${this.props.userId}`,
{ signal: this.controller.signal }
);
const user = await response.json();
this.setState({ user, loading: false, error: null });
} catch (error) {
if (error.name !== 'AbortError') {
this.setState({ error, loading: false });
}
}
};
shouldComponentUpdate(nextProps, nextState) {
// Only update if data actually changed
return (
this.props.userId !== nextProps.userId ||
this.state.user !== nextState.user ||
this.state.loading !== nextState.loading ||
this.state.error !== nextState.error
);
}
render() {
const { loading, error, user } = this.state;
if (loading && !user) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!user) return <div>No user found</div>;
return (
<div className="dashboard">
<h1>{user.name}</h1>
<p>Email: {user.email}</p>
<p>Role: {user.role}</p>
{loading && <span className="updating">Updating...</span>}
</div>
);
}
}
This example shows:
- Initializing state in
constructor()
- Deriving state from props with
getDerivedStateFromProps()
- Fetching data in
componentDidMount()
- Updating when props change in
componentDidUpdate()
- Cleaning up in
componentWillUnmount()
- Optimizing with
shouldComponentUpdate()
Error boundaries
Error boundaries catch JavaScript errors anywhere in their component tree:
import { Component } from 'preact';
class ErrorBoundary extends Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h1>Something went wrong</h1>
<p>{this.state.error.message}</p>
</div>
);
}
return this.props.children;
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>
Error boundaries only catch errors in child components, not in the error boundary itself.