Back to blog

Phase 1: React Fundamentals — Components, Props, State & Hooks

reactjavascriptfrontendcomponentshooks
Phase 1: React Fundamentals — Components, Props, State & Hooks

Welcome to Phase 1 of the React & Next.js series! This post teaches you the core building blocks of React — the concepts you'll use in every React application you ever build.

We'll go hands-on immediately. By the end, you'll understand why React exists, how to think in components, and how to manage state and side effects with hooks.

Time commitment: 5–7 days, 1–2 hours daily Prerequisites: Basic JavaScript (variables, functions, arrays, objects, promises)

What You'll Learn

By the end of Phase 1, you'll be able to:

✅ Explain what React is and why it was created
✅ Write JSX and understand how it maps to JavaScript
✅ Build reusable components and compose them together
✅ Pass data between components with props
✅ Manage local UI state with useState
✅ Handle side effects with useEffect
✅ Handle events and form inputs in React
✅ Render lists efficiently with keys
✅ Apply conditional rendering patterns


Setting Up Your Environment

You don't need to configure anything to start learning React. Use one of these options:

Option A: Online playground (zero setup)

Go to react.new — a ready-to-go React sandbox in your browser.

Option B: Local project with Vite (recommended for learning)

npm create vite@latest react-fundamentals -- --template react
cd react-fundamentals
npm install
npm run dev

Open http://localhost:5173 and you're running React.

Later in this series (Post RN-4), you'll switch to Next.js. For now, Vite is the fastest way to run plain React locally.


What Is React and Why Does It Exist?

Before writing code, understand the problem React solves.

The Problem: Keeping UI in Sync with Data

Imagine a shopping cart. When a user adds an item, the cart icon count should update, the cart panel should show the new item, and the total price should recalculate — all at once.

With vanilla JavaScript, you'd manually find each DOM element and update it. This becomes a maintenance nightmare as apps grow. Keeping the UI in sync with changing data is the core challenge of frontend development.

React's Solution: Declarative UI

React flips the model. Instead of imperatively updating the DOM, you describe what the UI should look like for a given state, and React figures out the DOM updates.

// Imperative (vanilla JS) — you manage the DOM
document.getElementById('count').textContent = count;
document.getElementById('cart').classList.add('visible');
 
// Declarative (React) — you describe the result
function Cart({ items }) {
  return (
    <div className={items.length > 0 ? 'cart visible' : 'cart'}>
      <span>{items.length}</span>
    </div>
  );
}

When items changes, React re-renders Cart automatically. You don't touch the DOM.

The Virtual DOM

React maintains a lightweight copy of the DOM in memory — the Virtual DOM. When state changes, React:

  1. Re-renders the component to a new Virtual DOM tree
  2. Diffs it against the previous tree (reconciliation)
  3. Applies only the minimal changes to the real DOM

This is fast. And more importantly, it lets you write declarative code without thinking about DOM manipulation.


JSX: HTML in JavaScript

JSX is the syntax React uses to describe UI. It looks like HTML but compiles to regular JavaScript function calls.

// JSX
const element = <h1 className="title">Hello, React!</h1>;
 
// What it compiles to (you never write this)
const element = React.createElement('h1', { className: 'title' }, 'Hello, React!');

JSX Rules

1. Return a single root element

// ❌ Two sibling elements — invalid
return (
  <h1>Title</h1>
  <p>Content</p>
);
 
// ✅ Wrapped in a div
return (
  <div>
    <h1>Title</h1>
    <p>Content</p>
  </div>
);
 
// ✅ Or use a Fragment (no extra DOM node)
return (
  <>
    <h1>Title</h1>
    <p>Content</p>
  </>
);

2. Use className instead of class

// ❌ HTML attribute
<div class="container">
 
// ✅ JSX attribute
<div className="container">

3. Self-close tags that have no children

<img src="photo.jpg" alt="Photo" />
<input type="text" />
<br />

4. JavaScript expressions go inside {}

const name = "Alice";
const age = 30;
 
return (
  <div>
    <h1>Hello, {name}!</h1>
    <p>Age: {age}</p>
    <p>Born: {new Date().getFullYear() - age}</p>
    <p>{age >= 18 ? 'Adult' : 'Minor'}</p>
  </div>
);

5. Attributes use camelCase

<input
  type="text"
  onChange={handleChange}
  autoComplete="off"
  tabIndex={1}
