精讀《正交的 React 組件》

1 引言

搭配了合適的設計模式的代碼,纔可擁有良好的可維護性,The Benefits of Orthogonal React Components 這篇文章就重點介紹了正交性原理。前端

所謂正交,即模塊之間不會相互影響。想象一個音響的音量與換臺按鈕間若是不是正交關係,控制音量同時可能影響換臺,這樣的設備很難維護:react

前端代碼也同樣,UI 與數據處理邏輯分離就是一種符合正交原則的設計,這樣有利於長期代碼質量維護。ios

2 概述

一個擁有良好正交性的 React App 會按照以下模塊分離設計:git

  1. UI 元素(展現型組件)。
  2. 取數邏輯(fetch library, REST or GraphQL)。
  3. 全局狀態管理(redux)。
  4. 持久化(local storage, cookies)。

文中經過兩個例子說明。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>
  );
}
複製代碼

Main 組件

上面例子中,<MyComponent> 就是一個 Main 組件,Main 組件封裝一些髒邏輯,即它要負責不一樣模塊的組裝,而這些模塊之間不須要知道彼此的存在。

一個應用會存在多個 Main 組件,它們負責拼裝各類做用域下的髒邏輯。

正交設計的好處

  • 容易維護: 正交組件邏輯相互隔離,不用擔憂連帶影響,所以能夠放心大膽的維護單個組件。
  • 易讀: 因爲邏輯分離致使了抽象,所以每一個模塊作的事情都相對單一,很容易猜想一個組件作的事情。
  • 可測試: 因爲邏輯分離,能夠採起逐個擊破的思路進行單測。

權衡

若是不採用正交設計,由於模塊之間的關聯致使應用最終變得難以維護。但若是將正交設計應用到極致,可能會多處許多沒必要要的抽象,這些抽象的複用僅此一次,形成過分設計。

3 精讀

正交設計必定程度能夠理解爲合理抽象,徹底不抽象與過分抽象都是不可取的,所以列舉了四塊須要抽象的要點: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 個職責清晰,相互隔離的模塊也許會更輕鬆。

4 總結

從正交設計角度來看,Hooks 解決了狀態管理與 UI 分離的問題,Suspense 解決了取數狀態與 UI 分離的問題,ErrorBoundary 解決了異常與 UI 分離的問題。

在你看來,React 還有哪些邏輯須要與 UI 分離?分別使用哪些方法呢?歡迎留言。

討論地址是:精讀《正交的 React 組件》 · Issue #221 · dt-fe/weekly

若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公衆號

版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章
相關標籤/搜索