Giới thiệu:
React Hooks là một tính năng mới được thêm vào trong React 16.8 cho phép bạn sử dụng các tính năng của React như trạng thái mà không cần viết một lớp.
Trước React Hooks, có một vấn đề là việc sử dụng lại logic phụ thuộc vào thành phần và rất khó để mô đun hóa logic một mình.
Tuy nhiên, bằng cách sử dụng hook tùy chỉnh của React Hooks, đây là một hàm để tạo hook của riêng bạn, bạn chỉ có thể sử dụng lại logic mà không phụ thuộc vào View.
Trong bài viết này, tôi sẽ chỉ cho bạn cách sử dụng React Hooks bằng cách xem cách nó cải thiện từ v1 đến v6.
Trong ví dụ sau, số lượng mã trong thành phần được giảm đi như sau.
Giới thiệu Ví dụ:
Tạo một hook tùy chỉnh useLocalHistory thực hiện phân trang giữa các thành phần.
Nó giống như API lịch sử của trình duyệt của bạn.
v1 móc tùy chỉnh không được sử dụng:
Thành phần này có sự kết hợp giữa View và logic, điều này làm cho mã khó đọc và kiểm tra logic.
- Trang.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | import React, { useState } from "react"; export const Page = () => { const topPage = 1; const lastPage = 4; const initHistory: number[] = [topPage]; const [history, setHistory] = useState<number[]>(initHistory); const currentPage = history[history.length - 1]; return ( <div> <div>currentPage: {currentPage}</div> <button onClick={() => { // not change if it is currently the top page if (currentPage === topPage) { return; } const nextHistory = [...history, topPage]; setHistory(nextHistory); }} > Top </button> <button onClick={() => { const nextPage = currentPage + 1; // I can't go beyond the last page if (lastPage < nextPage) { return; } const nextHistory = [...history, nextPage]; setHistory(nextHistory); }} > Next </button> <button onClick={() => { // I can't go back before the top page if (history.length <= 1) { return; } const nextHistory = [...history.slice(0, history.length - 1)]; setHistory(nextHistory); }} > Back </button> <button onClick={() => { // Does not move if it is currently the last page if (currentPage === lastPage) { return; } const nextHistory = [...history, lastPage]; setHistory(nextHistory); }} > Last </button> <button onClick={() => { setHistory(initHistory); }} > Delete History </button> </div> ); }; |
https://codesandbox.io/s/custom-hook-v1-tv2un
v2 Móc tùy chỉnh:
Tách logic khỏi thành phần thành các móc tùy chỉnh.
Lịch sử bị ẩn trong móc tùy chỉnh vì thông tin duy nhất mà thành phần cần là:
Giá trị là hiện tại
Các thao tác là Lên trên, Tiếp theo, Quay lại, Cuối cùng, Đặt lại
- Trang.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import React from "react"; import { useLocalHistory } from "./useLocalHistory"; export const Page: React.FC = () => { const topPage = 1; const lastPage = 4; const [currentPage, Top, Next, Back, Last, Reset] = useLocalHistory( topPage, lastPage ); return ( <div> <div>Current page: {currentPage}</div> <button onClick={Top}>Top</button> <button onClick={Next}>Next</button> <button onClick={Back}>Back</button> <button onClick={Last}>Last</button> <button onClick={Reset}>Reset</button> </div> ); }; |
- useLocalHistory.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | import { useState } from "react"; export const useLocalHistory = ( topPage: number, lastPage: number ): [number, () => void, () => void, () => void, () => void, () => void] => { const initHistory: number[] = [topPage]; const [history, setHistory] = useState<number[]>(initHistory); const currentPage = history[history.length - 1]; const Top = (): void => { // Not change if current page is top if (currentPage === topPage) { return; } const nextHistory = [...history, topPage]; setHistory(nextHistory); }; const Next = (): void => { const nextPage = currentPage + 1; // cannot next beyond last page if (lastPage < nextPage) { return; } const nextHistory = [...history, nextPage]; setHistory(nextHistory); }; const Back = (): void => { // can not forward beyond top page if (history.length <= 1) { return; } const nextHistory = [...history.slice(0, history.length - 1)]; setHistory(nextHistory); }; const Last = (): void => { // can not move if current page is last if (currentPage === lastPage) { return; } const nextHistory = [...history, lastPage]; setHistory(nextHistory); }; const Reset = (): void => { setHistory(initHistory); }; return [currentPage, Top, Next, Back, Last, Reset]; }; |
https://codesandbox.io/s/custom-hook-v2-phjz4
Thành phần Trang rất dễ đọc, tập trung vào Xem các triển khai liên quan.
Định nghĩa giao diện v3
Xác định giao diện LocalHistory trong useLocalHistory.ts
cung cấp chức năng lịch sử.
Thành phần Trang hoạt động thông qua giao diện LocalHistory.
- Trang.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | import React from "react"; import { useLocalHistory } from "../../utils/useLocalHistory"; export const Page: React.FC = () => { const topPage = 1; const lastPage = 4; const [currentPage, history] = useLocalHistory(topPage, lastPage); return ( <div> <div>currentPage: {currentPage}</div> <button onClick={history.Top}>Top</button> <button onClick={history.Next}>Next</button> <button onClick={history.Back}>Back</button> <button onClick={history.Last}>Last</button> <button onClick={history.Reset}>Reset</button> </div> ); }; |
- useLocalHistory.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | import { useState } from "react"; interface LocalHistory { Top: () => void; Next: () => void; Back: () => void; Last: () => void; Reset: () => void; } export const useLocalHistory = ( topPage: number, lastPage: number ): [number, LocalHistory] => { const initHistory: number[] = [topPage]; const [history, setHistory] = useState<number[]>(initHistory); const currentPage = history[history.length - 1]; const Top = (): void => { // Not change if current page is top if (currentPage === topPage) { return; } const nextHistory = [...history, topPage]; setHistory(nextHistory); }; const Next = (): void => { const nextPage = currentPage + 1; // cannot next beyond last page if (lastPage < nextPage) { return; } const nextHistory = [...history, nextPage]; setHistory(nextHistory); }; const Back = (): void => { // cannot go forward beyond top page if (history.length <= 1) { return; } const nextHistory = [...history.slice(0, history.length - 1)]; setHistory(nextHistory); }; const Last = (): void => { // can not move if current page is last if (currentPage === lastPage) { return; } const nextHistory = [...history, lastPage]; setHistory(nextHistory); }; const Reset = (): void => { setHistory(initHistory); }; return [currentPage, { Top, Next, Back, Last, Reset }]; }; |
https://codesandbox.io/s/custom-hook-v3-cb016
Bằng cách xác định giao diện LocalHistory, sự liên quan của một loạt các hoạt động trở nên rõ ràng.
Nó cũng giúp bạn dễ dàng chuyển một loạt các thao tác cho các thành phần khác.
Tách cấu trúc dữ liệu v4 thành các móc tùy chỉnh riêng biệt:
Lịch sử địa phương được hiện thực hóa bằng cấu trúc dữ liệu của Ngăn xếp (LIFO).
Cắt nó ra như một móc tùy chỉnh useStack.
Vì hook tùy chỉnh có thể được định cấu hình trong nhiều giai đoạn, useStack cắt ra khỏi useLocalHistory sẽ được thực thi.
Thành phần Trang vẫn giữ nguyên và sẽ bị bỏ qua.
- useLocalHistory.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | import { useStack } from "./useStack"; export interface LocalHistory { Top: () => void; Next: () => void; Back: () => void; Last: () => void; Reset: () => void; } export const useLocalHistory = ( topPage: number, lastPage: number ): [number, LocalHistory] => { const initHistory: number[] = [topPage]; const [currentPage, stack] = useStack<number>(initHistory); const Top = (): void => { // Not change if current page is top if (currentPage === topPage) { return; } stack.Push(topPage); }; const Next = (): void => { const nextPage = currentPage + 1; // cannot next beyond last page if (lastPage < nextPage) { return; } stack.Push(nextPage); }; const Back = (): void => { // cannot go forward beyond top page if (stack.Length() <= 1) { return; } stack.Pop(); }; const Last = (): void => { // cannot next beyond last page if (currentPage === lastPage) { return; } stack.Push(lastPage); }; const Reset = (): void => { stack.Reset(); }; return [currentPage, { Top, Next, Back, Last, Reset }]; }; |
- useStack.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | import { useState } from "react"; export interface Stack<T> { Pop: () => void; Push: (v: T) => void; Reset: () => void; Length: () => number; } // Define the Stack data structure as a custom hook export const useStack = <T>(init?: T[]): [T, Stack<T>] => { const initStack: T[] = init ?? []; const [stack, setStack] = useState<T[]>(initStack); const Pop = (): void => { if (stack.length === 0) { return; } const newStack = [...stack.slice(0, stack.length - 1)]; setStack(newStack); }; const Push = (v: T): void => { const newStack = [...stack, v]; setStack(newStack); }; const Reset = (): void => { setStack(initStack); }; const Length = (): number => stack.length; return [stack[stack.length - 1], { Pop, Push, Reset, Length }]; }; |
https://codesandbox.io/s/custom-hook-v4-mn2ub
Do đó, useLocalHistory chỉ có quyền điều khiển chuyển đổi màn hình dưới dạng logic mà không nhận thức được chi tiết triển khai của Stack.
Ngoài ra, mặc dù không được giải thích chi tiết nhưng setState cũng có cách nhận và cập nhật trạng thái trước đó.
https://codesandbox.io/s/custom-hook-v41-hokuq
v5 Sử dụng useReducer thay vì useState
v4 useStack yêu cầu bạn biết trạng thái hiện tại để thêm hoặc xóa khỏi mảng.
Đây không phải là vấn đề, nhưng nếu bạn muốn thực hiện xử lý cập nhật phụ thuộc vào trạng thái trước đó, chẳng hạn như khi vận hành một phần của mảng hoặc đối tượng, hãy sử dụng useReducer thay vì useState
để mô tả nó ngắn gọn hơn. Bạn sẽ có thể.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | useStack.ts import { useReducer } from "react"; type StackState<T> = T[]; type StackAction<T> = | { type: "ACTION_POP" } | { type: "ACTION_PUSH"; value: T } | { type: "ACTION_RESET"; initStack: T[] }; const stackReducer = <T>() => ( stack: StackState<T>, action: StackAction<T> ): StackState<T> => { switch (action.type) { case "ACTION_POP": if (stack.length === 0) { return stack; } return [...stack.slice(0, stack.length - 1)]; case "ACTION_PUSH": return [...stack, action.value]; case "ACTION_RESET": return action.initStack; } }; export interface Stack<T> { Pop: () => void; Push: (v: T) => void; Reset: () => void; Length: () => number; } export const useStack = <T>(init?: T[]): [T, Stack<T>] => { const initStack: T[] = init ?? []; const [stack, dispatch] = useReducer(stackReducer<T>(), initStack); // The previous state is not required, only the Action to be executed and the values required for the Action are required. const Pop = (): void => dispatch({ type: "ACTION_POP" }); const Push = (value: T): void => dispatch({ type: "ACTION_PUSH", value }); const Reset = (): void => dispatch({ type: "ACTION_RESET", initStack }); const Length = (): number => stack.length; return [stack[stack.length - 1], { Pop, Push, Reset, Length }]; }; |
Trong v4, hàm useStack là mã thủ tục sử dụng trạng thái trước đó của ngăn xếp, nhưng trong v5, như ACTIONS_POP, bây giờ nó đủ để kích hoạt một sự kiện mà không cần biết trạng thái trước đó. Ngoài ra, bộ giảm thiểu không cần phải viết mã thủ tục và có thể đạt được mục đích đơn giản bằng cách trả về một trạng thái mới.
Phương pháp viết này có thể quen thuộc với những người đã viết Redux, nhưng về cơ bản sử dụng useState
như được mô tả trong tài liệu chính thức. Chúng tôi khuyên bạn chỉ nên sử dụng useReducer nếu bạn có logic trạng thái phức tạp liên quan đến nhiều giá trị hoặc nếu trạng thái tiếp theo phụ thuộc vào trạng thái trước đó.
https://codesandbox.io/s/custom-hook-v5-38z1m
Tách thành phần Container v6 và thành phần Thuyết trình
Tách các lớp gây ra tác dụng phụ.
Trong Redux, khuôn khổ buộc các lớp thực hiện kết nối phải được tách biệt, đó là cùng một chính sách thiết kế.
Vì useLocalHistory
và useStack
tương tự nhau nên chúng bị bỏ qua.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | Page.tsx import React from "react"; import { LocalHistory, useLocalHistory } from "./useLocalHistory"; // Container component export const PageContainer: React.FC = () => { const topPage = 1; const lastPage = 4; const [currentPage, history] = useLocalHistory(topPage, lastPage); return <Page currentPage={currentPage} history={history} />; }; interface PageProps { currentPage: number; history: LocalHistory; } // Presentational component const Page: React.FC<PageProps> = ({ currentPage, history }: PageProps) => { return ( <div> <div>currentPage: {currentPage}</div> <button onClick={history.Top}>Top</button> <button onClick={history.Next}>Next</button> <button onClick={history.Back}>Back</button> <button onClick={history.Last}>Last</button> <button onClick={history.Reset}>Reset</button> </div> ); }; |
https://codesandbox.io/s/custom-hook-v6-yqrx4
Điều này làm cho thành phần Trang trở thành một hàm thuần túy nhận đối số và trả về giá trị trả về.
Thật tuyệt khi View trở thành một chức năng thuần túy, điều không thể tưởng tượng được trong quá trình phát triển GUI cũ.
Tuy nhiên, không phải lúc nào cũng cần áp dụng điều này. Hãy đưa ra quyết định phù hợp khi quyết định thuê.
Tóm lược
Các cải tiến sau đã được thực hiện từ v1 đến v6.
Tách miền trình bày
Ủy quyền các cấu trúc dữ liệu chung từ logic
lịch sử Lịch sử không chuyển đến thành phần Trang Ẩn thông tin (đóng gói)
Định nghĩa giao diện Nguyên tắc đóng mở (OCP)
Chức năng hóa của thành phần Trang Nhận các tác dụng phụ dưới dạng đối số mà không nhận chúng trong nội bộ
Làm rõ ý định thiết kế cho từng mô-đun Nguyên tắc trách nhiệm đơn lẻ (SRP)
Chúng tách biệt chế độ xem khỏi logic, cải thiện khả năng tái sử dụng, khả năng đọc và khả năng kiểm tra.
Với sự ra đời của các hook tùy chỉnh trong React Hooks, bạn có thể đóng gói các chi tiết về trạng thái và triển khai, chỉ hiển thị các giá trị và giao diện bạn cần cho các thành phần của mình.
Cũng như với PDS, các nguyên tắc ủy quyền, đóng gói và SOLID (OCP, SRP), các khả năng thiết kế phổ quát không khác gì thiết kế hướng đối tượng đã được phát triển cho đến nay là bắt buộc. Nó không yêu cầu các khả năng thiết kế dành riêng cho React Hooks.
Nếu bạn thực sự so sánh hook tùy chỉnh và class như sau, bạn có thể thấy rằng chúng tương tự nhau.
- lớp học
- Trạng thái: Trường thành viên
- Hoạt động: Phương pháp
- Khởi tạo: Constructor
- Móc tùy chỉnh
- Trạng thái: useState
- Hoạt động: Chức năng
- Khởi tạo: Đối số móc
Ngay cả khi bạn so sánh mã được triển khai, không có sự khác biệt lớn trong biểu thức cơ bản.
Thế giới được hiện thực hóa bởi React Hooks
Cho đến nay, logic của React phụ thuộc vào vòng đời của các thành phần React, và có một vấn đề là rất khó để mô đun hóa logic một mình và nó không thể sử dụng lại được. Nhưng với sự ra đời của React Hooks, nó đã được cải thiện.
Do đó, useStack và useLocalHistory được phát triển lần này không phụ thuộc vào View nên có thể tùy theo trường hợp sử dụng của bạn.
Bằng cách này, nó đã trở thành một thế giới mà logic React có thể dễ dàng được chia sẻ dưới dạng OSS.
Đây là ưu điểm lớn nhất của việc có thể thiết kế code gọn gàng.
OSS
Từ bây giờ, bạn nên xem qua React Hooks của OSS trước khi viết xử lý dựa trên trạng thái của riêng bạn.
Có thể bạn sẽ tìm thấy một cách triển khai tốt hơn dưới dạng PMNM phức tạp hơn so với cách bạn đã nghĩ ra.
Nó cũng sẽ rất hữu ích nếu bạn tự mình thực hiện.
https://github.com/streamich/react-use/