/>

Components: The Building Blocks

A React component is a JavaScript function that returns JSX. Components are the core unit of React — everything is a component.

Your First Component

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

That's it. A component is just a function. React calls it and puts the returned JSX into the DOM.

Component Rules

  • Names must start with a capital letterGreeting, not greeting. React uses this to distinguish components from HTML tags.
  • Must return JSX (or null to render nothing)
  • Can be used like HTML tags in other components

Composing Components

Components nest inside other components, just like HTML elements:

function Avatar() {
  return <img src="/avatar.png" alt="User avatar" className="avatar" />;
}
 
function UserBio() {
  return (
    <div className="bio">
      <p>Software developer. Coffee enthusiast.</p>
    </div>
  );
}
 
function UserCard() {
  return (
    <div className="card">
      <Avatar />
      <UserBio />
    </div>
  );
}
 
function App() {
  return (
    <main>
      <h1>Team Members</h1>
      <UserCard />
      <UserCard />
      <UserCard />
    </main>
  );
}

This is the React component tree — your entire UI is a tree of components.


Props: Passing Data to Components

Props (short for properties) let you pass data from a parent component to a child. They make components reusable.

Basic Props

function Greeting({ name, role }) {
  return (
    <div>
      <h2>Hello, {name}!</h2>
      <p>Role: {role}</p>
    </div>
  );
}
 
function App() {
  return (
    <div>
      <Greeting name="Alice" role="Admin" />
      <Greeting name="Bob" role="Editor" />
      <Greeting name="Carol" role="Viewer" />
    </div>
  );
}

Default Props

function Button({ label, variant = 'primary', disabled = false }) {
  return (
    <button className={`btn btn-${variant}`} disabled={disabled}>
      {label}
    </button>
  );
}
 
// Uses defaults
<Button label="Save" />
 
// Overrides defaults
<Button label="Delete" variant="danger" disabled={true} />

The children Prop

Any JSX placed between a component's opening and closing tags becomes the children prop:

function Card({ children, title }) {
  return (
    <div className="card">
      <h3 className="card-title">{title}</h3>
      <div className="card-body">{children}</div>
    </div>
  );
}
 
// Usage
<Card title="Profile">
  <p>Name: Alice</p>
  <p>Email: alice@example.com</p>
</Card>

Props Are Read-Only

Never modify props inside a component. Props flow one way — down from parent to child. A component can only read its props, never change them.

// ❌ Never do this
function BadComponent({ count }) {
  count = count + 1; // Mutating props — wrong!
  return <p>{count}</p>;
}
 
// ✅ Use state for values that change
function GoodComponent({ initialCount }) {
  const [count, setCount] = useState(initialCount);
  return <p>{count}</p>;
}

State: Making Components Reactive

Props are external data passed in. State is internal data that a component owns and can change. When state changes, React re-renders the component.

useState

import { useState } from 'react';
 
function Counter() {
  const [count, setCount] = useState(0); // Initial value: 0
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
      <button onClick={() => setCount(0)}>Reset</button>
    </div>
  );
}

useState returns an array with two items:

  1. The current state value (count)
  2. A setter function (setCount) — call this to update the value

State Updates Are Asynchronous

React batches state updates for performance. Never rely on state being updated immediately after calling the setter:

// ❌ Stale state — count may not reflect latest value
function Counter() {
  const [count, setCount] = useState(0);
 
  function handleTripleClick() {
    setCount(count + 1); // All three use the same stale count
    setCount(count + 1);
    setCount(count + 1);
    // Result: count goes from 0 to 1, not 3
  }
}
 
// ✅ Functional update — always uses the latest state
function Counter() {
  const [count, setCount] = useState(0);
 
  function handleTripleClick() {
    setCount(prev => prev + 1); // Uses latest value each time
    setCount(prev => prev + 1);
    setCount(prev => prev + 1);
    // Result: count goes from 0 to 3 ✅
  }
}

Object State

function ProfileForm() {
  const [user, setUser] = useState({
    name: '',
    email: '',
    age: 0,
  });
 
  function updateName(newName) {
    // ✅ Spread the existing state, then override the changed field
    setUser(prev => ({ ...prev, name: newName }));
  }
 
  return (
    <form>
      <input
        value={user.name}
        onChange={e => setUser(prev => ({ ...prev, name: e.target.value }))}
        placeholder="Name"
      />
      <input
        value={user.email}
        onChange={e => setUser(prev => ({ ...prev, email: e.target.value }))}
        placeholder="Email"
      />
    </form>
  );
}

