欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React組件、狀態(tài)管理、代碼優(yōu)化的技巧

 更新時間:2024年11月30日 14:10:50   作者:夕水  
文章總結(jié)了React組件設(shè)計、狀態(tài)管理、代碼組織和優(yōu)化的技巧,它涵蓋了使用Fragment、props解構(gòu)、defaultProps、key和ref的使用、渲染性能優(yōu)化等方面

文章總結(jié)了React組件設(shè)計、狀態(tài)管理、代碼組織和優(yōu)化的技巧,它涵蓋了使用Fragment、props解構(gòu)、defaultProps、key和ref的使用、渲染性能優(yōu)化等方面。

一. 組件相關(guān)

1. 使用自閉合組件

// 不好的寫法
<Component></Component>
// 推薦寫法
<Component />

2. 推薦使用Fragment組件而不是 DOM 元素來分組元素

在 React 中,每個組件必須返回單個元素。不要將多個元素包裝在 <div> 或 <span> 中,而是使用 <Fragment> 來保持 DOM 整潔。

不好的寫法:使用 div 會使 DOM 變得雜亂,并且可能需要更多 CSS 代碼。

import Header from "./header";
import Content from "./content";
import Footer from "./footer";

const Test = () => {
  return (
    <div>
      <Header />
      <Content />
      <Footer />
    </div>
  );
};

推薦寫法: <Fragment> 包裝元素而不影響 DOM 結(jié)構(gòu)。

import Header from "./header";
import Content from "./content";
import Footer from "./footer";

const Test = () => {
  return (
    // 如果元素不需要添加屬性,則可以使用簡寫形式<></>
    <Fragment>
      <Header />
      <Content />
      <Footer />
    </Fragment>
  );
};

3. 使用 React fragment 簡寫 <></>(除非你需要設(shè)置一個 key 屬性)

不好寫法:下面的代碼有點冗余。

const Test = () => {
  return (
    <Fragment>
      <Header />
      <Content />
      <Footer />
    </Fragment>
  );
};

推薦寫法:

const Test = () => {
  return (
    <>
      <Header />
      <Content />
      <Footer />
    </>
  );
};

除非你需要一個 key 屬性。

const Tools = ({ tools }) => {
    return (
        <Container>
            {
                tools?.map((item, index) => {
                    <Fragment key={`${item.id}-${index}`}>
                        <span>{ item.id }</span>
                        <span>{ item.name }</span>
                    <Fragment>
                })
            }
        </Container>
    )
}

4. 優(yōu)先分散使用 props,而不是單獨訪問每個 props

不好的寫法: 下面的代碼更難閱讀(特別是在項目較大時)。

const TodoLists = (props) => (
  <div className="todo-list">
    {props.todoList?.map((todo, index) => (
      <div className="todo-list-item" key={todo.uuid}>
        <p onClick={() => props.seeDetail?.(todo)}>
          {todo?.uuid}:{todo.text}
        </p>
        <div className="todo-list-item-btn-group">
          <button type="button" onClick={() => props.handleEdit?.(todo, index)}>
            編輯
          </button>
          <button
            type="button"
            onClick={() => props.handleDelete?.(todo, index)}
          >
            刪除
          </button>
        </div>
      </div>
    ))}
  </div>
);
export default TodoLists;

推薦寫法: 下面的代碼更加簡潔。

const TodoLists = ({ todoList, seeDetail, handleEdit, handleDelete }) => (
  <div className="todo-list">
    {todoList?.map((todo, index) => (
      <div className="todo-list-item" key={todo.uuid}>
        <p onClick={() => seeDetail?.(todo)}>
          {todo?.uuid}:{todo.text}
        </p>
        <div className="todo-list-item-btn-group">
          <button type="button" onClick={() => handleEdit?.(todo, index)}>
            編輯
          </button>
          <button type="button" onClick={() => handleDelete?.(todo, index)}>
            刪除
          </button>
        </div>
      </div>
    ))}
  </div>
);
export default TodoLists;

5. 設(shè)置 props 的默認(rèn)值時,在解構(gòu)時進行

不好的寫法: 你可能需要在多個地方定義默認(rèn)值并引入新變量。

const Text = ({ size, type }) => {
  const Component = type || "span";
  const comSize = size || "mini";
  return <Component size={comSize} />;
};

推薦寫法,直接在對象解構(gòu)里給出默認(rèn)值。

const Text = ({ size = "mini", type: Component = "span" }) => {
  return <Component size={comSize} />;
};

6. 傳遞字符串類型屬性時刪除花括號。

不好的寫法:帶花括號的寫法

<button type={"button"} className={"btn"}>
  按鈕
</button>

推薦寫法: 不需要花括號

<button type="button" className="btn">
  按鈕
</button>

7. 在使用 value && <Component {...props}/> 之前確保 value 值是布爾值,以防止顯示意外的值。

不好的寫法: 當(dāng)列表的長度為 0,則有可能顯示 0。

const DataList = ({ data }) => {
  return <Container>{data.length && <List data={data} />}</Container>;
};

推薦寫法: 當(dāng)列表沒有數(shù)據(jù)時,則不會渲染任何東西。

const DataList = ({ data }) => {
  return <Container>{data.length > 0 && <List data={data} />}</Container>;
};

8. 使用函數(shù)(內(nèi)聯(lián)或非內(nèi)聯(lián))避免中間變量污染你的上下文

不好的寫法: 變量 totalCount 和 totalPrice 使組件的上下文變得混亂。

const GoodList = ({ goods }) => {
  if (goods.length === 0) {
    return <>暫無數(shù)據(jù)</>;
  }
  let totalCount = 0;
  let totalPrice = 0;
  goods.forEach((good) => {
    totalCount += good.count;
    totalPrice += good.price;
  });
  return (
    <>
      總數(shù)量:{totalCount};總價:{totalPrice}
    </>
  );
};

推薦寫法: 將變量 totalCount 和 totalPrice 控制在一個函數(shù)內(nèi)。

const GoodList = ({ goods }) => {
  if (goods.length === 0) {
    return <>暫無數(shù)據(jù)</>;
  }
  // 使用函數(shù)
  const {
    totalCount,
    totalPrice,
  } = () => {
    let totalCount = 0,
      totalPrice = 0;
    goods.forEach((good) => {
      totalCount += good.count;
      totalPrice += good.price;
    });
    return { totalCount, totalPrice };
  };
  return (
    <>
      總數(shù)量:{totalCount};總價:{totalPrice}
    </>
  );
};

個人更喜歡的寫法: 封裝成 hooks 來使用。

const useTotalGoods = ({ goods }) => {
  let totalCount = 0,
    totalPrice = 0;
  goods.forEach((good) => {
    totalCount += good.count;
    totalPrice += good.price;
  });
  return { totalCount, totalPrice };
};
const GoodList = ({ goods }) => {
  if (goods.length === 0) {
    return <>暫無數(shù)據(jù)</>;
  }
  const { totalCount, totalPrice } = useTotalGoods(goods);
  return (
    <>
      總數(shù)量:{totalCount};總價:{totalPrice}
    </>
  );
};

9. 使用柯里化函數(shù)重用邏輯(并正確緩存回調(diào)函數(shù))

不好的寫法: 表單更新字段重復(fù)。

const UserLoginForm = () => {
  const [{ username, password }, setFormUserState] = useState({
    username: "",
    password: "",
  });

  return (
    <>
      <h1>登陸</h1>
      <form>
        <div class="form-item">
          <label>用戶名:</label>
          <input
            placeholder="請輸入用戶名"
            value={username}
            onChange={(e) =>
              setFormUserState((state) => ({
                ...state,
                username: e.target.value,
              }))
            }
          />
        </div>
        <div class="form-item">
          <label>密碼:</label>
          <input
            placeholder="請輸入密碼"
            value={username}
            type="password"
            onChange={(e) =>
              setFormUserState((state) => ({
                ...state,
                password: e.target.value,
              }))
            }
          />
        </div>
      </form>
    </>
  );
};

推薦寫法: 引入 createFormValueChangeHandler 方法,為每個字段返回正確的處理方法。

