
Introduction to State and Props
State and props are the two primary mechanisms for managing data in React components. Understanding how they work and when to use each is crucial for building dynamic and interactive applications.
- Props (short for properties) are passed from parent to child components
- State is managed within a component and can change over time
Let’s dive into each concept and see how they work together.
Props: Component Communication
What Are Props?
Props are inputs that a component receives from its parent. They are read-only and help make your components reusable by allowing them to receive different data.
Basic Props Usage
Props are passed as attributes in JSX:
function Welcome(props) {
 return <h1>Hello, {props.name}</h1>;
}
function App() {
 return (
  <div>
   <Welcome name="Alice" />
   <Welcome name="Bob" />
   <Welcome name="Charlie" />
  </div>
 );
}
In this example, the Welcome component receives a name prop with different values.
Destructuring Props
A common pattern is to destructure props for cleaner code:
function Welcome({ name, age }) {
 return (
  <div>
   <h1>Hello, {name}</h1>
   <p>You are {age} years old</p>
  </div>
 );
}
// Usage
<Welcome name="Alice" age={28} />
Default Props
You can specify default values for props:
function Button({ label = "Click Me", type = "primary" }) {
 return <button className={`btn-${type}`}>{label}</button>;
}
// Default props usage
<Button /> // Renders "Click Me" with primary class
<Button label="Submit" /> // Renders "Submit" with primary class
<Button label="Cancel" type="secondary" /> // Renders "Cancel" with secondary class
For class components, you can use the defaultProps static property:
class Button extends React.Component {
render() {
return (
<button className={`btn-${this.props.type}`}>
{this.props.label}
</button>
);
}
}
Button.defaultProps = {
label: "Click Me",
type: "primary"
};
Props Validation with PropTypes
While not required, validating props can help catch bugs. You’ll need to install the prop-types package:
npm install prop-types
Then use it in your components:
import PropTypes from 'prop-types';
function User({ name, age, isAdmin }) {
return (
<div>
<h1>{name}</h1>
<p>Age: {age}</p>
{isAdmin && <p>Admin User</p>}
</div>
);
}
User.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
isAdmin: PropTypes.bool
};
This helps catch errors such as passing a string to age or forgetting to pass the required name prop.
Children Props
The special children prop allows you to pass components as content:
function Card({ title, children }) {
return (
<div className="card">
<div className="card-header">
<h2>{title}</h2>
</div>
<div className="card-body">
{children}
</div>
</div>
);
}
// Usage
function App() {
return (
<Card title="Welcome">
<p>This is the card content!</p>
<button>Learn More</button>
</Card>
);
}
Passing Multiple Props with Spread Operator
You can pass multiple props at once using the spread operator:
function App() {
const userProps = {
name: "Alice",
age: 28,
location: "New York"
};
return <UserProfile {...userProps} />;
}
Props in Class Components
In class components, props are accessed via this.props:
class Welcome extends React.Component {
 render() {
  return <h1>Hello, {this.props.name}</h1>;
 }
}
State: Component Memory
What Is State?
State represents data that changes over time within a component. Unlike props, state is private and fully controlled by the component.
State in Class Components
In class components, state is initialized in the constructor and updated using setState():
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
increment = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
State Updates May Be Asynchronous
React may batch multiple setState() calls for performance. When state updates depend on the previous state, use the function form:
// This may not work as expected if multiple updates happen
this.setState({ count: this.state.count + 1 });
// Better approach with functional update
this.setState((prevState) => {
 return { count: prevState.count + 1 };
});
State Updates Are Merged
When you call setState(), React merges the object you provide into the current state:
constructor(props) {
super(props);
this.state = {
count: 0,
user: {
name: "Guest",
isLoggedIn: false
}
};
}
// This only updates count, user remains unchanged
this.setState({ count: 1 });
State in Function Components with Hooks
Function components use the useState hook to manage state:
import React, { useState } from 'react';
function Counter() {
// Initialize state with default value
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
For multiple state values, you can use multiple useState calls:
function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubscribed, setIsSubscribed] = useState(false);
return (
<form>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<label>
<input
type="checkbox"
checked={isSubscribed}
onChange={(e) => setIsSubscribed(e.target.checked)}
/>
Subscribe to newsletter
</label>
</form>
);
}
Complex State with useState
For complex state objects, remember that useState doesn’t automatically merge objects like setState does:
function UserProfile() {
const [user, setUser] = useState({
name: 'Guest',
email: '',
preferences: { theme: 'light', notifications: true }
});
// Incorrect: This will replace the entire user object
const updateEmail = (newEmail) => {
setUser({ email: newEmail }); // This loses name and preferences!
};
// Correct: Spread the previous state
const updateEmailCorrect = (newEmail) => {
setUser(prevUser => ({
...prevUser,
email: newEmail
}));
};
// For nested objects
const toggleTheme = () => {
setUser(prevUser => ({
...prevUser,
preferences: {
...prevUser.preferences,
theme: prevUser.preferences.theme === 'light' ? 'dark' : 'light'
}
}));
};
return (
<div>
<input
value={user.email}
onChange={(e) => updateEmailCorrect(e.target.value)}
/>
<button onClick={toggleTheme}>
Toggle Theme (Current: {user.preferences.theme})
</button>
</div>
);
}
When to Use Props vs. State
This table helps identify when to use props vs. state:
Props | State |
Passed from parent component | Defined within the component |
Cannot be changed by component | Can be changed by component |
Used for configuration | Used for component’s internal data |
Received as function parameters | Initialized and managed with useState or this.state |
Similar to function arguments | Similar to local variables |
Lifting State Up
Sometimes, multiple components need to reflect the same changing data. In such cases, lift the shared state up to their closest common ancestor:
function TemperatureCalculator() {
const [temperature, setTemperature] = useState('');
const [scale, setScale] = useState('c');
const handleCelsiusChange = (temperature) => {
setTemperature(temperature);
setScale('c');
};
const handleFahrenheitChange = (temperature) => {
setTemperature(temperature);
setScale('f');
};
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={handleCelsiusChange}
/>
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={handleFahrenheitChange}
/>
<BoilingVerdict celsius={parseFloat(celsius)} />
</div>
);
}
function BoilingVerdict({ celsius }) {
if (isNaN(celsius)) {
return null;
}
return celsius >= 100 ?
<p>The water would boil.</p> :
<p>The water would not boil.</p>;
}
function TemperatureInput({ scale, temperature, onTemperatureChange }) {
const scaleNames = {
c: 'Celsius',
f: 'Fahrenheit'
};
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input
value={temperature}
onChange={(e) => onTemperatureChange(e.target.value)}
/>
</fieldset>
);
}
// Helper functions
function toCelsius(fahrenheit) {
return (fahrenheit - 32) * 5 / 9;
}
function toFahrenheit(celsius) {
return (celsius * 9 / 5) + 32;
}
function tryConvert(temperature, convert) {
const input = parseFloat(temperature);
if (Number.isNaN(input)) {
return '';
}
const output = convert(input);
const rounded = Math.round(output * 1000) / 1000;
return rounded.toString();
}
Unidirectional Data Flow
React follows a unidirectional data flow:
- State lives in a parent component
- State is passed down to children as props
- When state changes are needed, children call functions passed as props
- Parent updates its state
- Updates flow down to children as new props
This pattern makes applications more predictable and easier to debug.
Controlled vs Uncontrolled Components
Controlled Components
In controlled components, form data is handled by React state:
function NameForm() {
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
alert('A name was submitted: ' + name);
};
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<button type="submit">Submit</button>
</form>
);
}
Uncontrolled Components
Uncontrolled components let the DOM handle form data:
function FileUploadForm() {
const fileInput = React.useRef(null);
const handleSubmit = (e) => {
e.preventDefault();
alert(`Selected file: ${fileInput.current.files[0].name}`);
};
return (
<form onSubmit={handleSubmit}>
<label>
Upload file:
<input type="file" ref={fileInput} />
</label>
<button type="submit">Submit</button>
</form>
);
}
Practical Example: Creating a Form with State and Props
Let’s create a user registration form that demonstrates state, props, and component composition:
// UserRegistrationForm.js
import React, { useState } from 'react';
function Input({ label, type, value, onChange, required }) {
return (
<div className="form-group">
<label>{label}{required && <span className="required">*</span>}</label>
<input
type={type}
value={value}
onChange={onChange}
required={required}
className="form-control"
/>
</div>
);
}
function UserRegistrationForm() {
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: ''
});
const [errors, setErrors] = useState({});
const [isSubmitted, setIsSubmitted] = useState(false);
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value
});
// Clear error when field is edited
if (errors[name]) {
setErrors({
...errors,
[name]: null
});
}
};
const validate = () => {
const newErrors = {};
// Basic validation
if (!formData.firstName.trim()) {
newErrors.firstName = 'First name is required';
}
if (!formData.lastName.trim()) {
newErrors.lastName = 'Last name is required';
}
if (!formData.email.trim()) {
newErrors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
newErrors.email = 'Email is invalid';
}
if (!formData.password) {
newErrors.password = 'Password is required';
} else if (formData.password.length < 8) {
newErrors.password = 'Password must be at least 8 characters';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'Passwords do not match';
}
return newErrors;
};
const handleSubmit = (e) => {
e.preventDefault();
const validationErrors = validate();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
// Form is valid, submit it
console.log('Form submitted:', formData);
setIsSubmitted(true);
};
if (isSubmitted) {
return (
<div className="success-message">
<h2>Registration Successful!</h2>
<p>Thank you for registering, {formData.firstName}!</p>
</div>
);
}
return (
<div className="registration-form">
<h2>Create an Account</h2>
<form onSubmit={handleSubmit}>
<div className="form-row">
<div className="form-column">
<Input
label="First Name"
type="text"
value={formData.firstName}
onChange={(e) => handleChange({ target: { name: 'firstName', value: e.target.value } })}
required={true}
/>
{errors.firstName && <div className="error">{errors.firstName}</div>}
</div>
<div className="form-column">
<Input
label="Last Name"
type="text"
value={formData.lastName}
onChange={(e) => handleChange({ target: { name: 'lastName', value: e.target.value } })}
required={true}
/>
{errors.lastName && <div className="error">{errors.lastName}</div>}
</div>
</div>
<Input
label="Email"
type="email"
value={formData.email}
onChange={(e) => handleChange({ target: { name: 'email', value: e.target.value } })}
required={true}
/>
{errors.email && <div className="error">{errors.email}</div>}
<Input
label="Password"
type="password"
value={formData.password}
onChange={(e) => handleChange({ target: { name: 'password', value: e.target.value } })}
required={true}
/>
{errors.password && <div className="error">{errors.password}</div>}
<Input
label="Confirm Password"
type="password"
value={formData.confirmPassword}
onChange={(e) => handleChange({ target: { name: 'confirmPassword', value: e.target.value } })}
required={true}
/>
{errors.confirmPassword && <div className="error">{errors.confirmPassword}</div>}
<button type="submit" className="submit-button">Register</button>
</form>
</div>
);
}
export default UserRegistrationForm;
Common Pitfalls
1. Mutating State Directly
Never modify state directly:
// Wrong
this.state.count = this.state.count + 1;
// Correct for class components
this.setState({ count: this.state.count + 1 });
// Correct for function components
setCount(count + 1);
2. Overusing State
Not everything needs to be in state. Derived values should be calculated during render:
// Don't do this
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
const [fullName, setFullName] = useState('John Doe');
// When updating firstName
setFirstName(newName);
setFullName(newName + ' ' + lastName);
// Better approach
const [firstName, setFirstName] = useState('John');
const [lastName, setLastName] = useState('Doe');
// Calculate fullName during render
const fullName = `${firstName} ${lastName}`;
3. Not Using Function Updates for State That Depends on Previous State
// Might cause issues with batched updates
setCount(count + 1);
// Better approach
setCount(prevCount => prevCount + 1);
4. Not Using Object Spread for Partial Updates
// Wrong - this will lose existing user data
setUser({ name: 'New Name' });
// Correct
setUser(prevUser => ({ ...prevUser, name: 'New Name' }));
Summary
In this chapter, we’ve covered:
- The differences between props and state
- How to pass and use props
- Props validation with PropTypes
- Managing state in class and function components
- Lifting state up for shared state
- Unidirectional data flow in React applications
- Controlled vs. uncontrolled components
- Common pitfalls when working with state and props
- A practical example implementing a form with state and props
Understanding state and props is crucial for building React applications. State allows components to be dynamic and interactive, while props enable component reusability and composition. In the next chapter, we’ll explore React DOM and Virtual DOM, which are key to React’s performance optimization.