paint-brush
Tối đa hóa kỹ năng phản ứng của bạn: Xây dựng ứng dụng danh sách việc cần làm từ đầu đến cuối (với TypeScript + Vite)từ tác giả@emotta
10,346 lượt đọc
10,346 lượt đọc

Tối đa hóa kỹ năng phản ứng của bạn: Xây dựng ứng dụng danh sách việc cần làm từ đầu đến cuối (với TypeScript + Vite)

từ tác giả Eduardo Motta de Moraes26m2023/02/02
Read on Terminal Reader

dài quá đọc không nổi

Trong bài đăng trên blog này, chúng ta sẽ đi qua mọi thứ bạn cần để hiểu và xây dựng một ứng dụng React cơ bản. Cho dù bạn là người mới bắt đầu làm quen với React hay một nhà phát triển dày dạn kinh nghiệm đang tìm cách cải thiện các kỹ năng của mình, hướng dẫn này đều dành cho bạn.
featured image - Tối đa hóa kỹ năng phản ứng của bạn: Xây dựng ứng dụng danh sách việc cần làm từ đầu đến cuối (với TypeScript + Vite)
Eduardo Motta de Moraes HackerNoon profile picture
0-item

Trong bài đăng trên blog này, chúng ta sẽ đi qua mọi thứ bạn cần để hiểuxây dựng một ứng dụng React cơ bản. Cho dù bạn là người mới bắt đầu làm quen với React hay một nhà phát triển dày dạn kinh nghiệm đang tìm cách cải thiện các kỹ năng của mình, hướng dẫn này đều dành cho bạn.


Hướng dẫn này sẽ đưa bạn qua toàn bộ quá trình xây dựng một ứng dụng danh sách việc cần làm đầy đủ chức năng, bao gồm thiết kế, bố cục, quản lý trạng thái, v.v. Chúng tôi sẽ sử dụng các thành phần chức năng và hook. Chúng ta sẽ tìm hiểu cách sử dụng trạng thái và đạo cụ để truyền dữ liệu giữa các thành phần cũng như cách xử lý thông tin nhập của người dùng và cập nhật trạng thái ứng dụng của bạn.


Khi kết thúc hướng dẫn này, chúng ta sẽ có hiểu biết vững chắc về cách xây dựng ứng dụng React từ đầu và bạn sẽ có thể sử dụng kiến thức mới tìm được để xây dựng các dự án React của riêng mình.

Vậy hãy bắt đầu!


*Bạn có thể tìm thấy mã của ứng dụng mà chúng tôi sẽ xây dựng tại đây và phiên bản trực tiếp tại đây .

Giới thiệu ngắn gọn

Chúng tôi sẽ sử dụng TypeScript để viết mã và Vite để phát triển và xây dựng ứng dụng.

bản đánh máy

TypeScript là một ngôn ngữ lập trình được gõ mạnh dựa trên JavaScript. Về mặt thực tế, nếu bạn đã biết JavaScript, tất cả những gì bạn cần học để sử dụng TypeScript là cách sử dụng các loại và giao diện.


Các loại và giao diện cho phép chúng tôi xác định các loại dữ liệu chúng tôi đang sử dụng trong mã. Với điều này, chúng tôi có thể bắt lỗi sớm và tránh các sự cố sau này.


Chẳng hạn, nếu một hàm nhận một number nhưng chúng ta truyền vào đó một string , TypeScript sẽ phàn nàn ngay lập tức:


 const someFunc = (parameter: number) => {...}; someFunc('1') // Argument of type 'string' is not assignable to parameter of type 'number'.


Nếu chúng tôi đang sử dụng JavaScript, chúng tôi có thể chỉ gặp lỗi sau này.


Không phải lúc nào chúng ta cũng cần chỉ định loại, vì TypeScript có thể tự động suy ra chúng thường xuyên hơn.


Bạn có thể tìm hiểu kiến thức cơ bản về TypeScript tại đây . (Hoặc chỉ cần bỏ qua các loại.)

vit

Cách phổ biến nhất để khởi động ứng dụng React có lẽ là sử dụng ứng dụng tạo-phản ứng . Thay vào đó, chúng tôi sẽ sử dụng Vite (phát âm như “veet”). Nhưng đừng băn khoăn, nó chỉ đơn giản — nhưng hiệu quả hơn.


Với các công cụ như webpack (được sử dụng bởi ứng dụng tạo-phản ứng bên trong), toàn bộ ứng dụng của bạn cần được nhóm trong một tệp trước khi có thể phân phối tới trình duyệt. Mặt khác, Vite tận dụng các mô-đun ES gốc trong trình duyệt để tạo gói hiệu quả hơn với Rollup , phục vụ các phần của mã nguồn khi cần.


