搭配了合適的設計模式的代碼,纔可擁有良好的可維護性,The Benefits of Orthogonal React Components 這篇文章就重點介紹了正交性原理。前端
所謂正交,即模塊之間不會相互影響。想象一個音響的音量與換臺按鈕間若是不是正交關係,控制音量同時可能影響換臺,這樣的設備很難維護:react
前端代碼也同樣,UI 與數據處理邏輯分離就是一種符合正交原則的設計,這樣有利於長期代碼質量維護。ios
一個擁有良好正交性的 React App 會按照以下模塊分離設計:git
文中經過兩個例子說明。github
好比一個展現僱員列表組件 <EmployeesPage>
:redux
import React, { useState } from "react";
import axios from "axios";
import EmployeesList from "./EmployeesList";
function EmployeesPage() {
const [isFetching, setFetching] = useState(false);
const [employees, setEmployees] = useState([]);
useEffect(function fetch() {
(async function() {
setFetching(true);
const response = await axios.get("/employees");
setEmployees(response.data);
setFetching(false);
})();
}, []);
if (isFetching) {
return <div>Fetching employees....</div>;
}
return <EmployeesList employees={employees} />; } 複製代碼
這樣設計看上去沒問題,但其實違背了正交原則,由於 EmployeesPage
既負責渲染 UI 又關心取數邏輯。正交的寫法以下:axios
import React, { Suspense } from "react";
import EmployeesList from "./EmployeesList";
function EmployeesPage({ resource }) {
return (
<Suspense fallback={<h1>Fetching employees....</h1>}>
<EmployeesFetch resource={resource} />
</Suspense>
);
}
function EmployeesFetch({ resource }) {
const employees = resource.employees.read();
return <EmployeesList employees={employees} />;
}
複製代碼
Suspense
將 loading 狀態剝離到父級組件,所以子組件只須要關心如何用數據,不需關心如何取數據(以及 loading 態)。設計模式
好比一個滾動到必定距離就出現 "jump to top" 的組件 <ScrollToTop>
,可能會這麼實現:api
import React, { useState, useEffect } from "react";
const DISTANCE = 500;
function ScrollToTop() {
const [crossed, setCrossed] = useState(false);
useEffect(function() {
const handler = () => setCrossed(window.scrollY > DISTANCE);
handler();
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
function onClick() {
window.scrollTo({
top: 0,
behavior: "smooth"
});
}
if (!crossed) {
return null;
}
return <button onClick={onClick}>Jump to top</button>;
}
複製代碼
能夠看到,在這個組件中,按鈕與滾動狀態判斷邏輯混合在了一塊兒。若是咱們將 「滾動到必定距離就渲染 UI」 抽象成通用組件 IfScrollCrossed
呢?微信
import { useState, useEffect } from "react";
function useScrollDistance(distance) {
const [crossed, setCrossed] = useState(false);
useEffect(
function() {
const handler = () => setCrossed(window.scrollY > distance);
handler();
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
},
[distance]
);
return crossed;
}
function IfScrollCrossed({ children, distance }) {
const isBottom = useScrollDistance(distance);
return isBottom ? children : null;
}
複製代碼
有了 IfScrollCrossed
,咱們就能專一寫 「點擊按鈕跳轉到頂部」 這個 UI 組件了:
function onClick() {
window.scrollTo({
top: 0,
behavior: "smooth"
});
}
function JumpToTop() {
return <button onClick={onClick}>Jump to top</button>;
}
複製代碼
最後將他們拼裝在一塊兒:
import React from "react";
// ...
const DISTANCE = 500;
function MyComponent() {
// ...
return (
<IfScrollCrossed distance={DISTANCE}> <JumpToTop /> </IfScrollCrossed>
);
}
複製代碼
這麼作,咱們的 <JumpToTop>
與 <IfScrollCrossed>
組件就是正交關係,並且邏輯更清晰。不只如此,這樣的抽象使 <IfScrollCrossed>
能夠被其餘場景複用:
import React from "react";
// ...
const DISTANCE_NEWSLETTER = 300;
function OtherComponent() {
// ...
return (
<IfScrollCrossed distance={DISTANCE_NEWSLETTER}> <SubscribeToNewsletterForm /> </IfScrollCrossed>
);
}
複製代碼
上面例子中,<MyComponent>
就是一個 Main 組件,Main 組件封裝一些髒邏輯,即它要負責不一樣模塊的組裝,而這些模塊之間不須要知道彼此的存在。
一個應用會存在多個 Main 組件,它們負責拼裝各類做用域下的髒邏輯。
若是不採用正交設計,由於模塊之間的關聯致使應用最終變得難以維護。但若是將正交設計應用到極致,可能會多處許多沒必要要的抽象,這些抽象的複用僅此一次,形成過分設計。
正交設計必定程度能夠理解爲合理抽象,徹底不抽象與過分抽象都是不可取的,所以列舉了四塊須要抽象的要點:UI 元素、取數邏輯、全局狀態管理、持久化。
全局狀態管理注入到組件,就是一種正交的抽象模式,即組件不用關心數據從哪來,而直接使用數據,而數據管理徹底交由數據流層管理。
取數邏輯每每是可能被忽略的一環,不管是像原文中直接關心到 fetch
方法的 UI 組件,仍是利用取數工具庫關心了 loading
狀態:
import useSWR from "swr";
function Profile() {
const { data, error } = useSWR("/api/user", fetcher);
if (error) return <div>failed to load</div>;
if (!data) return <div>loading...</div>;
return <div>hello {data.name}!</div>;
}
複製代碼
雖然將取數生命週期封裝到自定義 hook useSWR
中,但 error
信息對 UI 組件來講就是一個髒數據:這讓這個 UI 組件不只要渲染數據,還要擔憂取數是否會失敗,或者是否在 loading 中。
好在 Suspense 模式解決了這個問題:
import { Suspense } from "react";
import useSWR from "swr";
function Profile() {
const { data } = useSWR("/api/user", fetcher, { suspense: true });
return <div>hello, {data.name}</div>;
}
function App() {
return (
<Suspense fallback={<div>loading...</div>}> <Profile /> </Suspense>
);
}
複製代碼
這樣 <Profile>
只要專一於作數據渲染,而不用擔憂 useSWR('/api/user', fetcher, { suspense: true })
這個取數過程發生了什麼、是否取數失敗、是否在 loading
中。由於取數狀態由 Suspense
管理,而取數是否意外失敗由 ErrorBoundary
管理。
合理的抽象使組件邏輯變得更簡單,從而組件嵌套使用使不用擔憂額外影響。尤爲在大型項目中,不要擔憂正交抽象會使原本就不少的模塊數量再次膨脹,由於相比於維護 100 個相互影響,內部邏輯複雜的模塊,維護 200 個職責清晰,相互隔離的模塊也許會更輕鬆。
從正交設計角度來看,Hooks
解決了狀態管理與 UI 分離的問題,Suspense
解決了取數狀態與 UI 分離的問題,ErrorBoundary
解決了異常與 UI 分離的問題。
在你看來,React 還有哪些邏輯須要與 UI 分離?分別使用哪些方法呢?歡迎留言。
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)