Reack Hooks自從16.8發佈以來,社區已經有至關多的討論和應用了,不知道各位在公司裏有沒有用上這個酷炫的特性~css
今天分享一下利用React Hooks實現一個功能相對完善的todolist。vue
特色:react
codesandbox.io/s/react-hoo…ios
首先咱們引入antd做爲ui庫,節省掉無關的一些邏輯,快速的構建出咱們的頁面骨架axios
const TAB_ALL = "all"; const TAB_FINISHED = "finished"; const TAB_UNFINISHED = "unfinished"; const tabMap = { [TAB_ALL]: "所有", [TAB_FINISHED]: "已完成", [TAB_UNFINISHED]: "待完成" }; function App() { const [activeTab, setActiveTab] = useState(TAB_ALL); return ( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <Input /> <TodoList /> </div> </> ); } 複製代碼
有了界面之後,接下來就要獲取數據。api
這裏我新建了一個api.js專門用來模擬接口獲取數據,這裏面的邏輯大概看一下就好,不須要特別在乎。bash
const todos = [ { id: 1, text: "todo1", finished: true }, { id: 2, text: "todo2", finished: false }, { id: 3, text: "todo3", finished: true }, { id: 4, text: "todo4", finished: false }, { id: 5, text: "todo5", finished: false } ]; const delay = time => new Promise(resolve => setTimeout(resolve, time)); // 將方法延遲1秒 const withDelay = fn => async (...args) => { await delay(1000); return fn(...args); }; // 獲取todos export const fetchTodos = withDelay(params => { const { query, tab } = params; let result = todos; // tab頁分類 if (tab) { switch (tab) { case "finished": result = result.filter(todo => todo.finished === true); break; case "unfinished": result = result.filter(todo => todo.finished === false); break; default: break; } } // 帶參數查詢 if (query) { result = result.filter(todo => todo.text.includes(query)); } return Promise.resolve({ tab, result }); }); 複製代碼
這裏咱們封裝了個withDelay方法用來包裹函數,模擬異步請求接口的延遲,這樣方便咱們後面演示loading功能。markdown
獲取數據,最傳統的方式就是在組件中利用useEffect來完成請求,而且聲明依賴值來在某些條件改變後從新獲取數據,簡單寫一個:antd
import { fetchTodos } from './api' const TAB_ALL = "all"; const TAB_FINISHED = "finished"; const TAB_UNFINISHED = "unfinished"; const tabMap = { [TAB_ALL]: "所有", [TAB_FINISHED]: "已完成", [TAB_UNFINISHED]: "待完成" }; function App() { const [activeTab, setActiveTab] = useState(TAB_ALL); // 獲取數據 const [loading, setLoading] = useState(false) const [todos, setTodos] = useState([]) useEffect(() => { setLoading(true) fetchTodos({tab: activeTab}) .then(result => { setTodos(todos) }) .finally(() => { setLoading(false) }) }, [activeTab]) return ( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <Input /> <Spin spinning={loading} tip="稍等片刻~"> <!--把todos傳遞給組件--> <TodoList todos={todos}/> </Spin> </div> </> ); } 複製代碼
這樣很好,在公司內部新啓動的項目裏個人同事們也都是這麼寫的,可是這樣的獲取數據有幾個小問題。app
因此這裏要封裝一個專門用於請求的自定義hook。
忘了在哪看到的說法,自定hook其實就是把useXXX方法執行之後,把方法體裏的內容平鋪到組件內部,我以爲這種說法對於理解自定義hook很友好。
useTest() { const [test, setTest] = useState('') setInterval(() => { setTest(Math.random()) }, 1000) return {test, setTest} } function App() { const {test, setTest} = useTest() return <span>{test}</span> } 複製代碼
這段代碼等價於:
function App() { const [test, setTest] = useState('') setInterval(() => { setTest(Math.random()) }, 1000) return <span>{test}</span> } 複製代碼
是否是瞬間感受自定hook很簡單了~ 基於這個思路,咱們來封裝一下咱們須要的useRequest方法。
export const useRequest = (fn, dependencies) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); // 請求的方法 這個方法會自動管理loading const request = () => { setLoading(true); fn() .then(setData) .finally(() => { setLoading(false); }); }; // 根據傳入的依賴項來執行請求 useEffect(() => { request() }, dependencies); return { // 請求獲取的數據 data, // loading狀態 loading, // 請求的方法封裝 request }; }; 複製代碼
有了這個自定義hook,咱們組件內部的代碼又能夠精簡不少。
import { fetchTodos } from './api' import { useRequest } from './hooks' const TAB_ALL = "all"; const TAB_FINISHED = "finished"; const TAB_UNFINISHED = "unfinished"; const tabMap = { [TAB_ALL]: "所有", [TAB_FINISHED]: "已完成", [TAB_UNFINISHED]: "待完成" }; function App() { const [activeTab, setActiveTab] = useState(TAB_ALL); // 獲取數據 const {loading, data: todos} = useRequest(() => { return fetchTodos({ tab: activeTab }); }, [activeTab]) return ( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <Input /> <Spin spinning={loading} tip="稍等片刻~"> <!--把todos傳遞給組件--> <TodoList todos={todos}/> </Spin> </div> </> ); } 複製代碼
果真,樣板代碼少了不少,腰不酸了腿也不痛了,一口氣能發5個請求了!
在真實開發中咱們特別容易遇到的一個場景就是,tab切換並不改變視圖,而是去從新請求新的列表數據,在這種狀況下咱們可能就會遇到一個問題,以這個todolist舉例,咱們從所有
tab切換到已完成
tab,會去請求數據,可是若是咱們在已完成
tab的數據還沒請求完成時,就去點擊待完成
的tab頁,這時候就要考慮一個問題,異步請求的響應時間是不肯定的,極可能咱們發起的第一個請求已完成
最終耗時5s,第二個請求待完成
最終耗時1s,這樣第二個請求的數據返回,渲染完頁面之後,過了幾秒第一個請求的數據返回了,可是這個時候咱們的tab是停留在對應第二個請求待完成
上,這就形成了髒數據的bug。
這個問題其實咱們能夠利用useEffect的特性在useRequest封裝解決。
export const useRequest = (fn, dependencies, defaultValue = []) => { const [data, setData] = useState(defaultValue); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); const request = () => { // 定義cancel標誌位 let cancel = false; setLoading(true); fn() .then(res => { if (!cancel) { setData(res); } else { // 在請求成功取消掉後,打印測試文本。 const { tab } = res; console.log(`request with ${tab} canceled`); } }) .catch(() => { if (!cancel) { setError(error); } }) .finally(() => { if (!cancel) { setLoading(false); } }); // 請求的方法返回一個 取消掉此次請求的方法 return () => { cancel = true; }; }; // 重點看這段,在useEffect傳入的函數,返回一個取消請求的函數 // 這樣在下一次調用這個useEffect時,會先取消掉上一次的請求。 useEffect(() => { const cancelRequest = request(); return () => { cancelRequest(); }; // eslint-disable-next-line }, dependencies); return { data, setData, loading, error, request }; }; 複製代碼
其實這裏request裏實現的取消請求只是咱們模擬出來的取消,真實狀況下能夠利用axios等請求庫提供的方法作不同的封裝,這裏主要是講思路。 useEffect裏返回的函數其實叫作清理函數,在每次新一次執行useEffect時,都會先執行清理函數,咱們利用這個特性,就能成功的讓useEffect永遠只會用最新的請求結果去渲染頁面。
能夠去預覽地址快速點擊tab頁切換,看一下控制檯打印的結果。
如今須要加入一個功能,點擊列表中的項目,切換完成狀態,這時候useRequest
好像就不太合適了,由於useRequest
其實本質上是針對useEffect的封裝,而useEffect的使用場景是初始化和依賴變動的時候發起請求,可是這個新需求實際上是響應用戶的點擊而去主動發起請求,難道咱們又要手動寫setLoading之類的冗餘代碼了嗎?答案固然是不。
咱們利用高階函數的思想封裝一個自定義hook:useWithLoading
export function useWithLoading(fn) { const [loading, setLoading] = useState(false); const func = (...args) => { setLoading(true); return fn(...args).finally(() => { setLoading(false); }); }; return { func, loading }; } 複製代碼
它本質上就是對傳入的方法進行了一層包裹,在執行先後去更改loading狀態。
使用:
// 完成todo邏輯 const { func: onToggleFinished, loading: toggleLoading } = useWithLoading( async id => { await toggleTodo(id); } ); <TodoList todos={todos} onToggleFinished={onToggleFinished} /> 複製代碼
加入一個新功能,input的placeholder根據tab頁的切換去切換文案,注意,這裏咱們先提供一個錯誤的示例,這是剛從Vue2.x和React Class Component轉過來的人很容易犯的一個錯誤。
❌錯誤示例
import { fetchTodos } from './api' import { useRequest } from './hooks' const TAB_ALL = "all"; const TAB_FINISHED = "finished"; const TAB_UNFINISHED = "unfinished"; const tabMap = { [TAB_ALL]: "所有", [TAB_FINISHED]: "已完成", [TAB_UNFINISHED]: "待完成" }; function App() { // state放在一塊兒 const [activeTab, setActiveTab] = useState(TAB_ALL); const [placeholder, setPlaceholder] = useState(""); const [query, setQuery] = useState(""); // 反作用放在一塊兒 const {loading, data: todos} = useRequest(() => { return fetchTodos({ tab: activeTab }); }, [activeTab]) useEffect(() => { setPlaceholder(`在${tabMap[activeTab]}內搜索`); }, [activeTab]); const { func: onToggleFinished, loading: toggleLoading } = useWithLoading( async id => { await toggleTodo(id); } ); return ( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <Input /> <Spin spinning={loading} tip="稍等片刻~"> <!--把todos傳遞給組件--> <TodoList todos={todos}/> </Spin> </div> </> ); } 複製代碼
注意,在以前的vue和react開發中,由於vue代碼組織的方式都是 based on options
(基於選項如data, methods, computed組織),
React 也是state在一個地方統一初始化,而後class裏定義一堆一堆的xxx方法,這會致使新接手代碼的人閱讀邏輯十分困難。
因此hooks也解決了一個問題,就是咱們的代碼組織方式能夠 based on logical concerns
(基於邏輯關注點組織)了 不要再按照往常的思惟把useState useEffect分門別類的組織起來,看起來整齊可是毫無用處 !!
這裏上一張vue composition api介紹裏對於@vue/ui庫中一個組件的對比圖
顏色是用來區分功能點的,哪一種代碼組織方式更利於維護,一目瞭然了吧。Vue composition api 推崇的代碼組織方式是把邏輯拆分紅一個一個的自定hook function,這點和react hook的思路是一致的。
export default { setup() { // ... } } function useCurrentFolderData(nextworkState) { // ... } function useFolderNavigation({ nextworkState, currentFolderData }) { // ... } function useFavoriteFolder(currentFolderData) { // ... } function useHiddenFolders() { // ... } function useCreateFolder(openFolder) { // ... } 複製代碼
✔️正確示例
import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom"; import TodoInput from "./todo-input"; import TodoList from "./todo-list"; import { Spin, Tabs } from "antd"; import { fetchTodos, toggleTodo } from "./api"; import { useRequest, useWithLoading } from "./hook"; import "antd/dist/antd.css"; import "./styles/styles.css"; import "./styles/reset.css"; const { TabPane } = Tabs; const TAB_ALL = "all"; const TAB_FINISHED = "finished"; const TAB_UNFINISHED = "unfinished"; const tabMap = { [TAB_ALL]: "所有", [TAB_FINISHED]: "已完成", [TAB_UNFINISHED]: "待完成" }; function App() { const [activeTab, setActiveTab] = useState(TAB_ALL); // 數據獲取邏輯 const [query, setQuery] = useState(""); const { data: { result: todos = [] }, loading: listLoading } = useRequest(() => { return fetchTodos({ query, tab: activeTab }); }, [query, activeTab]); // placeHolder const [placeholder, setPlaceholder] = useState(""); useEffect(() => { setPlaceholder(`在${tabMap[activeTab]}內搜索`); }, [activeTab]); // 完成todo邏輯 const { func: onToggleFinished, loading: toggleLoading } = useWithLoading( async id => { await toggleTodo(id); } ); const loading = !!listLoading || !!toggleLoading; return ( <> <Tabs activeKey={activeTab} onChange={setActiveTab}> <TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} /> <TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} /> <TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} /> </Tabs> <div className="app-wrap"> <h1 className="app-title">Todo List</h1> <TodoInput placeholder={placeholder} onSetQuery={setQuery} /> <Spin spinning={loading} tip="稍等片刻~"> <TodoList todos={todos} onToggleFinished={onToggleFinished} /> </Spin> </div> </> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 複製代碼
React Hook提供了一種新思路讓咱們去更好的組織組件內部的邏輯代碼,使得功能複雜的大型組件更加易於維護。而且自定義Hook功能十分強大,在公司的項目中我也已經封裝了不少好用的自定義Hook好比UseTable, useTreeSearch, useTabs等,能夠結合各自公司使用的組件庫和ui交互需求把一些邏輯更細粒度的封裝起來,發揮你的想象力!useYourImagination!