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

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

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

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

一. 組件相關(guān)

1. 使用自閉合組件

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

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

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

不好的寫法:使用 div 會(huì)使 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 (
    // 如果元素不需要添加屬性,則可以使用簡(jiǎn)寫形式<></>
    <Fragment>
      <Header />
      <Content />
      <Footer />
    </Fragment>
  );
};

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

不好寫法:下面的代碼有點(diǎn)冗余。

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

推薦寫法:

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

除非你需要一個(gè) 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,而不是單獨(dú)訪問每個(gè) props

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

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;

推薦寫法: 下面的代碼更加簡(jiǎn)潔。

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)值時(shí),在解構(gòu)時(shí)進(jìn)行

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

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

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

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

6. 傳遞字符串類型屬性時(shí)刪除花括號(hào)。

不好的寫法:帶花括號(hào)的寫法

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

推薦寫法: 不需要花括號(hào)

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

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

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

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

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

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 <>暫無(wú)數(shù)據(jù)</>;
  }
  let totalCount = 0;
  let totalPrice = 0;
  goods.forEach((good) => {
    totalCount += good.count;
    totalPrice += good.price;
  });
  return (
    <>
      總數(shù)量:{totalCount};總價(jià):{totalPrice}
    </>
  );
};

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

const GoodList = ({ goods }) => {
  if (goods.length === 0) {
    return <>暫無(wú)數(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};總價(jià):{totalPrice}
    </>
  );
};

個(gè)人更喜歡的寫法: 封裝成 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 <>暫無(wú)數(shù)據(jù)</>;
  }
  const { totalCount, totalPrice } = useTotalGoods(goods);
  return (
    <>
      總數(shù)量:{totalCount};總價(jià):{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="請(qǐng)輸入用戶名"
            value={username}
            onChange={(e) =>
              setFormUserState((state) => ({
                ...state,
                username: e.target.value,
              }))
            }
          />
        </div>
        <div class="form-item">
          <label>密碼:</label>
          <input
            placeholder="請(qǐng)輸入密碼"
            value={username}
            type="password"
            onChange={(e) =>
              setFormUserState((state) => ({
                ...state,
                password: e.target.value,
              }))
            }
          />
        </div>
      </form>
    </>
  );
};

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

筆記: 如果你啟用了 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="請(qǐng)輸入用戶名"
            value={username}
            onChange={createFormValueChangeHandler("username")}
          />
        </div>
        <div class="form-item">
          <label>密碼:</label>
          <input
            placeholder="請(qǐng)輸入密碼"
            value={username}
            type="password"
            onChange={createFormValueChangeHandler("password")}
          />
        </div>
      </form>
    </>
  );
};

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

不好的寫法: OPTIONS 和 renderOption 不需要位于組件內(nèi)部,因?yàn)樗鼈儾灰蕾嚾魏?props 或狀態(tài)。此外,將它們保留在內(nèi)部意味著每次組件渲染時(shí)我們都會(huì)獲得新的對(duì)象引用。如果我們將 renderOption 傳遞給包裹在 memo 中的子組件,則會(huì)破壞緩存功能。

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

11. 存儲(chǔ)列表組件中選定的對(duì)象時(shí),存儲(chǔ)對(duì)象 ID,而不是整個(gè)對(duì)象

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

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

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

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

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

不好的寫法: 由于 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}
        />
      ))}
    </>
  );
};

推薦寫法: 我們引入了一個(gè)新組件 FormLists,它采用定義的表單項(xiàng)組件并且更加簡(jiǎn)潔。

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)和上下文都位于頂部時(shí),很容易發(fā)現(xiàn)哪些因素會(huì)觸發(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="請(qǐng)輸入用戶名"
          />
        </div>
        <div class="login-form-item">
          <label>密碼:</label>
          <input
            value={password}
            onChange={onHandleChangePassword}
            placeholder="請(qǐng)輸入密碼"
            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="請(qǐng)輸入用戶名"
          />
        </div>
        <div class="login-form-item">
          <label>密碼:</label>
          <input
            value={password}
            onChange={onHandleChangePassword}
            placeholder="請(qǐng)輸入密碼"
            type="password"
          />
        </div>
      </form>
    </div>
  );
};

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

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

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

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

不好的寫法: 每當(dāng) Timer 渲染時(shí),OtherSlowComponent 都會(huì)渲染,每次當(dāng)前時(shí)間更新時(shí)都會(huì)發(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)前時(shí)間:{dayjs(time).format("YYYY-MM-DD HH:mm:ss")}</h1>
      <OtherSlowComponent />
    </>
  );
};

