Skip to content
C Codeloom
React

React Props and State for Beginners

A practical beginner's guide to React props and state — how data flows down through props, how state holds data that changes over time, and the rules that keep your UI predictable.

·9 min read · By Yash Kesharwani
Beginner 11 min read

What you'll learn

  • What props are and how data flows down through your component tree
  • How to destructure props and set sensible defaults
  • What state is, and how it differs from props
  • How useState works at a practical level
  • How a child component can talk back to its parent
  • The rules that keep state changes predictable

Prerequisites

  • A working understanding of components and JSX — see Components and JSX
  • Comfort with destructuring and arrow functions

Props and state are the two ways data enters a React component. Once you understand the difference, the rest of React is largely the same idea applied in different places.

Props: passing data into a component

A prop is an input to a component. You pass props the same way you pass HTML attributes — as named values on the tag.

function Greeting(props) {
  return <h1>Hello, {props.name}</h1>;
}

<Greeting name="Ada" />

props is always an object. Inside the component, the value you passed is available as props.name.

Destructure your props

Almost every real codebase destructures props directly in the function signature. It reads better and removes a layer of indirection.

function Greeting({ name }) {
  return <h1>Hello, {name}</h1>;
}

You can destructure multiple props at once:

function UserCard({ name, role, avatarUrl }) {
  return (
    <div>
      <img src={avatarUrl} alt="" />
      <h2>{name}</h2>
      <p>{role}</p>
    </div>
  );
}

<UserCard name="Ada" role="Engineer" avatarUrl="/ada.png" />

Default values

Give a prop a default by using destructuring defaults:

function Button({ label = "Click me", disabled = false }) {
  return <button disabled={disabled}>{label}</button>;
}

Now <Button /> renders a button labelled “Click me” and <Button label="Save" /> overrides the default.

Props can be anything

A prop is just a JavaScript value. Strings, numbers, booleans, arrays, objects, and even functions are all valid.

<UserCard
  name="Ada"
  age={36}
  isActive
  tags={['react', 'web']}
  onSelect={() => console.log('clicked')}
/>

A few notes on syntax:

  • Wrap non-string values in { }. age=36 is invalid; age={36} is correct.
  • A boolean prop with no value (isActive) is shorthand for isActive={true}.
  • Functions are passed as props all the time — that is how children talk back to parents.

Props are read-only

Inside a component, you must never reassign a prop.

function Bad({ name }) {
  name = name.toUpperCase();   // do not do this
  return <h1>{name}</h1>;
}

If you need a derived value, compute it into a new variable:

function Good({ name }) {
  const displayName = name.toUpperCase();
  return <h1>{displayName}</h1>;
}

This rule keeps data flow one-directional and predictable. A child component never modifies its parent’s data — it asks the parent to do so, which we cover below.

Try it yourself. Build a Price component that accepts amount (number) and currency (string, default "USD"). Render it as $19.99 USD. Use destructuring with a default and one computed string. Render three different <Price> elements from App.jsx to confirm reuse.

State: data a component owns

Props come from outside. State is data the component owns and can change over time. State is the reason a counter can count, a form can hold typed text, a modal can open and close.

In modern React, state lives inside a hook called useState.

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

Three things are going on.

  1. useState(0) declares one piece of state with an initial value of 0.
  2. It returns an array of two things: the current value and a setter function. Destructuring gives them names — by convention, count and setCount.
  3. Calling setCount(...) tells React the value changed. React re-renders the component, and the new value shows up in the JSX.

You can have as many useState calls as you need:

function Form() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [agreed, setAgreed] = useState(false);
  // ...
}

We cover useState more thoroughly in the next post. The summary above is enough for this one.

Props vs. state at a glance

A short comparison that resolves most beginner confusion:

PropsState
Set byThe parentThe component itself
MutableNo — read-onlyYes — via the setter
Causes re-renderWhen the parent re-rendersWhen the setter is called
Lives inThe parent’s JSXThe component itself

Rule of thumb: if a piece of data is passed in from above, it is a prop. If the component itself creates or changes it, it is state.

Lifting state up

Often, two sibling components need to share the same data. React’s answer is to lift the state up to their common parent and pass it back down as props.

Consider a temperature input that shows whether water boils:

import { useState } from 'react';

function TemperatureInput({ temperature, onChange }) {
  return (
    <input
      type="number"
      value={temperature}
      onChange={(e) => onChange(e.target.value)}
    />
  );
}

