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.
Vite (Recommended)
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
- Vue.js Essentials - From Setup to Todo App
Complete Vue.js 3 guide from project setup to building a functional Todo …