Vite cũng có thể tăng tốc đáng kể thời gian phát triển với Thay thế mô-đun nóng — nghĩa là bất cứ khi nào có thay đổi đối với mã nguồn, chỉ những thay đổi đó được cập nhật chứ không phải toàn bộ ứng dụng.


Bên cạnh đó, Vite cung cấp hỗ trợ riêng cho Bản mô tả, JSX và TSX, CSS, v.v.


Tương tự như ứng dụng tạo-phản ứng, Vite cung cấp một công cụ có tên là tạo-vite, cho phép chúng tôi nhanh chóng bắt đầu một dự án mới bằng cách sử dụng các mẫu cơ bản, bao gồm các tùy chọn cho Vanilla JS hoặc sử dụng các thư viện như React.


Nói rõ hơn, chúng ta không cần một công cụ như Vite hoặc ứng dụng tạo-phản ứng để xây dựng các ứng dụng React, nhưng chúng giúp cuộc sống của chúng ta dễ dàng hơn bằng cách chăm sóc thiết lập dự án, đóng gói mã, sử dụng bộ chuyển đổi và hơn thế nữa.

Lặn vào React

JSX / TSX

React cho phép chúng tôi thêm đánh dấu trực tiếp vào mã mà sau này sẽ được biên dịch thành JavaScript thuần túy. Cái này được gọi là JSX . Khi chúng tôi đang sử dụng JSX, chúng tôi có thể lưu các tệp của mình dưới dạng .jsx cho JavaScript hoặc .tsx cho TypeScript.


Nó trông như thế này:


 const element = <h1>Hello, world!</h1>;


Nó tương tự như HTML, nhưng nó được nhúng trong tệp JavaScript và nó cho phép chúng ta thao tác đánh dấu bằng logic lập trình. Chúng ta cũng có thể thêm mã JavaScript bên trong JSX, miễn là nó nằm trong dấu ngoặc nhọn.


Chẳng hạn, nếu chúng ta có một mảng văn bản mà chúng ta muốn hiển thị dưới dạng các thành phần đoạn văn khác nhau, chúng ta có thể làm điều này:


 const paragraphs = ["First", "Second", "Third"]; paragraphs.map((paragraph) => <p>{paragraph}</p>);


Và nó sẽ được biên dịch thành một cái gì đó như thế này:


 <p>First</p> <p>Second</p> <p>Third</p>


Nhưng nếu chúng ta cố gắng làm điều đó, nó sẽ không hoạt động. Đó là bởi vì React hoạt động với các thành phần và JSX cần được hiển thị bên trong các thành phần này.

thành phần phản ứng

Các thành phần phản ứng có thể được viết bằng các lớp JavaScript hoặc chỉ các hàm đơn giản. Chúng tôi sẽ tập trung vào các thành phần chức năng, vì chúng là cách viết các thành phần React được đề xuất và cập nhật nhất hiện nay.


Một thành phần được xác định bởi một hàm sẽ trả về JSX sẽ được trình duyệt biên dịch và hiển thị. Vì vậy, để mở rộng ví dụ trên, nếu chúng ta muốn hiển thị các thành phần đoạn văn, nó sẽ giống như thế này:


 // Define the component const Component = () => { const paragraphs = ["First", "Second", "Third"]; return ( <> {paragraphs.map((paragraph) => ( <p>{paragraph}</p> ))} </> ); }; // Use the component in the same way you use an HTML element in the JSX const OtherComponent = () => { return <Component />; };

đạo cụ

Bây giờ có lẽ chúng tôi muốn sử dụng lại thành phần này với thông tin khác. Chúng ta có thể làm điều đó bằng cách sử dụng đạo cụ — chỉ là một đối tượng JavaScript chứa một số dữ liệu.


Trong ví dụ của chúng tôi, thay vì mã hóa cứng mảng, chúng tôi có thể chuyển nó vào thành phần. Kết quả sẽ giống nhau, nhưng bây giờ thành phần này sẽ được tái sử dụng.


Nếu chúng ta đang sử dụng TypeScript, chúng ta cần chỉ định các loại dữ liệu bên trong đối tượng đạo cụ (không có ngữ cảnh cho chúng là gì, vì vậy TypeScript không thể suy ra chúng), trong trường hợp này là một mảng các chuỗi ( string[] ).


 const Component = (props: { paragraphs: string[] }) => { <> {props.paragraphs.map((paragraph) => ( <p>{paragraph}</p> ))} </>; }; const OtherComponent = () => { const paragraphs = ["First", "Second", "Third"]; return <Component paragraphs={paragraphs} />; };

Tình trạng

Nếu chúng ta muốn tạo một thành phần tương tác, chúng ta sẽ cần lưu trữ thông tin ở trạng thái của thành phần đó để nó có thể “ghi nhớ” nó.


