React Props, State and Events: useState and Updating Without Mutation

MERN Stack Full Course: MongoDB, Express, React and Node.js · React Fundamentals · 6 min read · Updated

Props down, events up

  1. State is data that can change over time. The parent component keeps it with useState.
  2. The parent passes data to a child through props. Props are read-only.
  3. The user clicks a button in the child, and an event handler runs.
  4. The handler calls a function from the parent, which sets new state. We make a new array and never change the old one.
  5. React sees the new state and draws the screen again.

A screen that never changes is just a page. Real apps respond: you click, type, choose. React gives you three tools for that, and nearly everything else in the library builds on them. Props carry data *into* a component. State is data that belongs to a component and can *change*. Events are how the user's actions reach your code.

Props: inputs for a component

// Props are the inputs of a component. This one takes a `status` and picks a class from it.
export default function StatusBadge({ status }) {
  return <span className={`badge badge-${status}`}>{status}</span>;
}

Props are passed like HTML attributes, <StatusBadge status="applied" />, and received as one object, which we usually destructure right in the parameter list: function StatusBadge({ status }). Props are read only. A component must never change its own props, because they belong to its parent. If something has to change, the parent changes it and passes new props down.

import StatusBadge from './StatusBadge.jsx';

export const STATUSES = ['saved', 'applied', 'interview', 'offer', 'rejected'];

// `job` and `onStatusChange` are props. The card does not own the data; it only shows it and reports clicks.
export default function JobCard({ job, onStatusChange }) {
  return (
    <article className="card">
      <h2>{job.role}</h2>
      <p>{job.company}{job.location ? ` · ${job.location}` : ''}</p>
      <StatusBadge status={job.status} />
      <div className="row">
        <label className="muted" htmlFor={`status-${job.id}`}>Move to</label>
        <select id={`status-${job.id}`} value={job.status} onChange={(e) => onStatusChange(job.id, e.target.value)}>
          {STATUSES.map((s) => <option key={s} value={s}>{s}</option>)}
        </select>
      </div>
    </article>
  );
}

JobCard now receives two props: a job object, and a function onStatusChange. Passing a function as a prop is the way a child tells its parent that something happened. The card does not know how the list is stored, and it does not need to. This is the pattern: data flows down as props; events flow up as function calls.

State: data that changes

import { useState } from 'react';
import Header from './components/Header.jsx';
import JobCard from './components/JobCard.jsx';

const SAMPLE = [
  { id: 1, company: 'Northwind Bank', role: 'Teller', location: 'Leeds', status: 'applied' },
  { id: 2, company: 'Acme Logistics', role: 'Dispatcher', location: 'Pune', status: 'interview' },
];

export default function App() {
  // State is data that changes over time. When you call setJobs, React draws the screen again.
  const [jobs, setJobs] = useState(SAMPLE);

  function changeStatus(id, status) {
    // Never change state in place. Make a new array with a new object for the one job that changed.
    setJobs((current) => current.map((j) => (j.id === id ? { ...j, status } : j)));
  }

  return (
    <>
      <Header />
      <main className="container">
        <h1>My jobs ({jobs.length})</h1>
        {jobs.map((job) => <JobCard key={job.id} job={job} onStatusChange={changeStatus} />)}
      </main>
    </>
  );
}

const [jobs, setJobs] = useState(SAMPLE) creates a piece of state. useState returns a pair: the current value, and a function that sets a new one. When you call the setter, React remembers the new value and runs the component again to draw the screen for it. You never edit the page by hand. You change the state and the page follows.

Never change state in place

The most important rule in this lesson: do not mutate state. jobs[0].status = 'offer' followed by setJobs(jobs) does nothing useful, because React checks whether the value is *a different one* and finds the same array. You must give it a new array and new objects for what changed. That is why lesson 3 taught spread and map.

// Wrong: changes the existing object, so React may not notice
jobs[0].status = 'offer'; setJobs(jobs);

// Right: a new array, and a new object only for the job that changed
setJobs((current) => current.map((j) => (j.id === id ? { ...j, status } : j)));

Notice that the setter was given a function (current) => .... Use that form whenever the new state depends on the old one. It always receives the latest value, even if several updates happen close together, which a plain setJobs(jobs.map(...)) can miss.

Events

Attach a handler with a camel-case prop: onClick, onChange, onSubmit. You pass the function itself, not the result of calling it: onClick={save} is right, onClick={save()} runs save during drawing and is a bug. For inputs, e.target.value holds what the person typed. In JobCard, the <select>'s onChange calls the parent's function with the job id and the chosen status.

Keys

When you draw a list with .map(), give every item a `key` that is stable and unique among its siblings: here key={job.id}. React uses keys to tell which item is which when the list changes, so it keeps the right state with the right item. Do not use the array index as a key for lists that can be reordered or have items removed.

Your task

  1. Run the app. Change a job's status with the dropdown and watch the badge colour change.
  2. Add a state variable showOnlyActive and a checkbox that hides jobs whose status is rejected. Use .filter() while drawing; do not delete from the array.
  3. Add a button to each card that cycles the status to the next one in the list.
  4. Open React Developer Tools (a browser extension) and look at the state of App while you click.

How to check it worked

  • Choosing a status changes only that card.
  • You can explain why setJobs((current) => ...) is safer than setJobs(jobs.map(...)).

Summary

  • Props flow down and are read only; functions passed as props let children report events up.
  • useState gives a value and a setter; calling the setter redraws the component.
  • Never mutate state: create new arrays and objects, and give list items stable keys.

Download the solution (mern-lesson-26.zip)

Frequently asked questions

What is the difference between props and state?

Props are inputs a component receives from its parent and must not change. State is data the component owns and can change. When state changes React draws the component again.

Why does my state not update when I change an object?

React compares by reference. If you edit the same object and set it again, it looks unchanged. Create a new object or array (spread, map, filter) and set that instead.

Why do list items need keys?

Keys let React match old and new items when a list changes, so it can keep the right state and DOM with the right item. Use a stable unique id, not the index, when items can move or be removed.