Skip to content
Educora
Advanced24 min25 / 27

React fundamentals

Build interfaces from components: JSX, props, state with `useState`, side effects with `useEffect` and lists with keys — the core ideas of the most popular UI library.

Check yourself
In this lesson you will learn
  • Explain components and JSX, and how JSX turns into function calls
  • Pass data with props and keep changing data in useState, updating it immutably
  • Load data with useEffect (dependencies and cleanup) and render lists with stable keys

Remember the to-do list from the DOM lesson: find the element, create an li, append it, toggle a class... As an app grows, keeping the page in sync with the data by hand turns into a mess. React, created at Facebook and open-sourced in 2013, flips the approach: you describe what the interface should look like for the current data, and React updates the DOM itself. In short: UI = f(state).

Components and JSX

A component is a function that returns a description of the interface; its name starts with a capital letter. JSX is HTML-like syntax inside JavaScript, and inside curly braces { } you can write any JavaScript expression. In JSX you use className instead of class, and camelCase names such as onClick for events. A component returns one root element (or a <>...</> fragment).

JavaScript
function Greeting({ name, city }) {
  const hour = 9;
  return (
    <div className="card">
      <h2>Hello, {name}!</h2>
      <p>{city ? `You are in ${city}` : 'Where are you?'}</p>
      <p>{hour < 12 ? 'Good morning' : 'Good afternoon'}</p>
    </div>
  );
}

export default function App() {
  return (
    <>
      <Greeting name="Aysel" city="Baku" />
      <Greeting name="Murad" />
    </>
  );
}
Two cards appear on the screen: “Hello, Aysel! You are in Baku” and “Hello, Murad! Where are you?”. Browsers don't understand JSX directly — a build tool such as Vite turns it into plain JavaScript.

JSX is just function calls: <h2 className="title">Hi</h2> becomes a call that creates an object describing the element's type, props and children. Let's repeat this ourselves with a tiny h function — what React calls the “virtual DOM” is exactly a tree of such objects.

JavaScript
function h(type, props, ...children) {
  return { type, props: props ?? {}, children: children.flat() };
}

function render(node) {
  if (typeof node === 'string' || typeof node === 'number') return String(node);
  if (typeof node.type === 'function') return render(node.type(node.props));
  const attrs = Object.entries(node.props)
    .map(([key, value]) => ` ${key === 'className' ? 'class' : key}="${value}"`)
    .join('');
  return `<${node.type}${attrs}>${node.children.map(render).join('')}</${node.type}>`;
}

const Greeting = ({ name }) => h('h2', { className: 'title' }, 'Hello, ', name, '!');

const app = h('div', null, h(Greeting, { name: 'Aysel' }), h('p', null, 'Items: ', 3));
console.log(JSON.stringify(app.children[1]));
console.log(render(app));
▸ Expected output
{"type":"p","props":{},"children":["Items: ",3]}
<div><h2 class="title">Hello, Aysel!</h2><p>Items: 3</p></div>
React compares this tree with the previous one and changes only the differences in the DOM — this is called reconciliation.

Props

Props are a component's inputs, passed like HTML attributes: strings in quotes, everything else — numbers, arrays, objects, functions — in curly braces. Props are read-only: a component must not change them. Data flows down (parent → child), while events travel up through callbacks passed as props. The content between the tags goes into the children prop.

JavaScript
function ProductCard({ title, price, inStock = true, onBuy }) {
  return (
    <article className="product">
      <h3>{title}</h3>
      <p>{price.toFixed(2)} AZN</p>
      {inStock ? (
        <button onClick={() => onBuy(title)}>Buy</button>
      ) : (
        <span>Out of stock</span>
      )}
    </article>
  );
}

// inside a parent component:
<ProductCard title="Notebook" price={4.5} onBuy={(name) => console.log('Buying', name)} />
<ProductCard title="Pen" price={1} inStock={false} />
The first card has a “Buy” button — clicking it prints Buying Notebook in the console. The second card says “Out of stock”.

State with useState

State is data that changes over time and must update the interface: a counter, typed text, a list of tasks. const [value, setValue] = useState(initial) gives you the current value and a function that changes it. When setValue is called, React calls the component function again with the new value (a re-render). A direct assignment such as count = count + 1 does not update the screen. Hooks are called only at the top level of a component — never inside an if or a loop.

JavaScript
import { useState } from 'react';

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

  return (
    <div>
      <p>Clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount((c) => c + 3)}>+3</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}
JavaScript
let todos = [
  { id: 1, text: 'Learn JSX', done: true },
  { id: 2, text: 'Learn useState', done: false },
];

const addTodo = (list, text) => [...list, { id: list.length + 1, text, done: false }];
const toggleTodo = (list, id) =>
  list.map((todo) => (todo.id === id ? { ...todo, done: !todo.done } : todo));
const removeTodo = (list, id) => list.filter((todo) => todo.id !== id);