筆記: 如果你啟用了 ESLint 規(guī)則 jsx-no-bind,此技巧尤其有用。你只需將柯里化函數(shù)包裝在 useCallback 中。
const UserLoginForm = () => {
  const [{ username, password }, setFormUserState] = useState({
    username: "",
    password: "",
  });

  const createFormValueChangeHandler = (field: string) => {
    return (e) => {
      setFormUserState((state) => ({
        ...state,
        [field]: e.target.value,
      }));
    };
  };
  return (
    <>
      <h1>登陸</h1>
      <form>
        <div class="form-item">
          <label>用戶名:</label>
          <input
            placeholder="請輸入用戶名"
            value={username}
            onChange={createFormValueChangeHandler("username")}
          />
        </div>
        <div class="form-item">
          <label>密碼:</label>
          <input
            placeholder="請輸入密碼"
            value={username}
            type="password"
            onChange={createFormValueChangeHandler("password")}
          />
        </div>
      </form>
    </>
  );
};

10. 將不依賴組件 props/state 的數(shù)據(jù)移到組件外部,以獲得更干凈(和更高效)的代碼

不好的寫法: OPTIONS 和 renderOption 不需要位于組件內(nèi)部,因為它們不依賴任何 props 或狀態(tài)。此外,將它們保留在內(nèi)部意味著每次組件渲染時我們都會獲得新的對象引用。如果我們將 renderOption 傳遞給包裹在 memo 中的子組件,則會破壞緩存功能。

const ToolSelector = () => {
  const options = [
    {
      label: "html工具",
      value: "html-tool",
    },
    {
      label: "css工具",
      value: "css-tool",
    },
    {
      label: "js工具",
      value: "js-tool",
    },
  ];
  const renderOption = ({
    label,
    value,
  }: {
    label?: string;
    value?: string;
  }) => <Option value={value}>{label}</Option>;
  return (
    <Select placeholder="請選擇工具">
      {options.map((item, index) => (
        <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment>
      ))}
    </Select>
  );
};

推薦寫法: 將它們移出組件以保持組件干凈和引用穩(wěn)定。

const options = [
  {
    label: "html工具",
    value: "html-tool",
  },
  {
    label: "css工具",
    value: "css-tool",
  },
  {
    label: "js工具",
    value: "js-tool",
  },
];
const renderOption = ({ label, value }: { label?: string; value?: string }) => (
  <Option value={value}>{label}</Option>
);
const ToolSelector = () => {
  return (
    <Select placeholder="請選擇工具">
      {options.map((item, index) => (
        <Fragment key={`${item.value}-${index}`}>{renderOption(item)}</Fragment>
      ))}
    </Select>
  );
};
筆記: 在這個示例中,你可以通過使用選項元素內(nèi)聯(lián)來進一步簡化。
const options = [
  {
    label: "html工具",
    value: "html-tool",
  },
  {
    label: "css工具",
    value: "css-tool",
  },
  {
    label: "js工具",
    value: "js-tool",
  },
];
const ToolSelector = () => {
  return (
    <Select placeholder="請選擇工具">
      {options.map((item, index) => (
        <Option value={item.value} key={`${item.value}-${index}`}>
          {item.label}
        </Option>
      ))}
    </Select>
  );
};

11. 存儲列表組件中選定的對象時,存儲對象 ID,而不是整個對象

不好的寫法: 如果選擇了某個對象但隨后它發(fā)生了變化(即,我們收到了相同 ID 的全新對象引用),或者該對象不再存在于列表中,則 selectedItem 將保留過時的值或變得不正確。

const List = ({ data }) => {
  // 引用的是整個選中的是對象
  const [selectedItem, setSelectedItem] = useState<Item | undefined>();
  return (
    <>
      {selectedItem && <div>{selectedItem.value}</div>}
      <List
        data={data}
        onSelect={setSelectedItem}
        selectedItem={selectedItem}
      />
    </>
  );
};

推薦寫法: 我們通過 ID(應(yīng)該是穩(wěn)定的)存儲所選列表對象。這確保即使列表對象從列表中刪除或其某個屬性發(fā)生變化,UI 也應(yīng)該正確。

const List = ({ data }) => {
  const [selectedItemId, setSelectedItemId] = useState<string | number>();
  // 我們從列表中根據(jù)選中id查找出選定的列表對象
  const selectedItem = data.find((item) => item.id === selectedItemId);
  return (
    <>
      {selectedItemId && <div>{selectedItem.value}</div>}
      <List
        data={data}
        onSelect={setSelectedItemId}
        selectedItemId={selectedItemId}
      />
    </>
  );
};

12. 如果需要多次用到 prop 里面的值,那就引入一個新的組件

不好的寫法: 由于 type === null 的檢查使得代碼變得混亂。

注意: 由于hooks 規(guī)則,我們不能提前返回 null。
const CreatForm = ({ type }) => {
  const formList = useMemo(() => {
    if (type === null) {
      return [];
    }
    return getFormList({ type });
  }, [type]);
  const onHandleChange = useCallback(
    (id) => {
      if (type === null) {
        return;
      }
      // do something
    },
    [type]
  );
  if (type === null) {
    return null;
  }

  return (
    <>
      {formList.map(({ value, id, ...rest }, index) => (
        <item.component
          value={value}
          onChange={onHandleChange}
          key={id}
          {...rest}
        />
      ))}
    </>
  );
};

推薦寫法: 我們引入了一個新組件 FormLists,它采用定義的表單項組件并且更加簡潔。

const FormList = ({ type }) => {
  const formList = useMemo(() => getFormList({ type }), [type]);
  const onHandleChange = useCallback(
    (id) => {
      // do something
    },
    [type]
  );
  return (
    <>
      {formList.map(({ value, id, ...rest }, index) => (
        <item.component
          value={value}
          onChange={onHandleChange}
          key={id}
          {...rest}
        />
      ))}
    </>
  );
};
const CreateForm = ({ type }) => {
  if (type === null) {
    return null;
  }
  return <FormList type={type} />;
};

13. 將所有狀態(tài)(state)和上下文(context)分組到組件頂部

當(dāng)所有狀態(tài)和上下文都位于頂部時,很容易發(fā)現(xiàn)哪些因素會觸發(fā)組件重新渲染。

不好的寫法: 狀態(tài)和上下文分散,難以跟蹤。

const LoginForm = () => {
  const [username, setUsername] = useState("");
  const onHandleChangeUsername = (e) => {
    setUserName(e.target.value);
  };
  const [password, setPassword] = useState("");
  const onHandleChangePassword = (e) => {
    setPassword(e.target.value);
  };
  const theme = useContext(themeContext);

  return (
    <div class={`login-form login-form-${theme}`}>
      <h1>login</h1>
      <form>
        <div class="login-form-item">
          <label>用戶名:</label>
          <input
            value={username}
            onChange={onHandleChangeUsername}
            placeholder="請輸入用戶名"
          />
        </div>
        <div class="login-form-item">
          <label>密碼:</label>
          <input
            value={password}
            onChange={onHandleChangePassword}
            placeholder="請輸入密碼"
            type="password"
          />
        </div>
      </form>
    </div>
  );
};

推薦寫法: 所有狀態(tài)和上下文都集中在頂部,以便于快速定位。

const LoginForm = () => {
  // context
  const theme = useContext(themeContext);
  // state
  const [password, setPassword] = useState("");
  const [username, setUsername] = useState("");
  // method
  const onHandleChangeUsername = (e) => {
    setUserName(e.target.value);
  };
  const onHandleChangePassword = (e) => {
    setPassword(e.target.value);
  };

  return (
    <div class={`login-form login-form-${theme}`}>
      <h1>login</h1>
      <form>
        <div class="login-form-item">
          <label>用戶名:</label>
          <input
            value={username}
            onChange={onHandleChangeUsername}
            placeholder="請輸入用戶名"
          />
        </div>
        <div class="login-form-item">
          <label>密碼:</label>
          <input
            value={password}
            onChange={onHandleChangePassword}
            placeholder="請輸入密碼"
            type="password"
          />
        </div>
      </form>
    </div>
  );
};

二. 有效的設(shè)計模式與技巧

14. 利用 children 屬性來獲得更清晰的代碼(以及性能優(yōu)勢)

利用子組件 props 來獲得更簡潔的代碼(和性能優(yōu)勢)。使用子組件 props 有幾個好處:

  • 好處 1:你可以通過將 props 直接傳遞給子組件而不是通過父組件路由來避免 prop 混入。
  • 好處 2:你的代碼更具可擴展性,因為你可以輕松修改子組件而無需更改父組件。
  • 好處 3:你可以使用此技巧避免重新渲染組件(參見下面的示例)。