function BoilingVerdict({ celsius }) {
  if (celsius >= 100) return <p>The water would boil.</p>;
  return <p>The water would not boil.</p>;
}

function Calculator() {
  const [temperature, setTemperature] = useState('');
  return (
    <div>
      <TemperatureInput temperature={temperature} onChange={setTemperature} />
      <BoilingVerdict celsius={Number(temperature)} />
    </div>
  );
}

Notice how the state lives in Calculator. Both children receive what they need — the input gets the current value plus a callback to change it, the verdict gets the derived number. This pattern — state up, props down, callbacks back up — is the spine of every React app.

Callbacks: how children talk back

A child cannot modify its parent’s state directly. Instead, the parent passes a function as a prop and the child calls it.

function Toggle({ onToggle }) {
  return <button onClick={onToggle}>Toggle</button>;
}

function Panel() {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <Toggle onToggle={() => setOpen(!open)} />
      {open && <p>Panel is open.</p>}
    </div>
  );
}

By convention, callback props start with on (onClick, onSubmit, onSelect). It is a strong signal to readers that this prop is an event handler.

Rules that keep state predictable

A few rules that look pedantic until you violate them and watch the UI break.

1. Never mutate state directly

This is the most important rule in React.

// Wrong — React will not re-render
items.push(newItem);
setItems(items);

// Right — create a new array
setItems([...items, newItem]);

React compares values by reference. If you push into the same array, the reference does not change and React assumes nothing happened. Always create a new array or object.

Common patterns:

// Add to an array
setItems([...items, newItem]);

// Remove from an array
setItems(items.filter((item) => item.id !== id));

// Update an array item
setItems(items.map((item) =>
  item.id === id ? { ...item, done: true } : item
));

// Update an object
setUser({ ...user, name: newName });

The spread operator (...) is your closest friend in React.

2. State updates are asynchronous

Calling the setter does not immediately change the variable in the surrounding code.

function bad() {
  setCount(count + 1);
  console.log(count);   // still the old value!
}

The new value only shows up on the next render. If you need to update state based on the previous value, pass a function to the setter:

setCount((prev) => prev + 1);

This guarantees you read the most recent value. Always use the functional form when the new state depends on the old.

Two related values can live in one state object:

const [position, setPosition] = useState({ x: 0, y: 0 });
setPosition({ ...position, x: 100 });

But two unrelated values should stay separate:

const [name, setName] = useState('');
const [open, setOpen] = useState(false);

The cost of a single useState is essentially zero, so favour clarity.

Try it yourself. Build a TodoList with two child components — TodoInput and TodoItems. Lift the list state into the parent. The input adds a new item on submit. Each rendered item has a delete button that removes it. Use the immutable patterns from the rule above.

A small worked example

A working sign-up form combining props, state, and lifting:

import { useState } from 'react';

function Field({ label, value, onChange, type = 'text' }) {
  return (
    <label style={{ display: 'block', marginBottom: '0.5rem' }}>
      {label}
      <input
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
    </label>
  );
}

function SignUp() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const canSubmit = name.length > 0 && email.includes('@');

  function handleSubmit(event) {
    event.preventDefault();
    console.log({ name, email });
  }

  return (
    <form onSubmit={handleSubmit}>
      <Field label="Name" value={name} onChange={setName} />
      <Field label="Email" value={email} onChange={setEmail} type="email" />
      <button type="submit" disabled={!canSubmit}>Sign up</button>
    </form>
  );
}

export default SignUp;

Every idea in this post appears in those twenty-five lines. Field receives label, value, onChange, and type as props. SignUp owns the state. The button’s disabled prop is computed directly from state on every render. The setter for each field is passed down as a callback prop.

Recap

You now know:

  • Props pass data from parent to child and are read-only
  • State is data a component owns and can change with a setter
  • useState returns [value, setter]; calling the setter triggers a re-render
  • Sibling components share data by lifting state up to a common parent
  • Children talk back to parents by calling a callback prop
  • Never mutate state — always create new arrays and objects
  • Use the functional setter when the new state depends on the previous one

Next steps

The next post drills into the two hooks every React developer uses constantly — useState for state you have now seen, and useEffect for things that happen on the side: data fetching, subscriptions, and integration with the outside world.

→ Next: The Most Important React Hooks: useState and useEffect

Questions or feedback? Email codeloomdevv@gmail.com.