The method only calls this.setState() with static or computed values that do not depend on async operations.
Before:
class UserList extends React.Component {
componentWillMount() {
this.setState({ items: [], loading: false, page: 1 });
}
render() { ... }
}After - move to constructor:
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = { items: [], loading: false, page: 1 };
}
render() { ... }
}If constructor already exists, merge the state:
class UserList extends React.Component {
constructor(props) {
super(props);
// Existing state merged with componentWillMount state:
this.state = {
...this.existingState, // whatever was already here
items: [],
loading: false,
page: 1,
};
}
}The method fetches data, sets up subscriptions, interacts with external APIs, or touches the DOM.
Before:
class UserDashboard extends React.Component {
componentWillMount() {
this.subscription = this.props.eventBus.subscribe(this.handleEvent);
fetch(`/api/users/${this.props.userId}`)
.then(r => r.json())
.then(user => this.setState({ user, loading: false }));
this.setState({ loading: true });
}
}After - move to componentDidMount:
class UserDashboard extends React.Component {
constructor(props) {
super(props);
this.state = { loading: true, user: null }; // initial state here
}
componentDidMount() {
// All side effects move here - runs after first render
this.subscription = this.props.eventBus.subscribe(this.handleEvent);
fetch(`/api/users/${this.props.userId}`)
.then(r => r.json())
.then(user => this.setState({ user, loading: false }));
}
componentWillUnmount() {
// Always pair subscriptions with cleanup
this.subscription?.unsubscribe();
}
}Why this is safe: In React 18 concurrent mode, componentWillMount can be called multiple times before mounting. Side effects inside it can fire multiple times. componentDidMount is guaranteed to fire exactly once after mount.
The method reads this.props to compute an initial state value.
Before:
class PriceDisplay extends React.Component {
componentWillMount() {
this.setState({
formattedPrice: `$${this.props.price.toFixed(2)}`,
isDiscount: this.props.price < this.props.originalPrice,
});
}
}After - constructor with props:
class PriceDisplay extends React.Component {
constructor(props) {
super(props);
this.state = {
formattedPrice: `$${props.price.toFixed(2)}`,
isDiscount: props.price < props.originalPrice,
};
}
}Note: If this initial state needs to UPDATE when props change later, that's a getDerivedStateFromProps case - see componentWillReceiveProps.md Case B.
If a single componentWillMount does both state init AND side effects:
// Mixed - state init + fetch
componentWillMount() {
this.setState({ loading: true, items: [] }); // Case A
fetch('/api/items').then(r => r.json()) // Case B
.then(items => this.setState({ items, loading: false }));
}Split them:
constructor(props) {
super(props);
this.state = { loading: true, items: [] }; // Case A → constructor
}
componentDidMount() {
fetch('/api/items').then(r => r.json()) // Case B → componentDidMount
.then(items => this.setState({ items, loading: false }));
}