Task Manager: Building the UI Before Redux Arrives

Before wiring up Redux, build the task manager UI with local React state. The component structure and the collapsible pattern will stay. Redux just replaces the data source.

June 27, 20269 min read2 / 5

The folder structure is in place. Now we need something to manage. The app we are building: a task manager where users can create tasks with a title and date, and delete them.

The goal of this post is to build that UI entirely with local React state. No Redux yet. When Redux arrives in the next post, the structure stays. Only the data source changes.

What We Are Building

The task manager has three main sections:

  1. A header with a "Create" button that toggles the new task form
  2. A collapsible form for entering a task title and date
  3. A list of tasks, each with a delete button

Task manager component tree ExpandTask manager component tree

The interesting part is the collapsible form. It opens when you click "Create" and closes when you click "Save" or "Cancel." That toggle state lives in the Tasks component and controls rendering through a child component called Collapsible.

[!TIP] Run this yourself: The complete UI code for this post is in the code-practice repo. Clone it, npm install, and run npm start.

Project Setup

Two setup steps before writing components.

Font Awesome icons: add the CDN link in public/index.html after the existing <link> tags. Search "font awesome 5 cdn" on cdnjs.com and copy the stylesheet link. This gives the app access to icons like fa-calendar, fa-save, and fa-times.

HTML
<!-- public/index.html, inside <head> --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" />

Global CSS: create src/index.css with base resets (remove default margins/padding, set font family and size) and import it in src/index.js:

JavaScript
// src/index.js import './index.css';

This file applies globally to every component. All component-specific styles (card borders, button colors, flex layout) live in separate .css files next to each component.

Component Structure

Three components, one clear hierarchy:

Plain text
App └── Tasks ├── header (title + Create button) ├── Collapsible │ └── new task form (title + date inputs + Save/Cancel) ├── search box └── task list (one card per task)

App renders Tasks. Tasks owns the toggle state and renders Collapsible around the new task form. The list section renders one card per task in the initial data.

The Collapsible Component

This is the most reusable piece. Collapsible receives two props: isOpen (boolean) and children (whatever you wrap inside it). Its logic is one conditional:

JSX
// components/collapsible/Collapsible.js export default function Collapsible({ isOpen, children }) { if (isOpen) return <>{children}</>; return null; }

That is the entire component. When isOpen is true, it renders its children. When false, it renders nothing.

The caller owns the state. Collapsible does not know why it is open or closed. It only knows whether to render. This separation means the same component can wrap any content anywhere in the app.

Toggle State in Tasks

Tasks holds a single piece of state: whether the new task form is visible.

JSX
// components/tasks/Tasks.js import { useState } from 'react'; import Collapsible from '../collapsible/Collapsible'; export default function Tasks() { const [isNewTaskOpen, setIsNewTaskOpen] = useState(false); function onSaveClick() { setIsNewTaskOpen(false); } function onCancelClick() { setIsNewTaskOpen(false); } return ( <div className="outer-container"> <div className="container"> <div className="app-title-container"> <div className="app-title"><h1>Tasks</h1></div> {!isNewTaskOpen && ( <div className="create-button-container"> <button className="button create-button" onClick={() => setIsNewTaskOpen(true)} > <i className="fas fa-calendar" /> &nbsp; Create </button> </div> )} </div> <Collapsible isOpen={isNewTaskOpen}> <div className="new-task-container"> <h2>New Task</h2> <div className="form-group"> <label>Task Title</label> <div className="form-input"> <input type="text" /> </div> </div> <div className="form-group"> <label>Task Date and Time</label> <div className="form-input"> <input type="datetime-local" /> </div> </div> <div className="button-group"> <button className="button save-button" onClick={onSaveClick}> <i className="fas fa-save" /> &nbsp; Save Task </button> <button className="button cancel-button" onClick={onCancelClick}> <i className="fas fa-times" /> &nbsp; Cancel </button> </div> </div> </Collapsible> <div className="search-box"> <i className="fas fa-search" /> <input type="text" placeholder="Search tasks..." /> </div> <div className="content-body"> {/* static cards — replaced by useSelector once Redux connects */} <div className="task"> <div className="task-body"> <div className="task-title"> <i className="fas fa-tasks" /> <span>Design the database schema</span> </div> <div className="task-subtitle">2026-07-01T09:00</div> </div> <div className="task-options"> <button className="icon-button">&times;</button> </div> </div> </div> </div> </div> ); }

Two things to notice. The Create button only renders when the form is closed (!isNewTaskOpen). Both Save and Cancel call setIsNewTaskOpen(false). Later, Save will also dispatch a Redux action to actually store the task, but the UI toggle logic stays identical.

Initial State in the Data Folder

The task list currently renders static cards. Before wiring up Redux, we need sample data to confirm the list renders correctly.

In src/data/tasks.js:

JavaScript
export const initialTasks = [ { id: 1, taskTitle: 'Design the database schema', dateTime: '2026-07-01T09:00', }, { id: 2, taskTitle: 'Write API integration tests', dateTime: '2026-07-02T14:00', }, { id: 3, taskTitle: 'Review pull requests', dateTime: '2026-07-03T10:30', }, ];

This is the initial state that the Redux reducer will load when the store is created. When we connect Redux, the reducer receives initialTasks as its default state parameter. The UI will read from the store instead of a hardcoded list.

What Stays When Redux Arrives

When Redux connects to this app, only one thing changes: where the data comes from.

The Collapsible component stays identical. The toggle state (isNewTaskOpen) stays in Tasks as local state. It is UI state, not application state, and Redux is for application state. The component tree stays the same. The folder structure stays the same.

Redux replaces the task list data source. The static cards become dynamic cards driven by useSelector. The Save button dispatches an action using the same action creator pattern we built in the pure Redux series. That is the only swap.

The Essentials

  1. Collapsible is a pure rendering decision. It receives isOpen and children and renders one or the other. The caller owns the toggle state. Collapsible has no opinion about why it opens.
  2. UI state stays local. isNewTaskOpen belongs in Tasks with useState, not in Redux. Redux manages task data. React manages the form visibility toggle.
  3. initialTasks in data/tasks.js becomes the default state parameter in the reducer. The shape you define here is the shape the reducer and all selectors will work with.

Further Reading and Watching