iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0
Modern Web

設計系統 - Design System系列 第 12

[Day 12] Design System - Common Hook (二)

  • 分享至 

  • xImage
  •  

本系列文章會在筆者的部落格繼續連載!Design System 101 感謝大家的閱讀!

本文同步上傳到筆者的個人部落格,裡面透過 Sandpack 直接編輯程式碼!

useCallbackRef

useCallbackRef 主要是要解決傳入子組件的 props 不會因為父層組件重新渲染 (re-render) 進而導致子組件不必要的渲染。

為了確保子組件不會因為其他原因而重新渲染,我們可以使用 React.memo 來確保子組件不會因為其他原因而重新渲染,接下來透過下面的例子來說明。

首先在父層先加入 count 的 state,並且在子組件中加入一個 renderCountRef 來計算子組件重新渲染的次數。

import React, { useState, useCallback, useRef, memo } from 'react';

const ChildComponenet = memo(({ callback }) => {
  const renderCountRef = useRef(0);

  renderCountRef.current++;

  return (
    <div>
      <div>Child re-render: {renderCountRef?.current}</div>
    </div>
  );
});

export default () => {
  const [count, setCount] = React.useState(0);

  const callback = () => {
    console.log('count', count);
  };

  return (
    <>
      <h1>Without useCallbackRef</h1>
      <ChildComponenet callback={callback} />
      <button onClick={() => setCount((count) => count + 1)}>parent render: {count}</button>
    </>
  );
};

without callback ref

由上面的例子可以看到,當父層組件重新渲染的時候,子組件也會重新渲染,這是因為每次父層組件重新渲染的時候,會重新建立一個新的 callback。而這時候就可以使用 useCallbackRef 來解決這個問題。

import React, { useState, useCallback, useRef, memo } from 'react';
import { useCallbackRef } from './callback-ref';

const ChildComponenet = memo(({ callback }) => {
  const renderCountRef = useRef(0);

  renderCountRef.current++;

  return (
    <div>
      <div>Child re-render: {renderCountRef?.current}</div>
    </div>
  );
});

export default () => {
  const [count, setCount] = React.useState(0);

  const callback = () => console.log(count);
  const stableCallback = useCallbackRef(callback);

  return (
    <>
      <h1>With useCallbackRef</h1>
      <ChildComponenet callback={stableCallback} />
      <button onClick={() => setCount((count) => count + 1)}>parent render: {count}</button>
    </>
  );
};

with callback ref

useCallbackRef 主要是先將 callback 存在 useRef 中,並且透過 useEffect 來更新 callback 的值,這樣就可以確保 callback 不會因為父層組件重新渲染而重新建立。

import React, { useRef, useEffect, useMemo } from 'react';

export function useCallbackRef(callback) {
  const callbackRef = useRef(callback);

  useEffect(() => {
    callbackRef.current = callback;
  });

  return useMemo(
    () =>
      (...args) => {
        if (callbackRef.current) {
          return callbackRef.current(...args);
        }
      },
    [],
  );
}

useControlledState

在 Design System 中,許多 UI 都會需要管理當前狀態,像是 checkbox 組件的 check 與 un-check, accordion 組件的展開與收合等等。

而這些狀態通常都是由外部傳入,也有可能是由內部控制,這時候就可以使用 useControlledState 來處理這個問題。

API

而通常組件需要包含幾種狀態:

Name Type Description
props.defaultValue any 組件的初始狀態,這也是組件的預設狀態,可以不需要經由外部控制。
props.value any 組件的當前狀態,當外部想要傳入狀態去控制組件時,就可以傳入 value。
props.onChange function 當其他開發者想透過狀態改變時,處理其他的邏輯,這時候就可以傳入 onChange callback。

useUncontrolledState

在實作 useControlledState 之前我們先實作 useUncontrolledState ,其就是 useState 加上 callback

import { useState, useCallback, useRef } from 'react';
import { useCallbackRef } from './callback-ref';

export function useUncontrolledState({ defaultValue, onChange }) {
  const [value, setValue] = useState(defaultValue);
  const previousValueRef = useRef(value);
  const callback = useCallbackRef(onChange);

  useEffect(() => {
    if (value !== previousValueRef?.current) {
      callback(value);
      previousValueRef.current = value;
    }
  }, [value, previousValueRef, onChange]);

  return [value, setValue];
}

useControlledState

當外部傳入 value 時,就使用外部傳入的 value,否則就使用 useUncontrolledState 的 defaultValue。

import { useCallback } from 'react';
import { useUncontrolledState } from './uncontrolled-state';
import { useCallbackRef } from './callback-ref';

const useControlledState = ({ defaultValue, value, onChange }) => {
  const [uncontrolledState, setUncontrolledState] = useUncontrolledState({
    defaultValue,
    onChange,
  });

  const isControlled = value != null;
  const state = isControlled ? value : uncontrolledState;
  const callback = useCallbackRef(onChange);

  const setState = useCallback(
    (nextValue) => {
      if (isControlled) {
        const setter = nextValue;
        const v = typeof nextValue === 'function' ? setter(value) : nextValue;
        if (v !== value) callback?.(v);
      } else {
        setUncontrolledState(nextValue);
      }
    },
    [value, isControlled, callback],
  );

  return [state, setState];
};

接著我們就可以透過下面的例子來看 useControlledState 如何處理從外部傳入的狀態,內部狀態,以及 onChange callback。

useControlledState

上面看到不論是否透過外部傳入狀態,組件都能夠根據傳入的參數,去判斷當前是 controllable 還是 unControllable。

useMediaQuery

useMediaQuery 主要是用來處理 RWD 的問題,當瀏覽器的寬度改變時,就會重新計算 media query 的結果。

API

Name Type Description
query string media query 的條件,例如:'(min-width: 768px)'

實作

首先我們可以先透過 browse 的 API window.matchMedia 來取得 media query 的結果,在透過 useCallback 來訂閱瀏覽器的寬度變化,最後透過 useSyncExternalStore 來同步狀態。

import { useCallback, useSyncExternalStore } from 'react';

export const useMediaQuery = (query: string) => {
  const subscribe = useCallback(
    (onChange) => {
      if (!canUseMatchMedia) {
        return;
      }
      const mq = window.matchMedia(query);
      mq.addEventListener('change', onChange);
      return () => mq.removeEventListener('change', onChange);
    },
    [query],
  );

  const getSnapshot = useCallback(() => (canUseMatchMedia ? window.matchMedia(query).matches : false), [query]);

  return useSyncExternalStore(subscribe, getSnapshot);
};

mediaQuery


上一篇
[Day 11] Design System - Common Hook (一)
下一篇
[Day 13] Design System - Style Dictionary
系列文
設計系統 - Design System30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言