React Essentials - From Setup to Todo App

Complete React guide from project setup to building a functional Todo application. Includes modern hooks, TypeScript, and best practices.


Docker Setup

Dockerfile

 1# Build stage
 2FROM node:18-alpine AS builder
 3WORKDIR /app
 4COPY package*.json ./
 5RUN npm ci
 6COPY . .
 7RUN npm run build
 8
 9# Production stage
10FROM nginx:alpine
11COPY --from=builder /app/dist /usr/share/nginx/html
12COPY nginx.conf /etc/nginx/conf.d/default.conf
13EXPOSE 80
14CMD ["nginx", "-g", "daemon off;"]

Docker Compose

 1version: '3.8'
 2
 3services:
 4  react-app:
 5    build:
 6      context: .
 7      dockerfile: Dockerfile
 8    container_name: react-app
 9    ports:
10      - "3000:80"
11    restart: unless-stopped
12
13  # Development with hot reload
14  react-dev:
15    image: node:18-alpine
16    container_name: react-dev
17    working_dir: /app
18    volumes:
19      - ./:/app
20      - /app/node_modules
21    ports:
22      - "3000:3000"
23    command: npm run dev
24    environment:
25      - VITE_API_URL=http://localhost:8080
26    restart: unless-stopped

Project Setup

Create React App (CRA) - Legacy

1# Create new React app
2npx create-react-app my-app
3cd my-app
4npm start
5
6# With TypeScript
7npx create-react-app my-app --template typescript

⚠️ Gotcha: CRA is no longer recommended by React team. Use Vite instead.


1# Create new React app with Vite
2npm create vite@latest my-app -- --template react
3cd my-app
4npm install
5npm run dev
6
7# With TypeScript
8npm create vite@latest my-app -- --template react-ts

Node.js Version Requirements:

  • Vite 5.x: Node.js 18+ or 20+
  • Vite 4.x: Node.js 14.18+ or 16+

Check your Node version:

1node --version
2npm --version

⚠️ Gotcha: If you get "ERR_OSSL_EVP_UNSUPPORTED" on Node 17+, use Node 18+ or set:

1# Windows PowerShell
2$env:NODE_OPTIONS="--openssl-legacy-provider"
3
4# Linux/Mac
5export NODE_OPTIONS=--openssl-legacy-provider

Next.js (Full-Stack Framework)

1npx create-next-app@latest my-app
2cd my-app
3npm run dev

Features:

  • Server-side rendering (SSR)
  • Static site generation (SSG)
  • API routes
  • File-based routing

Basic React Concepts

Functional Components

 1// Basic component
 2function Welcome() {
 3  return <h1>Hello, World!</h1>;
 4}
 5
 6// Component with props
 7interface GreetingProps {
 8  name: string;
 9  age?: number; // Optional
10}
11
12function Greeting({ name, age }: GreetingProps) {
13  return (
14    <div>
15      <h1>Hello, {name}!</h1>
16      {age && <p>Age: {age}</p>}
17    </div>
18  );
19}
20
21// Usage
22<Greeting name="John" age={30} />

Hooks

useState

 1import { useState } from 'react';
 2
 3function Counter() {
 4  const [count, setCount] = useState(0);
 5  
 6  return (
 7    <div>
 8      <p>Count: {count}</p>
 9      <button onClick={() => setCount(count + 1)}>Increment</button>
10      <button onClick={() => setCount(count - 1)}>Decrement</button>
11      <button onClick={() => setCount(0)}>Reset</button>
12    </div>
13  );
14}

⚠️ Gotcha: State updates are asynchronous!

1// ❌ Wrong - won't work as expected
2setCount(count + 1);
3setCount(count + 1); // Still adds only 1
4
5// ✅ Correct - use functional update
6setCount(prev => prev + 1);
7setCount(prev => prev + 1); // Adds 2

useEffect

 1import { useState, useEffect } from 'react';
 2
 3function DataFetcher() {
 4  const [data, setData] = useState(null);
 5  const [loading, setLoading] = useState(true);
 6  const [error, setError] = useState(null);
 7  
 8  useEffect(() => {
 9    // Fetch data when component mounts
10    fetch('https://api.example.com/data')
11      .then(res => res.json())
12      .then(data => {
13        setData(data);
14        setLoading(false);
15      })
16      .catch(err => {
17        setError(err.message);
18        setLoading(false);
19      });
20  }, []); // Empty dependency array = run once on mount
21  
22  if (loading) return <div>Loading...</div>;
23  if (error) return <div>Error: {error}</div>;
24  
25  return <div>{JSON.stringify(data)}</div>;
26}

