跪拜 Guibai
← Back to the summary

A React Todo App as a Blueprint for Architecture-First Thinking

Source code here

4152d129-46d7-4157-a07c-0df9e40b7dad.png

A Todo application may only have 10 files, but its architectural thinking can be scaled to projects of any size.


I. The Summit: What This Project Does in One Sentence

This project builds a to-do application using a three-layer separation of "type definitions → state logic → UI components." The core innovation is extracting all business logic into a custom Hook (useTodos), so components are only responsible for "drawing the UI," not "thinking about the logic."

If you take away only one sentence, it's the one above. Below, we'll unpack it layer by layer.


II. The Mountainside: Four Architectural Layers, Supporting Each Other

For the second level of the pyramid, I break it down into four logical groups, each answering one question.

Group 1: The Foundation — The Type System (src/types/todo.ts)

Analogy: Draw the blueprints before building the house. Before writing any UI code, this project first defines "what a Todo looks like."

export interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export type FilterType = 'all' | 'completed' | 'uncompleted';

There are three design decisions here worth savoring:

  1. Using interface rather than type to define Todo — The comments note that "interface cannot apply for simple data types," but the deeper reason is: interface describes the "structure of an object," making it semantically more suitable for defining an entity; type is more suitable for union types (like FilterType). This is an embodiment of "using the right tool for the right job" in TypeScript.
  2. id uses string rather than number — Because Date.now().toString() is used later to generate IDs. This choice sacrifices a bit of performance (string comparison is slower than number comparison) in exchange for a guarantee of uniqueness (timestamp + type conversion), which is perfectly reasonable for a Todo-scale scenario.
  3. FilterType uses a literal union type'all' | 'completed' | 'uncompleted'. This means if you write setFilter('archived') in your code, TypeScript will directly throw an error, stopping the bug at the compilation stage. This is a classic practice of "letting the compiler check your logic for you."

Food for thought: Many beginners skip type definitions and jump straight to writing components. The result is a todo object scattered across 5 files, each with different assumptions about it. A type file is about writing down the "consensus" in advance, so the team (or even your future self) doesn't have to guess.


Group 2: The Brain — The Custom Hook (src/hooks/useTodos.ts)

Analogy: If components are the body's organs, the Hook is the brain. Organs are responsible for execution (displaying UI), and the brain is responsible for decision-making (managing state and logic).

export function useTodos() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<FilterType>('all');
  // ...addTodo, toggleTodo, deleteTodo, clearCompleted
  return { todos, filter, addTodo, toggleTodo, deleteTodo, clearCompleted };
}

This Hook's design has four points worth diving into:

2.1 The "Immutable Pattern" of State Updates

Every operation uses functional updates (setTodos(prev => ...)):

const toggleTodo = (id: string) => {
  setTodos(prev =>
    prev.map(item =>
      item.id === id ? { ...item, completed: !item.completed } : item
    )
  );
};

Why not just item.completed = !item.completed? Because React's rendering mechanism relies on reference comparison (prevTodos !== newTodos). If you directly mutate the original object, React will say, "Hmm, the reference is still the same, no need to re-render," and your UI won't update. This is the core reason for React's immutability — not a philosophical preference, but a technical constraint of the rendering mechanism.

A beginner's intuition is "can't I just change it?" but it's precisely this intuition that causes the most React bugs.

2.2 The filter State is Defined but Not Yet Used

filter exists, and FilterType is defined, but useTodos doesn't filter todos based on the filter. The commented-out filteredTodos function in the notes shows the author is aware of this gap:

// const filteredTodos = () => {
// }

This is an excellent teaching moment: The Hook exposes filter but doesn't expose the filtered results, meaning the filtering logic is either still being designed or intended to be done at the component layer. Both choices have pros and cons — putting it in the Hook is more cohesive, putting it in the component is more flexible. This blank space perfectly illustrates the trade-offs in architectural decision-making.

2.3 clearCompleted is a Batch Operation

const clearCompleted = () => {
  setTodos(prev => prev.filter(item => !item.completed));
};

Note that it uses filter rather than deleting one by one. This is declarative programming thinking: instead of saying "delete the 2nd, delete the 5th," you say "I want a list without completed items." Declarative code is less error-prone because you don't need to manually maintain index offset issues like "after deleting the 2nd, what position did the original 5th become?"