Chẳng hạn, nếu chúng ta muốn xác định một bộ đếm đơn giản hiển thị số lần nhấp vào nút, chúng ta cần một cách lưu trữ và cập nhật giá trị này. React cho phép chúng ta làm điều đó với useState hook ( hook là một chức năng cho phép bạn “kết nối” vào trạng thái React và các tính năng vòng đời của React).


Chúng tôi gọi hook useState với giá trị ban đầu và nó trả về cho chúng tôi một mảng có chính giá trị đó và một hàm để cập nhật nó.


 import { useState } from "react"; const Counter = () => { const [count, setCount] = useState(0); return ( <> <span>{count}</span> <button onClick={() => setCount(count + 1)}>Increment count</button> </> ); };


Với kiến thức này, giờ chúng ta đã sẵn sàng bắt đầu xây dựng ứng dụng React của mình.

Tạo dự án

phụ thuộc

Để sử dụng Vite, chúng tôi sẽ cần **nút **và trình quản lý gói.


Để cài đặt nút, chỉ cần chọn một trong các tùy chọn ở đây tùy thuộc vào hệ thống và cấu hình của bạn. Nếu đang sử dụng Linux hoặc Mac, bạn cũng có thể cài đặt nó bằng Homebrew .


Trình quản lý gói có thể là npm hoặc yarn . Trong bài đăng này, chúng tôi sẽ sử dụng npm .

Tạo dự án

Tiếp theo là thời gian để tạo dự án. Trong thiết bị đầu cuối, chúng tôi điều hướng đến thư mục sẽ tạo dự án, sau đó chạy lệnh tạo-vite.


 $ npm create vite@latest


Chúng tôi có thể được nhắc cài đặt các gói bổ sung (như tạo-vite). Nhập y và nhấn enter để tiếp tục.


 Need to install the following packages: [email protected] Ok to proceed? (y)


Tiếp theo, chúng tôi sẽ được nhắc nhập thông tin dự án.


Nhập tên của dự án. Tôi đã chọn my-react-project .


 ? Project name: › my-react-project


Chọn React làm “khuôn khổ”.


Về mặt kỹ thuật, React là một thư viện chứ không phải một framework , nhưng đừng lo lắng về điều đó.


 ? Select a framework: › - Use arrow-keys. Return to submit. Vanilla Vue ❯ React Preact Lit Svelte Others


Chọn TypeScript + SWC làm biến thể.


SWC (viết tắt của Speedy Web Compiler) là trình biên dịch TypeScript/JavaScript siêu nhanh được viết bằng Rust. Họ tuyên bố là “nhanh hơn 20 lần so với Babel trên một luồng và nhanh hơn 70 lần trên bốn lõi”.


 ? Select a variant: › - Use arrow-keys. Return to submit. JavaScript TypeScript JavaScript + SWC ❯ TypeScript + SWC


Vậy là xong, dự án đã được tạo. Để khởi động nó ở chế độ phát triển, chúng ta cần thay đổi thư mục dự án, cài đặt các phụ thuộc và chạy lệnh dev script.


 cd my-react-project npm install npm run dev


Sau vài giây, chúng ta sẽ thấy một cái gì đó tương tự như thế này:


 VITE v4.0.4 ready in 486 ms ➜ Local: http://localhost:5173/ ➜ Network: use --host to expose ➜ press h to show help


Nếu chúng ta mở trình duyệt và điều hướng đến http://localhost:5173 / chúng ta sẽ thấy trang Vite + React mặc định:



Điều này có nghĩa là mọi thứ vẫn như bình thường và chúng ta có thể bắt đầu làm việc với ứng dụng của mình.

xây dựng ứng dụng

Cấu trúc tệp và thiết lập ban đầu

Nếu chúng ta mở dự án trong trình chỉnh sửa mã hoặc IDE mà chúng ta chọn, chúng ta sẽ thấy cấu trúc tệp như sau:



Chúng tôi có thể xóa một số tệp soạn sẵn vì chúng tôi sẽ không sử dụng chúng (tất cả các tệp .svg và .css).

Mã trong chức năng Ứng dụng có thể bị xóa để lại cho chúng tôi điều này:


 function App() { return ( ) } export default App


Chúng ta sẽ quay lại tập tin này sau.

tạo kiểu

Tạo kiểu không phải là trọng tâm ở đây, nhưng chúng ta sẽ sử dụng Tailwind CSS, đây là một thư viện cho phép chúng ta tạo kiểu cho các phần tử HTML bằng cách thêm các lớp vào chúng. Thực hiện theo các hướng dẫn này để xem các phong cách được phản ánh trong dự án của riêng bạn.


