DailyDevDiet

logo - dailydevdiet

Learn. Build. Innovate. Elevate your coding skills with dailydevdiet!

Chapter 6: State and Props

State and Props

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:

PropsState
Passed from parent componentDefined within the component
Cannot be changed by componentCan be changed by component
Used for configurationUsed for component’s internal data
Received as function parametersInitialized and managed with useState or this.state
Similar to function argumentsSimilar 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:

  1. State lives in a parent component
  2. State is passed down to children as props
  3. When state changes are needed, children call functions passed as props
  4. Parent updates its state
  5. 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.

Additional Resources

Scroll to Top