Where to Put State

A key React skill is deciding which component should own each piece of state.

Rule: Put state in the lowest common ancestor of all components that need it.

// ❌ State duplicated in two components — they'll get out of sync
function ComponentA() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
 
function ComponentB() {
  const [count, setCount] = useState(0); // Different count!
  return <p>Count: {count}</p>;
}
 
// ✅ State lifted to the parent — single source of truth
function Parent() {
  const [count, setCount] = useState(0);
  return (
    <>
      <ComponentA count={count} onIncrement={() => setCount(c => c + 1)} />
      <ComponentB count={count} />
    </>
  );
}

This pattern is called lifting state up — it's fundamental to React architecture.


Event Handling

React handles events with camelCase attributes and function references (not strings):

// ❌ HTML style
<button onclick="handleClick()">Click me</button>
 
// ✅ React style
<button onClick={handleClick}>Click me</button>

Event Handler Patterns

function EventExamples() {
  // Inline arrow function
  const handleClick = () => console.log('Clicked!');
 
  // With event object
  const handleInput = (event) => {
    console.log(event.target.value);
  };
 
  // Passing arguments
  const handleItemClick = (id) => {
    console.log('Item clicked:', id);
  };
 
  return (
    <div>
      <button onClick={handleClick}>Click me</button>
      <input onChange={handleInput} />
 
      {/* ✅ Arrow function to pass arguments */}
      <button onClick={() => handleItemClick(42)}>Item 42</button>
 
      {/* ❌ This calls handleItemClick immediately on render! */}
      <button onClick={handleItemClick(42)}>Wrong!</button>
    </div>
  );
}

Controlled Inputs

React forms use controlled inputs — the input value is driven by state:

function SearchBar() {
  const [query, setQuery] = useState('');
 
  const handleSubmit = (e) => {
    e.preventDefault(); // Prevent page reload
    console.log('Searching for:', query);
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}                          // Driven by state
        onChange={e => setQuery(e.target.value)} // Updates state
        placeholder="Search..."
      />
      <button type="submit">Search</button>
      <p>Query: {query}</p>
    </form>
  );
}

Conditional Rendering

React doesn't have a template syntax like v-if or *ngIf. You use regular JavaScript:

Ternary Operator

function UserStatus({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? (
        <p>Welcome back!</p>
      ) : (
        <p>Please log in.</p>
      )}
    </div>
  );
}

Logical AND (&&)

Render something only if a condition is true:

function Notification({ hasMessages, messageCount }) {
  return (
    <div>
      <h1>Inbox</h1>
      {hasMessages && (
        <p>You have {messageCount} unread messages.</p>
      )}
    </div>
  );
}

Gotcha: 0 && <Component /> renders 0, not nothing! Use !!count && ... or a ternary instead.

Early Return

For complex conditions, returning early keeps components readable:

function UserDashboard({ user, isLoading, error }) {
  if (isLoading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  if (!user) return null;
 
  return (
    <div>
      <h1>Welcome, {user.name}</h1>
      {/* ... dashboard content */}
    </div>
  );
}

Rendering Lists

Use .map() to transform an array into JSX elements:

const fruits = ['Apple', 'Banana', 'Cherry'];
 
function FruitList() {
  return (
    <ul>
      {fruits.map(fruit => (
        <li key={fruit}>{fruit}</li>
      ))}
    </ul>
  );
}

The key Prop

Every item in a list needs a unique key prop. React uses keys to track which items changed, were added, or were removed.

const users = [
  { id: 1, name: 'Alice', role: 'Admin' },
  { id: 2, name: 'Bob', role: 'Editor' },
  { id: 3, name: 'Carol', role: 'Viewer' },
];
 
function UserList() {
  return (
    <ul>
      {users.map(user => (
        // ✅ Use a stable, unique ID as key
        <li key={user.id}>
          {user.name} — {user.role}
        </li>
      ))}
    </ul>
  );
}

Key rules:

  • Keys must be unique among siblings (not globally)
  • Use stable IDs from your data (database IDs, slugs, etc.)
  • Never use array index as key when the list can reorder or items can be added/removed — it causes subtle bugs
// ❌ Index as key — breaks when list reorders
{items.map((item, index) => (
  <Item key={index} data={item} />
))}
 
// ✅ Stable ID as key
{items.map(item => (
  <Item key={item.id} data={item} />
))}

useEffect: Managing Side Effects

Components should be pure — given the same props and state, they always return the same JSX. But real apps need side effects: fetching data, setting up subscriptions, updating the document title.

useEffect handles all of this.

Basic Usage

import { useState, useEffect } from 'react';
 
function DocumentTitle({ page }) {
  useEffect(() => {
    document.title = `Page: ${page}`;
  }, [page]); // Run when `page` changes
 
  return <h1>Current page: {page}</h1>;
}

The Dependency Array

The second argument to useEffect controls when it runs:

// ✅ Runs after every render
useEffect(() => {
  console.log('Rendered');
});
 
// ✅ Runs once, on mount only
useEffect(() => {
  console.log('Mounted');
}, []);
 
// ✅ Runs when `userId` or `filter` changes
useEffect(() => {
  fetchUser(userId, filter);
}, [userId, filter]);

Fetching Data

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
 
  useEffect(() => {
    setLoading(true);
    setError(null);
 
    fetch(`https://api.example.com/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('User not found');
        return res.json();
      })
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        setError(err.message);
        setLoading(false);
      });
  }, [userId]); // Re-fetch when userId changes
 
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
 
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

Cleanup

Some effects need cleanup — to avoid memory leaks and stale updates:

function Timer() {
  const [seconds, setSeconds] = useState(0);
 
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);
 
    // ✅ Cleanup: clear the interval when component unmounts
    return () => clearInterval(interval);
  }, []); // Empty array — set up once, clean up on unmount
 
  return <p>Time elapsed: {seconds}s</p>;
}
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
 
  useEffect(() => {
    let cancelled = false; // Flag to prevent stale updates
 
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) setResults(data); // Only update if still relevant
      });
 
    return () => { cancelled = true; }; // Cleanup on next render
  }, [query]);
 
  return <ul>{results.map(r => <li key={r.id}>{r.title}</li>)}</ul>;
}

Common useEffect Mistakes

// ❌ Missing dependency — stale closure
useEffect(() => {
  fetchData(userId); // userId used but not in deps array
}, []); // Bug: never re-fetches when userId changes
 
// ✅ Include all dependencies
useEffect(() => {
  fetchData(userId);
}, [userId]);
 
// ❌ Object in dependency array — causes infinite loop
useEffect(() => {
  fetchData(options);
}, [options]); // options is a new object on every render!
 
// ✅ Use primitive values or move object inside effect
useEffect(() => {
  const options = { limit: 10, page: currentPage };
  fetchData(options);
}, [currentPage]);

Putting It All Together: A Task Manager

Let's build a small task manager combining everything from this post:

import { useState, useEffect } from 'react';
 
function TaskItem({ task, onToggle, onDelete }) {
  return (
    <li className={task.done ? 'done' : ''}>
      <input
        type="checkbox"
        checked={task.done}
        onChange={() => onToggle(task.id)}
      />
      <span>{task.text}</span>
      <button onClick={() => onDelete(task.id)}>Delete</button>
    </li>
  );
}
 
function TaskForm({ onAdd }) {
  const [text, setText] = useState('');
 
  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;
    onAdd(text.trim());
    setText('');
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
        placeholder="Add a task..."
      />
      <button type="submit">Add</button>
    </form>
  );
}
 
function TaskManager() {
  const [tasks, setTasks] = useState([]);
  const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'done'
 
  // Persist tasks to localStorage
  useEffect(() => {
    const saved = localStorage.getItem('tasks');
    if (saved) setTasks(JSON.parse(saved));
  }, []);
 
  useEffect(() => {
    localStorage.setItem('tasks', JSON.stringify(tasks));
  }, [tasks]);
 
  const addTask = (text) => {
    setTasks(prev => [
      ...prev,
      { id: Date.now(), text, done: false },
    ]);
  };
 
  const toggleTask = (id) => {
    setTasks(prev =>
      prev.map(t => t.id === id ? { ...t, done: !t.done } : t)
    );
  };
 
  const deleteTask = (id) => {
    setTasks(prev => prev.filter(t => t.id !== id));
  };
 
  const filteredTasks = tasks.filter(t => {
    if (filter === 'active') return !t.done;
    if (filter === 'done') return t.done;
    return true;
  });
 
  return (
    <div className="task-manager">
      <h1>Tasks ({tasks.filter(t => !t.done).length} remaining)</h1>
 
      <TaskForm onAdd={addTask} />
 
      <div className="filters">
        {['all', 'active', 'done'].map(f => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            className={filter === f ? 'active' : ''}
          >
            {f}
          </button>
        ))}
      </div>
 
      {filteredTasks.length === 0 ? (
        <p>No tasks to show.</p>
      ) : (
        <ul>
          {filteredTasks.map(task => (
            <TaskItem
              key={task.id}
              task={task}
              onToggle={toggleTask}
              onDelete={deleteTask}
            />
          ))}
        </ul>
      )}
    </div>
  );
}
 
export default TaskManager;

This single example demonstrates:

  • Multiple components (TaskItem, TaskForm, TaskManager)
  • Props passing and children
  • useState for tasks, filter, and form input
  • useEffect for localStorage persistence
  • Controlled inputs
  • Event handlers
  • List rendering with keys
  • Conditional rendering

The React Developer Mindset

Before moving to Phase 2, internalize these thinking patterns:

Think in Components

Break any UI into a tree of components. Ask yourself:

  • What data does this component need? (props)
  • What does it own and change? (state)
  • What can I extract into a smaller, reusable component?

Data Flows Down, Events Flow Up

Data flows down via props. Changes flow up via callback functions passed as props. Never the other way around.

Keep Components Pure

A component's output should depend only on its props and state — no hidden dependencies, no random values, no directly mutating external data.

State is a Snapshot

When you call setCount(count + 1), React doesn't change count immediately. It schedules a re-render. The next time the component renders, it gets the new count. Think of state as a snapshot of a moment in time.


Practice Exercises

Work through these to cement Phase 1 concepts:

Exercise 1 — Toggle Build a component with a button that toggles between showing and hiding a paragraph of text.

Exercise 2 — Counter with Step Build a counter where the user can set a "step" amount (1, 5, 10) and increment/decrement by that step.

Exercise 3 — Shopping List Build a shopping list where the user can add items, check them off, and delete them. Show a count of unchecked items.

Exercise 4 — Fetch & Display Fetch a list of posts from https://jsonplaceholder.typicode.com/posts and display them. Show a loading state while fetching and an error state if the request fails.

Exercise 5 — Color Picker Build a component with three sliders (R, G, B, 0–255). Show a colored box that updates in real time as the sliders change.


Summary

In Phase 1, you've learned the foundational React concepts:

ConceptWhat It Does
JSXHTML-like syntax that compiles to React.createElement calls
ComponentsFunctions that return JSX — the building blocks of React UI
PropsRead-only data passed from parent to child
State (useState)Internal mutable data — changes trigger re-renders
useEffectRuns side effects (fetch, timers, subscriptions) after render
EventsHandle user interactions with camelCase event attributes
Lists & KeysRender arrays with .map(), use stable keys
Conditional renderingTernary, &&, or early return for conditional UI

These concepts are the complete foundation. Every advanced React pattern builds on them.


What's Next

In Post RN-3: Phase 2 — React Ecosystem, you'll extend this foundation with:

  • Client-side routing with React Router
  • Global state management
  • Form libraries (React Hook Form)
  • Data fetching with SWR or TanStack Query
  • Component libraries (Tailwind CSS, shadcn/ui)

Series Index

PostTitleStatus
RN-1React.js & Next.js Roadmap✅ Complete
RN-2Phase 1: React Fundamentals✅ You are here
RN-3Phase 2: React EcosystemComing Soon
RN-4Phase 3: Next.js Full-StackComing Soon
RN-5Deep Dive: React Hooks MasteryComing Soon
RN-6Deep Dive: State ManagementComing Soon
RN-7Deep Dive: App Router & Server ComponentsComing Soon
RN-8Deep Dive: Data Fetching PatternsComing Soon
RN-9Deep Dive: Performance OptimizationComing Soon
RN-10Deep Dive: Authentication in Next.jsComing Soon
RN-11Deep Dive: Testing React & Next.jsComing Soon
RN-12Deep Dive: Deploying Next.js AppsComing Soon

📬 Subscribe to Newsletter

Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.

We respect your privacy. Unsubscribe at any time.

💬 Comments

Sign in to leave a comment

We'll never post without your permission.