Episode 17 of 17

Creating a React Component

Build a React component that fetches data from your REST API — display, create, and delete items through a modern React front-end.

Creating a React Component

In the previous episode you built a plain HTML/JS front-end. Now let us upgrade to React — a popular JavaScript library for building user interfaces. You will create a React component that fetches ninjas from the API, displays them, and handles create and delete operations.

Setting Up React

npx create-react-app ninja-client
cd ninja-client
npm start

The React dev server runs on port 3000 by default. Since your API also uses port 3000, either change the API port to 4000 or add a proxy.

Adding a Proxy

// In ninja-client/package.json, add:
"proxy": "http://localhost:4000"

The proxy tells the React dev server to forward API requests to your Express server. So fetch('/api/ninjas') in React goes to http://localhost:4000/api/ninjas.

The Ninja List Component

// src/NinjaList.js
import { useState, useEffect } from 'react';

function NinjaList() {
    const [ninjas, setNinjas] = useState([]);
    const [name, setName] = useState('');
    const [rank, setRank] = useState('');

    // Fetch ninjas on component mount
    useEffect(() => {
        loadNinjas();
    }, []);

    async function loadNinjas() {
        const res = await fetch('/api/ninjas');
        const data = await res.json();
        setNinjas(data);
    }

    async function handleSubmit(e) {
        e.preventDefault();
        await fetch('/api/ninjas', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ name, rank: Number(rank) }),
        });
        setName('');
        setRank('');
        loadNinjas();
    }

    async function handleDelete(id) {
        await fetch(`/api/ninjas/${id}`, { method: 'DELETE' });
        loadNinjas();
    }

    return (
        <div className="ninja-app">
            <h1>Ninja Directory</h1>

            <form onSubmit={handleSubmit}>
                <input
                    value={name}
                    onChange={(e) => setName(e.target.value)}
                    placeholder="Ninja name"
                    required
                />
                <input
                    type="number"
                    value={rank}
                    onChange={(e) => setRank(e.target.value)}
                    placeholder="Rank"
                    min="1"
                    max="10"
                    required
                />
                <button type="submit">Add Ninja</button>
            </form>

            <div className="ninja-list">
                {ninjas.map(ninja => (
                    <div key={ninja._id} className="ninja-card">
                        <h3>{ninja.name}</h3>
                        <p>Rank: {ninja.rank}</p>
                        <button onClick={() => handleDelete(ninja._id)}>
                            Delete
                        </button>
                    </div>
                ))}
            </div>
        </div>
    );
}

export default NinjaList;

React Concepts Used

ConceptPurpose
useStateManages component state (ninjas list, form inputs)
useEffectRuns side effects — fetches data when the component mounts
fetch()Sends HTTP requests to the REST API
.map()Renders a list of components from an array
keyUnique identifier for each list item (React uses this for efficient re-rendering)

Using the Component

// src/App.js
import NinjaList from './NinjaList';

function App() {
    return (
        <div className="App">
            <NinjaList />
        </div>
    );
}

export default App;

Loading and Error States

const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

async function loadNinjas() {
    try {
        setLoading(true);
        const res = await fetch('/api/ninjas');
        if (!res.ok) throw new Error('Failed to fetch');
        const data = await res.json();
        setNinjas(data);
        setError(null);
    } catch (err) {
        setError(err.message);
    } finally {
        setLoading(false);
    }
}

// In the JSX:
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;

The Complete Architecture

React App (port 3000)          Express API (port 4000)          MongoDB
──────────────────           ──────────────────────           ────────
  NinjaList component              /api/ninjas                  ninjas
  │                                    │                       collection
  │── fetch GET /api/ninjas ────→     │── Ninja.find() ──────→ │
  │←── JSON array ──────────────     │←── documents ──────────│
  │                                    │                       │
  │── fetch POST /api/ninjas ───→     │── ninja.save() ──────→ │
  │←── JSON new ninja ─────────     │←── saved doc ──────────│

Key Takeaways

  • useEffect with an empty dependency array ([]) fetches data once when the component mounts
  • useState manages the ninja list and form input values
  • The proxy field in package.json forwards API requests to your Express server
  • Always handle loading and error states for a good user experience
  • After mutating data (POST, DELETE), re-fetch the list to keep the UI in sync
  • The React front-end communicates with the Express API using the Fetch API — the same pattern works with any REST API