Lesson 7 of 108 min read

React Basics

Share:WhatsAppLinkedIn

What you'll build

By the end of this lesson you will have a small single-page app with three components: a search bar, a list of posts fetched from a public API, and a post card. The app filters posts as you type, using component state and an effect for the initial fetch. You will understand the React mental model well enough to build most front-end features without looking things up.

Concepts

JSX and components

React lets you describe UIs as components, functions that return JSX. JSX looks like HTML written inside JavaScript. Under the hood, Babel compiles it to React.createElement(...) calls, but you never write those by hand.

// A simple component, a function that returns JSX
function Greeting({ name }) {
  return <h1>Hello, {name}!</h1>;
}

// Use it like an HTML tag
function App() {
  return (
    <div>
      <Greeting name="Priya" />
      <Greeting name="Rahul" />
    </div>
  );
}

Rules of JSX:

  • A component name must start with a capital letter. <greeting /> is an HTML tag, <Greeting /> is a component., JSX must return a single root element. Wrap siblings in a <div> or a Fragment (<>...</>)., JavaScript expressions go inside {}. You can put any expression there: variables, function calls, ternaries.
  • class is reserved in JavaScript, so use className in JSX. for becomes htmlFor.
function StatusBadge({ isActive }) {
  return (
    <span className={isActive ? "badge-green" : "badge-grey"}>
      {isActive ? "Active" : "Inactive"}
    </span>
  );
}

Props

Props (properties) are how you pass data from a parent component to a child. They are read-only, a child never modifies its own props.

function PostCard({ title, author, excerpt }) {
  return (
    <article className="card">
      <h2>{title}</h2>
      <p className="author">By {author}</p>
      <p>{excerpt}</p>
    </article>
  );
}

function PostList({ posts }) {
  return (
    <div className="post-list">
      {posts.map((post) => (
        <PostCard
          key={post.id}
          title={post.title}
          author={post.userId}
          excerpt={post.body.slice(0, 100) + "..."}
        />
      ))}
    </div>
  );
}

The key prop is required when rendering lists. React uses it to efficiently update only the items that changed. Use a stable unique identifier (a database ID) rather than the array index, array indices change when items are inserted or removed, which causes subtle rendering bugs.

useState

useState lets a component remember a value across renders. When you call the setter function, React re-renders the component with the new value.

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0); // initial value is 0

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

useState returns an array of two things: the current value and a setter function. Destructuring const [count, setCount] is the standard pattern.

Never directly mutate state: do not do count = count + 1 or myArray.push(item). Always call the setter. For arrays and objects, create a new value:

// Wrong, mutates the existing array
const [items, setItems] = useState([]);
items.push("new item"); // React does not know this changed

// Correct, creates a new array
setItems([...items, "new item"]);

useEffect

useEffect runs side effects, things that happen outside of rendering: fetching data, setting up subscriptions, interacting with browser APIs.

import { useState, useEffect } from "react";

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // This runs after the component renders
    let cancelled = false;

    async function fetchUser() {
      setLoading(true);
      setError(null);
      try {
        const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
        if (!res.ok) throw new Error(`${res.status}`);
        const data = await res.json();
        if (!cancelled) setUser(data);
      } catch (e) {
        if (!cancelled) setError(e.message);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    fetchUser();

    // Cleanup function, runs when the component unmounts or userId changes
    return () => { cancelled = true; };
  }, [userId]); // dependency array, re-runs when userId changes

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error}</p>;
  if (!user) return null;

  return <h2>{user.name}</h2>;
}

The second argument to useEffect is the dependency array. If it is empty [], the effect runs once after the first render. If it contains values like [userId], it re-runs whenever those values change.

Hands-on

Build a post browser SPA. First scaffold the project:

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

Replace src/App.jsx with:

import { useState, useEffect } from "react";
import "./App.css";

// ---- SearchBar ----
function SearchBar({ query, onQueryChange }) {
  return (
    <div className="search-bar">
      <input
        type="search"
        placeholder="Filter posts..."
        value={query}
        onChange={(e) => onQueryChange(e.target.value)}
        aria-label="Filter posts"
      />
    </div>
  );
}