Dependency Array:

 1useEffect(() => {
 2  // Runs on every render
 3});
 4
 5useEffect(() => {
 6  // Runs once on mount
 7}, []);
 8
 9useEffect(() => {
10  // Runs when count changes
11}, [count]);
12
13useEffect(() => {
14  // Cleanup function
15  const timer = setInterval(() => console.log('tick'), 1000);
16  
17  return () => clearInterval(timer); // Cleanup on unmount
18}, []);

⚠️ Gotcha: Missing dependencies cause stale closures!

 1// ❌ Wrong - count is stale
 2useEffect(() => {
 3  setInterval(() => {
 4    setCount(count + 1); // Always uses initial count
 5  }, 1000);
 6}, []);
 7
 8// ✅ Correct
 9useEffect(() => {
10  const timer = setInterval(() => {
11    setCount(prev => prev + 1); // Functional update
12  }, 1000);
13  
14  return () => clearInterval(timer);
15}, []);

useContext

 1import { createContext, useContext, useState } from 'react';
 2
 3// Create context
 4const ThemeContext = createContext<'light' | 'dark'>('light');
 5
 6// Provider component
 7function App() {
 8  const [theme, setTheme] = useState<'light' | 'dark'>('light');
 9  
10  return (
11    <ThemeContext.Provider value={theme}>
12      <Toolbar />
13      <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
14        Toggle Theme
15      </button>
16    </ThemeContext.Provider>
17  );
18}
19
20// Consumer component
21function Toolbar() {
22  const theme = useContext(ThemeContext);
23  
24  return (
25    <div style={{ background: theme === 'light' ? '#fff' : '#333' }}>
26      Current theme: {theme}
27    </div>
28  );
29}

useRef

 1import { useRef, useEffect } from 'react';
 2
 3function TextInput() {
 4  const inputRef = useRef<HTMLInputElement>(null);
 5  
 6  useEffect(() => {
 7    // Focus input on mount
 8    inputRef.current?.focus();
 9  }, []);
10  
11  return <input ref={inputRef} type="text" />;
12}
13
14// Store mutable value without re-rendering
15function Timer() {
16  const countRef = useRef(0);
17  
18  const handleClick = () => {
19    countRef.current += 1;
20    console.log(countRef.current); // Updates without re-render
21  };
22  
23  return <button onClick={handleClick}>Click me</button>;
24}

useMemo & useCallback

 1import { useMemo, useCallback, useState } from 'react';
 2
 3function ExpensiveComponent() {
 4  const [count, setCount] = useState(0);
 5  const [input, setInput] = useState('');
 6  
 7  // Memoize expensive calculation
 8  const expensiveValue = useMemo(() => {
 9    console.log('Computing expensive value...');
10    return count * 2;
11  }, [count]); // Only recompute when count changes
12  
13  // Memoize callback function
14  const handleClick = useCallback(() => {
15    console.log('Button clicked');
16    setCount(prev => prev + 1);
17  }, []); // Function reference stays the same
18  
19  return (
20    <div>
21      <p>Expensive value: {expensiveValue}</p>
22      <button onClick={handleClick}>Increment</button>
23      <input value={input} onChange={e => setInput(e.target.value)} />
24    </div>
25  );
26}

⚠️ Gotcha: Don't overuse memoization! Only use when you have actual performance issues.