不好的寫法: 每當(dāng) Timer 渲染時,OtherSlowComponent 都會渲染,每次當(dāng)前時間更新時都會發(fā)生這種情況。

const Container = () => <Timer />;

const Timer = () => {
  const [time, setTime] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => setTime(new Date()), 1000);
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <>
      <h1>當(dāng)前時間:{dayjs(time).format("YYYY-MM-DD HH:mm:ss")}</h1>
      <OtherSlowComponent />
    </>
  );
};

推薦寫法: Timer 呈現(xiàn)時,OtherSlowComponent 不會呈現(xiàn)。

const Container = () => (
  <Timer>
    <OtherSlowComponent />
  </Timer>
);

const Timer = ({ children }) => {
  const [time, setTime] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => setTime(new Date()), 1000);
    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <>
      <h1>當(dāng)前時間:{dayjs(time).formate("YYYY-MM-DD HH:mm:ss")}</h1>
      {children}
    </>
  );
};

15. 使用復(fù)合組件構(gòu)建可組合代碼

像搭積木一樣使用復(fù)合組件,將它們拼湊在一起以創(chuàng)建自定義 UI。這些組件在創(chuàng)建庫時效果極佳,可生成富有表現(xiàn)力且高度可擴展的代碼。以下是一個以reach.ui為示例的代碼:

<Menu>
  <MenuButton>
    操作吧 <span aria-hidden>?</span>
  </MenuButton>
  <MenuList>
    <MenuItem onSelect={() => alert("下載")}>下載</MenuItem>
    <MenuItem onSelect={() => alert("復(fù)制")}>創(chuàng)建一個復(fù)制</MenuItem>
    <MenuLink as="a"  rel="external nofollow" >
      跳轉(zhuǎn)鏈接
    </MenuLink>
  </MenuList>
</Menu>

16. 使用渲染函數(shù)或組件函數(shù) props 使你的代碼更具可擴展性

假設(shè)我們想要顯示各種列表,例如消息、個人資料或帖子,并且每個列表都應(yīng)該可排序。

為了實現(xiàn)這一點,我們引入了一個 List 組件以供重復(fù)使用。我們可以通過兩種方式解決這個問題:

不好的寫法:選項 1。

List 處理每個項目的渲染及其排序方式。這是有問題的,因為它違反了開放封閉原則。每當(dāng)添加新的項目類型時,此代碼都會被修改。

List.tsx:

export interface ListItem {
  id: string;
}
// 不好的列表組件寫法
// 我們還需要了解這些接口
type PostItem = ListItem & { title: string };
type UserItem = ListItem & { name: string; date: Date };
type ListNewItem =
  | { type: "post"; value: PostItem }
  | { type: "user"; value: UserItem };
interface BadListProps<T extends ListNewItem> {
  type: T["type"];
  items: Array<T["value"]>;
}

const SortList = <T extends ListNewItem>({ type, items }: BadListProps<T>) => {
  const sortItems = [...items].sort((a, b) => {
    // 我們還需注意這里的比較邏輯,這里或者直接使用下方導(dǎo)出的比較函數(shù)
    return 0;
  });

  return (
    <>
      <h2>{type === "post" ? "帖子" : "用戶"}</h2>
      <ul className="sort-list">
        {sortItems.map((item, index) => (
          <li className="sort-list-item" key={`${item.id}-${index}`}>
            {(() => {
              switch (type) {
                case "post":
                  return (item as PostItem).title;
                case "user":
                  return (
                    <>
                      <span>{(item as UserItem).name}</span>
                      <span> - </span>
                      <em>
                        加入時間: {(item as UserItem).date.toDateString()}
                      </em>
                    </>
                  );
              }
            })()}
          </li>
        ))}
      </ul>
    </>
  );
};

export function compareStrings(a: string, b: string): number {
  return a < b ? -1 : a == b ? 0 : 1;
}

推薦寫法:選項 2。

List 采用渲染函數(shù)或組件函數(shù),僅在需要時調(diào)用它們。

List.tsx:

export interface ListItem {
  id: string;
}
interface ListProps<T extends ListItem> {
  items: T[]; // 列表數(shù)據(jù)
  header: React.ComponentType; // 頭部組件
  itemRender: (item: T) => React.ReactNode; // 列表項
  itemCompare: (a: T, b: T) => number; // 列表項自定義排序函數(shù)
}

const SortList = <T extends ListItem>({
  items,
  header: Header,
  itemRender,
  itemCompare,
}: ListProps<T>) => {
  const sortedItems = [...items].sort(itemCompare);

  return (
    <>
      <Header />
      <ul className="sort-list">
        {sortedItems.map((item, index) => (
          <li className="sort-list-item" key={`${item.id}-${index}`}>
            {itemRender(item)}
          </li>
        ))}
      </ul>
    </>
  );
};

export default SortList;

完整示例代碼可前往這里查看。

17. 處理不同情況時,使用 value === case && <Component /> 以避免保留舊狀態(tài)

不好的寫法: 在如下示例中,在切換時計數(shù)器 count 不會重置。發(fā)生這種情況的原因是,在渲染同一組件時,其狀態(tài)在currentTab更改后保持不變。

tab.tsx:

const tabList = [
  {
    label: "首頁",
    value: "tab-1",
  },
  {
    label: "詳情頁",
    value: "tab-2",
  },
];

export interface TabItem {
  label: string;
  value: string;
}
export interface TabProps {
  tabs: TabItem[];
  currentTab: string | TabItem;
  onTab: (v: string | TabItem) => void;
  labelInValue?: boolean;
}

const Tab: React.FC<TabProps> = ({
  tabs = tabList,
  currentTab,
  labelInValue,
  onTab,
}) => {
  const currentTabValue = useMemo(
    () => (labelInValue ? (currentTab as TabItem)?.value : currentTab),
    [currentTab, labelInValue]
  );
  return (
    <div className="tab">
      {tabs?.map((item, index) => (
        <div
          className={`tab-item${
            currentTabValue === item.value ? " active" : ""
          }`}
          key={`${item.value}-${index}`}
          onClick={() => onTab?.(labelInValue ? item : item.value)}
        >
          {item.label}
        </div>
      ))}
    </div>
  );
};

export default Tab;

Resource.tsx:

export interface ResourceProps {
  type: string;
}
const Resource: React.FC<ResourceProps> = ({ type }) => {
  const [count, setCount] = useState(0);
  const onHandleClick = () => {
    setCount((c) => c + 1);
  };

  return (
    <div className="tab-content">
      你當(dāng)前在{type === "tab-1" ? "首頁" : "詳情頁"},
      <button onClick={onHandleClick} className="btn" type="button">
        點擊我
      </button>
      增加訪問{count}次數(shù)
    </div>
  );
};

推薦寫法: 根據(jù) currentTab 渲染組件或在類型改變時使用 key 強制重新渲染組件。

function App() {
  const [currentTab, setCurrentTab] = useState("tab-1");

  return (
    <>
      <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} />
      {currentTab === "tab-1" && <Resource type="tab-1" />}
      {currentTab === "tab-2" && <Resource type="tab-2" />}
    </>
  );
}
// 使用key屬性
function App() {
  const [currentTab, setCurrentTab] = useState("tab-1");

  return (
    <>
      <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} />
      <Resource type={currentTab} key={currentTab} />
    </>
  );
}

完整示例代碼可前往這里查看。

18. 始終使用錯誤邊界處理組件渲染錯誤

默認(rèn)情況下,如果你的應(yīng)用程序在渲染過程中遇到錯誤,整個 UI 都會崩潰。

為了防止這種情況,請使用錯誤邊界來:

  • 即使發(fā)生錯誤,也要保持應(yīng)用程序的某些部分正常運行。
  • 顯示用戶友好的錯誤消息并可選擇跟蹤錯誤。
提示:你可以使用 react-error-boundary 庫。

三. key 與 ref

19. 使用 crypto.randomUUID 或 Math.random 生成 key

map 調(diào)用(也就是列表渲染)中的 JSX 元素始終需要 key。

假設(shè)你的元素還沒有 key。在這種情況下,你可以使用 crypto.randomUUID、Math.random 或 uuid 庫生成唯一 ID。

注意:請注意,舊版瀏覽器中未定義 crypto.randomUUID。

