本系列文章會在筆者的部落格繼續連載!Design System 101 感謝大家的閱讀!
本文同步上傳到筆者的個人部落格,裡面透過 Sandpack 直接編輯程式碼!
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>
</>
);
};
由上面的例子可以看到,當父層組件重新渲染的時候,子組件也會重新渲染,這是因為每次父層組件重新渲染的時候,會重新建立一個新的 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>
</>
);
};
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);
}
},
[],
);
}
在 Design System 中,許多 UI 都會需要管理當前狀態,像是 checkbox 組件的 check 與 un-check, accordion 組件的展開與收合等等。
而這些狀態通常都是由外部傳入,也有可能是由內部控制,這時候就可以使用 useControlledState
來處理這個問題。
而通常組件需要包含幾種狀態:
Name | Type | Description |
---|---|---|
props.defaultValue | any | 組件的初始狀態,這也是組件的預設狀態,可以不需要經由外部控制。 |
props.value | any | 組件的當前狀態,當外部想要傳入狀態去控制組件時,就可以傳入 value。 |
props.onChange | function | 當其他開發者想透過狀態改變時,處理其他的邏輯,這時候就可以傳入 onChange callback。 |
在實作 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];
}
當外部傳入 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。
上面看到不論是否透過外部傳入狀態,組件都能夠根據傳入的參數,去判斷當前是 controllable 還是 unControllable。
useMediaQuery
主要是用來處理 RWD 的問題,當瀏覽器的寬度改變時,就會重新計算 media query 的結果。
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);
};