復雜狀態(tài)處理: 如何保持狀態(tài)一致性
1: 保證狀態(tài)最小化
在保證 State 完整性的同時倒信,也要保證它的最小化
: 某些數(shù)據(jù)如果能從已有的 State 中計算得到, 那么我們應該始終在用的時候去計算, 而不是把計算的結(jié)果存到某個 State 中, 這樣, 才能簡化我們的狀態(tài)處理邏輯
function FilterList({ data }) {
// 設置關鍵字的 State
const [searchKey, setSearchKey] = useState('');
// 設置最終要展示的數(shù)據(jù)狀態(tài),并用原始數(shù)據(jù)作為初始值
const [filtered, setFiltered] = useState(data);
// 處理用戶的搜索關鍵字
const handleSearch = useCallback(evt => {
setSearchKey(evt.target.value);
setFiltered(data.filter(item => {
return item.title.includes(evt.target.value)));
}));
}, [filtered])
return (
<div>
<input value={searchKey} onChange={handleSearch} />
{/* 根據(jù) filtered 數(shù)據(jù)渲染 UI */}
</div>
);
}
// 一致性, 根據(jù) data 關鍵字, 來緩存 filter 的值
function FilterList({ data }) {
const [searchKey, setSearchKey] = useState("");
// 每當 searchKey 或者 data 變化的時候拂到,重新計算最終結(jié)果
const filtered = useMemo(() => {
return data.filter((item) =>
item.title.toLowerCase().includes(searchKey.toLowerCase())
);
}, [searchKey, data]);
return (
<div className="08-filter-list">
<h2>Movies</h2>
<input
value={searchKey}
placeholder="Search..."
onChange={(evt) => setSearchKey(evt.target.value)}
/>
<ul style={{ marginTop: 20 }}>
{filtered.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</div>
);
}
2. 避免中間狀態(tài), 確保唯一數(shù)據(jù)源
在有的場景下墅垮,特別是原始狀態(tài)數(shù)據(jù)來自某個外部數(shù)據(jù)源壶辜,而非 state 或者 props
的時候催植,冗余狀態(tài)就沒那么明顯窖贤。這時候你就需要準確定位狀態(tài)的數(shù)據(jù)源究竟是什么,并且在開發(fā)中確保它始終是唯一的數(shù)據(jù)源草雕,以此避免定義中間狀態(tài)
異步處理: 如何向服務器發(fā)送請求
- 實現(xiàn)自己的 API Client
無論大小項目巷屿,在開始實現(xiàn)第一個請求的時候,通常我們要做的第一件事應該都是創(chuàng)建一個自己的 API Client
墩虹,之后所有的請求都會通過這個 Client 發(fā)出去嘱巾。而不是上來就用 fetch 或者是 axios 去直接發(fā)起請求
憨琳,因為那會造成大量的重復代碼
可以對你需要連接的服務端做一些通用的配置和處理,比如 Token旬昭、URL篙螟、錯誤處理等等
- 通用的 Header, 比如:
Authorization Token
- 服務器地址的配置
- 請求未認證, 錯誤處理等
import axios from "axios"
// 定義相關的 endpoint
const endPoints = {
test: "https://api.io/",
prod: "https://prod.myapi.io/",
staging: "https://staging.myapi.io/",
}
// 創(chuàng)建 axios 的實例
const instance = axios.create({
// 實際項目中根據(jù)當前環(huán)境設置 baseURL
baseURL: endPoints.test,
timeout: 30000,
// 為所有請求設置通用的 header
headers: { Authorization: "Bear mytoken" },
})
// 聽過 axios 定義攔截器預處理所有請求
instance.interceptors.response.use(
(res) => {
// 可以假如請求成功的邏輯,比如 log
return res
},
(err) => {
if (err.response.status === 403) {
// 統(tǒng)一處理未授權(quán)請求问拘,跳轉(zhuǎn)到登錄界面
document.location = "/login"
}
return Promise.reject(err)
}
)
export default instance
- 使用 Hooks 思考異步請求, 封裝遠程資源
- Data: 請求成功后的數(shù)據(jù)
- Error: 請求失敗, 錯誤信息
- Pending: loading
上面三個狀態(tài), 我們可以在 UI 上做一些處理, 寫一個 Hook
import { useState, useEffect } from "react"
import apiClient from "./apiClient"
// 將獲取文章的 API 封裝成一個遠程資源 Hook
const useArticle = (id) => {
// 設置三個狀態(tài)分別存儲 data, error, loading
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
// 重新獲取數(shù)據(jù)時重置三個狀態(tài)
setLoading(true)
setData(null)
setError(null)
apiClient
.get(`/posts/${id}`)
.then((res) => {
// 請求成功時設置返回數(shù)據(jù)到狀態(tài)
setLoading(false)
setData(res.data)
})
.catch((err) => {
// 請求失敗時設置錯誤狀態(tài)
setLoading(false)
setError(err)
})
}, [id]) // 當 id 變化時重新獲取數(shù)據(jù)
// 將三個狀態(tài)作為 Hook 的返回值
return {
loading,
error,
data,
}
}
多個 API 調(diào)用, 如何處理并發(fā)或串行請求?
例如: 需要顯示作者遍略、作者頭像,以及文章的評論列表, 需要發(fā)送三個請求 GetAvatar GetAuthor GetComments
Promise.all([fetch1, fetch2])
傳統(tǒng)思路, React 函數(shù)組件是一個同步的函數(shù), 沒法直接使用 await
, 而是要把請求通過副作用去觸發(fā)
從狀態(tài)變化的角度去組織異步調(diào)用, 通過不同的狀態(tài)組合
骤坐,來實現(xiàn)異步請求的邏輯
import { useState, useEffect } from "react"
import apiClient from "./apiClient"
export default (id) => {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
// 當 id 不存在绪杏,直接返回,不發(fā)送請求
if (!id) return
setLoading(true)
setData(null)
setError(null)
apiClient
.get(`/users/${id}`)
.then((res) => {
setLoading(false)
setData(res.data)
})
.catch((err) => {
setLoading(false)
setError(err)
})
}, [id])
return {
loading,
error,
data,
}
}
import useArticle from "./useArticle"
import useUser from "./useUser"
import useComments from "./useComments"
const ArticleView = ({ id }) => {
// article comments 并行
const { data: article, loading, error } = useArticle(id)
const { data: comments } = useComments(id)
// 串行的請求
const { data: user } = useUser(article?.userId)
if (error) return "Failed."
if (!article || loading) return "Loading..."
return (
<div className='exp-09-article-view'>
<h1>
{id}. {article.title}
</h1>
{user && (
<div className='user-info'>
<img src={user.avatar} height='40px' alt='user' />
<div>{user.name}</div>
<div>{article.createdAt}</div>
</div>
)}
<p>{article.content}</p>
<CommentList data={comments || []} />
</div>
)
}
函數(shù)組件設計模式:如何應對復雜條件渲染場景纽绍?
- 容器模式: 實現(xiàn)按條件執(zhí)行 Hooks
Hooks 必須在頂層作用域調(diào)用寞忿,而不能放在條件判斷、循環(huán)等語句中顶岸,同時也不能在可能的 return 語句之后執(zhí)行腔彰。換句話說,Hooks 必須按順序被執(zhí)行到辖佣。
但假如我們希望實現(xiàn)一下 Modal, 像下面代碼會報錯
import { Modal } from "antd"
import useUser from "useUser"
function UserInfoModal({ visible, userId, ...rest }) {
// 當 visible 為 false 時霹抛,不渲染任何內(nèi)容
if (!visible) return null
// 這一行 Hook 在可能的 return 之后,會報錯卷谈!
const { data, loading, error } = useUser(userId)
return (
<Modal visible={visible} {...rest}>
{/* 對話框的內(nèi)容 */}
</Modal>
)
}
我們可以使用容器模式: 把條件判斷的結(jié)果放到兩個組件之中杯拐,確保真正 render UI 的組件收到的所有屬性都是有值的
// 定義一個容器組件用于封裝真正的 UserInfoModal
export default function UserInfoModalWrapper({
visible,
...rest // 使用 rest 獲取除了 visible 之外的屬性
}) {
// 如果對話框不顯示,則不 render 任何內(nèi)容
if (!visible) return null
// 否則真正執(zhí)行對話框的組件邏輯
return <UserInfoModal visible {...rest} />
}
把判斷條件放到 Hooks 中去
const ArticleView = ({ id }) => {
const { data: article, loading } = useArticle(id)
let user = null
// Hook 不能放到條件語句中世蔗,那我們應該如何做呢
if (article?.userId) user = useUser(article?.userId).data
// 組件其它邏輯
}
function useUser(id) {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
useEffect(() => {
// 當 id 不存在端逼,直接返回,不發(fā)送請求
if (!id) return
// 獲取用戶信息的邏輯
})
}
- render props 模式重用 UI 邏輯
render props
就是把一個 render 函數(shù)作為屬性傳遞給某個組件污淋,由這個組件去執(zhí)行這個函數(shù)從而 render 實際的內(nèi)容顶滩。
在 Class 組件時期,render props 和 HOC(高階組件)兩種模式可以說是進行邏輯重用的兩把利器寸爆,但是實際上礁鲁,HOC 的所有場景幾乎都可以用 render props 來實現(xiàn)×薅梗可以說仅醇,Hooks 是邏輯重用的第一選擇。
舉例演示: 計數(shù)器, 演示純數(shù)據(jù)邏輯的重用, 就是重用的業(yè)務邏輯自己不產(chǎn)生任何 UI
import { useState, useCallback } from "react"
// 把計數(shù)邏輯封裝到一個自己不 render 任何 UI 的組件中
function CounterRenderProps({ children }) {
const [count, setCount] = useState(0)
const increment = useCallback(() => {
setCount(count + 1)
}, [count])
const decrement = useCallback(() => {
setCount(count - 1)
}, [count])
return children({ count, increment, decrement })
}
function CounterRenderPropsExample() {
return (
// children 這個特殊屬性魔种。也就是組件開始 tag 和結(jié)束 tag 之間的內(nèi)容析二,其實是會作為 children 屬性傳遞給組件
<CounterRenderProps>
{({ count, increment, decrement }) => {
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
)
}}
</CounterRenderProps>
)
}
在上面這種場景下, Hooks 更方便
import { useState, useCallback } from "react"
function useCounter() {
// 定義 count 這個 state 用于保存當前數(shù)值
const [count, setCount] = useState(0)
// 實現(xiàn)加 1 的操作
const increment = useCallback(() => setCount(count + 1), [count])
// 實現(xiàn)減 1 的操作
const decrement = useCallback(() => setCount(count - 1), [count])
// 將業(yè)務邏輯的操作 export 出去供調(diào)用者使用
return { count, increment, decrement }
}
事件處理: 如何創(chuàng)建自定義事件
- React 中使用原生事件: 約定使用駱駝體 (onMouseOver, onChange) 等
- React 原生事件的原理: 合成事件 由于虛擬 DOM 的存在, 在 React 綁定一個事件到原生的 DOM 節(jié)點, 事件也不會綁定在對應的節(jié)點上, 而是所有的事件都綁定在根節(jié)點上. 然后由 React 統(tǒng)一監(jiān)聽和管理, 代理模式, 分發(fā)到具體的虛擬 DOM 上
- React17 版本前: 綁定在 document, 之后, 綁定在整個 App 上的根節(jié)點上
- 虛擬 DOM render 的時候, DOM 可能還沒有真實的 render 到頁面上, 所以無法綁定事件
- React 屏蔽底層事件的細節(jié), 避免瀏覽器兼容問題
創(chuàng)建自定義事件
- 原生事件是瀏覽器機制
- 自定義事件是組件自己的行為, 本質(zhì)是一種回調(diào)機制
- 通過
props 給組件傳遞一個回調(diào)函數(shù)
,然后在組件中的某個時機节预,比如用戶輸入叶摄,或者某個請求完成時漆改,去調(diào)用這個傳過來的回調(diào)函數(shù)就可以了 - 習慣上以
onSomething
命名
- 通過
import { useState } from "react"
// 創(chuàng)建一個無狀態(tài)的受控組件
function ToggleButton({ value, onChange }) {
const handleClick = () => {
onChange(!value)
}
return (
<button style={{ width: "60px" }} onClick={handleClick}>
按鈕
</button>
)
}
Hooks 封裝鍵盤事件
import { useEffect, useState } from "react"
// 使用 document.body 作為默認的監(jiān)聽節(jié)點
const useKeyPress = (domNode = document.body) => {
const [key, setKey] = useState(null)
useEffect(() => {
const handleKeyPress = (evt) => {
setKey(evt.keyCode)
}
// 監(jiān)聽按鍵事件
domNode.addEventListener("keypress", handleKeyPress)
return () => {
// 接觸監(jiān)聽按鍵事件
domNode.removeEventListener("keypress", handleKeyPress)
}
}, [domNode])
return key
}
Form: Hooks 給 Form 處理帶來的那些新變化
- 受控組件 和 非受控組件
function MyForm() {
const [value, setValue] = useState("")
const handleChange = useCallback((evt) => {
setValue(evt.target.value)
}, [])
// React 統(tǒng)一了表單組件的 onChange 事件
return <input value={value} onChange={handleChange} />
}
// 非受控組件 表單元素的值不是由父組件決定的,而是完全內(nèi)部的狀態(tài)
import { useRef } from "react"
export default function MyForm() {
// 定義一個 ref 用于保存 input 節(jié)點的引用
const inputRef = useRef()
const handleSubmit = (evt) => {
evt.preventDefault()
// 使用的時候直接從 input 節(jié)點獲取值
alert("Name: " + inputRef.current.value)
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type='text' ref={inputRef} />
</label>
<input type='submit' value='Submit' />
</form>
)
}
- 使用 Hooks 簡化表單處理
我們對每一個表單元素, 都是遵循這樣處理
- 設置一個 state 綁定 value
- 監(jiān)聽表單元素的 onChange 事件, 同步 value 到 state
維護表單組件的狀態(tài)邏輯: 核心
- 字段的名字
- 綁定 value 值
- 處理 onChange 事件
import { useState, useCallback } from "react"
const useForm = (initialValues = {}) => {
// 設置整個 form 的狀態(tài):values
const [values, setValues] = useState(initialValues)
// 提供一個方法用于設置 form 上的某個字段的值
const setFieldValue = useCallback((name, value) => {
setValues((values) => ({
...values,
[name]: value,
}))
}, [])
// 返回整個 form 的值以及設置值的方法
return { values, setFieldValue }
}
;<input
value={values.email || null}
onChange={(evt) => setFieldValue("email", evt.target.value)}
/>
- 處理表單驗證
- 如何定義這樣的錯誤狀態(tài)
- 如何設置這個錯誤狀態(tài)
// 除了初始值之外,還提供了一個 validators 對象,
// 用于提供針對某個字段的驗證函數(shù)
const useForm = (initialValues = {}, validators) => {
const [values, setValues] = useState(initialValues)
// 定義了 errors 狀態(tài)
const [errors, setErrors] = useState({})
const setFieldValue = useCallback(
(name, value) => {
setValues((values) => ({
...values,
[name]: value,
}))
// 如果存在驗證函數(shù)藤为,則調(diào)用驗證用戶輸入
if (validators[name]) {
const errMsg = validators[name](value)
setErrors((errors) => ({
...errors,
// 如果返回錯誤信息,則將其設置到 errors 狀態(tài)樊破,否則清空錯誤狀態(tài)
[name]: errMsg || null,
}))
}
},
[validators]
)
// 將 errors 狀態(tài)也返回給調(diào)用者
return { values, errors, setFieldValue }
}
function MyForm() {
// 用 useMemo 緩存 validators 對象
const validators = useMemo(() => {
return {
name: (value) => {
// 要求 name 的長度不得小于 2
if (value.length < 2) return "Name length should be no less than 2."
return null
},
email: (value) => {
// 簡單的實現(xiàn)一個 email 驗證邏輯:必須包含 @ 符號。
if (!value.includes("@")) return "Invalid email address"
return null
},
}
}, [])
// 從 useForm 的返回值獲取 errors 狀態(tài)
const { values, errors, setFieldValue } = useForm({}, validators)
// UI 渲染邏輯...
}
路由管理
路由管理唆铐,就是讓你的頁面能夠根據(jù) URL 的變化進行頁面的切換哲戚,這是前端應用中一個非常重要的機制
URL 的全稱是 Uniform Resource Locator,中文意思是“統(tǒng)一資源定位符”艾岂,表明 URL 是用于唯一的定位某個資源的
- 路由工作原理
在前端路由管理中顺少,則一般只在主內(nèi)容區(qū)域 Content 部分變化, Header 和 Sider 是不會變化的王浴。
實現(xiàn)路由機制的核心邏輯就是根據(jù) URL 路徑這個狀態(tài)脆炎,來決定在主內(nèi)容區(qū)域顯示什么組件, 示意代碼
const MyRouter = ({ children }) => {
const routes = _.keyBy(
children.map((c) => c.props),
"path"
)
const [hash] = useHash()
// 通過 URL 中的 hash,也就是“#”后面的部分來決定具體渲染哪個組件到主區(qū)域
const Page = routes[hash.replace("#", "")]?.component
// 如果路由不存在就返回 Not found.
return Page ? <Page /> : "Not found."
}
// 定義了一個空組件 Route氓辣,來接收路由的具體參數(shù) path 和 component秒裕,從而以聲明式的方式去定義路由
const Route = () => null
function SamplePages {
return (
<div className="sample-pages">
{/* 定義了側(cè)邊導航欄 */}
<div className="sider">
<a href="#page1">Page 1</a>
<a href="#page2">Page 2</a>
<a href="#page3">Page 3</a>
<a href="#page4">Page 4</a>
</div>
<div className="exp-15-page-container">
{/* 定義路由配置 */}
<MyRouter>
<Route path="page1" component={Page1} />
<Route path="page2" component={Page2} />
<Route path="page3" component={Page3} />
<Route path="page4" component={Page4} />
</MyRouter>
</div>
</>
);
};
按需加載
import 語句,定義按需加載的起始模塊
按需加載钞啸,就是指在某個組件需要被渲染到頁面時几蜻,才會去實際地下載這個頁面,以及這個頁面依賴的所有代碼
// return promise
import(someModule)
// 演示使用 import 語句
function ProfilePage() {
// 定義一個 state 用于存放需要加載的組件
const [RealPage, setRealPage] = useState(null)
// 根據(jù)路徑動態(tài)加載真正的組件實現(xiàn)
import("./RealProfilePage").then((comp) => {
setRealPage(Comp)
})
// 如果組件未加載則顯示 Loading 狀態(tài)
if (!RealPage) return "Loading...."
// 組件加載成功后則將其渲染到界面
return <RealPage />
}