Nếu không, bạn có thể bỏ qua các lớp trong mã.

Suy nghĩ về thiết kế: bố trí các thành phần

Quá trình thiết kế là một phần không thể thiếu trong quá trình phát triển ứng dụng và không nên bỏ qua.

Để xây dựng ứng dụng danh sách việc cần làm, trước tiên chúng ta cần nghĩ đến bố cục các thành phần.


Chúng tôi bắt đầu bằng cách mô phỏng một giao diện người dùng cơ bản và phác thảo hệ thống phân cấp của các thành phần liên quan.


Nếu bạn không phải là nhà thiết kế, nó không cần phải hoàn hảo hay giao diện người dùng cuối cùng về màu sắc và vị trí chính xác — điều quan trọng hơn là phải nghĩ đến cấu trúc thành phần.


Lý tưởng nhất là các thành phần của chúng tôi chỉ chịu trách nhiệm cho một việc, tuân theo nguyên tắc chịu trách nhiệm duy nhất .


Trong hình ảnh bên dưới, các tên màu tím là các thành phần chúng ta sẽ xây dựng — mọi thứ khác đều là các thành phần HTML gốc. Nếu chúng ở trong nhau, điều đó có nghĩa là có khả năng sẽ có mối quan hệ cha-con.

Đạo cụ: xây dựng phiên bản tĩnh

Sau khi có bản phác thảo, chúng ta có thể bắt đầu xây dựng phiên bản tĩnh của ứng dụng. Điều đó có nghĩa là, chỉ có các yếu tố giao diện người dùng, nhưng chưa có tính tương tác. Phần này khá đơn giản và bao gồm rất nhiều thao tác gõ và ít suy nghĩ khi bạn đã hiểu rõ về nó.


Bạn có thể tìm thấy mã cho phiên bản tĩnh trong kho lưu trữ GitHub này, trong nhánh “phiên bản tĩnh”. Mã cho ứng dụng hoạt động đầy đủ là nhánh chính.


Thùng đựng hàng


Như đã nêu ở trên, chúng ta sẽ có một Vùng chứa được sử dụng lại cho mọi phần của ứng dụng. Vùng chứa này hiển thị một trong những cách tạo thành các phần tử khác nhau: bằng cách chuyển chúng dưới dạng phần tử con.


 // src/components/Container.tsx const Container = ({ children, title, }: { children: JSX.Element | JSX.Element[]; title?: string; }) => { return ( <div className="bg-green-600 p-4 border shadow rounded-md"> {title && <h2 className="text-xl pb-2 text-white">{title}</h2>} <div>{children}</div> </div> ); }; export default Container;


Nó nhận một đối tượng đạo cụ có tham số children thuộc loại JSX.Element | JSX.Element[] . Điều này có nghĩa là chúng ta có thể soạn nó với bất kỳ phần tử HTML nào khác hoặc bất kỳ thành phần nào khác mà chúng ta tạo. Nó có thể được hiển thị ở bất cứ đâu chúng ta muốn bên trong vùng chứa — trong trường hợp này là bên trong div thứ hai.


Trong ứng dụng của chúng tôi, nó sẽ hiển thị từng phần (được xác định bên dưới) khi chúng tôi sử dụng chúng bên trong thành phần Ứng dụng.


Vùng chứa cũng có một giá trị string tùy chọn có tên title , sẽ được hiển thị bên trong h2 bất cứ khi nào nó tồn tại.


 // src/App.tsx import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; function App() { return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="sm:w-[640px] border shadow p-10 flex flex-col gap-10"> <Container title={"Summary"}> <Summary /> </Container> <Container> <Input /> </Container> <Container title={"Tasks"}> <Tasks /> </Container> </div> </div> </div> ); } export default App;


Bản tóm tắt


Phần đầu tiên là phần tóm tắt (Summary component) hiển thị ba mục (SummaryItem): tổng số nhiệm vụ, số nhiệm vụ đang chờ xử lý và số nhiệm vụ đã hoàn thành. Đây là một cách khác để soạn các thành phần: chỉ cần sử dụng chúng trong câu lệnh trả về của một thành phần khác.


(Tuy nhiên, điều quan trọng là không bao giờ xác định một thành phần bên trong một thành phần khác, vì điều đó có thể dẫn đến các lỗi và trình kết xuất lại không cần thiết.)