20. 確保你的列表項 id 是穩(wěn)定的(即:它們在渲染中是不會發(fā)生變化的)

盡可能的讓 id/key 可以穩(wěn)定。

否則,React 可能會無用地重新渲染某些組件,或者觸發(fā)一些功能異常,如下例所示。

不好的寫法: 每次 App 組件渲染時 selectItemId 都會發(fā)生變化,因此設(shè)置 id 的值將永遠(yuǎn)不會正確。

const App = () => {
  const [items, setItems] = useState([]);
  const [selectItemId, setSelectItemId] = useState(undefined);

  const loadItems = () => {
    fetchItems().then((res) => setItems(res));
  };
  //  請求列表
  useEffect(() => {
    loadItems();
  }, []);

  // 添加列表id,這是一種很糟糕的做法
  const newItems = items.map((item) => ({ ...item, id: crypto.randomUUID() }));

  return (
    <List
      items={newItems}
      selectedItemId={selectItemId}
      onSelectItem={setSelectItemId}
    />
  );
};

推薦寫法: 當(dāng)我們獲取列表項的時候添加 id。

const App = () => {
  const [items, setItems] = useState([]);
  const [selectItemId, setSelectItemId] = useState(undefined);

  const loadItems = () => {
    // 獲取列表數(shù)據(jù)并通過 id 保存
    fetchItems().then((res) =>
      // 一旦獲得結(jié)果,我們就會添加“id”
      setItems(res.map((item) => ({ ...item, id: crypto.randomUUID() })))
    );
  };
  //  請求列表
  useEffect(() => {
    loadItems();
  }, []);

  return (
    <List
      items={items}
      selectedItemId={selectItemId}
      onSelectItem={setSelectItemId}
    />
  );
};

21. 策略性地使用 key 屬性來觸發(fā)組件重新渲染

想要強制組件從頭開始重新渲染?只需更改其 key 屬性即可。

在下面的示例中,我們使用此技巧在切換到新選項卡時重置錯誤邊界。(該示例基于前面第 17 點所展示的示例改造)

Resource.tsx:

export interface ResourceProps {
  type: string;
}
const Resource: React.FC<ResourceProps> = ({ type }) => {
  const [count, setCount] = useState(0);
  const onHandleClick = () => {
    setCount((c) => c + 1);
  };

  // 新增拋出異常的代碼
  useEffect(() => {
    if (type === "tab-1") {
      throw new Error("該選項不可切換");
    }
  }, []);

  return (
    <div className="tab-content">
      你當(dāng)前在{type === "tab-1" ? "首頁" : "詳情頁"},
      <button onClick={onHandleClick} className="btn" type="button">
        點擊我
      </button>
      增加訪問{count}次數(shù)
    </div>
  );
};

App.tsx:

import { ErrorBoundary } from "react-error-boundary";

const App = () => {
  const [currentTab, setCurrentTab] = useState("tab-1");

  return (
    <>
      <Tab currentTab={currentTab} onTab={(v) => setCurrentTab(v as string)} />
      <ErrorBoundary
        fallback={<div className="error">組件渲染發(fā)生了一些錯誤</div>}
        key={currentTab}
        // 如果沒有key屬性,當(dāng)currentTab值為“tab-2”時也會呈現(xiàn)錯誤
      >
        <Resource type={currentTab} />
      </ErrorBoundary>
    </>
  );
};

完整示例代碼可前往這里查看。

22. 使用 ref 回調(diào)函數(shù)執(zhí)行諸如監(jiān)控大小變化和管理多個節(jié)點元素等任務(wù)。

你知道可以將函數(shù)傳遞給 ref 屬性而不是 ref 對象嗎?

它的工作原理如下:

  • 當(dāng) DOM 節(jié)點添加到屏幕時,React 會以 DOM 節(jié)點作為參數(shù)調(diào)用該函數(shù)。
  • 當(dāng) DOM 節(jié)點被移除時,React 會以 null 調(diào)用該函數(shù)。

在下面的示例中,我們使用此技巧跳過 useEffect。

不好的寫法: 使用 useEffect 關(guān)注輸入框焦點

const FocusInput = () => {
  const ref = useRef<HTMLInputElement>();

  useEffect(() => {
    ref.current?.focus();
  }, []);

  return <input ref={ref} type="text" />;
};

推薦寫法: 我們在輸入可用時立即聚焦輸入。

const FocusInput = () => {
  const ref = useCallback((node) => node?.focus(), []);
  return <input ref={ref} type="text" />;
};

四. 組織 react 代碼

23. 將 React 組件與其資源(例如樣式、圖像等)放在一起

始終將每個 React 組件與相關(guān)資源(如樣式和圖像)放在一起。

這樣,當(dāng)不再需要組件時,可以更輕松地刪除它們。
它還簡化了代碼導(dǎo)航,因為你需要的一切都集中在一個地方。

24. 限制組件文件大小

包含大量組件和導(dǎo)出內(nèi)容的大文件可能會令人困惑。

此外,隨著更多內(nèi)容的添加,它們往往會變得更大。

因此,請以合理的文件大小為目標(biāo),并在合理的情況下將組件拆分為單獨的文件。

25. 限制功能組件文件中的返回語句數(shù)量

功能組件中的多個返回語句使得很難看到組件返回的內(nèi)容。

對于我們可以搜索渲染術(shù)語的類組件來說,這不是問題。

一個方便的技巧是盡可能使用不帶括號的箭頭函數(shù)(VSCode 有一個針對此的操作)。

不好的寫法: 更難發(fā)現(xiàn)組件返回語句。

export interface UserInfo {
  id: string;
  name: string;
  age: number;
}
export interface UserListProps {
  users: UserInfo[];
  searchUser: string;
  onSelectUser: (u: UserInfo) => void;
}
const UserList: React.FC<UserListProps> = ({
  users,
  searchUser,
  onSelectUser,
}) => {
  // 多余return語句
  const filterUsers = users?.filter((user) => {
    return user.name.includes(searchUser);
  });

  const onSelectUserHandler = (user) => {
    // 多余return語句
    return () => {
      onSelectUser(user);
    };
  };

  return (
    <>
      <h2>用戶列表</h2>
      <ul>
        {filterUsers.map((user, index) => {
          return (
            <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}>
              <p>
                <span>用戶id</span>
                <span>{user.id}</span>
              </p>
              <p>
                <span>用戶名</span>
                <span>{user.name}</span>
              </p>
              <p>
                <span>用戶年齡</span>
                <span>{user.age}</span>
              </p>
            </li>
          );
        })}
      </ul>
    </>
  );
};

推薦寫法: 組件僅有一個返回語句。

export interface UserInfo {
  id: string;
  name: string;
  age: number;
}
export interface UserListProps {
  users: UserInfo[];
  searchUser: string;
  onSelectUser: (u: UserInfo) => void;
}
const UserList: React.FC<UserListProps> = ({
  users,
  searchUser,
  onSelectUser,
}) => {
  const filterUsers = users?.filter((user) => user.name.includes(searchUser));

  const onSelectUserHandler = (user) => () => onSelectUser(user);

  return (
    <>
      <h2>用戶列表</h2>
      <ul>
        {filterUsers.map((user, index) => (
          <li key={`${user.id}-${index}`} onClick={onSelectUserHandler(user)}>
            <p>
              <span>用戶id</span>
              <span>{user.id}</span>
            </p>
            <p>
              <span>用戶名</span>
              <span>{user.name}</span>
            </p>
            <p>
              <span>用戶年齡</span>
              <span>{user.age}</span>
            </p>
          </li>
        ))}
      </ul>
    </>
  );
};

26. 優(yōu)先使用命名導(dǎo)出而不是默認(rèn)導(dǎo)出

讓我們比較一下這兩種方法:

//默認(rèn)導(dǎo)出
export default function App() {
  // 組件內(nèi)容
}
// 命名導(dǎo)出
export function App() {
  // 組件內(nèi)容
}

我們現(xiàn)在就像如下這樣導(dǎo)入組件:

// 默認(rèn)導(dǎo)入
import App from "/path/to/App";
// 命名導(dǎo)入
import { App } from "/path/to/App";

默認(rèn)導(dǎo)出存在如下一些問題:

  • 如果組件被重命名,編輯器將不會自動重命名導(dǎo)出。

例如,如果將 App 重命名為 Index,我們將得到以下內(nèi)容:

// 默認(rèn)導(dǎo)入名字并未更改
import App from "/path/to/Index";
// 命名導(dǎo)入名字已更改
import { Index } from "/path/to/Index";
  • 很難看出從具有默認(rèn)導(dǎo)出的文件中導(dǎo)出了什么。

例如,在命名導(dǎo)入的情況下,一旦我們輸入 import { } from "/path/to/file",當(dāng)我將光標(biāo)放在括號內(nèi)時就會獲得自動完成功能。

  • 默認(rèn)導(dǎo)出很難重新再導(dǎo)出。

例如,如果我想從 index 文件重新導(dǎo)出 App 組件,我必須執(zhí)行以下操作:

export { default as App } from "/path/to/App";

使用命名導(dǎo)出的解決方案更加直接。

export { App } from "/path/to/App";

因此,建議默認(rèn)使用命名導(dǎo)出。

注意:即使你使用的是 React lazy,你仍然可以使用命名導(dǎo)出。請參閱此處的介紹示例。

五. 高效的狀態(tài)管理

27. 永遠(yuǎn)不要為可以從其他 state 或 props 派生的值創(chuàng)建新的 state

state 越多 = 麻煩越多。

每個 state 都可能觸發(fā)重新渲染,并使重置 state 變得麻煩。

因此,如果可以從 state 或 props 中派生出值,則跳過添加新的 state。

不好的做法:filteredUsers 不需要處于 state 中。

const FilterUserComponent = ({ users }) => {
  const [filters, setFilters] = useState([]);
  // 創(chuàng)建了新的state
  const [filteredUsers, setFilteredUsers] = useState([]);

  const filterUsersMethod = (filters, users) => {
    // 過濾邏輯方法
  };

  useEffect(() => {
    setFilteredUsers(filterUsersMethod(filters, users));
  }, [users, filters]);

  return (
    <Card>
      <Filters filters={filters} onChangeFilters={setFilters} />
      {filteredUsers.length > 0 && <UserList users={filteredUsers} />}
    </Card>
  );
};

推薦做法: filteredUsers 由 users 和 filters 決定。

const FilterUserComponent = ({ users }) => {
  const [filters, setFilters] = useState([]);

  const filterUsersMethod = (filters, users) => {
    // 過濾邏輯方法
  };

  const filteredUsers = filterUsersMethod(filters, users);

  return (
    <Card>
      <Filters filters={filters} onChangeFilters={setFilters} />
      {filteredUsers.length > 0 && <UserList users={filteredUsers} />}
    </Card>
  );
};

28. 將 state 創(chuàng)建在僅需要更新的組件內(nèi)部,以減少組件的重新渲染

每當(dāng)組件內(nèi)部的狀態(tài)發(fā)生變化時,React 都會重新渲染該組件及其所有子組件(包裹在 memo 中的子組件除外)。

即使這些子組件不使用已更改的狀態(tài),也會發(fā)生這種情況。為了最大限度地減少重新渲染,請盡可能將狀態(tài)移到組件樹的下方。

不好的做法: 當(dāng) type 發(fā)生改變時,會使不依賴 type 狀態(tài)的 LeftList 和 RightList 組件也觸發(fā)重新渲染。

const App = () => {
  const [type, setType] = useState("");

  return (
    <Container>
      <LeftList />
      <Main type={type} setType={setType} />
      <RightList />
    </Container>
  );
};

const mainBtnList = [
  {
    label: "首頁",
    value: "home",
  },
  {
    label: "詳情頁",
    value: "detail",
  },
];
const Main = ({ type, setType }) => {
  return (
    <>
      {mainBtnList.map((item, index) => (
        <Button
          className={`${type.value === type ? "active" : ""}`}
          key={`${item.value}-${index}`}
          onClick={() => setType(item.value)}
        >
          {item.label}
        </Button>
      ))}
    </>
  );
};

推薦做法: 將狀態(tài)耦合到 Main 組件內(nèi)部,僅影響 Main 組件的重新渲染。

const App = () => {
  return (
    <Container>
      <LeftList />
      <Main />
      <RightList />
    </Container>
  );
};

const mainBtnList = [
  {
    label: "首頁",
    value: "home",
  },
  {
    label: "詳情頁",
    value: "detail",
  },
];
const Main = () => {
  const [type, setType] = useState("");
  return (
    <>
      {mainBtnList.map((item, index) => (
        <Button
          className={`${type.value === type ? "active" : ""}`}
          key={`${item.value}-${index}`}
          onClick={() => setType(item.value)}
        >
          {item.label}
        </Button>
      ))}
    </>
  );
};

29. 定義需要明確初始狀態(tài)和當(dāng)前狀態(tài)的區(qū)別

不好的做法: 不清楚 userInfo 只是初始值,這可能會導(dǎo)致狀態(tài)管理的混亂或錯誤。

const UserInfo = ({ userInfo }) => {
  const [userInfo, setUserInfo] = useState(userInfo);

  return (
    <Card>
      <Title>當(dāng)前用戶: {userInfo?.name}</Title>
      <UserInfoDetail detail={userInfo?.detail} />
    </Card>
  );
};

推薦做法: 命名可以清楚地表明什么是初始狀態(tài),什么是當(dāng)前狀態(tài)。

const UserInfo = ({ initialUserInfo }) => {
  const [userInfo, setUserInfo] = useState(initialUserInfo);

  return (
    <Card>
      <Title>當(dāng)前用戶: {userInfo?.name}</Title>
      <UserInfoDetail detail={userInfo?.detail} />
    </Card>
  );
};

30. 根據(jù)之前的狀態(tài)更新狀態(tài),尤其是在使用 useCallback 進行緩存時

React 允許你將更新函數(shù)從 useState 傳遞給 set 函數(shù)。

此更新函數(shù)使用當(dāng)前狀態(tài)來計算下一個狀態(tài)。

每當(dāng)需要根據(jù)之前狀態(tài)更新狀態(tài)時,都可以使用此行為,尤其是在使用 useCallback 包裝的函數(shù)內(nèi)部。事實上,這種方法可以避免將狀態(tài)作為鉤子依賴項之一。

不好的做法: 無論什么時候,當(dāng) todoList 變化的時候,onHandleAddTodo 和 onHandleRemoveTodo 都會跟著改變。

const App = () => {
  const [todoList, setTodoList] = useState([]);

  const onHandleAddTodo = useCallback(
    (todo) => {
      setTodoList([...todoList, todo]);
    },
    [todoList]
  );

  const onHandleRemoveTodo = useCallback(
    (todo) => {
      setTodoList([...todoList].filter((item) => item.id !== todo.id));
    },
    [todoList]
  );
  return (
    <div className="App">
      <TodoInput onAddTodo={onHandleAddTodo} />
      <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} />
    </div>
  );
};

推薦做法: 即使 todoList 發(fā)生變化,onHandleAddTodo 和 onHandleRemoveTodo 仍然保持不變。

const App = () => {
  const [todoList, setTodoList] = useState([]);

  const onHandleAddTodo = useCallback((todo) => {
    setTodoList((prevTodoList) => [...prevTodoList, todo]);
  }, []);

  const onHandleRemoveTodo = useCallback((todo) => {
    setTodoList((prevTodoList) =>
      [...prevTodoList].filter((item) => item.id !== todo.id)
    );
  }, []);
  return (
    <div className="App">
      <TodoInput onAddTodo={onHandleAddTodo} />
      <TodoList todoList={todoList} onRemoveTodo={onHandleRemoveTodo} />
    </div>
  );
};

31. 使用 useState 中的函數(shù)進行延遲初始化并提高性能,因為它們只被調(diào)用一次。

在 useState 中使用函數(shù)可確保初始狀態(tài)僅計算一次。

這可以提高性能,尤其是當(dāng)初始狀態(tài)來自“昂貴”操作(例如從本地存儲讀取)時。

不好的做法:每次組件渲染時,我們都會從本地存儲讀取主題。

const THEME_LOCAL_STORAGE_KEY = "page_theme_key";

const Theme = ({ theme, onChangeTheme }) => {
  // ....
};

const App = ({ children }) => {
  const [theme, setTheme] = useState(
    localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const onChangeTheme = (theme: string) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div className={`app${theme ? ` ${theme}` : ""}`}>
      <Theme onChange={onChangeTheme} theme={theme} />
      <div>{children}</div>
    </div>
  );
};

