Handling State Derived from Props Safely
A common React anti-pattern is initializing state from props without proper validation or update handling, which can lead to bugs when props change. Here’s how to do it correctly.
The Problematic Pattern
// ❌ Dangerous - copies props to state without validation or updates
class UserProfile extends React.Component {
constructor(props) {
super(props);
this.state = {
username: props.username,
email: props.email
};
}
// ...
}
Recommended Solutions
1. For Class Components: Use getDerivedStateFromProps
class UserProfile extends React.Component {
state = {
username: '',
email: ''
};
static getDerivedStateFromProps(props, state) {
return {
username: props.username || state.username,
email: props.email || state.email
};
}
}
2. For Function Components: Use useEffect
with Dependencies
function UserProfile({ username, email }) {
const [userData, setUserData] = useState({ username: '', email: '' });
useEffect(() => {
setUserData({
username: username || 'Anonymous',
email: email || 'no-email@example.com'
});
}, [username, email]); // Only update when these props change
}
3. With Validation (Recommended)
function UserProfile({ username, email }) {
const [userData, setUserData] = useState(() => ({
username: validateUsername(username),
email: validateEmail(email)
}));
useEffect(() => {
setUserData({
username: validateUsername(username),
email: validateEmail(email)
});
}, [username, email]);
function validateUsername(name) {
if (!name || typeof name !== 'string') return 'Anonymous';
return name.trim();
}
function validateEmail(email) {
const re = /\S+@\S+\.\S+/;
return re.test(email) ? email : 'invalid-email@example.com';
}
}
When to Use Derived State
Only use derived state when:
- You need to modify the prop values before using them
- You need to maintain local versions that can change independently
- You need to compute expensive values from props
Anti-Patterns to Avoid
- Unconditional state copying:
// ❌ Bad - overwrites local state every time props change
useEffect(() => {
setUserData({ username, email });
}, [username, email]);
- Ignoring prop changes:
// ❌ Bad - state becomes stale if props change
const [userData] = useState({ username, email });
- Two-way data binding:
// ❌ Avoid - makes data flow hard to understand
// (parent manages some state, child manages other state)
Best Practices
- Validate all incoming props before using them
- Use memoization for expensive computations:
const fullName = useMemo(() => {
return `${firstName} ${lastName}`;
}, [firstName, lastName]);
- Consider lifting state up if synchronization becomes complex
- Use TypeScript to catch prop type issues at compile time
- Document expected prop shapes with PropTypes or TypeScript interfaces
Remember that in most cases, you don’t need to copy props to state – you can use the props directly. Only use derived state when you specifically need to modify or control the prop values within your component.