Hiện tại, chúng ta chỉ có thể sử dụng dữ liệu tĩnh trong hai thành phần.


 // src/components/Summary/SummaryItem.tsx const SummaryItem = ({ itemName, itemValue, }: { itemName: string; itemValue: number; }) => { return ( <article className="bg-green-50 w-36 rounded-sm flex justify-between p-2"> <h3 className="font-bold">{itemName}</h3> <span className="bg-green-900 text-white px-2 rounded-sm"> {itemValue} </span> </article> ); }; export default SummaryItem; // src/components/Summary/Summary.tsx import SummaryItem from "./SummaryItem"; const Summary = () => { return ( <> <div className="flex justify-between"> <SummaryItem itemName={"Total"} itemValue={3} /> <SummaryItem itemName={"To do"} itemValue={2} /> <SummaryItem itemName={"Done"} itemValue={1} /> </div> </> ); }; export default Summary;


Bạn sẽ nhận thấy SummaryItem có hai đạo cụ: itemName , thuộc loại chuỗi và itemValue , thuộc loại number . Các đạo cụ này được chuyển khi thành phần SummaryItem được sử dụng bên trong thành phần Tóm tắt, sau đó được hiển thị trong JSX của SummaryItem.


nhiệm vụ


Tương tự, đối với phần nhiệm vụ (phần cuối cùng), chúng ta có một thành phần Nhiệm vụ hiển thị các thành phần Nhiệm vụ.


Ngoài ra với dữ liệu tĩnh cho bây giờ. Sau đó, chúng ta sẽ cần chuyển tên tác vụtrạng thái xuống dưới dạng đạo cụ cho thành phần TaskItem để làm cho nó có thể tái sử dụng và động.


 // src/components/Tasks/TaskItem.tsx const TaskItem = () => { return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm"> <div className="flex gap-2 items-center"> <input type="checkbox" /> Task name </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3"> Delete </button> </div> ); }; export default TaskItem; // src/components/Tasks/Tasks.tsx import TaskItem from "./TaskItem"; const Tasks = () => { return ( <div className="flex flex-col gap-2"> <TaskItem /> </div> ); }; export default Tasks;


Đầu vào


Cuối cùng, thành phần Đầu vào là một biểu mẫu có nhãn, đầu vào là loại văn bản và một nút để “Thêm tác vụ”. Hiện tại nó không làm gì cả, nhưng chúng tôi sẽ sớm thay đổi điều đó.


 // src/components/Input.tsx const InputContainer = () => { return ( <form action="" className="flex flex-col gap-4"> <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" /> </div> <button type="button" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;

Trạng thái: thêm tương tác

Để thêm tính tương tác trong React, chúng ta cần lưu trữ thông tin ở trạng thái của thành phần.


Nhưng trước khi làm điều đó, chúng ta cần suy nghĩ về cách chúng ta muốn dữ liệu thay đổi theo thời gian. Chúng ta cần xác định một biểu diễn tối thiểu của dữ liệu này và xác định những thành phần nào chúng ta nên sử dụng để lưu trữ trạng thái này.


Một đại diện tối thiểu của nhà nước


Trạng thái phải chứa mọi thông tin cần thiết để làm cho ứng dụng của chúng ta có tính tương tác — ngoài ra không có gì khác. Nếu chúng ta có thể tính toán một giá trị từ một giá trị khác, chúng ta chỉ nên giữ một trong số chúng ở trạng thái. Điều này làm cho mã của chúng tôi không chỉ ít dài dòng hơn mà còn ít bị lỗi hơn liên quan đến các giá trị trạng thái mâu thuẫn.


Trong ví dụ của chúng tôi, chúng tôi có thể nghĩ rằng chúng tôi cần theo dõi các giá trị cho tổng số nhiệm vụ, nhiệm vụ đang chờ xử lý và nhiệm vụ đã hoàn thành.


Nhưng để theo dõi các nhiệm vụ, chỉ cần có một mảng với các đối tượng đại diện cho từng nhiệm vụ và trạng thái của nó (đang chờ xử lý hoặc đã hoàn thành) là đủ.


 const tasks = [ { name: "task one", done: false, }, { name: "task two", done: true, }, ];


Với dữ liệu này, chúng tôi luôn có thể tìm thấy tất cả các thông tin khác mà chúng tôi cần tại thời điểm kết xuất bằng các phương thức mảng. Chúng tôi cũng tránh khả năng xảy ra mâu thuẫn,— chẳng hạn như có tổng cộng 4 nhiệm vụ, nhưng chỉ có 1 nhiệm vụ đang chờ xử lý và 1 nhiệm vụ đã hoàn thành chẳng hạn.


Chúng tôi cũng cần trạng thái trong biểu mẫu của mình (trong thành phần Đầu vào) để chúng tôi có thể làm cho nó tương tác.


Nhà nước nên sống ở đâu


Hãy nghĩ về nó theo cách này: thành phần nào cần truy cập dữ liệu mà chúng ta sẽ lưu trữ ở trạng thái? Nếu nó là một thành phần duy nhất, trạng thái có thể tồn tại trong chính thành phần này. Nếu có nhiều thành phần cần dữ liệu, thì bạn nên tìm cha chung của các thành phần này.