推薦做法: 當(dāng)組件掛載時,我們僅只會讀取本地存儲一次。

// ...

const App = ({ children }) => {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || "dark"
  );

  const onChangeTheme = (theme: string) => {
    setTheme(theme);
    localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
  };

  return (
    <div className={`app${theme ? ` ${theme}` : ""}`}>
      <Theme onChange={onChangeTheme} theme={theme} />
      <div>{children}</div>
    </div>
  );
};

32. 使用 React 上下文來處理廣泛需要的靜態(tài)狀態(tài),以防止 prop 鉆取

每當(dāng)我有一些數(shù)據(jù)時,我都會使用 React 上下文:

  • 在多個地方都需要(例如,主題、當(dāng)前用戶等)
  • 主要是靜態(tài)或只讀的(即,用戶不能/不會經(jīng)常更改數(shù)據(jù))
  • 這種方法有助于避免 prop 鉆取(即,通過組件層次結(jié)構(gòu)的多個層傳遞數(shù)據(jù)或狀態(tài))。

來看一個示例的部分代碼:

context.ts

// UserInfo接口來自測試數(shù)據(jù)

export const userInfoContext = createContext<string | UserInfoData>("loading");

export const useUserInfo = <T extends UserInfoData>() => {
  const value = useContext(userInfoContext);
  if (value == null) {
    throw new Error("Make sure to wrap the userInfoContext inside provider");
  }
  return value as T;
};

App.tsx

function App() {
  const [userInfoData, setUserInfoData] = useState<UserInfoData | string>(
    "loading"
  );

  useEffect(() => {
    getCurrentUser().then(setUserInfoData);
  }, []);

  if (userInfoData === "loading") {
    return <Loading />;
  }

  return (
    <div className="app">
      <userInfoContext.Provider value={userInfoData}>
        <Header />
        <Sidebar />
        <Main />
      </userInfoContext.Provider>
    </div>
  );
}

header.tsx:

const Header: React.FC<HeaderProps> = (props) => {
  // 使用context
  const userInfo = useUserInfo();
  return (
    <header className="header" {...props}>
      歡迎回來{userInfo?.name}
    </header>
  );
};

main.tsx:

const Main: React.FC<MainProps> = ({ title }) => {
  const { posts } = useUserInfo();
  return (
    <div className="main">
      <h2 className="title">{title}</h2>
      <ul className="list">
        {posts?.map((post, index) => (
          <li className="list-item" key={`${post.id}-${index}`}>
            {post.title}
          </li>
        ))}
      </ul>
    </div>
  );
};

33. React Context:將 react 上下文分為經(jīng)常變化的部分和不經(jīng)常變化的部分,以提高應(yīng)用程序性能

React 上下文的一個挑戰(zhàn)是,只要上下文數(shù)據(jù)發(fā)生變化,所有使用該上下文的組件都會重新渲染,即使它們不使用發(fā)生變化的上下文部分。

解決方案是什么?使用單獨的上下文。

在下面的示例中,我們創(chuàng)建了兩個上下文:一個用于操作(常量),另一個用于狀態(tài)(可以更改)。

export interface TodosInfoItem {
  id?: string;
  title?: string;
  completed?: boolean;
}
export interface TodosInfo {
  search?: string;
  todos: TodosInfoItem[];
}

export const todosStateContext = createContext<TodosInfo>(void 0);
export const todosActionContext = createContext<Dispatch<ReducerActionParams>>(
  void 0
);

export interface ReducerActionParams extends TodosInfoItem {
  type?: string;
  value?: string;
}
export const getTodosReducer = (
  state: TodosInfo,
  action: ReducerActionParams
) => {
  switch (action.type) {
    case TodosActionType.ADD_TODO:
      return {
        ...state,
        todos: [
          ...state.todos,
          {
            id: crypto.randomUUID(),
            title: action.title,
            completed: false,
          },
        ],
      };
    case TodosActionType.REMOVE_TODO:
      return {
        ...state,
        todos: [...state.todos].filter((item) => item.id !== action.id),
      };
    case TodosActionType.TOGGLE_TODO_STATUS:
      return {
        ...state,
        todos: [...state.todos].map((item) =>
          item.id === action.id ? { ...item, completed: !item.completed } : item
        ),
      };
    case TodosActionType.SET_SEARCH_TERM:
      return {
        ...state,
        search: action.value,
      };
    default:
      return state;
  }
};

完整示例代碼前往這里查看。

34. React Context:當(dāng)值計算不直接時,引入 Provider 組件

不好的做法:App 內(nèi)部有太多邏輯來管理 theme context。

const THEME_LOCAL_STORAGE_KEY = "current-project-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
  theme: DEFAULT_THEME,
  setTheme: () => null,
});

const App = () => {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
  );

  useEffect(() => {
    if (theme !== "system") {
      updateRootElementTheme(theme);
      return;
    }
    // 我們需要根據(jù)系統(tǒng)主題獲取要應(yīng)用的主題類
    const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
      .matches
      ? "dark"
      : "light";

    updateRootElementTheme(systemTheme);
    // 然后觀察系統(tǒng)主題的變化并相應(yīng)地更新根元素
    const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
    const listener = (event) => {
      updateRootElementTheme(event.matches ? "dark" : "light");
    };
    darkThemeMq.addEventListener("change", listener);
    return () => darkThemeMq.removeEventListener("change", listener);
  }, [theme]);

  const themeContextValue = {
    theme,
    setTheme: (theme) => {
      localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
      setTheme(theme);
    },
  };

  const [selectedUserId, setSelectedUserId] = useState(undefined);
  const onUserSelect = (id) => {
    // 待做:一些邏輯
    setSelectedUserId(id);
  };

  const users = useSWR("/api/users", fetcher);

  return (
    <div className="App">
      <ThemeContext.Provider value={themeContextValue}>
        <UserList
          users={users}
          onUserSelect={onUserSelect}
          selectedUserId={selectedUserId}
        />
      </ThemeContext.Provider>
    </div>
  );
};

推薦:主題 context 相關(guān)的邏輯封裝在 ThemeProvider 中。

const THEME_LOCAL_STORAGE_KEY = "current-project-theme";
const DEFAULT_THEME = "light";
const ThemeContext = createContext({
  theme: DEFAULT_THEME,
  setTheme: () => null,
});

const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState(
    () => localStorage.getItem(THEME_LOCAL_STORAGE_KEY) || DEFAULT_THEME
  );

  useEffect(() => {
    if (theme !== "system") {
      updateRootElementTheme(theme);
      return;
    }
    // 我們需要根據(jù)系統(tǒng)主題獲取要應(yīng)用的主題類
    const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
      .matches
      ? "dark"
      : "light";

    updateRootElementTheme(systemTheme);
    // 然后觀察系統(tǒng)主題的變化并相應(yīng)地更新根元素
    const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
    const listener = (event) => {
      updateRootElementTheme(event.matches ? "dark" : "light");
    };
    darkThemeMq.addEventListener("change", listener);
    return () => darkThemeMq.removeEventListener("change", listener);
  }, [theme]);

  const themeContextValue = {
    theme,
    setTheme: (theme) => {
      localStorage.setItem(THEME_LOCAL_STORAGE_KEY, theme);
      setTheme(theme);
    },
  };

  return (
    <div className="App">
      <ThemeContext.Provider value={themeContextValue}>
        {children}
      </ThemeContext.Provider>
    </div>
  );
};

const App = () => {
  const [selectedUserId, setSelectedUserId] = useState(undefined);
  const onUserSelect = (id) => {
    // 待做:一些邏輯
    setSelectedUserId(id);
  };

  const users = useSWR("/api/users", fetcher);

  return (
    <div className="App">
      <ThemeProvider>
        <UserList
          users={users}
          onUserSelect={onUserSelect}
          selectedUserId={selectedUserId}
        />
      </ThemeProvider>
    </div>
  );
};

35. 考慮使用 useReducer hook 作為輕量級狀態(tài)管理解決方案

每當(dāng)我的狀態(tài)或復(fù)雜狀態(tài)中的值太多并且不想依賴外部庫時,我都會使用 useReducer。

當(dāng)與上下文結(jié)合使用時,它對于更廣泛的狀態(tài)管理需求特別有效。

示例:這里。

36. 使用 useImmer 或 useImmerReducer 簡化狀態(tài)更新

