To start it one need to install react project. I prefer `pnpm` and `nvm` for managing packages and node versions respectively. Here are my steps to install React:
Step 1: Install pnpm
If you don’t already have pnpm installed, install it globally via npm or corepack:
npm install -g pnpm
Or, if you’re using Node.js >=16.13, you can enable it with:
corepack enable
Step 2: Create a Project Directory
Create a new directory for your React app and navigate into it:
mkdir react-todo
cd react-todo
Step 3: Initialize the Project
Initialize the project with pnpm:
pnpm init
Follow the prompts or press Enter to accept the defaults.
Step 4: Install React and ReactDOM
Install React and ReactDOM libraries, along with vite for an optimized development experience:
pnpm add react react-dom
pnpm add -D vite
Step 5: Set Up Vite
Initialize a basic vite project setup:
pnpm exec vite
Follow the prompts, choosing react as the framework and any additional options as desired.
Step 6: Update package.json Scripts
Edit your package.json file to include a start script for Vite:
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
}
Step 7: Create the App Structure
Make a basic React app structure:
1. Create src directory:
mkdir src
2. Create index.html:
In the root of your project, add the following contents:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello World</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
3. Create src/main.jsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
4. Create src/App.jsx:
import React from 'react';
const App = () => {
return <h1>TODO:</h1>;
};
export default App;
Step 8: Start the Development Server
Run the development server to preview your app:
pnpm dev
Open your browser and navigate to the URL displayed in the terminal (usually http://localhost:5173 ).
Step 9: Confirm the Project Structure
Ensure your project structure looks like this:
├── package.json
├── pnpm-lock.yaml
├── node_modules/
├── vite.config.js (optional, for custom configurations)
├── index.html
└── src/
├── App.jsx
├── main.jsx
Step 10: Solve issues that may occur
Mine was having `ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "dev" not found ` error. I have solved it by manually running vite for now:
user@ro react-todo % pnpm dev
ERR_PNPM_RECURSIVE_EXEC_FIRST_FAIL Command "dev" not found
user@ro react-todo % pnpm list
Legend: production dependency, optional only, dev only
react-todo@1.0.0 /Users/user/dev/me/react-todo
dependencies:
react 19.0.0
react-dom 19.0.0
devDependencies:
vite 6.0.5
user@ro react-todo % pnpm exec vite
Port 5173 is in use, trying another one...
VITE v6.0.5 ready in 84 ms
➜ Local: http://localhost:5174/
➜ Network: use --host to expose
➜ press h + enter to show help
That did the trick and I was up and runnging for farther manipulations.
Here’s how to extend the “Hello World” app into a basic TODO app with the following features:
• Add tasks.
• Mark tasks as complete.
• Store tasks on a server.
We’ll achieve this in a step-by-step manner.
Step 11: Update the App to a TODO Component
Replace the src/App.jsx content with a basic TODO app.
import React, { useState, useEffect } from 'react';
const App = () => {
const [tasks, setTasks] = useState([]);
const [newTask, setNewTask] = useState('');
// Fetch tasks from the server
useEffect(() => {
fetch('http://localhost:3000/tasks')
.then((response) => response.json())
.then((data) => setTasks(data))
.catch((error) => console.error('Error fetching tasks:', error));
}, []);
// Add a new task
const addTask = () => {
if (!newTask.trim()) return;
const task = { text: newTask, completed: false };
fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task),
})
.then((response) => response.json())
.then((savedTask) => {
setTasks([...tasks, savedTask]);
setNewTask('');
})
.catch((error) => console.error('Error adding task:', error));
};
// Toggle task completion
const toggleTask = (taskId) => {
const taskToUpdate = tasks.find((task) => task.id === taskId);
if (!taskToUpdate) return;
fetch(`http://localhost:3000/tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...taskToUpdate, completed: !taskToUpdate.completed }),
})
.then((response) => response.json())
.then((updatedTask) => {
setTasks(tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)));
})
.catch((error) => console.error('Error updating task:', error));
};
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: 'auto', fontFamily: 'Arial, sans-serif' }}>
<h1>TODO App</h1>
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Enter a task..."
style={{ flex: 1, padding: '8px' }}
/>
<button onClick={addTask} style={{ padding: '8px 12px' }}>
Add
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{tasks.map((task) => (
<li
key={task.id}
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '8px 0',
textDecoration: task.completed ? 'line-through' : 'none',
}}
>
<span onClick={() => toggleTask(task.id)} style={{ cursor: 'pointer' }}>
{task.text}
</span>
</li>
))}
</ul>
</div>
);
};
export default App;
Step 12: Set Up the Server
We’ll use a simple JSON server to handle tasks.
1. Install JSON Server globally if not already installed:
pnpm add -g json-server
2. Create a db.json file at the root of your project with initial data:
{
"tasks": [
{ "id": 1, "text": "Sample Task 1", "completed": false },
{ "id": 2, "text": "Sample Task 2", "completed": true }
]
}
3. Start the server:
json-server --watch todo.json --port 3000
The server will be running at http://localhost:3000.
Step 13: Test the App
1. Run the React app:
pnpm dev
2. Visit the app in your browser (e.g., http://localhost:5173).
3. Add tasks, toggle their completion, and see the data persist on the server.
Step 14: Add Styles (Optional)
You can improve the app’s look by adding styles in a separate src/styles.css file and importing it into src/main.jsx:
import './styles.css';
For example, a styles.css file might contain:
body {
font-family: Arial, sans-serif;
background: #f9f9f9;
margin: 0;
padding: 0;
}
h1 {
color: #333;
}
button {
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
Let’s style the TODO items to include checkboxes for marking tasks as complete. This will provide a better user experience with a clean, modern look.
Step 1: Update the App.jsx
Modify the TODO list in App.jsx to include checkboxes for marking tasks as complete. Here’s the updated code:
import React, { useState, useEffect } from 'react';
const App = () => {
const [tasks, setTasks] = useState([]);
const [newTask, setNewTask] = useState('');
// Fetch tasks from the server
useEffect(() => {
fetch('http://localhost:3000/tasks')
.then((response) => response.json())
.then((data) => setTasks(data))
.catch((error) => console.error('Error fetching tasks:', error));
}, []);
// Add a new task
const addTask = () => {
if (!newTask.trim()) return;
const task = { text: newTask, completed: false };
fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task),
})
.then((response) => response.json())
.then((savedTask) => {
setTasks([...tasks, savedTask]);
setNewTask('');
})
.catch((error) => console.error('Error adding task:', error));
};
// Toggle task completion
const toggleTask = (taskId) => {
const taskToUpdate = tasks.find((task) => task.id === taskId);
if (!taskToUpdate) return;
fetch(`http://localhost:3000/tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...taskToUpdate, completed: !taskToUpdate.completed }),
})
.then((response) => response.json())
.then((updatedTask) => {
setTasks(tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)));
})
.catch((error) => console.error('Error updating task:', error));
};
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: 'auto', fontFamily: 'Arial, sans-serif' }}>
<h1>TODO App</h1>
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Enter a task..."
style={{ flex: 1, padding: '8px' }}
/>
<button onClick={addTask} style={{ padding: '8px 12px' }}>
Add
</button>
</div>
<ul style={{ listStyle: 'none', padding: 0 }}>
{tasks.map((task) => (
<li
key={task.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 0',
borderBottom: '1px solid #ddd',
}}
>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', gap: '10px' }}>
<input
type="checkbox"
checked={task.completed}
onChange={() => toggleTask(task.id)}
/>
<span
style={{
textDecoration: task.completed ? 'line-through' : 'none',
color: task.completed ? '#999' : '#000',
}}
>
{task.text}
</span>
</label>
</li>
))}
</ul>
</div>
);
};
export default App;
Step 2: Style the App
To enhance the design, you can add a styles.css file. Here’s a simple example:
src/styles.css:
body {
font-family: Arial, sans-serif;
background: #f9f9f9;
margin: 0;
padding: 0;
}
h1 {
text-align: center;
color: #333;
}
button {
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover {
background-color: #0056b3;
}
input[type="text"] {
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
ul {
padding: 0;
margin: 0;
}
li {
transition: background-color 0.3s ease;
}
li:hover {
background-color: #f0f0f0;
}
Result
• Each TODO item now has a checkbox.
• Checked tasks have their text styled with a strikethrough and muted color.
• The overall app has a clean, minimal design.
Let’s break the App.jsx into separate components for better readability and maintainability. We’ll create three components:
1. TaskInput: For adding new tasks.
2. TaskItem: For rendering individual task items.
3. TaskList: For rendering the list of tasks.
Updated App.jsx
Here’s the updated App.jsx with the new structure:
import React, { useState, useEffect } from 'react';
import TaskInput from './TaskInput';
import TaskList from './TaskList';
const App = () => {
const [tasks, setTasks] = useState([]);
// Fetch tasks from the server
useEffect(() => {
fetch('http://localhost:3000/tasks')
.then((response) => response.json())
.then((data) => setTasks(data))
.catch((error) => console.error('Error fetching tasks:', error));
}, []);
// Add a new task
const addTask = (text) => {
const task = { text, completed: false };
fetch('http://localhost:3000/tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task),
})
.then((response) => response.json())
.then((savedTask) => setTasks([...tasks, savedTask]))
.catch((error) => console.error('Error adding task:', error));
};
// Toggle task completion
const toggleTask = (taskId) => {
const taskToUpdate = tasks.find((task) => task.id === taskId);
if (!taskToUpdate) return;
fetch(`http://localhost:3000/tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...taskToUpdate, completed: !taskToUpdate.completed }),
})
.then((response) => response.json())
.then((updatedTask) => {
setTasks(tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)));
})
.catch((error) => console.error('Error updating task:', error));
};
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: 'auto', fontFamily: 'Arial, sans-serif' }}>
<h1>TODO App</h1>
<TaskInput onAddTask={addTask} />
<TaskList tasks={tasks} onToggleTask={toggleTask} />
</div>
);
};
export default App;
TaskInput.jsx
This component handles adding new tasks.
import React, { useState } from 'react';
const TaskInput = ({ onAddTask }) => {
const [newTask, setNewTask] = useState('');
const handleAddTask = () => {
if (!newTask.trim()) return;
onAddTask(newTask);
setNewTask('');
};
return (
<div style={{ display: 'flex', gap: '10px', marginBottom: '20px' }}>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Enter a task..."
style={{ flex: 1, padding: '8px' }}
/>
<button onClick={handleAddTask} style={{ padding: '8px 12px' }}>
Add
</button>
</div>
);
};
export default TaskInput;
TaskItem.jsx
This component represents a single task with a checkbox.
import React from 'react';
const TaskItem = ({ task, onToggleTask }) => {
return (
<li
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 0',
borderBottom: '1px solid #ddd',
}}
>
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', gap: '10px' }}>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggleTask(task.id)}
/>
<span
style={{
textDecoration: task.completed ? 'line-through' : 'none',
color: task.completed ? '#999' : '#000',
}}
>
{task.text}
</span>
</label>
</li>
);
};
export default TaskItem;
TaskList.jsx
This component renders the list of tasks using the TaskItem component.
import React from 'react';
import TaskItem from './TaskItem';
const TaskList = ({ tasks, onToggleTask }) => {
return (
<ul style={{ listStyle: 'none', padding: 0 }}>
{tasks.map((task) => (
<TaskItem key={task.id} task={task} onToggleTask={onToggleTask} />
))}
</ul>
);
};
export default TaskList;
Final Project Structure
Your project structure should now look like this:
src/
├── App.jsx
├── TaskInput.jsx
├── TaskItem.jsx
├── TaskList.jsx
├── main.jsx
└── styles.css
Benefits of Refactoring🚀
• Readability: Smaller components are easier to understand.
• Reusability: Components like TaskItem can be reused in other parts of the app if needed.
• Scalability: Adding new features or modifying existing ones is simpler when components are separated.
Lets modify it farther to make App.jsx even smaller. What can we do?
To make App.jsx even smaller and more focused, we can extract state management and business logic into custom hooks or context providers. Here’s how to achieve that:
Step 1: Create a Custom Hook for Task Management
Move the logic for fetching tasks, adding tasks, and toggling task completion into a custom hook called useTasks.
src/hooks/useTasks.js:
import { useState, useEffect } from 'react';
const useTasks = (apiUrl) => {
const [tasks, setTasks] = useState([]);
useEffect(() => {
fetch(`${apiUrl}/tasks`)
.then((response) => response.json())
.then((data) => setTasks(data))
.catch((error) => console.error('Error fetching tasks:', error));
}, [apiUrl]);
const addTask = (text) => {
const task = { text, completed: false };
fetch(`${apiUrl}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task),
})
.then((response) => response.json())
.then((savedTask) => setTasks([...tasks, savedTask]))
.catch((error) => console.error('Error adding task:', error));
};
const toggleTask = (taskId) => {
const taskToUpdate = tasks.find((task) => task.id === taskId);
if (!taskToUpdate) return;
fetch(`${apiUrl}/tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...taskToUpdate, completed: !taskToUpdate.completed }),
})
.then((response) => response.json())
.then((updatedTask) => {
setTasks(tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)));
})
.catch((error) => console.error('Error updating task:', error));
};
return { tasks, addTask, toggleTask };
};
export default useTasks;
Step 2: Update App.jsx to Use the Custom Hook
With the logic moved to the custom hook, App.jsx is now much simpler:
src/App.jsx:
import React from 'react';
import useTasks from './hooks/useTasks';
import TaskInput from './TaskInput';
import TaskList from './TaskList';
const App = () => {
const apiUrl = 'http://localhost:3000'; // API URL
const { tasks, addTask, toggleTask } = useTasks(apiUrl);
return (
<div style={{ padding: '20px', maxWidth: '400px', margin: 'auto', fontFamily: 'Arial, sans-serif' }}>
<h1>TODO App</h1>
<TaskInput onAddTask={addTask} />
<TaskList tasks={tasks} onToggleTask={toggleTask} />
</div>
);
};
export default App;
Step 3: Refactor the API URL to a Context (Optional)
If you plan to reuse the API URL or centralize configuration, you can create an ApiProvider using React Context.
src/context/ApiProvider.jsx:
import React, { createContext, useContext } from 'react';
const ApiContext = createContext();
export const ApiProvider = ({ children, apiUrl }) => (
<ApiContext.Provider value={apiUrl}>{children}</ApiContext.Provider>
);
export const useApi = () => useContext(ApiContext);
Step 4: Update the App to Use the API Context
Wrap the app in the ApiProvider and refactor the useTasks hook to use the API URL from context:
Update src/main.jsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { ApiProvider } from './context/ApiProvider';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<ApiProvider apiUrl="http://localhost:3000">
<App />
</ApiProvider>
);
Update src/hooks/useTasks.js:
import { useState, useEffect } from 'react';
import { useApi } from '../context/ApiProvider';
const useTasks = () => {
const apiUrl = useApi();
const [tasks, setTasks] = useState([]);
useEffect(() => {
fetch(`${apiUrl}/tasks`)
.then((response) => response.json())
.then((data) => setTasks(data))
.catch((error) => console.error('Error fetching tasks:', error));
}, [apiUrl]);
const addTask = (text) => {
const task = { text, completed: false };
fetch(`${apiUrl}/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task),
})
.then((response) => response.json())
.then((savedTask) => setTasks([...tasks, savedTask]))
.catch((error) => console.error('Error adding task:', error));
};
const toggleTask = (taskId) => {
const taskToUpdate = tasks.find((task) => task.id === taskId);
if (!taskToUpdate) return;
fetch(`${apiUrl}/tasks/${taskId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...taskToUpdate, completed: !taskToUpdate.completed }),
})
.then((response) => response.json())
.then((updatedTask) => {
setTasks(tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)));
})
.catch((error) => console.error('Error updating task:', error));
};
return { tasks, addTask, toggleTask };
};
export default useTasks;
Resulting Project Structure
The project is now modular and clean:
src/ ├── App.jsx ├── context/ │ └── ApiProvider.jsx ├── hooks/ │ └── useTasks.js ├── TaskInput.jsx ├── TaskItem.jsx ├── TaskList.jsx ├── main.jsx └── styles.css
Benefits of Further Refactoring
1. Smaller App.jsx: Focuses purely on rendering.
2. Reusability: useTasks can be reused in other components or applications.
3. Centralized Config: The API URL is managed globally with React Context.
4. Scalability: Adding more hooks or features doesn’t clutter the main app file.
Lets add new component to this menu. That would navigate us through the app...
To add navigation to the app, we’ll incorporate a menu component and use React Router for navigation between pages. Here’s how we can enhance the app:
Step 1: Install React Router
Run the following command to add React Router to your project:
pnpm add react-router-dom
Step 2: Create a Menu Component
The Menu component will act as the navigation bar.
src/Menu.jsx:
import React from 'react';
import { NavLink } from 'react-router-dom';
const Menu = () => {
const linkStyle = {
padding: '10px 20px',
textDecoration: 'none',
color: '#007bff',
fontWeight: 'bold',
};
const activeStyle = {
color: '#0056b3',
textDecoration: 'underline',
};
return (
<nav style={{ marginBottom: '20px', borderBottom: '1px solid #ddd', paddingBottom: '10px' }}>
<NavLink to="/" style={linkStyle} activeStyle={activeStyle} exact>
Home
</NavLink>
<NavLink to="/tasks" style={linkStyle} activeStyle={activeStyle}>
Tasks
</NavLink>
<NavLink to="/about" style={linkStyle} activeStyle={activeStyle}>
About
</NavLink>
</nav>
);
};
export default Menu;
Step 3: Update App.jsx for Routing
We’ll use React Router to define routes for the app. Update App.jsx to include navigation and routing logic.
src/App.jsx:
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Menu from './Menu';
import TaskPage from './pages/TaskPage';
import AboutPage from './pages/AboutPage';
const App = () => {
return (
<Router>
<div style={{ padding: '20px', maxWidth: '600px', margin: 'auto', fontFamily: 'Arial, sans-serif' }}>
<h1>TODO App</h1>
<Menu />
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/tasks" element={<TaskPage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</div>
</Router>
);
};
const HomePage = () => (
<div>
<h2>Welcome to the TODO App</h2>
<p>Navigate to the "Tasks" page to manage your tasks or learn more about this app on the "About" page.</p>
</div>
);
export default App;
Step 4: Create Pages for Tasks and About
src/pages/TaskPage.jsx:
This page renders the TODO functionality.
import React from 'react';
import TaskInput from '../TaskInput';
import TaskList from '../TaskList';
import useTasks from '../hooks/useTasks';
const TaskPage = () => {
const apiUrl = 'http://localhost:3000'; // API URL
const { tasks, addTask, toggleTask } = useTasks(apiUrl);
return (
<div>
<h2>Task Manager</h2>
<TaskInput onAddTask={addTask} />
<TaskList tasks={tasks} onToggleTask={toggleTask} />
</div>
);
};
export default TaskPage;
src/pages/AboutPage.jsx:
This page displays information about the app.
import React from 'react';
const AboutPage = () => {
return (
<div>
<h2>About This App</h2>
<p>
This is a simple TODO app built with React. It allows you to create tasks, mark them as complete,
and store data on a server. Navigation is handled with React Router.
</p>
</div>
);
};
export default AboutPage;
Step 5: Final Project Structure
After these changes, your project structure will look like this:
src/ ├── App.jsx ├── Menu.jsx ├── TaskInput.jsx ├── TaskItem.jsx ├── TaskList.jsx ├── context/ │ └── ApiProvider.jsx ├── hooks/ │ └── useTasks.js ├── pages/ │ ├── AboutPage.jsx │ ├── TaskPage.jsx ├── main.jsx └── styles.css
Step 6: Result
• Home Page: A welcome page with navigation instructions.
• Tasks Page: The TODO management functionality.
• About Page: Information about the app.
Menu Navigation
The menu lets users switch between the “Home,” “Tasks,” and “About” pages seamlessly.
Here is a final result on my github with some errors corrected that I'm leaving for the reader to handle as a practice task.
https://github.com/garmoncheg/react-todo
Hope it helps someone starting with react.
Comments
Post a Comment