Trong ví dụ của chúng tôi, trạng thái cần thiết để kiểm soát thành phần Đầu vào chỉ cần được truy cập ở đó, vì vậy nó có thể là cục bộ của thành phần này.


 // src/components/Input.tsx import { useState } from "react"; const InputContainer = () => { const [newTask, setNewTask] = useState(""); // Initialize newTask and setNewTask return ( <form action="" className="flex flex-col gap-4"> <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" type="text" value={newTask} // Set the input value to newTask onChange={(e) => setNewTask(e.target.value)} // Set newTask to the input value whenever the user types something /> </div> <button type="submit" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;


Điều này đang làm là hiển thị giá trị newTask của chúng tôi trong đầu vào và gọi hàm setNewTask bất cứ khi nào đầu vào thay đổi (nghĩa là khi người dùng nhập nội dung nào đó).


Chúng tôi sẽ không thấy bất kỳ thay đổi ngay lập tức nào trong giao diện người dùng, nhưng điều này là cần thiết để chúng tôi có thể kiểm soát đầu vào và có quyền truy cập vào giá trị của nó để sử dụng sau này.


Tuy nhiên, trạng thái để theo dõi các tác vụ phải được xử lý theo cách khác, vì nó cần được truy cập trong các thành phần SummaryItem (chúng ta cần hiển thị số lượng tổng số, các tác vụ đang chờ xử lý và đã hoàn thành) cũng như trong các thành phần TaskItem (chúng ta cần để hiển thị các tác vụ). Và nó cần phải cùng một trạng thái vì thông tin này phải luôn đồng bộ.


Hãy xem cây thành phần của chúng tôi (bạn có thể sử dụng các công cụ dành cho nhà phát triển React cho việc này).



Chúng ta có thể thấy rằng thành phần cha phổ biến đầu tiên là Ứng dụng. Vì vậy, đây là nơi trạng thái của chúng tôi dành cho các nhiệm vụ sẽ tồn tại.


Với trạng thái đã có, tất cả những gì còn lại sẽ là truyền dữ liệu xuống dưới dạng đạo cụ cho các thành phần cần sử dụng nó.


(Chúng tôi chưa lo lắng về cách thực hiện và duy trì bất kỳ thay đổi nào đối với trạng thái gốc, đó là phần tiếp theo.)


 // src/App.tsx import { useState } from "react"; import { v4 as uuidv4 } from "uuid"; import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; export interface Task { name: string; done: boolean; id: string; } const initialTasks = [ { name: "task one", done: false, id: uuidv4(), }, { name: "task two", done: true, id: uuidv4(), }, ]; function App() { const [tasks, setTasks] = useState<Task[]>(initialTasks); return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="border shadow p-10 flex flex-col gap-10 sm:w-[640px]"> <Container title={"Summary"}> <Summary tasks={tasks} /> </Container> <Container> <Input /> </Container> <Container title={"Tasks"}> <Tasks tasks={tasks} /> </Container> </div> </div> </div> ); } export default App;


Ở đây, chúng tôi đang khởi tạo giá trị tác vụ bằng dữ liệu giả ( initialTasks ), chỉ để chúng tôi có thể hình dung nó trước khi ứng dụng kết thúc. Sau này, chúng ta có thể thay đổi nó thành một mảng trống, vì vậy người dùng mới sẽ không thấy bất kỳ tác vụ nào khi mở ứng dụng mới.


Bên cạnh các thuộc tính namedone , chúng tôi cũng đang thêm một id vào các đối tượng tác vụ của mình, vì nó sẽ sớm cần thiết.


Chúng tôi đang xác định một interface với các loại giá trị trong các đối tượng tác vụ và chuyển giao diện đó cho hàm useState . Điều này là cần thiết trong trường hợp này, vì TypeScript sẽ không thể suy ra nó khi chúng ta thay đổi giá trị ban đầu của các tasks thành một mảng trống hoặc khi chúng ta chuyển nó dưới dạng đạo cụ.


Cuối cùng, lưu ý rằng chúng tôi đang chuyển các nhiệm vụ xuống dưới dạng đạo cụ cho các thành phần Tóm tắt và Nhiệm vụ. Những thành phần này sẽ cần phải được thay đổi để phù hợp với điều đó.


 // src/components/Summary/Summary.tsx import { Task } from "../../App"; import SummaryItem from "./SummaryItem"; const Summary = ({ tasks }: { tasks: Task[] }) => { const total = tasks.length; const pending = tasks.filter((t) => t.done === false).length; const done = tasks.filter((t) => t.done === true).length; return ( <> <div className="flex flex-col gap-1 sm:flex-row sm:justify-between"> <SummaryItem itemName={"Total"} itemValue={total} /> <SummaryItem itemName={"To do"} itemValue={pending} /> <SummaryItem itemName={"Done"} itemValue={done} /> </div> </> ); }; export default Summary;