Complete Todo App Example

  1// src/App.tsx
  2import { useState } from 'react';
  3import './App.css';
  4
  5interface Todo {
  6  id: number;
  7  text: string;
  8  completed: boolean;
  9}
 10
 11function App() {
 12  const [todos, setTodos] = useState<Todo[]>([]);
 13  const [input, setInput] = useState('');
 14  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
 15  
 16  // Add todo
 17  const addTodo = () => {
 18    if (input.trim() === '') return;
 19    
 20    const newTodo: Todo = {
 21      id: Date.now(),
 22      text: input,
 23      completed: false,
 24    };
 25    
 26    setTodos([...todos, newTodo]);
 27    setInput('');
 28  };
 29  
 30  // Toggle todo completion
 31  const toggleTodo = (id: number) => {
 32    setTodos(todos.map(todo =>
 33      todo.id === id ? { ...todo, completed: !todo.completed } : todo
 34    ));
 35  };
 36  
 37  // Delete todo
 38  const deleteTodo = (id: number) => {
 39    setTodos(todos.filter(todo => todo.id !== id));
 40  };
 41  
 42  // Edit todo
 43  const editTodo = (id: number, newText: string) => {
 44    setTodos(todos.map(todo =>
 45      todo.id === id ? { ...todo, text: newText } : todo
 46    ));
 47  };
 48  
 49  // Filter todos
 50  const filteredTodos = todos.filter(todo => {
 51    if (filter === 'active') return !todo.completed;
 52    if (filter === 'completed') return todo.completed;
 53    return true;
 54  });
 55  
 56  // Clear completed
 57  const clearCompleted = () => {
 58    setTodos(todos.filter(todo => !todo.completed));
 59  };
 60  
 61  const activeCount = todos.filter(todo => !todo.completed).length;
 62  
 63  return (
 64    <div className="app">
 65      <h1>Todo App</h1>
 66      
 67      {/* Input */}
 68      <div className="input-container">
 69        <input
 70          type="text"
 71          value={input}
 72          onChange={e => setInput(e.target.value)}
 73          onKeyPress={e => e.key === 'Enter' && addTodo()}
 74          placeholder="What needs to be done?"
 75        />
 76        <button onClick={addTodo}>Add</button>
 77      </div>
 78      
 79      {/* Filter buttons */}
 80      <div className="filters">
 81        <button
 82          className={filter === 'all' ? 'active' : ''}
 83          onClick={() => setFilter('all')}
 84        >
 85          All ({todos.length})
 86        </button>
 87        <button
 88          className={filter === 'active' ? 'active' : ''}
 89          onClick={() => setFilter('active')}
 90        >
 91          Active ({activeCount})
 92        </button>
 93        <button
 94          className={filter === 'completed' ? 'active' : ''}
 95          onClick={() => setFilter('completed')}
 96        >
 97          Completed ({todos.length - activeCount})
 98        </button>
 99      </div>
100      
101      {/* Todo list */}
102      <ul className="todo-list">
103        {filteredTodos.map(todo => (
104          <TodoItem
105            key={todo.id}
106            todo={todo}
107            onToggle={toggleTodo}
108            onDelete={deleteTodo}
109            onEdit={editTodo}
110          />
111        ))}
112      </ul>
113      
114      {/* Footer */}
115      {todos.length > 0 && (
116        <div className="footer">
117          <span>{activeCount} item{activeCount !== 1 ? 's' : ''} left</span>
118          <button onClick={clearCompleted}>Clear completed</button>
119        </div>
120      )}
121    </div>
122  );
123}
124
125// TodoItem component
126interface TodoItemProps {
127  todo: Todo;
128  onToggle: (id: number) => void;
129  onDelete: (id: number) => void;
130  onEdit: (id: number, text: string) => void;
131}
132
133function TodoItem({ todo, onToggle, onDelete, onEdit }: TodoItemProps) {
134  const [isEditing, setIsEditing] = useState(false);
135  const [editText, setEditText] = useState(todo.text);
136  
137  const handleEdit = () => {
138    if (editText.trim() === '') {
139      onDelete(todo.id);
140    } else {
141      onEdit(todo.id, editText);
142    }
143    setIsEditing(false);
144  };
145  
146  if (isEditing) {
147    return (
148      <li className="todo-item editing">
149        <input
150          type="text"
151          value={editText}
152          onChange={e => setEditText(e.target.value)}
153          onBlur={handleEdit}
154          onKeyPress={e => e.key === 'Enter' && handleEdit()}
155          autoFocus
156        />
157      </li>
158    );
159  }
160  
161  return (
162    <li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
163      <input
164        type="checkbox"
165        checked={todo.completed}
166        onChange={() => onToggle(todo.id)}
167      />
168      <span onDoubleClick={() => setIsEditing(true)}>{todo.text}</span>
169      <button onClick={() => onDelete(todo.id)}>×</button>
170    </li>
171  );
172}
173
174export default App;