推薦寫法: Timer 呈現(xiàn)時(shí),OtherSlowComponent 不會(huì)呈現(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)前時(shí)間:{dayjs(time).formate("YYYY-MM-DD HH:mm:ss")}</h1>
      {children}
    </>
  );
};

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

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

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

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

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

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

不好的寫法:選項(xiàng) 1。

List 處理每個(gè)項(xiàng)目的渲染及其排序方式。這是有問題的,因?yàn)樗`反了開放封閉原則。每當(dāng)添加新的項(xiàng)目類型時(shí),此代碼都會(huì)被修改。

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>
                        加入時(shí)間: {(item as UserItem).date.toDateString()}
                      </em>
                    </>
                  );
              }
            })()}
          </li>
        ))}
      </ul>
    </>
  );
};

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

推薦寫法:選項(xiàng) 2。

List 采用渲染函數(shù)或組件函數(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; // 列表項(xiàng)
  itemCompare: (a: T, b: T) => number; // 列表項(xiàng)自定義排序函數(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. 處理不同情況時(shí),使用 value === case && <Component /> 以避免保留舊狀態(tài)

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

tab.tsx:

const tabList = [
  {
    label: "首頁(yè)",
    value: "tab-1",
  },
  {
    label: "詳情頁(yè)",
    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" ? "首頁(yè)" : "詳情頁(yè)"},
      <button onClick={onHandleClick} className="btn" type="button">
        點(diǎn)擊我
      </button>
      增加訪問{count}次數(shù)
    </div>
  );
};

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

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. 始終使用錯(cuò)誤邊界處理組件渲染錯(cuò)誤

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

為了防止這種情況,請(qǐng)使用錯(cuò)誤邊界來:

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

三. key 與 ref

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

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

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

注意:請(qǐng)注意,舊版瀏覽器中未定義 crypto.randomUUID。

20. 確保你的列表項(xiàng) id 是穩(wěn)定的(即:它們?cè)阡秩局惺遣粫?huì)發(fā)生變化的)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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("該選項(xiàng)不可切換");
    }
  }, []);

  return (
    <div className="tab-content">
      你當(dāng)前在{type === "tab-1" ? "首頁(yè)" : "詳情頁(yè)"},
      <button onClick={onHandleClick} className="btn" type="button">
        點(diǎn)擊我
      </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ā)生了一些錯(cuò)誤</div>}
        key={currentTab}
        // 如果沒有key屬性,當(dāng)currentTab值為“tab-2”時(shí)也會(huì)呈現(xiàn)錯(cuò)誤
      >
        <Resource type={currentTab} />
      </ErrorBoundary>
    </>
  );
};

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

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

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

它的工作原理如下:

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

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

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

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

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

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

推薦寫法: 我們?cè)谳斎肟捎脮r(shí)立即聚焦輸入。

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

四. 組織 react 代碼

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

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

這樣,當(dāng)不再需要組件時(shí),可以更輕松地刪除它們。
它還簡(jiǎn)化了代碼導(dǎo)航,因?yàn)槟阈枰囊磺卸技性谝粋€(gè)地方。

24. 限制組件文件大小

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

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

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

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

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

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

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

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

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語(yǔ)句
  const filterUsers = users?.filter((user) => {
    return user.name.includes(searchUser);
  });

  const onSelectUserHandler = (user) => {
    // 多余return語(yǔ)句
    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>
    </>
  );
};

推薦寫法: 組件僅有一個(gè)返回語(yǔ)句。

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)出存在如下一些問題:

  • 如果組件被重命名,編輯器將不會(huì)自動(dòng)重命名導(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)放在括號(hào)內(nèi)時(shí)就會(huì)獲得自動(dòng)完成功能。

  • 默認(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)出。請(qǐng)參閱此處的介紹示例。

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

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

state 越多 = 麻煩越多。

每個(gè) 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ā)生變化時(shí),React 都會(huì)重新渲染該組件及其所有子組件(包裹在 memo 中的子組件除外)。

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

不好的做法: 當(dāng) type 發(fā)生改變時(shí),會(huì)使不依賴 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: "首頁(yè)",
    value: "home",
  },
  {
    label: "詳情頁(yè)",
    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: "首頁(yè)",
    value: "home",
  },
  {
    label: "詳情頁(yè)",
    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 只是初始值,這可能會(huì)導(dǎo)致狀態(tài)管理的混亂或錯(cuò)誤。

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 進(jìn)行緩存時(shí)

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

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

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

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

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ù)進(jìn)行延遲初始化并提高性能,因?yàn)樗鼈冎槐徽{(diào)用一次。

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

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

不好的做法:每次組件渲染時(shí),我們都會(huì)從本地存儲(chǔ)讀取主題。

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)組件掛載時(shí),我們僅只會(huì)讀取本地存儲(chǔ)一次。

// ...

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ù)時(shí),我都會(huì)使用 React 上下文:

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

來看一個(gè)示例的部分代碼:

context.ts

// UserInfo接口來自測(cè)試數(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 上下文的一個(gè)挑戰(zhàn)是,只要上下文數(shù)據(jù)發(fā)生變化,所有使用該上下文的組件都會(huì)重新渲染,即使它們不使用發(fā)生變化的上下文部分。

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

在下面的示例中,我們創(chuàng)建了兩個(gè)上下文:一個(gè)用于操作(常量),另一個(gè)用于狀態(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)值計(jì)算不直接時(shí),引入 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 作為輕量級(jí)狀態(tài)管理解決方案

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

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

示例:這里。

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

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

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

這就是 useImmer 和 useImmerReducer 提供更簡(jiǎn)單的替代方案的地方。它們?cè)试S你編寫自動(dòng)轉(zhuǎn)換為不可變更新的“可變”代碼。

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

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>郵箱號(hào): </label>
        <input type="email" value={email} onChange={onEmailChange} />
      </div>
      <div className="form-item">
        <label>密碼:</label>
        <input type="password" value={password} onChange={onPasswordChange} />
      </div>
    </div>
  );
};

推薦做法: 更直接一點(diǎn),我們可以直接修改 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)管理解決方案)來跨多個(gè)組件訪問復(fù)雜的客戶端狀態(tài)

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

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

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

為了獲得簡(jiǎn)化的體驗(yàn),我建議使用 redux-tooltkit。

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

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

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

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

要了解它的用途,請(qǐng)觀看這個(gè)精彩的視頻。

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

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

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

memo 確保組件僅在其 props 發(fā)生變化時(shí)重新渲染,而不僅僅是因?yàn)槠涓附M件重新渲染。

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

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

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

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

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

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

示例如下:

const UserList = memo(
  ({ users }) => {
    return <div>{JSON.stringify(users)}</div>;
  },
  (prevProps, nextProps) => {
    // 僅當(dāng)最后一個(gè)用戶或列表大小發(fā)生變化時(shí)才重新渲染
    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.聲明緩存組件時(shí),優(yōu)先使用命名函數(shù)而不是箭頭函數(shù)

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

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

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

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

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

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

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

我通常會(huì)使用 useMemo:

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

不好的做法:UserInfoList 的 memo 不會(huì)阻止重新渲染,因?yàn)槊看武秩緯r(shí)都會(huì)重新創(chuàng)建樣式。

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

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

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

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

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

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

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

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

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

  // 每次渲染`handleThemeChange`都會(huì)改變
  // 因此,每次渲染后都會(huì)觸發(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 中可確保僅在必要時(shí)重新創(chuàng)建它,從而減少不必要的執(zhí)行。

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

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

當(dāng)你創(chuàng)建自定義鉤子與他人共享時(shí),記住返回的值和函數(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)容,因此不會(huì)出現(xiàn)意外的性能問題。

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

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

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

  // 包裹在 `useCallback` 中并使用 `ref` 值,這樣函數(shù)就不會(huì)改變
  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)用時(shí),請(qǐng)考慮對(duì)以下代碼使用懶加載和 Suspense:

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

在下面的示例,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)連接嗎?

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

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

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

切勿一次性渲染一長(zhǎng)串項(xiàng)目,例如聊天消息、日志或無(wú)限列表。

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

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

不好的做法:NonVirtualList 會(huì)同時(shí)呈現(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 僅渲染可能可見的項(xiàng)目。

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>
      )}
    />
  );
};

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

七. 總結(jié)

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

相關(guān)文章

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

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

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

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

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

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

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

    如何強(qiáng)制刷新react hooks組件

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

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

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

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

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

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

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

    react?事項(xiàng)懶加載的三種方法及使用場(chǎng)景

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

    react源碼層探究setState作用

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

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

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

最新評(píng)論