使用 useState 和 useReducer 等鉤子時,狀態(tài)必須是不可變的(即,所有更改都需要創(chuàng)建新狀態(tài),而不是修改當(dāng)前狀態(tài))。

這通常很難實現(xiàn)。

這就是 useImmer 和 useImmerReducer 提供更簡單的替代方案的地方。它們允許你編寫自動轉(zhuǎn)換為不可變更新的“可變”代碼。

不好的做法: 我們必須小心確保我們正在創(chuàng)建一個新的狀態(tài)對象。

export const App = () => {
  const [{ email, password }, setState] = useState({
    email: "",
    password: "",
  });
  const onEmailChange = (event) => {
    setState((prevState) => ({ ...prevState, email: event.target.value }));
  };
  const onPasswordChange = (event) => {
    setState((prevState) => ({ ...prevState, password: event.target.value }));
  };

  return (
    <div className="App">
      <h1>歡迎登陸</h1>
      <div class="form-item">
        <label>郵箱號: </label>
        <input type="email" value={email} onChange={onEmailChange} />
      </div>
      <div className="form-item">
        <label>密碼:</label>
        <input type="password" value={password} onChange={onPasswordChange} />
      </div>
    </div>
  );
};

推薦做法: 更直接一點,我們可以直接修改 draftState。

import { useImmer } from "use-immer";

export const App = () => {
  const [{ email, password }, setState] = useImmer({
    email: "",
    password: "",
  });
  const onEmailChange = (event) => {
    setState((draftState) => {
      draftState.email = event.target.value;
    });
  };
  const onPasswordChange = (event) => {
    setState((draftState) => {
      draftState.password = event.target.value;
    });
  };

  // 剩余代碼
};

37. 使用 Redux(或其他狀態(tài)管理解決方案)來跨多個組件訪問復(fù)雜的客戶端狀態(tài)

每當(dāng)出現(xiàn)以下情況時,我都會求助于 Redux:

我有一個復(fù)雜的 FE 應(yīng)用程序,其中包含大量共享的客戶端狀態(tài)(例如,儀表板應(yīng)用程序)

  • 我希望用戶能夠回到過去并恢復(fù)更改。
  • 我不希望我的組件像使用 React 上下文那樣不必要地重新渲染。
  • 我有太多開始難以控制的上下文。

為了獲得簡化的體驗,我建議使用 redux-tooltkit。

?? 注意:你還可以考慮 Redux 的其他替代方案,例如 Zustand 或 Recoil。

38. Redux:使用 Redux DevTools 調(diào)試你的狀態(tài)

Redux DevTools 瀏覽器擴展是調(diào)試 Redux 項目的有用工具。

它允許你實時可視化你的狀態(tài)和操作,在刷新時保持狀態(tài)持久性等等。

要了解它的用途,請觀看這個精彩的視頻。

六. React 代碼優(yōu)化

39. 使用 memo 防止不必要的重新渲染

當(dāng)處理渲染成本高昂且父組件頻繁更新的組件時,將它們包裝在 memo 中可能會改變渲染規(guī)則。

memo 確保組件僅在其 props 發(fā)生變化時重新渲染,而不僅僅是因為其父組件重新渲染。

在以下示例中,我通過 useGetInfoData 從服務(wù)器獲取一些數(shù)據(jù)。如果數(shù)據(jù)沒有變化,將 UserInfoList 包裝在 memo 中將阻止它在數(shù)據(jù)的其他部分更新時重新渲染。

export const App = () => {
  const { currentUserInfo, users } = useGetInfoData();
  return (
    <div className="App">
      <h1>信息面板</h1>
      <CurrentUserInfo data={currentUserInfo} />
      <UserInfoList users={users} />
    </div>
  );
};

const UserInfoList = memo(({ users }) => {
  // 剩余實現(xiàn)
});
一旦 React 編譯器變得穩(wěn)定,這個小技巧可能就不再有用了。

40. 用 memo 指定一個相等函數(shù)來指示 React 如何比較 props。

默認(rèn)情況下,memo 使用Object.is將每個 prop 與其先前的值進行比較。

但是,對于更復(fù)雜或特定的場景,指定自定義相等函數(shù)可能比默認(rèn)比較或重新渲染更有效。

示例如下:

const UserList = memo(
  ({ users }) => {
    return <div>{JSON.stringify(users)}</div>;
  },
  (prevProps, nextProps) => {
    // 僅當(dāng)最后一個用戶或列表大小發(fā)生變化時才重新渲染
    const prevLastUser = prevProps.users[prevProps.users.length - 1];
    const nextLastUser = nextProps.users[nextProps.users.length - 1];
    return (
      prevLastUser.id === nextLastUser.id &&
      prevProps.users.length === nextProps.users.length
    );
  }
);

41.聲明緩存組件時,優(yōu)先使用命名函數(shù)而不是箭頭函數(shù)

定義緩存組件時,使用命名函數(shù)而不是箭頭函數(shù)可以提高 React DevTools 中的清晰度。

箭頭函數(shù)通常會導(dǎo)致像 _c2 這樣的通用名稱,這會使調(diào)試和分析更加困難。

不好的做法:對緩存組件使用箭頭函數(shù)會導(dǎo)致 React DevTools 中的名稱信息量較少。

const UserInfoList = memo(({ users }) => {
  // 剩余實現(xiàn)邏輯
});

推薦做法: 該組件的名稱將在 DevTools 中可見。

const UserInfoList = memo(function UserInfoList({ users }) {
  // 剩余實現(xiàn)邏輯
});

42. 使用 useMemo 緩存昂貴的計算或保留引用

我通常會使用 useMemo:

  • 當(dāng)我有昂貴的計算,不應(yīng)該在每次渲染時重復(fù)這些計算時。
  • 如果計算值是非原始值,用作 useEffect 等鉤子中的依賴項。
  • 計算出的非原始值將作為 prop 傳遞給包裹在 memo 中的組件;否則,這將破壞緩存,因為 React 使用 Object.is 來檢測 props 是否發(fā)生變化。

不好的做法:UserInfoList 的 memo 不會阻止重新渲染,因為每次渲染時都會重新創(chuàng)建樣式。

export const UserInfo = () => {
  const { profileInfo, users, baseStyles } = useGetUserInfoData();
  // 每次重新渲染我們都會得到一個樣式對象
  const styles = { ...baseStyles, margin: 10 };
  return (
    <div className="App">
      <h1>用戶頁</h1>
      <Profile data={profileInfo} />
      <UserInfoList users={users} styles={styles} />
    </div>
  );
};

const UserInfoList = memo(function UserInfoListFn({ users, styles }) {
  /// 剩余實現(xiàn)
});

推薦做法: useMemo 的使用確保只有當(dāng) baseStyles 發(fā)生變化時,styles 才會發(fā)生變化,從而使 memo 能夠有效防止不必要的重新渲染。

export const UserInfo = () => {
  const { profileInfo, users, baseStyles } = useGetUserInfoData();
  // 每次重新渲染我們都會得到一個樣式對象
  const styles = useMemo(() => ({ ...baseStyles, margin: 10 }), [baseStyles]);
  return (
    <div className="App">
      <h1>用戶頁</h1>
      <Profile data={profileInfo} />
      <UserInfoList users={users} styles={styles} />
    </div>
  );
};

const UserInfoList = memo(function UserInfoListFn({ users, styles }) {
  /// 剩余實現(xiàn)
});

43. 使用 useCallback 緩存函數(shù)

useCallback 與 useMemo 類似,但專為緩存函數(shù)而設(shè)計。

不好的做法:每當(dāng) theme 發(fā)生變化時,handleThemeChange 都會被調(diào)用兩次,并且我們會將日志推送到服務(wù)器兩次。

const useTheme = () => {
  const [theme, setTheme] = useState("light");

  // 每次渲染`handleThemeChange`都會改變
  // 因此,每次渲染后都會觸發(fā)該效果
  const handleThemeChange = (newTheme) => {
    sendLog(["Theme changed"], {
      context: {
        theme: newTheme,
      },
    });
    setTheme(newTheme);
  };

  useEffect(() => {
    const dqMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
    handleThemeChange(dqMediaQuery.matches ? "dark" : "light");
    const listener = (event) => {
      handleThemeChange(event.matches ? "dark" : "light");
    };
    dqMediaQuery.addEventListener("change", listener);
    return () => {
      dqMediaQuery.removeEventListener("change", listener);
    };
  }, [handleThemeChange]);

  return theme;
};