CSS (App.css)

  1.app {
  2  max-width: 600px;
  3  margin: 50px auto;
  4  padding: 20px;
  5  font-family: Arial, sans-serif;
  6}
  7
  8h1 {
  9  text-align: center;
 10  color: #333;
 11}
 12
 13.input-container {
 14  display: flex;
 15  gap: 10px;
 16  margin-bottom: 20px;
 17}
 18
 19.input-container input {
 20  flex: 1;
 21  padding: 10px;
 22  font-size: 16px;
 23  border: 1px solid #ddd;
 24  border-radius: 4px;
 25}
 26
 27.input-container button {
 28  padding: 10px 20px;
 29  font-size: 16px;
 30  background: #4CAF50;
 31  color: white;
 32  border: none;
 33  border-radius: 4px;
 34  cursor: pointer;
 35}
 36
 37.input-container button:hover {
 38  background: #45a049;
 39}
 40
 41.filters {
 42  display: flex;
 43  gap: 10px;
 44  margin-bottom: 20px;
 45}
 46
 47.filters button {
 48  flex: 1;
 49  padding: 8px;
 50  background: #f0f0f0;
 51  border: 1px solid #ddd;
 52  border-radius: 4px;
 53  cursor: pointer;
 54}
 55
 56.filters button.active {
 57  background: #2196F3;
 58  color: white;
 59}
 60
 61.todo-list {
 62  list-style: none;
 63  padding: 0;
 64}
 65
 66.todo-item {
 67  display: flex;
 68  align-items: center;
 69  gap: 10px;
 70  padding: 10px;
 71  border: 1px solid #ddd;
 72  border-radius: 4px;
 73  margin-bottom: 10px;
 74}
 75
 76.todo-item.completed span {
 77  text-decoration: line-through;
 78  color: #999;
 79}
 80
 81.todo-item input[type="checkbox"] {
 82  width: 20px;
 83  height: 20px;
 84  cursor: pointer;
 85}
 86
 87.todo-item span {
 88  flex: 1;
 89  cursor: pointer;
 90}
 91
 92.todo-item button {
 93  width: 30px;
 94  height: 30px;
 95  background: #f44336;
 96  color: white;
 97  border: none;
 98  border-radius: 4px;
 99  cursor: pointer;
100  font-size: 20px;
101}
102
103.todo-item button:hover {
104  background: #da190b;
105}
106
107.todo-item.editing input {
108  flex: 1;
109  padding: 8px;
110  font-size: 16px;
111  border: 1px solid #2196F3;
112  border-radius: 4px;
113}
114
115.footer {
116  display: flex;
117  justify-content: space-between;
118  align-items: center;
119  margin-top: 20px;
120  padding-top: 20px;
121  border-top: 1px solid #ddd;
122}
123
124.footer button {
125  padding: 8px 16px;
126  background: #f44336;
127  color: white;
128  border: none;
129  border-radius: 4px;
130  cursor: pointer;
131}
132
133.footer button:hover {
134  background: #da190b;
135}

Common Gotchas

1. Key Prop in Lists

1// ❌ Wrong - using index as key
2{todos.map((todo, index) => (
3  <TodoItem key={index} todo={todo} />
4))}
5
6// ✅ Correct - using unique ID
7{todos.map(todo => (
8  <TodoItem key={todo.id} todo={todo} />
9))}

2. Event Handlers

1// ❌ Wrong - calls function immediately
2<button onClick={handleClick()}>Click</button>
3
4// ✅ Correct - passes function reference
5<button onClick={handleClick}>Click</button>
6
7// ✅ Correct - with arguments
8<button onClick={() => handleClick(id)}>Click</button>

3. Conditional Rendering

 1// ✅ Ternary operator
 2{isLoading ? <Spinner /> : <Content />}
 3
 4// ✅ Logical AND
 5{error && <ErrorMessage error={error} />}
 6
 7// ✅ Nullish coalescing
 8{data?.items?.length ?? 0}
 9
10// ❌ Wrong - renders "0" or "false"
11{items.length && <List items={items} />}
12
13// ✅ Correct
14{items.length > 0 && <List items={items} />}

4. Forms and Controlled Inputs

1// ❌ Uncontrolled (avoid)
2<input type="text" />
3
4// ✅ Controlled
5const [value, setValue] = useState('');
6<input type="text" value={value} onChange={e => setValue(e.target.value)} />

Related Snippets