2.4 The Return Value is an Object, Not an Array

return { todos, filter, addTodo, toggleTodo, deleteTodo, clearCompleted };

Compared to return [todos, filter, addTodo, ...], the benefit of object destructuring is that you can pick what you need when using it, and the names are fixed:

const { addTodo, toggleTodo } = useTodos(); // ✅ Order doesn't matter, clear at a glance
const [todos, , addTodo] = useTodos();       // ❌ Need to remember positions, hard to maintain

Group 3: The Body — The Component Layer (src/component/)

The four component files are currently shells: Todoinput.tsx, Todoitem.tsx, TodoList.tsx, TodoFilter.tsx.

This is precisely evidence of an architecture-first working style. Before writing any specific UI, the author has already clarified, "I need four components, responsible for input, displaying a single item, displaying the list, and filtering." This is like erecting the walls of a house before building it — although it's not decorated yet, the spatial division is already clear.

From the naming, we can infer the component tree structure:

App
├── TodoInput    (Input box + Add button)
├── TodoFilter   (Three filter options: All/Completed/Uncompleted)
└── TodoList     (List container)
    └── TodoItem × N  (Each todo)

This is the embryonic form of a classic container-presentation component pattern: TodoList is the container, TodoItem is purely presentational.


Group 4: The Entry — The Application Assembly Layer (App.tsx + main.tsx)

main.tsx is the ignition switch for the entire application:

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

StrictMode in development mode will intentionally call certain functions twice (like useState's initializer function, useEffect) to help expose problems with "impure side effects." Many beginners see console.log printed twice and think it's a bug, but it's actually React doing a stress test for you.

App.tsx is currently still Vite's template page, which means the author is at the stage of "infrastructure complete, ready to assemble."


III. The Foot of the Mountain: A Panoramic View of Data Flow

Stringing the four layers together, how does data flow? Describe it in one sentence:

The user types in TodoInput → calls the Hook's addTodo → inside the Hook, setTodos updates the state → React detects the state change → re-renders TodoList → each TodoItem receives the latest data.

This forms a closed loop of unidirectional data flow:

User Action → Component Event → Hook Method → setState → Component Re-render → New UI

Why is unidirectional data flow important? Because it makes bugs traceable. If data could flow bidirectionally (a child component directly modifying a parent component's state), when a problem like "how did this todo get mysteriously deleted" arises, you'd need to investigate N possible places that could have modified it. Unidirectional flow means you just need to trace backward along the arrow, and you can always find the source.


A Metaphor to Tie the Whole Article Together

Imagine you are in a restaurant:

Role Corresponds to Responsibility
Recipe (Type Defs) types/todo.ts Defines what each dish looks like and what attributes it has
Head Chef (Hook) hooks/useTodos.ts Holds the state of all orders, decides the logic for CRUD
Waiter (Components) component/*.tsx Receives customer instructions, passes them to the kitchen, then presents the results to the customer
Restaurant Manager (App.tsx) App.tsx Assigns waiters to their positions, coordinates the entire process
Restaurant Door (main.tsx) main.tsx The entrance for the customer (user)

The customer doesn't need to know how the kitchen operates; they just talk to the waiter. The waiter doesn't need to know how to cook; they just pass the request to the head chef. The head chef doesn't need to know which table the dish goes to; they just maintain the state of the orders. Each layer only minds its own business — this is the essence of layered architecture.


IV. Follow-up Suggestions for This Project

As an added value of code review, if you want to continue improving this project:

  1. Implement filteredTodos in the Hook — Return a filtered list based on the filter state, keeping filtering logic and state together.
  2. Inject the Hook into components — Call useTodos() in App.tsx and distribute the return values to child components via props.
  3. Consider useReducer instead of useState — When the types of operations increase (add, toggle, delete, clear, edit), useReducer centralizes state change logic into a single reducer function, making it easier to maintain than scattered setXxx calls.
  4. Add localStorage persistence — Data loss on refresh is a classic pain point for Todo apps; you can add useEffect in the Hook for synchronization.

Summary: Good architecture is not "refactored out after writing a lot of code," but rather, before writing the first line of code, you first think through three questions: "Where do the type definitions go? Where does the state go? How are the components split?" This Todo project is a great demonstration — not many files, but each layer has clear responsibilities and doesn't overstep its bounds.