Chúng tôi đã cập nhật thành phần Tóm tắt để nó hiện chấp nhận tasks dưới dạng chỗ dựa. Chúng tôi cũng đã xác định giá trị total , pendingdone , giá trị này sẽ được chuyển xuống dưới dạng đạo cụ cho các thành phần SummaryItem thay cho giá trị tĩnh itemValue mà chúng tôi đã có trước đó.


 // src/components/Tasks/Tasks.tsx import { Task } from "../../App"; import TaskItem from "./TaskItem"; const Tasks = ({ tasks }: { tasks: Task[] }) => { return ( <div className="flex flex-col gap-2"> {tasks.map((t) => ( <TaskItem key={t.id} name={t.name} /> ))} </div> ); }; export default Tasks; // src/components/Tasks/TaskItem.tsx import { useState } from "react"; const TaskItem = ({ name }: { name: string }) => { const [done, setDone] = useState(false); return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm gap-4"> <div className="flex gap-2 items-center"> <input type="checkbox" checked={done} onChange={() => setDone(!done)} /> {name} </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3"> Delete </button> </div> ); }; export default TaskItem;


Đối với thành phần Nhiệm vụ, chúng tôi cũng lấy task s làm chỗ dựa và ánh xạ thuộc tính name của nó tới các thành phần TaskItem. Kết quả là, chúng ta nhận được một thành phần TaskItem cho mỗi đối tượng bên trong mảng tasks . Chúng tôi cũng cập nhật thành phần TaskItem để chấp nhận name làm chỗ dựa.


Đây là lúc mà id trở nên hữu ích, vì chúng ta cần chuyển một khóa duy nhất mỗi khi chúng ta có một danh sách các thành phần con. Nếu chúng tôi không thêm khóa, điều này có thể dẫn đến lỗi khi kết xuất lại . (Trong một ứng dụng sản xuất, id rất có thể đến từ phần phụ trợ.)


Kết quả bây giờ là thế này:



Chúng tôi đã có thể thấy các số tóm tắt và tên nhiệm vụ phản ánh dữ liệu giả của chúng tôi. Nhưng chúng tôi vẫn thiếu một cách để thêm hoặc xóa các tác vụ.


Thêm luồng dữ liệu nghịch đảo


Để hoàn thành ứng dụng của mình, chúng ta cần một cách để thay đổi trạng thái thành phần Ứng dụng (nơi có dữ liệu tác vụ) từ các thành phần con Input và TaskItem.


Để làm điều đó, chúng ta có thể sử dụng các hàm được tạo bởi useState hook để xác định các trình xử lý sự kiện và chuyển chúng xuống dưới dạng đạo cụ. Khi chúng tôi làm điều đó, chúng tôi chỉ cần gọi chúng trong quá trình tương tác người dùng thích hợp từ các thành phần con.


Đảm bảo không bao giờ thay đổi trạng thái bất cứ khi nào bạn cập nhật nó , vì điều này sẽ dẫn đến lỗi. Luôn thay thế đối tượng trạng thái bằng một đối tượng mới khi cập nhật nó.


Dưới đây là thành phần Ứng dụng cuối cùng của chúng tôi với các trình xử lý được khai báo và truyền dưới dạng đạo cụ cho các thành phần Đầu vào và Tác vụ.


handleSubmit trả về một mảng mới với các tác vụ cũ cộng với tác vụ mới. toggleDoneTask trả về một mảng mới với thuộc tính done ngược lại, cho id đã chỉ định. handleDeleteTask trả về một mảng mới không có tác vụ với id đã chỉ định.


 // src/App.tsx import { FormEvent, useState } from "react"; import { v4 as uuidv4 } from "uuid"; import Container from "./components/Container"; import Input from "./components/Input"; import Summary from "./components/Summary/Summary"; import Tasks from "./components/Tasks/Tasks"; export interface Task { name: string; done: boolean; id: string; } function App() { const [tasks, setTasks] = useState<Task[]>([]); const handleSubmit = (e: FormEvent<HTMLFormElement>, value: string) => { e.preventDefault(); const newTask = { name: value, done: false, id: uuidv4(), }; setTasks((tasks) => [...tasks, newTask]); }; const toggleDoneTask = (id: string, done: boolean) => { setTasks((tasks) => tasks.map((t) => { if (t.id === id) { t.done = done; } return t; }) ); }; const handleDeleteTask = (id: string) => { setTasks((tasks) => tasks.filter((t) => t.id !== id)); }; return ( <div className="flex justify-center m-5"> <div className="flex flex-col items-center"> <div className="border shadow p-10 flex flex-col gap-10 sm:w-[640px]"> <Container title={"Summary"}> <Summary tasks={tasks} /> </Container> <Container> <Input handleSubmit={handleSubmit} /> </Container> <Container title={"Tasks"}> <Tasks tasks={tasks} toggleDone={toggleDoneTask} handleDelete={handleDeleteTask} /> </Container> </div> </div> </div> ); } export default App;