const before = todos;
todos = addTodo(todos, 'Learn useEffect');
todos = toggleTodo(todos, 2);
todos = removeTodo(todos, 1);

console.log(todos.map((t) => `${t.done ? '[x]' : '[ ]'} ${t.text}`));
console.log(before === todos, before.length);
▸ Expected output
[ '[x] Learn useState', '[ ] Learn useEffect' ]
false 2
In a React component these functions are called like setTodos((list) => addTodo(list, text)). The old array stays untouched — React sees the new reference and updates the screen.

Effects with useEffect

Rendering must be pure: it just computes the interface from props and state. Work that touches the outside world — loading data, timers, subscriptions — is a side effect and goes into useEffect(fn, dependencies). React runs the effect after the render has reached the screen. The dependency array controls the timing: [] — once after the first render; [userId] — every time userId changes; no array — after every render. The function returned from an effect is the cleanup: it cancels timers and requests when the component disappears or before the effect runs again.

JavaScript
import { useEffect, useState } from 'react';

function UserCard({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState('');

  useEffect(() => {
    const controller = new AbortController();
    setUser(null);
    fetch(`https://api.example.com/users/${userId}`, { signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(setUser)
      .catch((err) => {
        if (err.name !== 'AbortError') setError(err.message);
      });
    return () => controller.abort();
  }, [userId]);

  if (error) return <p>Error: {error}</p>;
  if (!user) return <p>Loading...</p>;
  return <h2>{user.name}</h2>;
}
When userId changes, the cleanup cancels the old request — a late answer for the old user cannot overwrite the new one.

Lists and keys

You render an array with map: JSX is returned for every item, and each one needs a key — an identifier that is unique among its siblings and stable (usually the id from the data). Keys let React recognise items between renders: when something is added, removed or moved, it knows which DOM node belongs to which item. If the list can change, don't use the index as the key.

JavaScript
function TodoList({ todos, onToggle }) {
  if (todos.length === 0) return <p>Nothing to do!</p>;

  return (
    <ul>
      {todos.map((todo) => (
        <li key={todo.id} className={todo.done ? 'done' : ''} onClick={() => onToggle(todo.id)}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}
  1. 1
    Create a project

    Run in a terminal: npm create vite@latest my-app -- --template react

  2. 2
    Install the dependencies

    cd my-app and npm install

  3. 3
    Start it

    npm run dev, then open http://localhost:5173 in a browser. The page updates instantly as you edit src/App.jsx.

Exercise

The useReducer hook works with reducers exactly like this. Complete cartReducer(cart, action): add adds a new product with qty: 1, increment increases a product's quantity by 1, remove deletes a product. Don't change any array or object — always return a new one.

Exercise · JavaScript
function cartReducer(cart, action) {
  switch (action.type) {
    case 'add':
      // return a new array with { id: action.id, qty: 1 } at the end
    case 'increment':
      // return a new array where that item has qty + 1
    case 'remove':
      // return a new array without that item
    default:
      return cart;
  }
}

const start = [{ id: 'tea', qty: 1 }];
const actions = [
  { type: 'add', id: 'bread' },
  { type: 'increment', id: 'tea' },
  { type: 'increment', id: 'tea' },
  { type: 'remove', id: 'bread' },
];

console.log(actions.reduce(cartReducer, start));
console.log(start);
▸ Expected output
[ { id: 'tea', qty: 3 } ]
[ { id: 'tea', qty: 1 } ]
Exercise

The h and render functions from the lesson are given. Use h to write a TodoList({ todos }) component: it returns a ul with one li per task, and the li of a finished task gets className: 'done'.

Exercise · JavaScript
function h(type, props, ...children) {
  return { type, props: props ?? {}, children: children.flat() };
}

function render(node) {
  if (typeof node === 'string' || typeof node === 'number') return String(node);
  if (typeof node.type === 'function') return render(node.type(node.props));
  const attrs = Object.entries(node.props)
    .map(([key, value]) => ` ${key === 'className' ? 'class' : key}="${value}"`)
    .join('');
  return `<${node.type}${attrs}>${node.children.map(render).join('')}</${node.type}>`;
}

// map todos to li elements inside the ul
const TodoList = ({ todos }) => h('ul', null);

const todos = [
  { id: 1, text: 'Learn JSX', done: true },
  { id: 2, text: 'Learn hooks', done: false },
];
console.log(render(h(TodoList, { todos })));
▸ Expected output
<ul><li class="done">Learn JSX</li><li>Learn hooks</li></ul>

Key points

  • A component is a function that returns UI from props; JSX compiles into function calls that describe elements.
  • Props flow down and are read-only; events travel up through callbacks passed as props.
  • useState gives a value and a setter; calling the setter re-renders; update arrays and objects immutably.
  • useEffect is for side effects after the render: the dependency array picks the timing, and the returned function cleans up.
  • Lists are rendered with map, and each item needs a stable, unique key — not the index if the list can change.

Check yourself

10 questions. Every correct answer earns XP.

1 / 10
What does JSX become after the build step?