{"id":148909,"date":"2026-09-14T04:31:42","date_gmt":"2026-09-14T04:31:42","guid":{"rendered":"\/ca\/tutorials\/react-tutorial"},"modified":"2026-09-14T04:31:42","modified_gmt":"2026-09-14T04:31:42","slug":"react-tutorial","status":"publish","type":"post","link":"\/ca\/tutorials\/react-tutorial\/","title":{"rendered":"React tutorial: Learn React from setup to project"},"content":{"rendered":"<p class=\"wp-block-paragraph\">This React tutorial teaches you how to set up a React app, write JSX, build components, pass props, manage state, use hooks, handle forms, add routes, fetch API data, and build a small working project.<\/p><p class=\"wp-block-paragraph\">It works like a beginner React course, with practical lessons, examples, and exercises built around a task tracker app you&rsquo;ll complete by the end.<\/p><p class=\"wp-block-paragraph\">Here&rsquo;s the learning path in this React tutorial:<\/p><ol class=\"wp-block-list\">\n<li>Prepare a React project with Vite, Node.js, and npm.<\/li>\n\n\n\n<li>Learn JSX, build React components, and pass data with props.<\/li>\n\n\n\n<li>Add interactive state, events, and controlled forms.<\/li>\n\n\n\n<li>Use hooks like <strong>useState<\/strong> and <strong>useEffect<\/strong> to manage logic.<\/li>\n\n\n\n<li>Add React Router and fetch API data.<\/li>\n\n\n\n<li>Build a simple React task tracker project.<\/li>\n<\/ol><p class=\"wp-block-paragraph\"><\/p><h2 class=\"wp-block-heading\" id=\"h-what-will-you-learn-in-this-react-tutorial\">What will you learn in this React tutorial?<\/h2><p class=\"wp-block-paragraph\">You&rsquo;ll learn <a href=\"\/ca\/tutorials\/what-is-react\/\">React<\/a> from project setup to an app that&rsquo;s ready to publish online, with examples and short exercises that help you apply each concept as you go.<\/p><p class=\"wp-block-paragraph\">The lessons use the same task tracker as a running project, so you can see how individual React features work together in a complete app.<\/p><p class=\"wp-block-paragraph\">The table below shows what you&rsquo;ll practice in each lesson and how it contributes to the final project:<\/p><figure tabindex=\"0\" class=\"wp-block-table\"><table><tbody><tr><td><strong>Lesson<\/strong><\/td><td><strong>Concept<\/strong><\/td><td><strong>Practice task<\/strong><\/td><td><strong>Project outcome<\/strong><\/td><\/tr><tr><td>1<\/td><td>Node.js, npm, and Vite<\/td><td>Start the development server<\/td><td>React project running locally<\/td><\/tr><tr><td>2<\/td><td>JSX and rendering<\/td><td>Update the page markup<\/td><td>First custom interface<\/td><\/tr><tr><td>3<\/td><td>Components and props<\/td><td>Build reusable task items<\/td><td>Structured task list<\/td><\/tr><tr><td>4<\/td><td>State and events<\/td><td>Toggle and delete tasks<\/td><td>Interactive task tracker<\/td><\/tr><tr><td>5<\/td><td>Forms<\/td><td>Validate and add tasks<\/td><td>User-created tasks<\/td><\/tr><tr><td>6<\/td><td>Hooks<\/td><td>Run an effect and create a custom hook<\/td><td>Reusable component logic<\/td><\/tr><tr><td>7<\/td><td>React Router<\/td><td>Add multiple page views<\/td><td>Navigation between views<\/td><\/tr><tr><td>8<\/td><td>API data<\/td><td>Adapt fetched data and handle request states<\/td><td>External task data with loading and error feedback<\/td><\/tr><tr><td>9<\/td><td>Final project<\/td><td>Test and build the app for production<\/td><td>Complete task tracker ready to deploy<\/td><\/tr><\/tbody><\/table><\/figure><h3 class=\"wp-block-heading\">React tutorial prerequisites<\/h3><p class=\"wp-block-paragraph\">The prerequisites for following this React tutorial are basic knowledge of HTML, CSS, JavaScript, and running commands in a terminal. We&rsquo;ll show you how to install Node.js and <a href=\"\/ca\/tutorials\/what-is-npm\/\">npm<\/a>, so you don&rsquo;t need them beforehand.<\/p><p class=\"wp-block-paragraph\">You can follow along even if you&rsquo;ve never used React before.<\/p><p class=\"wp-block-paragraph\"><a href=\"\/ca\/tutorials\/what-is-javascript\/\">JavaScript is<\/a> the most important prerequisite because you&rsquo;ll use it when working with React to create components, handle events, update state, and work with data.<\/p><p class=\"wp-block-paragraph\">You should be familiar with JavaScript variables, functions, arrays, objects, destructuring, modules, and array methods such as <strong>map()<\/strong> and <strong>filter()<\/strong>.<\/p><h3 class=\"wp-block-heading\">What app will you build in this React tutorial?<\/h3><p class=\"wp-block-paragraph\">You&rsquo;ll build a React task tracker that lets you add, complete, reopen, and delete tasks, then open individual task details and navigate between pages.<\/p><p class=\"wp-block-paragraph\">The finished app will also load starter tasks from an API, show loading and error feedback, and include a responsive layout that works on smaller screens. By the end, you&rsquo;ll have a complete production build that&rsquo;s ready to deploy.<\/p><h2 class=\"wp-block-heading\" id=\"h-how-do-you-set-up-a-react-project\">How do you set up a React project?<\/h2><p class=\"wp-block-paragraph\">You set up the React project by installing <a href=\"\/ca\/tutorials\/what-is-node-js\/\">Node.js<\/a> and npm, then using Vite to create and run the app.<\/p><p class=\"wp-block-paragraph\">The following sections walk you through installing the required tools, creating the React project, and understanding its main files.<\/p><p class=\"wp-block-paragraph\">Check our <a href=\"\/ca\/tutorials\/node-js-tutorial\/\">Node.js tutorial<\/a> if you want to learn more about Node.js or need a refresher on the basics.<\/p><h3 class=\"wp-block-heading\">Install Node.js and npm<\/h3><p class=\"wp-block-paragraph\">To install Node.js and npm, download the latest Long-Term Support (LTS) version of Node.js from <a href=\"https:\/\/nodejs.org\/en\/download\" target=\"_blank\" rel=\"noopener\">its official website<\/a>. Then, double-click the installer to install it on your computer. npm comes with Node.js, so you don&rsquo;t need to install it separately.<\/p><p class=\"wp-block-paragraph\">You need Node.js to run tools like Vite, while npm installs and manages the packages your React project uses.<\/p><p class=\"wp-block-paragraph\">After installation, open your terminal and check that both tools are available:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">node --version\nnpm --version<\/pre><p class=\"wp-block-paragraph\">Each command should return a version number. Reinstall Node.js if either command doesn&rsquo;t return one.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e625b2ac\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e625b2ac\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/terminal-node-js-npm-versions.png\/w=1024,h=1024,fit=scale-down\" alt=\"Terminal showing installed Node.js and npm version numbers\" class=\"wp-image-156920\" title=\"terminal-node-js-npm-versions\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><h3 class=\"wp-block-heading\">Create a React app with Vite<\/h3><p class=\"wp-block-paragraph\">Create the task tracker app with <a href=\"\/ca\/tutorials\/what-is-vite\/\">Vite<\/a> by running the project setup command, installing its packages, and starting the local development server.<\/p><p class=\"wp-block-paragraph\">This tutorial uses Vite because Create React App, a tool previously used to start React projects, is deprecated. Vite gives you a ready-to-use React project, so you can start coding without configuring the setup yourself.<\/p><p class=\"wp-block-paragraph\">Run the following commands in your terminal:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">npm create vite@latest react-task-tracker -- --template react\ncd react-task-tracker\nnpm install\nnpm run dev<\/pre><p class=\"wp-block-paragraph\">The <strong>&ndash;template react<\/strong> option creates a JavaScript React project, while <strong>npm install<\/strong> installs the packages listed in <strong>package.json<\/strong>. Then, <strong>npm run dev<\/strong> starts the development server.<\/p><p class=\"wp-block-paragraph\">Vite will show a local address in your terminal, such as<a href=\"http:\/\/localhost:5173\" target=\"_blank\" rel=\"noopener\"> <\/a><strong>http:\/\/localhost:5173<\/strong>. Open it in your browser to see the starter React app.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e625f086\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e625f086\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/terminal-vite-development-server-localhost.png\/w=1024,h=1024,fit=scale-down\" alt=\"Terminal showing the Vite development server running on localhost\" class=\"wp-image-156921\" title=\"terminal-vite-development-server-localhost\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><h3 class=\"wp-block-heading\">Understand the React project files<\/h3><p class=\"wp-block-paragraph\">The main React project files you&rsquo;ll work with are inside the <strong>src<\/strong> folder, including <strong>main.jsx<\/strong>, <strong>App.jsx<\/strong>, and the project&rsquo;s CSS files. As you build the task tracker, you&rsquo;ll also create folders for components and pages.<\/p><p class=\"wp-block-paragraph\">Here&rsquo;s what you&rsquo;ll use each important file or folder for:<\/p><figure tabindex=\"0\" class=\"wp-block-table\"><table><tbody><tr><td><strong>File or folder<\/strong><\/td><td><strong>What you use it for<\/strong><\/td><\/tr><tr><td><strong>package.json<\/strong><\/td><td>Lists the project packages and commands, such as <strong>npm run dev<\/strong><\/td><\/tr><tr><td><strong>index.html<\/strong><\/td><td>Provides the HTML page where your React app appears<\/td><\/tr><tr><td><strong>src\/main.jsx<\/strong><\/td><td>Starts the React app and displays the main <strong>App<\/strong> component<\/td><\/tr><tr><td><strong>src\/App.jsx<\/strong><\/td><td>Contains the main app component and, later, its routes<\/td><\/tr><tr><td><strong>src\/App.css<\/strong><\/td><td>Stores styles for the main app<\/td><\/tr><tr><td><strong>src\/index.css<\/strong><\/td><td>Stores global styles for the app<\/td><\/tr><tr><td><strong>src\/components<\/strong><\/td><td>Stores reusable components you&rsquo;ll create, such as <strong>Header.jsx<\/strong> and <strong>TaskItem.jsx<\/strong><\/td><\/tr><tr><td><strong>src\/pages<\/strong><\/td><td>Stores the page components you&rsquo;ll create for React Router<\/td><\/tr><\/tbody><\/table><\/figure><p class=\"wp-block-paragraph\">Match filename capitalization exactly, such as <strong>App.jsx<\/strong> and <strong>TaskItem.jsx<\/strong>, because some operating systems treat uppercase and lowercase filenames differently.<\/p><h2 class=\"wp-block-heading\" id=\"h-how-does-react-render-a-user-interface\">How does React render a user interface?<\/h2><p class=\"wp-block-paragraph\">React renders a user interface by running your components, using the JSX they return to determine what should appear, and updating the page in your browser with the result.<\/p><p class=\"wp-block-paragraph\">This process has three main steps:<\/p><ol class=\"wp-block-list\">\n<li><strong>Trigger<\/strong>. The initial render starts when React runs <strong>createRoot(&hellip;).render()<\/strong> in <strong>src\/main.jsx<\/strong>. Later, changes to state can trigger another render.<\/li>\n\n\n\n<li><strong>Render<\/strong>. React runs your components and reads their JSX to determine what the interface should look like.<\/li>\n\n\n\n<li><strong>Commit<\/strong>. React applies the necessary changes to the page so you see the latest interface.<\/li>\n<\/ol><p class=\"wp-block-paragraph\">For example, changing a task&rsquo;s status triggers another render. React calculates the latest interface, then commits the necessary changes to the page.<\/p><h3 class=\"wp-block-heading\">What is JSX in React?<\/h3><p class=\"wp-block-paragraph\">JSX is a JavaScript syntax extension that lets you write HTML-like markup inside React components. You use it to describe what should appear on the page while keeping that markup close to the JavaScript that controls it.<\/p><p class=\"wp-block-paragraph\">JSX looks similar to HTML, but it follows a few different rules:<\/p><ul class=\"wp-block-list\">\n<li>Wrap multiple elements in one parent element or a fragment such as <strong>&lt;&gt;&hellip;&lt;\/&gt;<\/strong>.<\/li>\n\n\n\n<li>Close every tag, including self-closing tags such as <strong>&lt;img \/&gt;<\/strong>.<\/li>\n\n\n\n<li>Use <strong>className<\/strong> instead of the HTML <strong>class<\/strong> attribute.<\/li>\n\n\n\n<li>Write most attributes in camelCase, such as <strong>onClick<\/strong>.<\/li>\n\n\n\n<li>Put JavaScript values or expressions inside curly braces, such as <strong>{taskName}<\/strong>.<\/li>\n<\/ul><p class=\"wp-block-paragraph\">For example, replace the contents of <strong>src\/App.jsx<\/strong> with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const taskName = 'Learn JSX';\n\nfunction App() {\n   return (\n      &lt;main className=\"task-list\"&gt;\n         &lt;h1&gt;Task tracker&lt;\/h1&gt;\n         &lt;p&gt;{taskName}&lt;\/p&gt;\n      &lt;\/main&gt;\n   );\n}\n\nexport default App;<\/pre><p class=\"wp-block-paragraph\">Here, the JSX defines the heading and paragraph that appear in your browser. The <strong>{taskName}<\/strong> expression inserts the value assigned to the <strong>taskName<\/strong> variable into the paragraph.<\/p><p class=\"wp-block-paragraph\">For a quick exercise, change <strong>&lsquo;Learn JSX&rsquo;<\/strong> to another task. Then, inside the <strong>&lt;main&gt;<\/strong> element, add another paragraph below <strong>&lt;p&gt;{taskName}&lt;\/p&gt;<\/strong>, such as <strong>&lt;p&gt;Not started&lt;\/p&gt;<\/strong>.<\/p><p class=\"wp-block-paragraph\">Save <strong>App.jsx<\/strong>, then check that the updated task and the new status appear in your browser.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e6263526\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e6263526\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-jsx-task-status.png\/w=1024,h=1024,fit=scale-down\" alt=\"Browser showing a Task tracker heading, task name, and Not started status\" class=\"wp-image-156913\" title=\"task-tracker-jsx-task-status\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><h3 class=\"wp-block-heading\">How do you render your first React component?<\/h3><p class=\"wp-block-paragraph\">You render your first React component by importing <strong>App.jsx<\/strong> into <strong>src\/main.jsx<\/strong> and passing <strong>&lt;App \/&gt;<\/strong> to <strong>render()<\/strong>. The Vite project you created earlier already includes this setup.<\/p><p class=\"wp-block-paragraph\">Open <strong>src\/main.jsx<\/strong> to see how it works:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { StrictMode } from 'react';\nimport { createRoot } from 'react-dom\/client';\nimport '.\/index.css';\nimport App from '.\/App.jsx';\n\ncreateRoot(document.getElementById('root')).render(\n   &lt;StrictMode&gt;\n      &lt;App \/&gt;\n   &lt;\/StrictMode&gt;,\n);<\/pre><p class=\"wp-block-paragraph\">The <strong>document.getElementById(&lsquo;root&rsquo;)<\/strong> part finds the element with the <strong>root<\/strong> ID in <strong>index.html<\/strong>. React then displays the app inside that element, which is why the JSX you added to <strong>App.jsx<\/strong> appears in your browser.<\/p><p class=\"wp-block-paragraph\">You&rsquo;ll add more components inside <strong>App<\/strong> as you build the task tracker. <strong>StrictMode<\/strong> also helps you catch common problems while developing, and its extra checks don&rsquo;t run in production.<\/p><h3 class=\"wp-block-heading\">How does React update the DOM?<\/h3><p class=\"wp-block-paragraph\">React updates the Document Object Model (DOM) during the commit step by applying the changes needed to make the page match the latest render.<\/p><p class=\"wp-block-paragraph\">The DOM represents the elements currently displayed in your browser. React can update a changed element without recreating unrelated parts of the page.<\/p><p class=\"wp-block-paragraph\">For example, when a task changes from <strong>Open<\/strong> to <strong>Done<\/strong>, React can update the task&rsquo;s displayed status while leaving unchanged elements, such as the page heading, alone.<\/p><h2 class=\"wp-block-heading\" id=\"h-how-do-react-components-work\">How do React components work?<\/h2><p class=\"wp-block-paragraph\">React components work as reusable JavaScript functions that return JSX for part of the interface. Each one handles one part, such as a header, task list, or individual task, then combines with other components to build the full page.<\/p><p class=\"wp-block-paragraph\">Keeping these parts separate makes your React project easier to update.<\/p><h3 class=\"wp-block-heading\">Create your first React component<\/h3><p class=\"wp-block-paragraph\">Create your first React component by defining a JavaScript function with a capitalized name and returning JSX from it. You&rsquo;ll start with the task tracker header.<\/p><p class=\"wp-block-paragraph\">First, create the <strong>src\/components<\/strong> folder. Inside it, create <strong>Header.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function Header() {\n   return (\n      &lt;header className=\"header\"&gt;\n         &lt;h1&gt;Task tracker&lt;\/h1&gt;\n      &lt;\/header&gt;\n   );\n}\n\nexport default Header;<\/pre><p class=\"wp-block-paragraph\">The <strong>export default Header<\/strong> line lets you import the component into another file.<\/p><p class=\"wp-block-paragraph\">Next, replace the contents of <strong>src\/App.jsx<\/strong> with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import Header from '.\/components\/Header.jsx';\n\nfunction App() {\n   return (\n      &lt;main className=\"app\"&gt;\n         &lt;Header \/&gt;\n      &lt;\/main&gt;\n   );\n}\n\nexport default App;<\/pre><p class=\"wp-block-paragraph\">Add <strong>&lt;Header \/&gt;<\/strong> inside <strong>App<\/strong> to display the header in the page. Save both files, and you should see <strong>Task tracker<\/strong> in your browser.<\/p><h3 class=\"wp-block-heading\">Pass data with props<\/h3><p class=\"wp-block-paragraph\">To pass data with props, add values to a child component when you render it, then read those values inside the child.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/TaskItem.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function TaskItem({ title, done }) {\n   return (\n      &lt;li className=\"task-item\"&gt;\n         {done ? '&#10003; ' : '&#9675; '}\n         {title}\n      &lt;\/li&gt;\n   );\n}\n\nexport default TaskItem;<\/pre><p class=\"wp-block-paragraph\">The <strong>{ title, done }<\/strong> syntax reads the <strong>title<\/strong> and <strong>done<\/strong> props passed to the component.<\/p><p class=\"wp-block-paragraph\">Now import <strong>TaskItem<\/strong> into <strong>src\/App.jsx<\/strong> and render it twice:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import Header from '.\/components\/Header.jsx';\nimport TaskItem from '.\/components\/TaskItem.jsx';\n\nfunction App() {\n   return (\n      &lt;main className=\"app\"&gt;\n         &lt;Header \/&gt;\n         &lt;ul&gt;\n            &lt;TaskItem title=\"Learn JSX\" done={true} \/&gt;\n            &lt;TaskItem title=\"Build a component\" done={false} \/&gt;\n         &lt;\/ul&gt;\n      &lt;\/main&gt;\n   );\n}\n\nexport default App;<\/pre><p class=\"wp-block-paragraph\">The first <strong>TaskItem<\/strong> appears complete, while the second appears incomplete because they receive different <strong>done<\/strong> values.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e6267d82\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e6267d82\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-task-list-completed-incomplete.png\/w=1024,h=1024,fit=scale-down\" alt=\"Browser showing completed and incomplete tasks passed with React props\" class=\"wp-image-156918\" title=\"task-tracker-task-list-completed-incomplete\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\">Note that props are read-only. A child component can use the values it receives, but it should never change them. Only the parent passing the prop can change it.<\/p><h3 class=\"wp-block-heading\">Compose components into a page<\/h3><p class=\"wp-block-paragraph\">Compose components into a page by creating a <strong>TaskList <\/strong>component, nesting it inside <strong>App<\/strong>, and rendering a <strong>TaskItem<\/strong> for each task in the list.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/TaskList.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import TaskItem from '.\/TaskItem.jsx';\n\nfunction TaskList({ tasks }) {\n   if (tasks.length === 0) {\n      return &lt;p&gt;No tasks yet.&lt;\/p&gt;;\n   }\n\n   return (\n      &lt;ul className=\"task-list\"&gt;\n         {tasks.map((task) =&gt; (\n            &lt;TaskItem\n               key={task.id}\n               title={task.title}\n               done={task.done}\n            \/&gt;\n         ))}\n      &lt;\/ul&gt;\n   );\n}\n\nexport default TaskList;<\/pre><p class=\"wp-block-paragraph\">The <strong>map()<\/strong> method creates one <strong>TaskItem<\/strong> for each task in the array. The <strong>key<\/strong> gives React a stable way to identify each task when the list changes.<\/p><p class=\"wp-block-paragraph\">Next, update <strong>src\/App.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import Header from '.\/components\/Header.jsx';\nimport TaskList from '.\/components\/TaskList.jsx';\n\nconst tasks = [\n   { id: 1, title: 'Learn JSX', done: true },\n   { id: 2, title: 'Build a component', done: false },\n];\n\nfunction App() {\n   return (\n      &lt;&gt;\n         &lt;Header \/&gt;\n         &lt;main className=\"app\"&gt;\n            &lt;TaskList tasks={tasks} \/&gt;\n         &lt;\/main&gt;\n      &lt;\/&gt;\n   );\n}\n\nexport default App;<\/pre><p class=\"wp-block-paragraph\">This structure keeps the task-list markup out of <strong>App.jsx<\/strong> and gives each part of the interface a clear responsibility.<\/p><h2 class=\"wp-block-heading\" id=\"h-how-do-state-and-events-make-a-react-app-interactive\">How do state and events make a React app interactive?<\/h2><p class=\"wp-block-paragraph\">State and events make a React app interactive by storing data that can change (state) and responding to actions such as clicks (events). For the task tracker app, you&rsquo;ll use state to store the tasks and click events to mark them complete or delete them.<\/p><p class=\"wp-block-paragraph\">React updates the page to show the new data when you interact with a task and its state changes.<\/p><h3 class=\"wp-block-heading\">Add state with useState<\/h3><p class=\"wp-block-paragraph\">Add state with <strong>useState<\/strong> by calling it inside your component and passing the starting value. <strong>useState<\/strong> gives you the current value and a function for updating it. Use it when your component needs to remember data that can change.<\/p><p class=\"wp-block-paragraph\">In <strong>src\/App.jsx<\/strong>, import <strong>useState<\/strong> and move the task data into state:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useState } from 'react';\nimport Header from '.\/components\/Header.jsx';\nimport TaskList from '.\/components\/TaskList.jsx';\n\nconst initialTasks = [\n   { id: 1, title: 'Learn JSX', done: true },\n   { id: 2, title: 'Build a component', done: false },\n];\n\nfunction App() {\n   const [tasks, setTasks] = useState(initialTasks);\n\n   return (\n      &lt;&gt;\n         &lt;Header \/&gt;\n         &lt;main className=\"app\"&gt;\n            &lt;TaskList tasks={tasks} \/&gt;\n         &lt;\/main&gt;\n      &lt;\/&gt;\n   );\n}\n\nexport default App;<\/pre><p class=\"wp-block-paragraph\">In <strong>const [tasks, setTasks] = useState(initialTasks)<\/strong>, <strong>tasks<\/strong> contains the current task list, while <strong>setTasks<\/strong> updates it. <strong>initialTasks<\/strong> provides the starting value when the component first renders.<\/p><p class=\"wp-block-paragraph\">For practice, temporarily change <strong>useState(initialTasks)<\/strong> to <strong>useState([])<\/strong>. The task list should show <strong>No tasks yet.<\/strong> Change it back to <strong>useState(initialTasks)<\/strong> before continuing.<\/p><h3 class=\"wp-block-heading\">Handle click events<\/h3><p class=\"wp-block-paragraph\">To handle click events, pass functions to the <strong>onClick<\/strong> props in <strong>TaskItem<\/strong> and connect them to the task state in <strong>App<\/strong>.<\/p><p class=\"wp-block-paragraph\">First, add these functions below the <strong>useState<\/strong> line in <strong>App.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function toggleTask(id) {\n   setTasks((currentTasks) =&gt;\n      currentTasks.map((task) =&gt;\n         task.id === id\n            ? { ...task, done: !task.done }\n            : task\n      )\n   );\n}\n\nfunction deleteTask(id) {\n   setTasks((currentTasks) =&gt;\n      currentTasks.filter((task) =&gt; task.id !== id)\n   );\n}<\/pre><p class=\"wp-block-paragraph\">Then, pass both functions to <strong>TaskList<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;TaskList\n   tasks={tasks}\n   onToggle={toggleTask}\n   onDelete={deleteTask}\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">Next, update <strong>src\/components\/TaskList.jsx,<\/strong> so it passes the functions and each task ID to <strong>TaskItem<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import TaskItem from '.\/TaskItem.jsx';\n\nfunction TaskList({ tasks, onToggle, onDelete }) {\n   if (tasks.length === 0) {\n      return &lt;p&gt;No tasks yet.&lt;\/p&gt;;\n   }\n\n   return (\n      &lt;ul className=\"task-list\"&gt;\n         {tasks.map((task) =&gt; (\n            &lt;TaskItem\n               key={task.id}\n               id={task.id}\n               title={task.title}\n               done={task.done}\n               onToggle={onToggle}\n               onDelete={onDelete}\n            \/&gt;\n         ))}\n      &lt;\/ul&gt;\n   );\n}\n\nexport default TaskList;<\/pre><p class=\"wp-block-paragraph\">Finally, update <strong>src\/components\/TaskItem.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function TaskItem({\n   id,\n   title,\n   done,\n   onToggle,\n   onDelete,\n}) {\n   return (\n      &lt;li className=\"task-item\"&gt;\n         &lt;span&gt;\n            {done ? '&#10003; ' : '&#9675; '}\n            {title}\n         &lt;\/span&gt;\n\n         &lt;button\n            type=\"button\"\n            onClick={() =&gt; onToggle(id)}\n         &gt;\n            {done ? 'Mark open' : 'Mark done'}\n         &lt;\/button&gt;\n\n         &lt;button\n            type=\"button\"\n            onClick={() =&gt; onDelete(id)}\n         &gt;\n            Delete\n         &lt;\/button&gt;\n      &lt;\/li&gt;\n   );\n}\n\nexport default TaskItem;<\/pre><p class=\"wp-block-paragraph\">Click <strong>Mark done<\/strong> or <strong>Mark open<\/strong> to change a task&rsquo;s status. Click <strong>Delete<\/strong> to remove it from the list.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e626c200\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e626c200\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-task-list-task-actions.png\/w=1024,h=1024,fit=scale-down\" alt=\"Task tracker with Mark done, Mark open, and Delete buttons\" class=\"wp-image-156919\" title=\"task-tracker-task-list-task-actions\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\">The arrow function in <strong>onClick={() =&gt; onToggle(id)}<\/strong> waits until you click the button before calling <strong>onToggle<\/strong>. Writing <strong>onClick={onToggle(id)}<\/strong> instead would call the function immediately while React renders the component.<\/p><p class=\"wp-block-paragraph\">For a quick test, temporarily add a <strong>Log task<\/strong> button inside the <strong>&lt;li className=&rdquo;task-item&rdquo;&gt;<\/strong> element in <strong>TaskItem.jsx<\/strong> and give it <strong>onClick={() =&gt; console.log(title)}<\/strong>.<\/p><p class=\"wp-block-paragraph\">Click the button and confirm that the task title appears in your browser&rsquo;s developer console. Then, remove the button before continuing.<\/p><h3 class=\"wp-block-heading\">Update arrays and objects without mutation<\/h3><p class=\"wp-block-paragraph\">Update arrays and objects in React state by creating new versions instead of changing the existing state directly. The <strong>toggleTask<\/strong> and <strong>deleteTask<\/strong> functions you just added both follow this pattern.<\/p><p class=\"wp-block-paragraph\">In <strong>toggleTask<\/strong>, <strong>map()<\/strong> creates a new array. For the task with the matching ID, the spread syntax copies the existing task into a new object and changes its <strong>done<\/strong> value:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">currentTasks.map((task) =&gt;\n   task.id === id\n      ? { ...task, done: !task.done }\n      : task\n)<\/pre><p class=\"wp-block-paragraph\">The other task objects stay unchanged.<\/p><p class=\"wp-block-paragraph\">In <strong>deleteTask<\/strong>, <strong>filter()<\/strong> creates a new array without the task whose ID matches the one you want to remove:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">currentTasks.filter((task) =&gt; task.id !== id)<\/pre><p class=\"wp-block-paragraph\">Avoid changing state directly, for example:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">task.done = true;<\/pre><p class=\"wp-block-paragraph\">Instead, create a new object with the updated value:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{ ...task, done: true }<\/pre><p class=\"wp-block-paragraph\">The same rule applies to arrays. Instead of methods such as <strong>push()<\/strong>, create a new array and pass it to <strong>setTasks<\/strong>.<\/p><h2 class=\"wp-block-heading\" id=\"h-how-do-react-forms-collect-user-input\">How do React forms collect user input?<\/h2><p class=\"wp-block-paragraph\">React forms collect user input by storing field values in state and updating them as you type. In the task tracker, <strong>TaskForm<\/strong> will control the title field and pass submitted values to <strong>App<\/strong>.<\/p><h3 class=\"wp-block-heading\">Build a controlled form<\/h3><p class=\"wp-block-paragraph\">You can build a controlled form by setting the input value from React state and updating that state whenever you type.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/TaskForm.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useState } from 'react';\n\nfunction TaskForm({ onAdd }) {\n   const [title, setTitle] = useState('');\n\n   function handleSubmit(event) {\n      event.preventDefault();\n      onAdd(title);\n      setTitle('');\n   }\n\n   return (\n      &lt;form className=\"task-form\" onSubmit={handleSubmit}&gt;\n         &lt;label htmlFor=\"task-title\"&gt;\n            Task name\n         &lt;\/label&gt;\n\n         &lt;input\n            id=\"task-title\"\n            name=\"task-title\"\n            type=\"text\"\n            placeholder=\"Add a new task...\"\n            value={title}\n            onChange={(event) =&gt; setTitle(event.target.value)}\n         \/&gt;\n\n         &lt;button type=\"submit\"&gt;\n            Add task\n         &lt;\/button&gt;\n      &lt;\/form&gt;\n   );\n}\n\nexport default TaskForm;<\/pre><p class=\"wp-block-paragraph\">The <strong>value={title}<\/strong> prop keeps the field value connected to <strong>title<\/strong> state, while <strong>onChange<\/strong> updates that state as you type.<\/p><p class=\"wp-block-paragraph\">When you submit the form, <strong>handleSubmit<\/strong> prevents the browser&rsquo;s default page reload, passes the current title to <strong>onAdd<\/strong>, and clears the field.<\/p><h3 class=\"wp-block-heading\">Validate input before updating state<\/h3><p class=\"wp-block-paragraph\">Validate the task title before calling <strong>onAdd<\/strong> so empty or whitespace-only values don&rsquo;t become tasks.<\/p><p class=\"wp-block-paragraph\">In <strong>TaskForm.jsx<\/strong>, replace <strong>handleSubmit<\/strong> with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function handleSubmit(event) {\n   event.preventDefault();\n\n   const trimmedTitle = title.trim();\n\n   if (trimmedTitle === '') {\n      return;\n   }\n\n   onAdd(trimmedTitle);\n   setTitle('');\n}<\/pre><p class=\"wp-block-paragraph\">The <strong>trim()<\/strong> method removes spaces from the beginning and end of the title. A value containing only spaces becomes an empty string, so the function stops before passing it to <strong>onAdd<\/strong>.<\/p><p class=\"wp-block-paragraph\">This validation is enough for the task tracker. You can add other rules, such as a maximum title length, when your project needs them.<\/p><h3 class=\"wp-block-heading\">Add new tasks to the project<\/h3><p class=\"wp-block-paragraph\">To add new tasks to your project, connect <strong>TaskForm<\/strong> to the <strong>tasks<\/strong> state in <strong>App.jsx<\/strong>.<\/p><p class=\"wp-block-paragraph\">First, import <strong>TaskForm<\/strong> at the top of <strong>src\/App.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import TaskForm from '.\/components\/TaskForm.jsx';<\/pre><p class=\"wp-block-paragraph\">Then, add this function inside <strong>App<\/strong>, below your existing <strong>useState<\/strong> line:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function addTask(title) {\n   const newTask = {\n      id: Date.now(),\n      title,\n      done: false,\n   };\n\n   setTasks((currentTasks) =&gt; [\n      ...currentTasks,\n      newTask,\n   ]);\n}<\/pre><p class=\"wp-block-paragraph\">Finally, render <strong>TaskForm<\/strong> above <strong>TaskList<\/strong> and pass <strong>addTask<\/strong> through the <strong>onAdd<\/strong> prop:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;TaskForm onAdd={addTask} \/&gt;\n\n&lt;TaskList\n   tasks={tasks}\n   onToggle={toggleTask}\n   onDelete={deleteTask}\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">When you submit a valid title, <strong>addTask<\/strong> creates a new task with an ID and an initial <strong>done<\/strong> value of <strong>false<\/strong>, then adds it to the task list.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e6270380\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e6270380\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-task-form-added-task.png\/w=1024,h=1024,fit=scale-down\" alt=\"React task form with an input, Add task button, and newly added task\" class=\"wp-image-156916\" title=\"task-tracker-task-form-added-task\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><h2 class=\"wp-block-heading\" id=\"h-how-do-react-hooks-manage-logic-and-side-effects\">How do React hooks manage logic and side effects?<\/h2><p class=\"wp-block-paragraph\">React hooks manage component logic by letting you store changing data, connect to external systems, and reuse logic across components.<\/p><p class=\"wp-block-paragraph\">You&rsquo;ve already used <strong>useState<\/strong> for task data and form values. Next, you&rsquo;ll use <strong>useEffect<\/strong> to run code that connects your component to something outside React, such as an API.<\/p><p class=\"wp-block-paragraph\">React also provides other hooks, including <strong>useRef<\/strong>, <strong>useMemo<\/strong>, and <strong>useCallback<\/strong>, but you don&rsquo;t need them for this task tracker yet.<\/p><h3 class=\"wp-block-heading\">Use useEffect to fetch data<\/h3><p class=\"wp-block-paragraph\">To use <strong>useEffect<\/strong> to fetch data, start the request inside the effect and store the response in state.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/ApiTasks.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useEffect, useState } from 'react';\n\nfunction ApiTasks() {\n   const [tasks, setTasks] = useState([]);\n\n   useEffect(() =&gt; {\n      let ignore = false;\n\n      async function loadTasks() {\n         try {\n            const response = await fetch(\n               'https:\/\/jsonplaceholder.typicode.com\/todos?_limit=5'\n            );\n\n            if (!response.ok) {\n               throw new Error(\n                  `Request failed with status ${response.status}`\n               );\n            }\n\n            const data = await response.json();\n\n            if (!ignore) {\n               setTasks(data);\n            }\n         } catch (error) {\n            if (!ignore) {\n               console.error(error);\n            }\n         }\n      }\n\n      loadTasks();\n\n      return () =&gt; {\n         ignore = true;\n      };\n   }, []);\n\n   return (\n      &lt;ul&gt;\n         {tasks.map((task) =&gt; (\n            &lt;li key={task.id}&gt;\n               {task.completed ? '&#10003; ' : '&#9675; '}\n               {task.title}\n            &lt;\/li&gt;\n         ))}\n      &lt;\/ul&gt;\n   );\n}\n\nexport default ApiTasks;<\/pre><p class=\"wp-block-paragraph\">This example requests five sample tasks from <a href=\"https:\/\/jsonplaceholder.typicode.com\" target=\"_blank\" rel=\"noopener\">JSONPlaceholder<\/a> after the component renders. When the request succeeds, <strong>setTasks<\/strong> stores the response and triggers another render with the task data.<\/p><p class=\"wp-block-paragraph\">The empty dependency array in <strong>useEffect(&hellip;, [])<\/strong> means the effect runs once after the component first appears, instead of after every render. With <strong>StrictMode<\/strong> enabled, React runs the effect twice during development to help you spot missing cleanup.<\/p><p class=\"wp-block-paragraph\">This doesn&rsquo;t happen in production, so two requests in your network tab are expected.<\/p><p class=\"wp-block-paragraph\">The <strong>ignore<\/strong> variable prevents the request from updating state after React cleans up the effect.<\/p><p class=\"wp-block-paragraph\">To check the result, temporarily import <strong>ApiTasks<\/strong> into <strong>src\/App.jsx<\/strong> and render <strong>&lt;ApiTasks \/&gt;<\/strong> below <strong>TaskList<\/strong>. Five sample tasks should appear after the request completes.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e6274656\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e6274656\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-api-tasks-jsonplaceholder.png\/w=1024,h=1024,fit=scale-down\" alt=\"Five tasks fetched from JSONPlaceholder and displayed in a list\" class=\"wp-image-156908\" title=\"task-tracker-api-tasks-jsonplaceholder\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\">Remove the temporary <strong>&lt;ApiTasks \/&gt;<\/strong> render and its import after confirming the result.<\/p><h3 class=\"wp-block-heading\">Avoid common useEffect mistakes<\/h3><p class=\"wp-block-paragraph\">Avoid common <strong>useEffect<\/strong> mistakes by reserving effects for code that needs to synchronize with something outside React, not values you can calculate during rendering or actions you can handle directly.<\/p><p class=\"wp-block-paragraph\">For example, calculate the number of completed tasks directly from <strong>tasks<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const completedTasks = tasks.filter(\n   (task) =&gt; task.done\n);<\/pre><p class=\"wp-block-paragraph\">React recalculates <strong>completedTasks<\/strong> when the component renders, so you don&rsquo;t need an effect to store the result separately.<\/p><p class=\"wp-block-paragraph\">Keep user-triggered logic in event handlers as well. For example, add a task in the form submission handler and delete a task in its click handler, rather than using an effect.<\/p><p class=\"wp-block-paragraph\">Also check an effect&rsquo;s dependencies when it updates state. Updating a value that causes the same effect to run again can create an update loop.<\/p><h3 class=\"wp-block-heading\">Create a custom hook<\/h3><p class=\"wp-block-paragraph\">You can create a custom hook by moving reusable logic that uses state or other hooks into a JavaScript function whose name starts with <strong>use<\/strong>. The function can call other React hooks and return the values or functions your components need.<\/p><p class=\"wp-block-paragraph\">For example, create <strong>src\/hooks\/useTaskFilter.js<\/strong> to keep the task-filtering logic in one place:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useState } from 'react';\n\nfunction useTaskFilter(tasks) {\n   const [filter, setFilter] = useState('all');\n\n   const filteredTasks = tasks.filter((task) =&gt; {\n      if (filter === 'done') {\n         return task.done;\n      }\n\n      if (filter === 'active') {\n         return !task.done;\n      }\n\n      return true;\n   });\n\n   return {\n      filter,\n      setFilter,\n      filteredTasks,\n   };\n}\n\nexport default useTaskFilter;<\/pre><p class=\"wp-block-paragraph\">A component can then call <strong>useTaskFilter(tasks)<\/strong> to get the current filter, change it with <strong>setFilter<\/strong>, and display <strong>filteredTasks<\/strong>. Each component that calls the hook gets its own filter state.<\/p><p class=\"wp-block-paragraph\">You&rsquo;ll use this hook in the state and events exercise later, so keep the file even though the rest of the project doesn&rsquo;t import it.<\/p><p class=\"wp-block-paragraph\">You don&rsquo;t need to create a custom hook for every piece of logic, though. Keep simple logic inside the component when that makes the code easier to follow.<\/p><h2 class=\"wp-block-heading\" id=\"h-how-do-react-router-and-api-data-turn-components-into-an-app\">How do React Router and API data turn components into an app?<\/h2><p class=\"wp-block-paragraph\">React Router and API data turn your components into an app by connecting components to URLs and filling them with data from external sources.<\/p><p class=\"wp-block-paragraph\">In the task tracker, React Router will add dashboard, task details, and about views, while API data will provide tasks that aren&rsquo;t hardcoded in the project.<\/p><h3 class=\"wp-block-heading\">Add pages with React Router<\/h3><p class=\"wp-block-paragraph\">To add pages with React Router, install it first, then wrap <strong>App<\/strong> with <strong>BrowserRouter<\/strong> and map URL paths to components.<\/p><p class=\"wp-block-paragraph\">Install the latest version of React Router:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">npm install react-router<\/pre><p><div class=\"announcement-block announcement-block--important\">\n            <span class=\"announcement-block__heading\">\n                <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n                    <path fill-rule=\"evenodd\" clip-rule=\"evenodd\"\n                          d=\"M12 22.5C17.799 22.5 22.5 17.799 22.5 12C22.5 6.20101 17.799 1.5 12 1.5C6.20101 1.5 1.5 6.20101 1.5 12C1.5 17.799 6.20101 22.5 12 22.5ZM13.637 7.65198C13.637 6.74791 12.9041 6.01501 12 6.01501C11.0959 6.01501 10.363 6.74791 10.363 7.65198C10.5335 9.53749 10.875 13.383 10.875 13.383C10.875 14.0043 11.3787 14.508 12 14.508C12.6213 14.508 13.125 14.0043 13.125 13.383V13.38L13.637 7.65198ZM11.9927 15.714C11.3714 15.714 10.8677 16.2177 10.8677 16.839C10.8677 17.4603 11.3714 17.964 11.9927 17.964H12.0073C12.6286 17.964 13.1323 17.4603 13.1323 16.839C13.1323 16.2177 12.6286 15.714 12.0073 15.714H11.9927Z\"\n                          fill=\"#FEA419\"\/>\n                <\/svg>\n                Important\n            <\/span>\n            <p class=\"announcement-block__content\">\n                <strong>Important!<\/strong> React Router v7 and later use <strong>react-router<\/strong> as the main package. Older tutorials use <strong>react-router-dom<\/strong>, so import the routing APIs used in this project from <strong>react-router<\/strong>. \n            <\/p><\/div>\n\n\n\n<\/p><p class=\"wp-block-paragraph\">Next, update <strong>src\/main.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { StrictMode } from 'react';\nimport { createRoot } from 'react-dom\/client';\nimport { BrowserRouter } from 'react-router';\nimport '.\/index.css';\nimport App from '.\/App.jsx';\n\ncreateRoot(document.getElementById('root')).render(\n   &lt;StrictMode&gt;\n      &lt;BrowserRouter&gt;\n         &lt;App \/&gt;\n      &lt;\/BrowserRouter&gt;\n   &lt;\/StrictMode&gt;,\n);<\/pre><p class=\"wp-block-paragraph\"><strong>BrowserRouter<\/strong> lets React Router use the browser URL to determine which view to display.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/pages\/About.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function About() {\n   return (\n      &lt;main className=\"app\"&gt;\n         &lt;h2&gt;About&lt;\/h2&gt;\n         &lt;p&gt;A task tracker built while learning React.&lt;\/p&gt;\n      &lt;\/main&gt;\n   );\n}\n\nexport default About;<\/pre><p class=\"wp-block-paragraph\">Then, create <strong>src\/pages\/TaskDetails.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useParams } from 'react-router';\n\nfunction TaskDetails({ tasks }) {\n   const { taskId } = useParams();\n\n   const task = tasks.find(\n      (task) =&gt; task.id === Number(taskId)\n   );\n\n   if (!task) {\n      return &lt;p&gt;Task not found.&lt;\/p&gt;;\n   }\n\n   return (\n      &lt;main className=\"app\"&gt;\n         &lt;h2&gt;{task.title}&lt;\/h2&gt;\n         &lt;p&gt;Status: {task.done ? 'Done' : 'Open'}&lt;\/p&gt;\n      &lt;\/main&gt;\n   );\n}\n\nexport default TaskDetails;<\/pre><p class=\"wp-block-paragraph\">The <strong>:taskId<\/strong> part of the URL identifies which task to display.<\/p><p class=\"wp-block-paragraph\">In <strong>src\/App.jsx<\/strong>, keep your existing state and task functions. Import <strong>Routes<\/strong>, <strong>Route<\/strong>, and the new page components:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { Route, Routes } from 'react-router';\nimport About from '.\/pages\/About.jsx';\nimport TaskDetails from '.\/pages\/TaskDetails.jsx';<\/pre><p class=\"wp-block-paragraph\">Then, replace the current return statement in <strong>App<\/strong> with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">return (\n   &lt;&gt;\n      &lt;Header \/&gt;\n\n      &lt;Routes&gt;\n         &lt;Route\n            path=\"\/\"\n            element={\n               &lt;main className=\"app\"&gt;\n                  &lt;TaskForm onAdd={addTask} \/&gt;\n\n                  &lt;TaskList\n                     tasks={tasks}\n                     onToggle={toggleTask}\n                     onDelete={deleteTask}\n                  \/&gt;\n               &lt;\/main&gt;\n            }\n         \/&gt;\n\n         &lt;Route\n            path=\"\/tasks\/:taskId\"\n            element={&lt;TaskDetails tasks={tasks} \/&gt;}\n         \/&gt;\n\n         &lt;Route\n            path=\"\/about\"\n            element={&lt;About \/&gt;}\n         \/&gt;\n      &lt;\/Routes&gt;\n   &lt;\/&gt;\n);<\/pre><p class=\"wp-block-paragraph\">The <strong>\/<\/strong> route displays your dashboard, <strong>\/tasks\/:taskId<\/strong> displays an individual task, and <strong>\/about<\/strong> displays the about page.<\/p><p class=\"wp-block-paragraph\">To navigate between the main views, import <strong>Link<\/strong> into <strong>Header.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { Link } from 'react-router';<\/pre><p class=\"wp-block-paragraph\">Then, add these links inside the <strong>&lt;header&gt;<\/strong> element:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;nav&gt;\n   &lt;Link to=\"\/\"&gt;Dashboard&lt;\/Link&gt;\n   &lt;Link to=\"\/about\"&gt;About&lt;\/Link&gt;\n&lt;\/nav&gt;<\/pre><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e62788f5\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e62788f5\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-header-dashboard-about-navigation.png\/w=1024,h=1024,fit=scale-down\" alt=\"React task tracker dashboard with Dashboard and About navigation links\" class=\"wp-image-156912\" title=\"task-tracker-header-dashboard-about-navigation\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\">To open the task details page, import <strong>Link<\/strong> into <strong>TaskItem.jsx<\/strong> and add this link inside the task item:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;Link to={`\/tasks\/${id}`}&gt;\n   View details\n&lt;\/Link&gt;<\/pre><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e627d90e\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e627d90e\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-task-details-title-status.png\/w=1024,h=1024,fit=scale-down\" alt=\"React task details page showing the selected task title and status\" class=\"wp-image-156915\" title=\"task-tracker-task-details-title-status\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\">You can now move between views without reloading the entire page.<\/p><h3 class=\"wp-block-heading\">Fetch and display API data<\/h3><p class=\"wp-block-paragraph\">You can fetch and display API data in the task tracker by converting the response to the same data structure your existing task components use.<\/p><p class=\"wp-block-paragraph\">JSONPlaceholder stores a task&rsquo;s completion status in <strong>completed<\/strong>, while your task tracker uses <strong>done<\/strong>. In <strong>src\/components\/ApiTasks.jsx<\/strong>, replace:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">setTasks(data);<\/pre><p class=\"wp-block-paragraph\">with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const apiTasks = data.map((task) =&gt; ({\n   id: task.id,\n   title: task.title,\n   done: task.completed,\n}));\n\nsetTasks(apiTasks);<\/pre><p class=\"wp-block-paragraph\">Then, update the task status in the returned JSX:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">{task.done ? '&#10003; ' : '&#9675; '}\n{task.title}<\/pre><p class=\"wp-block-paragraph\">Each API task now has the same <strong>id<\/strong>, <strong>title<\/strong>, and <strong>done<\/strong> properties as the task objects used elsewhere in the project. Keeping one consistent structure means your components don&rsquo;t need separate logic for local and API data.<\/p><h3 class=\"wp-block-heading\">Show loading and error states<\/h3><p class=\"wp-block-paragraph\">To show loading and error states, track the request status in <strong>ApiTasks.jsx<\/strong> and display feedback before the task list.<\/p><p class=\"wp-block-paragraph\">Add two state values below the existing <strong>tasks<\/strong> state:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const [loading, setLoading] = useState(true);\nconst [error, setError] = useState('');<\/pre><p class=\"wp-block-paragraph\">In <strong>loadTasks<\/strong>, replace the current <strong>catch<\/strong> block with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">} catch {\n   if (!ignore) {\n      setError('Could not load tasks.');\n   }\n}<\/pre><p class=\"wp-block-paragraph\">Then, add a <strong>finally<\/strong> block after it:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">finally {\n   if (!ignore) {\n      setLoading(false);\n   }\n}<\/pre><p class=\"wp-block-paragraph\">Before the component returns the task list, add:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">if (loading) {\n   return &lt;p&gt;Loading tasks...&lt;\/p&gt;;\n}\n\nif (error) {\n   return &lt;p&gt;{error}&lt;\/p&gt;;\n}<\/pre><p class=\"wp-block-paragraph\">Now <strong>Loading tasks&hellip;<\/strong> appears while the request is running, <strong>Could not load tasks.<\/strong> appears when it fails, and the task list appears after a successful request.<\/p><p class=\"wp-block-paragraph\">For practice, temporarily replace the API endpoint with <strong>https:\/\/jsonplaceholder.typicode.com\/not-a-real-endpoint<\/strong> and confirm that the error message appears.<\/p><p class=\"wp-block-paragraph\">Restore the original endpoint after testing it. Then, remove <strong>&lt;ApiTasks \/&gt;<\/strong> and its import from <strong>App.jsx<\/strong> before continuing.<\/p><h2 class=\"wp-block-heading\" id=\"h-how-to-build-a-small-react-project-from-start-to-finish\">How to build a small React project from start to finish<\/h2><p class=\"wp-block-paragraph\">To build a small React project from start to finish, combine the components, state, forms, hooks, routing, and API techniques into one app.<\/p><p class=\"wp-block-paragraph\">You&rsquo;ll continue with the task tracker project you built earlier, so you don&rsquo;t need to create another one.<\/p><p class=\"wp-block-paragraph\">Complete each step and check the result before continuing. This makes it easier to catch problems before you add another feature.<\/p><h3 class=\"wp-block-heading\">1. Create the project structure<\/h3><p class=\"wp-block-paragraph\">Create the project structure by separating reusable components and page-level views inside <strong>src<\/strong>. The final task tracker only needs a few files:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"raw\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">react-task-tracker\/\n&#9500;&#9472;&#9472; index.html\n&#9500;&#9472;&#9472; package.json\n&#9500;&#9472;&#9472; vite.config.js\n&#9492;&#9472;&#9472; src\/\n    &#9500;&#9472;&#9472; components\/\n    &#9474;   &#9500;&#9472;&#9472; Header.jsx\n    &#9474;   &#9500;&#9472;&#9472; TaskForm.jsx\n    &#9474;   &#9500;&#9472;&#9472; TaskItem.jsx\n    &#9474;   &#9492;&#9472;&#9472; TaskList.jsx\n    &#9500;&#9472;&#9472; pages\/\n    &#9474;   &#9500;&#9472;&#9472; About.jsx\n    &#9474;   &#9500;&#9472;&#9472; Dashboard.jsx\n    &#9474;   &#9492;&#9472;&#9472; TaskDetails.jsx\n    &#9500;&#9472;&#9472; App.css\n    &#9500;&#9472;&#9472; App.jsx\n    &#9500;&#9472;&#9472; index.css\n    &#9492;&#9472;&#9472; main.jsx<\/pre><p class=\"wp-block-paragraph\">This structure covers the core task tracker. The exercises later add optional files such as <strong>EmptyState.jsx<\/strong>, <strong>TaskStats.jsx<\/strong>, <strong>useTaskFilter.js<\/strong>, and <strong>ApiTaskDetails.jsx<\/strong>.<\/p><p class=\"wp-block-paragraph\">Use <strong>components<\/strong> for reusable interface elements such as the header, form, and task items. Use <strong>pages<\/strong> for complete views that React Router connects to URLs.<\/p><p class=\"wp-block-paragraph\">Create any missing folders in your editor. On macOS or Linux, you can also run:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">mkdir -p src\/components src\/pages<\/pre><p class=\"wp-block-paragraph\"><strong>Checkpoint<\/strong>: Confirm that <strong>components<\/strong> and <strong>pages<\/strong> appear inside <strong>src<\/strong> and that the files are organized as shown above.<\/p><h3 class=\"wp-block-heading\">2. Build the layout components<\/h3><p class=\"wp-block-paragraph\">You can build the task tracker layout by adding a shared header and a centered content area for the app&rsquo;s main features.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/Header.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function Header() {\n   return (\n      &lt;header className=\"header\"&gt;\n         &lt;h1&gt;Task tracker&lt;\/h1&gt;\n      &lt;\/header&gt;\n   );\n}\n\nexport default Header;<\/pre><p class=\"wp-block-paragraph\">Then, replace <strong>src\/App.jsx<\/strong> with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import Header from '.\/components\/Header.jsx';\nimport '.\/App.css';\n\nfunction App() {\n   return (\n      &lt;&gt;\n         &lt;Header \/&gt;\n\n         &lt;main className=\"container\"&gt;\n            &lt;p&gt;Your task tracker is ready for tasks.&lt;\/p&gt;\n         &lt;\/main&gt;\n      &lt;\/&gt;\n   );\n}\n\nexport default App;<\/pre><p class=\"wp-block-paragraph\">Next, replace the starter styles in <strong>src\/index.css<\/strong> with the global styles:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">:root {\n   font-family: Arial, sans-serif;\n   color: #1f2937;\n   background: #f3f4f6;\n}\n\n* {\n   box-sizing: border-box;\n}\n\nbody {\n   margin: 0;\n}\n\nbutton,\ninput {\n   font: inherit;\n}\n\nbutton {\n   cursor: pointer;\n}<\/pre><p class=\"wp-block-paragraph\">Then, replace <strong>src\/App.css<\/strong> with the layout styles:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">.header {\n   padding: 1rem 1.5rem;\n   background: #ffffff;\n   border-bottom: 1px solid #d1d5db;\n}\n\n.header h1 {\n   margin: 0;\n}\n\n.container {\n   width: min(720px, calc(100% - 2rem));\n   margin: 2rem auto;\n}<\/pre><p class=\"wp-block-paragraph\">Keeping the global rules in <strong>index.css<\/strong> prevents Vite&rsquo;s starter styles from affecting the page layout. <strong>App.css<\/strong> can then contain styles specific to the task tracker interface.<\/p><p class=\"wp-block-paragraph\"><strong>Checkpoint<\/strong>: You should see the <strong>Task tracker<\/strong> heading above the placeholder text, with the content centered on a light gray background.<\/p><h3 class=\"wp-block-heading\">3. Build the task list<\/h3><p class=\"wp-block-paragraph\">To build the task list, use <strong>TaskItem<\/strong> to display each task and <strong>TaskList<\/strong> to render the full collection.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/TaskItem.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function TaskItem({\n   id,\n   title,\n   done,\n   onToggle,\n   onDelete,\n}) {\n   return (\n      &lt;li className={`task-item ${done ? 'done' : ''}`}&gt;\n         &lt;span className=\"task-title\"&gt;\n            {done ? '&#10003; ' : '&#9675; '}\n            {title}\n         &lt;\/span&gt;\n\n         &lt;div className=\"task-actions\"&gt;\n            &lt;button\n               type=\"button\"\n               onClick={() =&gt; onToggle(id)}\n            &gt;\n               {done ? 'Mark open' : 'Mark done'}\n            &lt;\/button&gt;\n\n            &lt;button\n               type=\"button\"\n               onClick={() =&gt; onDelete(id)}\n            &gt;\n               Delete\n            &lt;\/button&gt;\n         &lt;\/div&gt;\n      &lt;\/li&gt;\n   );\n}\n\nexport default TaskItem;<\/pre><p class=\"wp-block-paragraph\">Then, create <strong>src\/components\/TaskList.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import TaskItem from '.\/TaskItem.jsx';\n\nfunction TaskList({\n   tasks,\n   onToggle,\n   onDelete,\n}) {\n   if (tasks.length === 0) {\n      return &lt;p&gt;No tasks yet.&lt;\/p&gt;;\n   }\n\n   return (\n      &lt;ul className=\"task-list\"&gt;\n         {tasks.map((task) =&gt; (\n            &lt;TaskItem\n               key={task.id}\n               id={task.id}\n               title={task.title}\n               done={task.done}\n               onToggle={onToggle}\n               onDelete={onDelete}\n            \/&gt;\n         ))}\n      &lt;\/ul&gt;\n   );\n}\n\nexport default TaskList;<\/pre><p class=\"wp-block-paragraph\">Add these styles to <strong>src\/App.css<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">.task-list {\n   display: grid;\n   gap: 0.75rem;\n   padding: 0;\n   margin: 1rem 0 0;\n   list-style: none;\n}\n\n.task-item {\n   display: flex;\n   align-items: center;\n   justify-content: space-between;\n   gap: 1rem;\n   padding: 1rem;\n   background: #ffffff;\n   border: 1px solid #d1d5db;\n   border-radius: 0.5rem;\n}\n\n.task-item.done .task-title {\n   color: #6b7280;\n   text-decoration: line-through;\n}\n\n.task-actions {\n   display: flex;\n   gap: 0.5rem;\n}<\/pre><p class=\"wp-block-paragraph\"><strong>TaskList<\/strong> will appear once <strong>App<\/strong> passes task data and action handlers to it.<\/p><p class=\"wp-block-paragraph\"><strong>Checkpoint<\/strong>: Save <strong>TaskItem.jsx<\/strong> and <strong>TaskList.jsx<\/strong>, then check that the project still runs without errors in the browser or developer console.<\/p><h3 class=\"wp-block-heading\">4. Add task state<\/h3><p class=\"wp-block-paragraph\">Add the shared task state and action functions in <strong>App.jsx<\/strong> so <strong>TaskList<\/strong> can display, complete, reopen, and delete tasks. Start with two local tasks so you can verify these actions before connecting the app to API data.<\/p><p class=\"wp-block-paragraph\">Replace <strong>src\/App.jsx<\/strong> with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useState } from 'react';\nimport Header from '.\/components\/Header.jsx';\nimport TaskList from '.\/components\/TaskList.jsx';\nimport '.\/App.css';\n\nconst initialTasks = [\n   {\n      id: 1,\n      title: 'Build the task list',\n      done: true,\n   },\n   {\n      id: 2,\n      title: 'Add React state',\n      done: false,\n   },\n];\n\nfunction App() {\n   const [tasks, setTasks] = useState(initialTasks);\n\n   function toggleTask(id) {\n      setTasks((currentTasks) =&gt;\n         currentTasks.map((task) =&gt;\n            task.id === id\n               ? { ...task, done: !task.done }\n               : task\n         )\n      );\n   }\n\n   function deleteTask(id) {\n      setTasks((currentTasks) =&gt;\n         currentTasks.filter((task) =&gt; task.id !== id)\n      );\n   }\n\n   return (\n      &lt;&gt;\n         &lt;Header \/&gt;\n\n         &lt;main className=\"container\"&gt;\n            &lt;TaskList\n               tasks={tasks}\n               onToggle={toggleTask}\n               onDelete={deleteTask}\n            \/&gt;\n         &lt;\/main&gt;\n      &lt;\/&gt;\n   );\n}\n\nexport default App;<\/pre><p class=\"wp-block-paragraph\">Keep <strong>tasks<\/strong> in <strong>App.jsx<\/strong> so the dashboard and task-details page can use the same task data when you add routes later.<\/p><p class=\"wp-block-paragraph\"><strong>Checkpoint<\/strong>: Select <strong>Mark done<\/strong> or <strong>Mark open<\/strong> and confirm that the task status changes immediately. Delete a task and confirm that it disappears from the list. Delete both tasks to confirm that <strong>No tasks yet.<\/strong> appears.<\/p><h3 class=\"wp-block-heading\">5. Add the task form<\/h3><p class=\"wp-block-paragraph\">Add the task form by connecting <strong>TaskForm<\/strong> to <strong>App<\/strong> so you can create validated tasks and add them to the shared task list.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/TaskForm.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useState } from 'react';\n\nfunction TaskForm({ onAdd }) {\n   const [title, setTitle] = useState('');\n\n   function handleSubmit(event) {\n      event.preventDefault();\n\n      const trimmedTitle = title.trim();\n\n      if (trimmedTitle === '') {\n         return;\n      }\n\n      onAdd(trimmedTitle);\n      setTitle('');\n   }\n\n   return (\n      &lt;form\n         className=\"task-form\"\n         onSubmit={handleSubmit}\n      &gt;\n         &lt;label htmlFor=\"task-title\"&gt;\n            Task name\n         &lt;\/label&gt;\n\n         &lt;div className=\"task-form-row\"&gt;\n            &lt;input\n               id=\"task-title\"\n               name=\"task-title\"\n               type=\"text\"\n               placeholder=\"Add a new task...\"\n               value={title}\n               onChange={(event) =&gt;\n                  setTitle(event.target.value)\n               }\n            \/&gt;\n\n            &lt;button type=\"submit\"&gt;\n               Add task\n            &lt;\/button&gt;\n         &lt;\/div&gt;\n      &lt;\/form&gt;\n   );\n}\n\nexport default TaskForm;<\/pre><p class=\"wp-block-paragraph\">Next, import <strong>TaskForm<\/strong> into <strong>src\/App.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import TaskForm from '.\/components\/TaskForm.jsx';<\/pre><p class=\"wp-block-paragraph\">Add <strong>addTask<\/strong> inside <strong>App<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function addTask(title) {\n   const newTask = {\n      id: Date.now(),\n      title,\n      done: false,\n   };\n\n   setTasks((currentTasks) =&gt; [\n      ...currentTasks,\n      newTask,\n   ]);\n}<\/pre><p class=\"wp-block-paragraph\">Then, render <strong>TaskForm<\/strong> above <strong>TaskList<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;TaskForm onAdd={addTask} \/&gt;\n\n&lt;TaskList\n   tasks={tasks}\n   onToggle={toggleTask}\n   onDelete={deleteTask}\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">Add the form styles to <strong>src\/App.css<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">.task-form {\n   display: grid;\n   gap: 0.5rem;\n   margin-bottom: 1.5rem;\n}\n\n.task-form-row {\n   display: flex;\n   gap: 0.5rem;\n}\n\n.task-form input {\n   flex: 1;\n   min-width: 0;\n   padding: 0.75rem;\n}<\/pre><p class=\"wp-block-paragraph\"><strong>addTask<\/strong> creates each new task in the same <strong>id<\/strong>, <strong>title<\/strong>, and <strong>done<\/strong> format used by the rest of the app, so newly added tasks work with the existing toggle and delete actions.<\/p><p class=\"wp-block-paragraph\"><strong>Checkpoint:<\/strong> Add a valid task and confirm that it appears in the list. Mark it done, reopen it, and delete it to confirm that new tasks work with the existing actions. Then, submit a title containing only spaces and confirm that no task is added.<\/p><h3 class=\"wp-block-heading\">6. Add routes<\/h3><p class=\"wp-block-paragraph\">To add routes, create dashboard, task-details, and about pages, then connect them to URLs with React Router.<\/p><p class=\"wp-block-paragraph\">First, update <strong>src\/components\/Header.jsx<\/strong> so users can navigate between the main pages:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { Link } from 'react-router';\n\nfunction Header() {\n   return (\n      &lt;header className=\"header\"&gt;\n         &lt;h1&gt;Task tracker&lt;\/h1&gt;\n\n         &lt;nav&gt;\n            &lt;Link to=\"\/\"&gt;Dashboard&lt;\/Link&gt;\n            &lt;Link to=\"\/about\"&gt;About&lt;\/Link&gt;\n         &lt;\/nav&gt;\n      &lt;\/header&gt;\n   );\n}\n\nexport default Header;<\/pre><p class=\"wp-block-paragraph\">Next, create <strong>src\/pages\/Dashboard.jsx<\/strong> to group the task form, task list, and completion count on the main page:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import TaskForm from '..\/components\/TaskForm.jsx';\nimport TaskList from '..\/components\/TaskList.jsx';\n\nfunction Dashboard({\n   tasks,\n   onAdd,\n   onToggle,\n   onDelete,\n}) {\n   const total = tasks.length;\n   const completed = tasks.filter(\n      (task) =&gt; task.done\n   ).length;\n\n   return (\n      &lt;section&gt;\n         &lt;p className=\"stats\"&gt;\n            {completed} of {total} tasks completed\n         &lt;\/p&gt;\n\n         &lt;TaskForm onAdd={onAdd} \/&gt;\n\n         &lt;TaskList\n            tasks={tasks}\n            onToggle={onToggle}\n            onDelete={onDelete}\n         \/&gt;\n      &lt;\/section&gt;\n   );\n}\n\nexport default Dashboard;<\/pre><p class=\"wp-block-paragraph\">Create <strong>src\/pages\/About.jsx<\/strong> for the second main view:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function About() {\n   return (\n      &lt;section&gt;\n         &lt;h2&gt;About this app&lt;\/h2&gt;\n\n         &lt;p&gt;\n            This task tracker is a beginner React project\n            for practicing components, state, forms,\n            hooks, routing, and API data.\n         &lt;\/p&gt;\n      &lt;\/section&gt;\n   );\n}\n\nexport default About;<\/pre><p class=\"wp-block-paragraph\">Then, create <strong>src\/pages\/TaskDetails.jsx<\/strong> so each task can have its own URL:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useParams } from 'react-router';\n\nfunction TaskDetails({ tasks }) {\n   const { taskId } = useParams();\n\n   const task = tasks.find(\n      (task) =&gt; task.id === Number(taskId)\n   );\n\n   if (!task) {\n      return &lt;p&gt;Task not found.&lt;\/p&gt;;\n   }\n\n   return (\n      &lt;section&gt;\n         &lt;h2&gt;{task.title}&lt;\/h2&gt;\n         &lt;p&gt;Status: {task.done ? 'Done' : 'Open'}&lt;\/p&gt;\n      &lt;\/section&gt;\n   );\n}\n\nexport default TaskDetails;<\/pre><p class=\"wp-block-paragraph\">The <strong>:taskId<\/strong> part of <strong>\/tasks\/:taskId<\/strong> is a dynamic URL segment. For example, opening <strong>\/tasks\/2<\/strong> gives <strong>useParams()<\/strong> a <strong>taskId<\/strong> value of <strong>&ldquo;2&rdquo;<\/strong>. <strong>Number(taskId)<\/strong> converts that string to a number so it can match the numeric task IDs.<\/p><p class=\"wp-block-paragraph\">Next, add a link from each task to its details page. Import <strong>Link<\/strong> at the top of <strong>src\/components\/TaskItem.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { Link } from 'react-router';<\/pre><p class=\"wp-block-paragraph\">Then, add the <strong>View details<\/strong> link inside <strong>&lt;div className=&rdquo;task-actions&rdquo;&gt;<\/strong>, above the first button:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;div className=\"task-actions\"&gt;\n   &lt;Link to={`\/tasks\/${id}`}&gt;\n      View details\n   &lt;\/Link&gt;\n\n   &lt;button\n      type=\"button\"\n      onClick={() =&gt; onToggle(id)}\n   &gt;\n      {done ? 'Mark open' : 'Mark done'}\n   &lt;\/button&gt;\n\n   &lt;button\n      type=\"button\"\n      onClick={() =&gt; onDelete(id)}\n   &gt;\n      Delete\n   &lt;\/button&gt;\n&lt;\/div&gt;<\/pre><p class=\"wp-block-paragraph\">Enable browser routing in <strong>src\/main.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { StrictMode } from 'react';\nimport { createRoot } from 'react-dom\/client';\nimport { BrowserRouter } from 'react-router';\nimport '.\/index.css';\nimport App from '.\/App.jsx';\n\ncreateRoot(document.getElementById('root')).render(\n   &lt;StrictMode&gt;\n      &lt;BrowserRouter&gt;\n         &lt;App \/&gt;\n      &lt;\/BrowserRouter&gt;\n   &lt;\/StrictMode&gt;,\n);<\/pre><p class=\"wp-block-paragraph\">Next, import the routing components and page components into <strong>src\/App.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { Route, Routes } from 'react-router';\nimport Dashboard from '.\/pages\/Dashboard.jsx';\nimport About from '.\/pages\/About.jsx';\nimport TaskDetails from '.\/pages\/TaskDetails.jsx';<\/pre><p class=\"wp-block-paragraph\">Replace the current return statement in <strong>App<\/strong> with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">return (\n   &lt;&gt;\n      &lt;Header \/&gt;\n\n      &lt;main className=\"container\"&gt;\n         &lt;Routes&gt;\n            &lt;Route\n               path=\"\/\"\n               element={\n                  &lt;Dashboard\n                     tasks={tasks}\n                     onAdd={addTask}\n                     onToggle={toggleTask}\n                     onDelete={deleteTask}\n                  \/&gt;\n               }\n            \/&gt;\n\n            &lt;Route\n               path=\"\/tasks\/:taskId\"\n               element={\n                  &lt;TaskDetails tasks={tasks} \/&gt;\n               }\n            \/&gt;\n\n            &lt;Route\n               path=\"\/about\"\n               element={&lt;About \/&gt;}\n            \/&gt;\n         &lt;\/Routes&gt;\n      &lt;\/main&gt;\n   &lt;\/&gt;\n);<\/pre><p class=\"wp-block-paragraph\">The <strong>\/<\/strong> route shows the dashboard, <strong>\/tasks\/:taskId<\/strong> shows the selected task, and <strong>\/about<\/strong> shows information about the project.<\/p><p class=\"wp-block-paragraph\">Finally, update the header styles in <strong>src\/App.css<\/strong> and add styling for its navigation:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">.header {\n   display: flex;\n   align-items: center;\n   justify-content: space-between;\n   gap: 1rem;\n   padding: 1rem 1.5rem;\n   background: #ffffff;\n   border-bottom: 1px solid #d1d5db;\n}\n\n.header nav {\n   display: flex;\n   gap: 1rem;\n}<\/pre><p class=\"wp-block-paragraph\"><strong>Checkpoint:<\/strong> Open <strong>Dashboard<\/strong> and <strong>About<\/strong>, then select <strong>View details<\/strong> on a task. Each action should update the URL and display the corresponding page without a full page reload.<\/p><h3 class=\"wp-block-heading\">7. Fetch starter data<\/h3><p class=\"wp-block-paragraph\">Fetch the starter tasks into <strong>App.jsx<\/strong> so the dashboard and task-details page can use the same API-loaded task data.<\/p><p class=\"wp-block-paragraph\">Remove the <strong>initialTasks<\/strong> array, then update the React import at the top of <strong>src\/App.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useEffect, useState } from 'react';<\/pre><p class=\"wp-block-paragraph\">Replace <strong>useState(initialTasks)<\/strong> with state for the tasks and request status:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const [tasks, setTasks] = useState([]);\nconst [loading, setLoading] = useState(true);\nconst [error, setError] = useState('');<\/pre><p class=\"wp-block-paragraph\">Then, add the API request inside <strong>App<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">useEffect(() =&gt; {\n   let ignore = false;\n\n   async function loadTasks() {\n      try {\n         const response = await fetch(\n            'https:\/\/jsonplaceholder.typicode.com\/todos'\n         );\n\n         if (!response.ok) {\n            throw new Error(\n               `Request failed with status ${response.status}`\n            );\n         }\n\n         const data = await response.json();\n\n         const apiTasks = data\n            .slice(0, 4)\n            .map((task) =&gt; ({\n               id: task.id,\n               title: task.title,\n               done: task.completed,\n            }));\n\n         if (!ignore) {\n            setTasks(apiTasks);\n         }\n      } catch {\n         if (!ignore) {\n            setError('Could not load tasks.');\n         }\n      } finally {\n         if (!ignore) {\n            setLoading(false);\n         }\n      }\n   }\n\n   loadTasks();\n\n   return () =&gt; {\n      ignore = true;\n   };\n}, []);<\/pre><p class=\"wp-block-paragraph\">JSONPlaceholder uses <strong>completed<\/strong> for each task&rsquo;s status, while the task tracker uses <strong>done<\/strong>. The <strong>map()<\/strong> call converts the API response to the same <strong>id<\/strong>, <strong>title<\/strong>, and <strong>done<\/strong> structure used throughout the app.<\/p><p class=\"wp-block-paragraph\">Next, pass <strong>loading<\/strong> and <strong>error<\/strong> to <strong>Dashboard<\/strong> in the <strong>\/<\/strong> route:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;Route\n   path=\"\/\"\n   element={\n      &lt;Dashboard\n         tasks={tasks}\n         loading={loading}\n         error={error}\n         onAdd={addTask}\n         onToggle={toggleTask}\n         onDelete={deleteTask}\n      \/&gt;\n   }\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">Update the <strong>Dashboard<\/strong> parameters:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function Dashboard({\n   tasks,\n   loading,\n   error,\n   onAdd,\n   onToggle,\n   onDelete,\n}) {<\/pre><p class=\"wp-block-paragraph\">Then, add these checks before calculating <strong>total<\/strong> and <strong>completed<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">if (loading) {\n   return &lt;p&gt;Loading tasks...&lt;\/p&gt;;\n}\n\nif (error) {\n   return &lt;p&gt;{error}&lt;\/p&gt;;\n}<\/pre><p class=\"wp-block-paragraph\">The dashboard now shows <strong>Loading tasks&hellip;<\/strong> while the request runs and <strong>Could not load tasks.<\/strong> if the request fails.<\/p><p class=\"wp-block-paragraph\">Pass the same request state to <strong>TaskDetails<\/strong> so a direct visit to a task URL doesn&rsquo;t show <strong>Task not found.<\/strong> before the tasks finish loading:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;Route\n   path=\"\/tasks\/:taskId\"\n   element={\n      &lt;TaskDetails\n         tasks={tasks}\n         loading={loading}\n         error={error}\n      \/&gt;\n   }\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">Then, update <strong>src\/pages\/TaskDetails.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { useParams } from 'react-router';\n\nfunction TaskDetails({\n   tasks,\n   loading,\n   error,\n}) {\n   const { taskId } = useParams();\n\n   if (loading) {\n      return &lt;p&gt;Loading task...&lt;\/p&gt;;\n   }\n\n   if (error) {\n      return &lt;p&gt;{error}&lt;\/p&gt;;\n   }\n\n   const task = tasks.find(\n      (task) =&gt; task.id === Number(taskId)\n   );\n\n   if (!task) {\n      return &lt;p&gt;Task not found.&lt;\/p&gt;;\n   }\n\n   return (\n      &lt;section&gt;\n         &lt;h2&gt;{task.title}&lt;\/h2&gt;\n         &lt;p&gt;Status: {task.done ? 'Done' : 'Open'}&lt;\/p&gt;\n      &lt;\/section&gt;\n   );\n}\n\nexport default TaskDetails;<\/pre><p class=\"wp-block-paragraph\">Checking <strong>loading<\/strong> before looking for the task prevents the details page from treating an empty task array as a missing task while the API request is still running.<\/p><p class=\"wp-block-paragraph\">Tasks you add, complete, reopen, or delete after loading remain in React state for the current session. Refreshing the page shows the four starter tasks again because the app doesn&rsquo;t save those changes to permanent storage.<\/p><p class=\"wp-block-paragraph\"><strong>Checkpoint:<\/strong> Refresh the dashboard and confirm that <strong>Loading tasks&hellip;<\/strong> appears before four starter tasks load. Then, open a task details page and refresh it directly. You should see <strong>Loading task&hellip;<\/strong> before the selected task appears instead of briefly seeing <strong>Task not found.<\/strong><\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e6282dec\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e6282dec\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-dashboard-stats-form-task-list.png\/w=1024,h=1024,fit=scale-down\" alt=\"Finished React task tracker with stats, form, API tasks, and controls\" class=\"wp-image-156910\" title=\"task-tracker-dashboard-stats-form-task-list\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><h3 class=\"wp-block-heading\">8. Test and polish the app<\/h3><p class=\"wp-block-paragraph\">Test and polish the app by adding responsive styles, checking its main features and routes, and creating a production build.<\/p><p class=\"wp-block-paragraph\">First, add this responsive styling to <strong>src\/App.css<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@media (max-width: 600px) {\n   .header {\n      align-items: flex-start;\n      flex-direction: column;\n   }\n\n   .task-form-row {\n      flex-direction: column;\n   }\n\n   .task-item {\n      align-items: stretch;\n      flex-direction: column;\n   }\n\n   .task-actions {\n      flex-wrap: wrap;\n   }\n}<\/pre><p class=\"wp-block-paragraph\">At widths of <strong>600px<\/strong> or less, the navigation, form, and task controls now have more room by stacking or wrapping instead of staying in a single row.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e6286b19\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e6286b19\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-mobile-dashboard-responsive-layout.png\/w=1024,h=1024,fit=scale-down\" alt=\"Responsive React task tracker with stacked controls on a mobile layout\" class=\"wp-image-156914\" title=\"task-tracker-mobile-dashboard-responsive-layout\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\">Next, test the complete app:<\/p><ul class=\"wp-block-list\">\n<li>Refresh the dashboard and confirm that the loading message is followed by four starter tasks.<\/li>\n\n\n\n<li>Add a valid task and confirm that it appears once.<\/li>\n\n\n\n<li>Submit an empty or whitespace-only title and confirm that no task is added.<\/li>\n\n\n\n<li>Mark an open task as done and confirm that its status and completed count update.<\/li>\n\n\n\n<li>Reopen a completed task and confirm that the count updates again.<\/li>\n\n\n\n<li>Delete a task and confirm that it disappears and the total count changes.<\/li>\n\n\n\n<li>Select <strong>View details<\/strong> and confirm that the correct task title and status appear.<\/li>\n\n\n\n<li>Refresh a task-details page directly and confirm that <strong>Loading task&hellip;<\/strong> appears before the task loads.<\/li>\n\n\n\n<li>Navigate between <strong>Dashboard<\/strong> and <strong>About<\/strong>, then test the browser&rsquo;s back and forward buttons.<\/li>\n\n\n\n<li>Temporarily use an invalid API endpoint and confirm that <strong>Could not load tasks.<\/strong> appears. Restore the working endpoint afterward.<\/li>\n<\/ul><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e628a58a\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e628a58a\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-dashboard-api-error.png\/w=1024,h=1024,fit=scale-down\" alt=\"React task tracker displaying the Could not load tasks error message\" class=\"wp-image-156909\" title=\"task-tracker-dashboard-api-error\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><ul class=\"wp-block-list\">\n<li>Resize the browser to a phone-width screen and confirm that the header, form, task items, and action buttons remain usable.<\/li>\n\n\n\n<li>Check the developer console for React warnings or errors.<\/li>\n<\/ul><p class=\"wp-block-paragraph\">After the app passes these checks, create the production build:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">npm run build<\/pre><p class=\"wp-block-paragraph\">A successful build confirms that Vite can compile the app for production.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e628e34d\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e628e34d\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/terminal-vite-production-build-success.png\/w=1024,h=1024,fit=scale-down\" alt=\"Terminal showing a successful Vite production build\" class=\"wp-image-156922\" title=\"terminal-vite-production-build-success\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\">Task changes still reset after a refresh because the app stores them only in React state rather than permanent storage.<\/p><p class=\"wp-block-paragraph\"><strong>Checkpoint:<\/strong> Confirm that the app passes the checklist without React warnings or errors and that <strong>npm run build<\/strong> completes successfully.<\/p><h2 class=\"wp-block-heading\" id=\"h-what-react-exercises-should-beginners-complete\">What React exercises should beginners complete?<\/h2><p class=\"wp-block-paragraph\">Beginners should complete React exercises on JSX and components, props, state and events, forms, routing, and API data.<\/p><p class=\"wp-block-paragraph\">The exercises below extend your finished task tracker with optional features that help you practice each concept one at a time.<\/p><h3 class=\"wp-block-heading\">JSX and components exercise<\/h3><p class=\"wp-block-paragraph\">For the JSX and components exercise, create a reusable <strong>EmptyState.jsx<\/strong> component that appears when the task list is empty.<\/p><p class=\"wp-block-paragraph\">Your component should:<\/p><ul class=\"wp-block-list\">\n<li>Return valid JSX with one parent element.<\/li>\n\n\n\n<li>Include the heading <strong>No tasks yet<\/strong>.<\/li>\n\n\n\n<li>Include a short message that tells you to add your first task.<\/li>\n\n\n\n<li>Replace the existing <strong>No tasks yet.<\/strong> paragraph in <strong>TaskList.jsx<\/strong>.<\/li>\n<\/ul><p class=\"wp-block-paragraph\"><strong>Expected output<\/strong>: Delete all tasks, and the new empty-state message should appear instead of the task list.<\/p><h3 class=\"wp-block-heading\">Props exercise<\/h3><p class=\"wp-block-paragraph\">Practice props by replacing the existing dashboard stats with a reusable <strong>TaskStats.jsx<\/strong> component that receives the total, completed, and remaining task counts from <strong>Dashboard.jsx<\/strong>.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/components\/TaskStats.jsx<\/strong> and add props for <strong>total<\/strong>, <strong>completed<\/strong>, and <strong>remaining<\/strong>.<\/p><p class=\"wp-block-paragraph\">In <strong>Dashboard.jsx<\/strong>, calculate the remaining tasks:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const remaining = total - completed;<\/pre><p class=\"wp-block-paragraph\">Then, import <strong>TaskStats<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import TaskStats from '..\/components\/TaskStats.jsx';<\/pre><p class=\"wp-block-paragraph\">Replace the existing stats paragraph:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;p className=\"stats\"&gt;\n   {completed} of {total} tasks completed\n&lt;\/p&gt;<\/pre><p class=\"wp-block-paragraph\">with:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;TaskStats\n   total={total}\n   completed={completed}\n   remaining={remaining}\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">Inside <strong>TaskStats.jsx<\/strong>, display all three values with JSX.<\/p><p class=\"wp-block-paragraph\"><strong>Expected output<\/strong>: The dashboard shows one summary, such as <strong>4 total, 1 done, 3 remaining<\/strong>. Adding, completing, reopening, or deleting a task should update the values automatically.<\/p><h3 class=\"wp-block-heading\">State and events exercise<\/h3><p class=\"wp-block-paragraph\">In the state and events exercise, use the <strong>useTaskFilter<\/strong> hook to add <strong>All<\/strong>, <strong>Active<\/strong>, and <strong>Done<\/strong> filter buttons to the dashboard.<\/p><p class=\"wp-block-paragraph\">In <strong>Dashboard.jsx<\/strong>, import the hook:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import useTaskFilter from '..\/hooks\/useTaskFilter.js';<\/pre><p class=\"wp-block-paragraph\">Then, call it inside <strong>Dashboard<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">const {\n   filter,\n   setFilter,\n   filteredTasks,\n} = useTaskFilter(tasks);<\/pre><p class=\"wp-block-paragraph\">Add the filter buttons above <strong>TaskList<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;div className=\"task-filters\"&gt;\n   &lt;button\n      type=\"button\"\n      onClick={() =&gt; setFilter('all')}\n   &gt;\n      All\n   &lt;\/button&gt;\n\n   &lt;button\n      type=\"button\"\n      onClick={() =&gt; setFilter('active')}\n   &gt;\n      Active\n   &lt;\/button&gt;\n\n   &lt;button\n      type=\"button\"\n      onClick={() =&gt; setFilter('done')}\n   &gt;\n      Done\n   &lt;\/button&gt;\n&lt;\/div&gt;<\/pre><p class=\"wp-block-paragraph\">Finally, pass <strong>filteredTasks<\/strong> to <strong>TaskList<\/strong> instead of <strong>tasks<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;TaskList\n   tasks={filteredTasks}\n   onToggle={onToggle}\n   onDelete={onDelete}\n\/&gt;<\/pre><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e62929a6\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e62929a6\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-dashboard-task-filters-done.png\/w=1024,h=1024,fit=scale-down\" alt=\"React task tracker filtering completed tasks with All, Active, and Done buttons\" class=\"wp-image-156911\" title=\"task-tracker-dashboard-task-filters-done\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\"><strong>Expected output<\/strong>: <strong>All<\/strong> shows every task, <strong>Active<\/strong> shows tasks that aren&rsquo;t complete, and <strong>Done<\/strong> shows completed tasks. Adding, completing, reopening, or deleting a task should update the filtered list automatically.<\/p><h3 class=\"wp-block-heading\">Forms exercise<\/h3><p class=\"wp-block-paragraph\">For the forms exercise, add a due-date field to <strong>TaskForm<\/strong>, save the selected date with each new task, and display it in the task list.<\/p><p class=\"wp-block-paragraph\">Your changes should:<\/p><ol class=\"wp-block-list\">\n<li>Store the due date with another <strong>useState<\/strong> call in <strong>TaskForm.jsx<\/strong>.<\/li>\n\n\n\n<li>Add an input with <strong>type=&rdquo;date&rdquo;<\/strong> to the form.<\/li>\n\n\n\n<li>Require both the task title and due date before calling <strong>onAdd<\/strong>.<\/li>\n\n\n\n<li>Pass the due date to <strong>onAdd<\/strong> with the title.<\/li>\n\n\n\n<li>Update <strong>addTask<\/strong> so each new task stores a <strong>dueDate<\/strong> property.<\/li>\n\n\n\n<li>Pass <strong>dueDate={task.dueDate}<\/strong> from <strong>TaskList.jsx<\/strong> to <strong>TaskItem<\/strong>.<\/li>\n\n\n\n<li>Add <strong>dueDate<\/strong> to the <strong>TaskItem<\/strong> function parameters and display it when the task has a due date.<\/li>\n\n\n\n<li>Clear both form fields after a valid submission.<\/li>\n<\/ol><p class=\"wp-block-paragraph\">For step 6, update the <strong>TaskItem<\/strong> call in <strong>TaskList.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;TaskItem\n   key={task.id}\n   id={task.id}\n   title={task.title}\n   done={task.done}\n   dueDate={task.dueDate}\n   onToggle={onToggle}\n   onDelete={onDelete}\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">Then, update the <strong>TaskItem<\/strong> parameters:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">function TaskItem({\n   id,\n   title,\n   done,\n   dueDate,\n   onToggle,\n   onDelete,\n}) {<\/pre><p class=\"wp-block-paragraph\">Display the date wherever it fits naturally in the task markup, for example, below the title.<\/p><div class=\"wp-block-image\"><figure data-wp-context='{\"imageId\":\"6aa78e62968e7\"}' data-wp-interactive=\"core\/image\" data-wp-key=\"6aa78e62968e7\" class=\"aligncenter size-large wp-lightbox-container\"><img decoding=\"async\" data-wp-class--hide=\"state.isContentHidden\" data-wp-class--show=\"state.isContentVisible\" data-wp-init=\"callbacks.setButtonStyles\" data-wp-on--click=\"actions.showLightbox\" data-wp-on--load=\"callbacks.setButtonStyles\" data-wp-on--pointerdown=\"actions.preloadImage\" data-wp-on--pointerenter=\"actions.preloadImageWithDelay\" data-wp-on--pointerleave=\"actions.cancelPreload\" data-wp-on-window--resize=\"callbacks.setButtonStyles\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2026\/09\/task-tracker-task-form-due-date.png\/w=1024,h=1024,fit=scale-down\" alt=\"React task form with a due date field and a task displaying its due date\" class=\"wp-image-156917\" title=\"task-tracker-task-form-due-date\"><button class=\"lightbox-trigger\" type=\"button\" aria-haspopup=\"dialog\" data-wp-bind--aria-label=\"state.thisImage.triggerButtonAriaLabel\" data-wp-init=\"callbacks.initTriggerButton\" data-wp-on--click=\"actions.showLightbox\" data-wp-style--right=\"state.thisImage.buttonRight\" data-wp-style--top=\"state.thisImage.buttonTop\">\n\t\t\t<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"12\" height=\"12\" fill=\"none\" viewbox=\"0 0 12 12\">\n\t\t\t\t<path fill=\"#fff\" d=\"M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z\"><\/path>\n\t\t\t<\/svg>\n\t\t<\/button><\/figure><\/div><p class=\"wp-block-paragraph\"><strong>Expected output<\/strong>: A newly added task shows its selected due date. The form shouldn&rsquo;t add a task when the title or due date is missing.<\/p><h3 class=\"wp-block-heading\">Routing and API exercise<\/h3><p class=\"wp-block-paragraph\">For the routing and API exercise, create a separate <strong>ApiTaskDetails.jsx<\/strong> page that loads one task from JSONPlaceholder using the <strong>taskId<\/strong> route parameter.<\/p><p class=\"wp-block-paragraph\">Create <strong>src\/pages\/ApiTaskDetails.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import { Link, useParams } from 'react-router';\nimport { useEffect, useState } from 'react';\n\nfunction ApiTaskDetails() {\n   const { taskId } = useParams();\n   const [task, setTask] = useState(null);\n   const [loading, setLoading] = useState(true);\n   const [error, setError] = useState('');\n\n   useEffect(() =&gt; {\n      let ignore = false;\n\n      async function loadTask() {\n         setLoading(true);\n         setError('');\n\n         try {\n            const response = await fetch(\n               `https:\/\/jsonplaceholder.typicode.com\/todos\/${taskId}`\n            );\n\n            if (!response.ok) {\n               throw new Error(\n                  `Request failed with status ${response.status}`\n               );\n            }\n\n            const data = await response.json();\n\n            if (!ignore) {\n               setTask(data);\n            }\n         } catch {\n            if (!ignore) {\n               setError('Could not load task.');\n            }\n         } finally {\n            if (!ignore) {\n               setLoading(false);\n            }\n         }\n      }\n\n      loadTask();\n\n      return () =&gt; {\n         ignore = true;\n      };\n   }, [taskId]);\n\n   if (loading) {\n      return &lt;p&gt;Loading task...&lt;\/p&gt;;\n   }\n\n   if (error) {\n      return &lt;p&gt;{error}&lt;\/p&gt;;\n   }\n\n   return (\n      &lt;section&gt;\n         &lt;h2&gt;{task.title}&lt;\/h2&gt;\n         &lt;p&gt;\n            Status: {task.completed ? 'Done' : 'Open'}\n         &lt;\/p&gt;\n\n         &lt;Link to=\"\/\"&gt;Back to dashboard&lt;\/Link&gt;\n      &lt;\/section&gt;\n   );\n}\n\nexport default ApiTaskDetails;<\/pre><p class=\"wp-block-paragraph\">Next, import <strong>ApiTaskDetails<\/strong> into <strong>src\/App.jsx<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import ApiTaskDetails from '.\/pages\/ApiTaskDetails.jsx';<\/pre><p class=\"wp-block-paragraph\">Then, add a separate route for the exercise inside <strong>Routes<\/strong>:<\/p><pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">&lt;Route\n   path=\"\/api-tasks\/:taskId\"\n   element={&lt;ApiTaskDetails \/&gt;}\n\/&gt;<\/pre><p class=\"wp-block-paragraph\">The <strong>taskId<\/strong> value in the URL determines which JSONPlaceholder task the page requests. For example, <strong>\/api-tasks\/1<\/strong> requests task 1, while <strong>\/api-tasks\/2<\/strong> requests task 2.<\/p><p class=\"wp-block-paragraph\"><strong>Expected output:<\/strong> Opening <strong>\/api-tasks\/1<\/strong> shows <strong>Loading task&hellip;<\/strong> before displaying task 1 and its status. Changing the URL to another valid task ID loads that task, while an unsuccessful request shows <strong>Could not load task.<\/strong> The <strong>Back to dashboard<\/strong> link returns you to the main task list.<\/p><h2 class=\"wp-block-heading\" id=\"h-what-common-react-mistakes-should-beginners-avoid\">What common React mistakes should beginners avoid?<\/h2><p class=\"wp-block-paragraph\">Common React mistakes beginners should avoid include changing state directly, using unstable list keys, misusing <strong>useEffect<\/strong>, and overcomplicating components or data flow.<\/p><figure tabindex=\"0\" class=\"wp-block-table\"><table><tbody><tr><td><strong>Mistake<\/strong><\/td><td><strong>Why it causes problems<\/strong><\/td><td><strong>Better approach<\/strong><\/td><\/tr><tr><td>Changing state directly<\/td><td>React may not re-render when you change an existing array or object and reuse the same reference. You can also accidentally change data that other code still relies on<\/td><td>Create a new array or object with methods such as <strong>map()<\/strong>, <strong>filter()<\/strong>, or spread syntax, then pass it to the state setter<\/td><\/tr><tr><td>Using missing or unstable <strong>key<\/strong> values<\/td><td>React can associate a rendered item with the wrong data when you add, delete, or reorder list items. For example, an input value or component state can appear on the wrong task<\/td><td>Use a stable ID from the item data, such as <strong>task.id<\/strong><\/td><\/tr><tr><td>Overusing <strong>useEffect<\/strong><\/td><td>Using an effect to calculate values from existing state can trigger an extra render. An effect that updates one of its own dependencies can also run repeatedly and create an update loop<\/td><td>Calculate values during rendering and handle user actions in event handlers. Use <strong>useEffect<\/strong> when you need to synchronize with something outside React<\/td><\/tr><tr><td>Building oversized components<\/td><td>A change to one feature can require editing a file that also contains unrelated form, list, routing, or data-loading logic. This makes it harder to find where a problem starts and change one feature without affecting another<\/td><td>Split the component when a part of the interface has its own clear responsibility, such as a form, task list, or navigation<\/td><\/tr><tr><td>Passing props through many unused components<\/td><td>Every component between the data source and its destination must accept and forward the prop, even when it doesn&rsquo;t use the value. Renaming or changing that prop can require edits across several files<\/td><td>Keep state close to the components that use it. Move shared state to their closest common parent, and consider context when deeply nested components need the same data<\/td><\/tr><tr><td>Adding advanced libraries too early<\/td><td>You have more APIs, configuration, and data-flow patterns to learn at the same time. This can make it unclear whether a value or behavior comes from React or the added library<\/td><td>Learn components, props, state, events, forms, and hooks first. Add a library when your project has a specific problem it can solve<\/td><\/tr><\/tbody><\/table><\/figure><h2 class=\"wp-block-heading\" id=\"h-what-should-you-do-after-building-your-react-app\">What should you do after building your React app?<\/h2><p class=\"wp-block-paragraph\">After building your project locally, <a href=\"\/ca\/tutorials\/how-to-deploy-react-app\/\">deploy your React app<\/a> so other people can access it online. Once it&rsquo;s live, keep improving the project as you learn new React skills.<\/p><p class=\"wp-block-paragraph\">When deploying your app, choose a reliable hosting provider such as Hostinger. The <a href=\"\/ca\/web-apps-hosting\/react-hosting\">React hosting plans<\/a> include a free domain for one year on annual plans.<\/p><p class=\"wp-block-paragraph\">They also include free managed SSL certificates that remain active as long as your app is hosted with Hostinger, so you can serve it securely over HTTPS.<\/p><p class=\"wp-block-paragraph\">After deployment, test the live app to make sure it works as it did on your local computer. Check the navigation, forms, task actions, API requests, and loading and error states.<\/p><p class=\"wp-block-paragraph\">Because React Router handles routes in the browser, some hosts return a 404 error when you refresh a URL such as <strong>\/about<\/strong>. Set your host to serve <strong>index.html<\/strong> for all routes to fix this.<\/p><p class=\"wp-block-paragraph\">Publishing your app isn&rsquo;t the end of the project. You can add persistent task storage so changes survive a refresh or <a href=\"\/ca\/tutorials\/react-performance-optimization\/\">improve React performance<\/a> to speed up your app.<\/p><p class=\"wp-block-paragraph\">Focus on one useful improvement at a time so you can understand and test each change before adding another.<\/p><figure class=\"wp-block-image size-full\"><a class=\"hgr-tutorials-cta hgr-tutorials-cta-web-hosting\" href=\"\/ca\/web-hosting\" target=\"_blank\" rel=\"noreferrer noopener\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"300\" src=\"https:\/\/imagedelivery.net\/LqiWLm-3MGbYHtFuUbcBtA\/wp-content\/uploads\/sites\/2\/2023\/11\/Web-hosting_in-text-banner.png\/public\" alt=\"Hostinger web hosting banner\" class=\"wp-image-98604\"  sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n","protected":false},"excerpt":{"rendered":"<p>This React tutorial teaches you how to set up a React app, write JSX, build components, pass props, manage state, use hooks, handle forms, add routes, fetch API data, and build a small working project. It works like a beginner React course, with practical lessons, examples, and exercises built around a task tracker app you&rsquo;ll [&#8230;]<\/p>\n<p><a class=\"btn btn-secondary understrap-read-more-link\" href=\"\/ca\/tutorials\/react-tutorial\/\">Read More&#8230;<\/a><\/p>\n","protected":false},"author":411,"featured_media":148910,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"React tutorial for beginners: Build an app","rank_math_description":"Learn React from this tutorial: Understand JSX, components, props, state, hooks, forms, routing, APIs, and build a task tracker app from scratch in %currentyear%.","rank_math_focus_keyword":"react tutorial","footnotes":""},"categories":[22724],"tags":[],"class_list":["post-148909","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-web-app"],"hreflangs":[{"locale":"en-US","link":"https:\/\/www.hostinger.com\/tutorials\/react-tutorial","default":1},{"locale":"en-PH","link":"https:\/\/www.hostinger.com\/ph\/tutorials\/react-tutorial","default":0},{"locale":"en-MY","link":"https:\/\/www.hostinger.com\/my\/tutorials\/react-tutorial","default":0},{"locale":"en-GB","link":"https:\/\/www.hostinger.com\/uk\/tutorials\/react-tutorial","default":0},{"locale":"en-IN","link":"https:\/\/www.hostinger.com\/in\/tutorials\/react-tutorial","default":0},{"locale":"en-CA","link":"https:\/\/www.hostinger.com\/ca\/tutorials\/react-tutorial","default":0},{"locale":"en-AU","link":"https:\/\/www.hostinger.com\/au\/tutorials\/react-tutorial","default":0},{"locale":"en-NG","link":"https:\/\/www.hostinger.com\/ng\/tutorials\/react-tutorial","default":0}],"_links":{"self":[{"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/posts\/148909","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/users\/411"}],"replies":[{"embeddable":true,"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/comments?post=148909"}],"version-history":[{"count":0,"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/posts\/148909\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/media\/148910"}],"wp:attachment":[{"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/media?parent=148909"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/categories?post=148909"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.hostinger.com\/ca\/tutorials\/wp-json\/wp\/v2\/tags?post=148909"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}