// ---- PostCard ----
function PostCard({ title, body }) {
  return (
    <article className="card">
      <h3>{title}</h3>
      <p>{body.slice(0, 120)}...</p>
    </article>
  );
}

// ---- PostList ----
function PostList({ posts, loading, error }) {
  if (loading) return <p className="status">Loading posts...</p>;
  if (error) return <p className="status error">Error: {error}</p>;
  if (posts.length === 0) return <p className="status">No posts match your search.</p>;

  return (
    <div className="post-grid">
      {posts.map((post) => (
        <PostCard key={post.id} title={post.title} body={post.body} />
      ))}
    </div>
  );
}

// ---- App (root) ----
export default function App() {
  const [allPosts, setAllPosts] = useState([]);
  const [query, setQuery] = useState("");
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function loadPosts() {
      try {
        const res = await fetch("https://jsonplaceholder.typicode.com/posts");
        if (!res.ok) throw new Error(`${res.status}`);
        const data = await res.json();
        setAllPosts(data);
      } catch (e) {
        setError(e.message);
      } finally {
        setLoading(false);
      }
    }
    loadPosts();
  }, []); // empty array = run once on mount

  const filtered = allPosts.filter(
    (p) =>
      p.title.toLowerCase().includes(query.toLowerCase()) ||
      p.body.toLowerCase().includes(query.toLowerCase())
  );

  return (
    <div className="app">
      <header>
        <h1>Post Browser</h1>
        <SearchBar query={query} onQueryChange={setQuery} />
        <p className="count">{filtered.length} posts</p>
      </header>
      <main>
        <PostList posts={filtered} loading={loading} error={error} />
      </main>
    </div>
  );
}

Replace src/App.css with:

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f5f7fa; color: #333; }
.app { max-width: 960px; margin: 0 auto; padding: 24px 16px; }
header { margin-bottom: 24px; }
h1 { font-size: 2rem; margin-bottom: 12px; }
.search-bar input { width: 100%; padding: 10px 14px; font-size: 1rem; border: 1px solid #ccc; border-radius: 6px; }
.count { margin-top: 8px; color: #888; font-size: 0.875rem; }
.post-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.card { background: white; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; }
.card h3 { margin-bottom: 8px; font-size: 1rem; text-transform: capitalize; }
.card p { font-size: 0.875rem; color: #555; line-height: 1.5; }
.status { padding: 24px 0; color: #888; }
.status.error { color: red; }

Open http://localhost:5173. You will see 100 posts from the API. Start typing in the search box, posts filter live as you type. No page reload, no extra fetch. The filtering is pure computation over the allPosts array, which is already in memory. React re-renders only the components that depend on changed state.

Common pitfalls

  • Missing the key prop on list items. React warns about this in the console and may silently produce wrong rendering behaviour when the list order changes. Always add key with a unique stable value.
  • Calling a hook conditionally or inside a loop. React requires hooks to be called in the same order on every render. Wrapping useState in an if block will cause an error. Move the conditional logic inside the hook, not around it.
  • Mutating state directly. state.push(item) or state.count++ do not trigger a re-render. Always use the setter from useState and create new values for arrays and objects.
  • Infinite useEffect loops. If your effect sets state and that state is in the dependency array, the effect will run on every render indefinitely. Think carefully about dependencies. If you see the network tab firing requests in a loop, this is the cause.
  • Forgetting to handle the async cleanup in useEffect. If a component unmounts while a fetch is in flight, calling setState on an unmounted component is a no-op but it can hide bugs. Use a cancelled flag as shown in the UserProfile example to guard all state updates.

What to try next

  1. Add a pagination control below the post list: show 10 posts at a time with Previous and Next buttons. Use a page state variable and Array.slice() to extract the right range.
  2. Extract the data-fetching logic into a custom hook called usePosts. Move the allPosts, loading, and error state and the useEffect into it, and return all three. The App component then calls const { allPosts, loading, error } = usePosts().
  3. Add a "Sort by title" toggle button. Maintain a sortAsc boolean state. Before filtering, sort the allPosts array alphabetically by title when sortAsc is true. Remember that Array.sort() mutates the original, spread first: [...allPosts].sort(...).

Test Your Knowledge

Take a quick quiz on this lesson

Start Quiz →

Prefer watching over reading?

Subscribe for free.

Subscribe on YouTube