Đây là thành phần Đầu vào cuối cùng sử dụng handleSubmit để cập nhật trạng thái thành phần Ứng dụng.


 // src/components/Input.tsx import { FormEvent, useState } from "react"; const InputContainer = ({ handleSubmit, }: { handleSubmit: (e: FormEvent<HTMLFormElement>, value: string) => void; }) => { const [newTaskName, setNewTaskName] = useState(""); return ( <form action="" className="flex flex-col gap-4" onSubmit={(e) => { handleSubmit(e, newTaskName); setNewTaskName(""); }} > <div className="flex flex-col"> <label className="text-white">Enter your next task:</label> <input className="p-1 rounded-sm" type="text" value={newTaskName} onChange={(e) => setNewTaskName(e.target.value)} /> </div> <button type="submit" className="bg-green-100 rounded-lg hover:bg-green-200 p-1" > Add task </button> </form> ); }; export default InputContainer;


Đây là thành phần Nhiệm vụ cuối cùng mà chúng tôi đã cập nhật để chuyển các đạo cụ từ Ứng dụng xuống TaskItem. Chúng tôi cũng đã thêm một toán tử bậc ba để trả về "Chưa có tác vụ nào!" khi không có nhiệm vụ.


 // src/components/Tasks/Tasks.tsx import { Task } from "../../App"; import TaskItem from "./TaskItem"; const Tasks = ({ tasks, toggleDone, handleDelete, }: { tasks: Task[]; toggleDone: (id: string, done: boolean) => void; handleDelete: (id: string) => void; }) => { return ( <div className="flex flex-col gap-2"> {tasks.length ? ( tasks.map((t) => ( <TaskItem key={t.id} name={t.name} done={t.done} id={t.id} toggleDone={toggleDone} handleDelete={handleDelete} /> )) ) : ( <span className="text-green-100">No tasks yet!</span> )} </div> ); }; export default Tasks;


Và đây là thành phần TaskItem cuối cùng, sử dụng toggleDonehandleDelete để cập nhật trạng thái thành phần Ứng dụng.


 // src/components/Tasks/TaskItem.tsx const TaskItem = ({ name, done, id, toggleDone, handleDelete, }: { name: string; done: boolean; id: string; toggleDone: (id: string, done: boolean) => void; handleDelete: (id: string) => void; }) => { return ( <div className="flex justify-between bg-white p-1 px-3 rounded-sm gap-4"> <div className="flex gap-2 items-center"> <input type="checkbox" checked={done} onChange={() => toggleDone(id, !done)} /> {name} </div> <button className="bg-green-200 hover:bg-green-300 rounded-lg p-1 px-3" type="button" onClick={() => handleDelete(id)} > Delete </button> </div> ); }; export default TaskItem;


Và đây là ứng dụng cuối cùng của chúng tôi sau khi chúng tôi thêm một số tác vụ!



Nếu bạn đang viết mã, bạn có thể triển khai ứng dụng của riêng mình bằng cách làm theo các hướng dẫn sau.

Bạn có thể tìm thấy repo với tất cả mã mà chúng tôi đã trải qua tại đây và phiên bản trực tiếp của ứng dụng tại đây .

Từ cuối cùng

Tóm lại, xây dựng một ứng dụng danh sách việc cần làm có thể là một cách tuyệt vời để tìm hiểu và củng cố hiểu biết của chúng ta về React và các nguyên tắc của nó. Bằng cách chia nhỏ quy trình thành các bước nhỏ và làm theo các phương pháp hay nhất, chúng tôi có thể tạo một ứng dụng hoạt động hiệu quả trong một khoảng thời gian tương đối ngắn.


Chúng tôi đã bảo hiểm:


  • các khái niệm chính về thành phần, trạng thái và luồng dữ liệu nghịch đảo.

  • thiết kế và kiến trúc của ứng dụng.

  • thực tiễn tốt nhất như nguyên tắc chịu trách nhiệm duy nhất


Bằng cách làm theo các bước được nêu trong hướng dẫn này, giờ đây bạn sẽ có hiểu biết vững chắc về cách xây dựng một ứng dụng React đơn giản và có thể áp dụng nó cho các dự án của riêng bạn.


Chúc mừng mã hóa!