推薦做法:將 handleThemeChange 包裝在 useCallback 中可確保僅在必要時重新創(chuàng)建它,從而減少不必要的執(zhí)行。

const handleThemeChange = useCallback((newTheme) => {
  sendLog(["Theme changed"], {
    context: {
      theme: newTheme,
    },
  });
  setTheme(newTheme);
}, []);

44. 緩存回調(diào)函數(shù)或使用程序鉤子返回的值以避免性能問題

當(dāng)你創(chuàng)建自定義鉤子與他人共享時,記住返回的值和函數(shù)至關(guān)重要。

這種做法可以使你的鉤子更高效,并防止任何使用它的人出現(xiàn)不必要的性能問題。

不好的做法:loadData 沒有被緩存并產(chǎn)生了性能問題。

const useLoadData = (fetchData) => {
  const [result, setResult] = useState({
    type: "pending",
  });

  const loadData = async () => {
    setResult({ type: "loading" });
    try {
      const data = await fetchData();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  };

  return { result, loadData };
};

推薦做法: 我們緩存所有內(nèi)容,因此不會出現(xiàn)意外的性能問題。

const useLoadData = (fetchData) => {
  const [result, setResult] = useState({
    type: "pending",
  });

  // 包裹在 `useRef` 中并使用 `ref` 值,這樣函數(shù)就不會改變
  const fetchDataRef = useRef(fetchData);

  useEffect(() => {
    fetchDataRef.current = fetchData;
  }, [fetchData]);

  // 包裹在 `useCallback` 中并使用 `ref` 值,這樣函數(shù)就不會改變
  const loadData = useCallback(async () => {
    setResult({ type: "loading" });
    try {
      const data = await fetchDataRef.current();
      setResult({ type: "loaded", data });
    } catch (err) {
      setResult({ type: "error", error: err });
    }
  }, []);

  // 使用useMemo緩存值
  return useMemo(() => ({ result, loadData }), [result, loadData]);
};

45. 利用懶加載和 Suspense 讓你的應(yīng)用加載更快

構(gòu)建應(yīng)用時,請考慮對以下代碼使用懶加載和 Suspense:

  • 加載成本高。
  • 僅與某些用戶相關(guān)(如高級功能)。
  • 對于初始用戶交互而言并非立即需要。

在下面的示例,Slider 資源(JS + CSS)僅在你單擊卡片后加載。

//...
const LazyLoadedSlider = lazy(() => import("./Slider"));
//...
const App = () => {
  // ....
  return (
    <div className="container">
      {/* .... */}
      {selectedUser != null && (
        <Suspense fallback={<div>Loading...</div>}>
          <LazyLoadedSlider
            avatar={selectedUser.avatar}
            name={selectedUser.name}
            address={selectedUser.address}
            onClose={closeSlider}
          />
        </Suspense>
      )}
    </div>
  );
};

46. 限制網(wǎng)絡(luò)以模擬慢速網(wǎng)絡(luò)

你知道可以直接在 Chrome 中模擬慢速互聯(lián)網(wǎng)連接嗎?

這在以下情況下尤其有用:

  • 用戶報告加載時間緩慢,而你無法在更快的網(wǎng)絡(luò)上復(fù)制。
  • 你正在實施懶加載,并希望觀察文件在較慢條件下的加載方式,以確保適當(dāng)?shù)募虞d狀態(tài)。

47. 使用 react-window 或 react-virtuoso 高效渲染列表

切勿一次性渲染一長串項目,例如聊天消息、日志或無限列表。

這樣做可能會導(dǎo)致瀏覽器卡死崩潰。相反,可以使用虛擬化列表,這意味著僅渲染可能對用戶可見的項目子集。

react-window、react-virtuoso 或 @tanstack/react-virtual 等庫就是為此目的而設(shè)計的。

不好的做法:NonVirtualList 會同時呈現(xiàn)所有 50,000 條日志行,即使它們不可見。

const NonVirtualList = ({ items }: { items: LogLineItem[] }) => {
  return (
    <div style={{ height: "100%" }}>
      {items?.map((log, index) => (
        <div
          key={log.id}
          style={{
            padding: "5px",
            borderBottom:
              index === items.length - 1 ? "none" : "1px solid #535455",
          }}
        >
          <LogLine log={log} index={index} />
        </div>
      ))}
    </div>
  );
};

推薦做法: VirtualList 僅渲染可能可見的項目。

const VirtualList = ({ items }: { items: LogLineItem[] }) => {
  return (
    <Virtuoso
      style={{ height: "100%" }}
      data={items}
      itemContent={(index, log) => (
        <div
          key={log.id}
          style={{
            padding: "5px",
            borderBottom:
              index === items.length - 1 ? "none" : "1px solid #535455",
          }}
        >
          <LogLine log={log} index={index} />
        </div>
      )}
    />
  );
};

你可以在這個完整的示例中在兩個選項之間切換,并注意使用 NonVirtualList 時應(yīng)用程序的性能有多糟糕。

七. 總結(jié)

到此這篇關(guān)于React組件、狀態(tài)管理、代碼優(yōu)化的技巧的文章就介紹到這了,更多相關(guān)React實踐小技巧內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • React?diff算法超詳細(xì)講解

    React?diff算法超詳細(xì)講解

    渲染真實DOM的開銷很大,有時候我們修改了某個數(shù)據(jù),直接渲染到真實dom上會引起整個dom樹的重繪和重排。我們希望只更新我們修改的那一小塊dom,而不是整個dom,diff算法就幫我們實現(xiàn)了這點。diff算法的本質(zhì)就是:找出兩個對象之間的差異,目的是盡可能做到節(jié)點復(fù)用
    2022-11-11
  • React Hooks獲取數(shù)據(jù)實現(xiàn)方法介紹

    React Hooks獲取數(shù)據(jù)實現(xiàn)方法介紹

    這篇文章主要介紹了react hooks獲取數(shù)據(jù),文中給大家介紹了useState dispatch函數(shù)如何與其使用的Function Component進行綁定,實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下
    2022-10-10
  • 詳解使用React全家桶搭建一個后臺管理系統(tǒng)

    詳解使用React全家桶搭建一個后臺管理系統(tǒng)

    本篇文章主要介紹了使用React全家桶搭建一個后臺管理系統(tǒng),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-11-11
  • 如何強制刷新react hooks組件

    如何強制刷新react hooks組件

    這篇文章主要介紹了如何強制刷新react hooks組件問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-11-11
  • React-Native中禁用Navigator手勢返回的示例代碼

    React-Native中禁用Navigator手勢返回的示例代碼

    本篇文章主要介紹了React-Native中禁用Navigator手勢返回的示例代碼,具有一定的參考價值,有興趣的可以了解一下
    2017-09-09
  • 通過示例源碼解讀React首次渲染流程

    通過示例源碼解讀React首次渲染流程

    這篇文章主要為大家通過示例源碼解讀React的首次渲染流程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-03-03
  • useEffect中return函數(shù)的作用和執(zhí)行時機解讀

    useEffect中return函數(shù)的作用和執(zhí)行時機解讀

    這篇文章主要介紹了useEffect中return函數(shù)的作用和執(zhí)行時機,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-01-01
  • react?事項懶加載的三種方法及使用場景

    react?事項懶加載的三種方法及使用場景

    這篇文章主要介紹了react?事項懶加載的三種方法及使用場景,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-07-07
  • react源碼層探究setState作用

    react源碼層探究setState作用

    寫react的時候,踩了幾次坑發(fā)現(xiàn)setstate之后state不會立刻更新,于是判定setstate就是異步的方法,但是直到有一天,我想立刻拿到更新的state去傳參另一個方法的時候,才問自己,為什么setstate是異步的?準(zhǔn)確地說,在React內(nèi)部機制能檢測到的地方,setState就是異步的
    2022-10-10
  • JavaScript的React Web庫的理念剖析及基礎(chǔ)上手指南

    JavaScript的React Web庫的理念剖析及基礎(chǔ)上手指南

    這篇文章主要介紹了JavaScript的React Web庫的理念剖析及基礎(chǔ)上手指南,React Web的目的即是把本地的React Native應(yīng)用程序項目變?yōu)閃eb應(yīng)用程序,需要的朋友可以參考下
    2016-05-05

最新評論