
Yamada UIで頻出のユーティリティを見ていく
packages/utils/src/react.tsx
1. DOMAttributes
2. PropGetter
3. RequiredPropGetter
4. MaybeRenderProp
5. UseIsMountedProps
6. UseIsMountedReturn
7. FunctionReturningPromise
これは関数がPromiseを返すことを表現する型。
export type FunctionReturningPromise = (...args: any[]) => Promise<any>
8. AsyncState
非同期処理の状態を表現する方。
export type AsyncState<T> = | { loading: boolean error?: undefined value?: undefined } | { loading: true error?: Error | undefined value?: T } | { loading: false error: Error value?: undefined } | { loading: false error?: undefined value: T }
この型で4つの状態を表現できる。
- 初期状態
- ローディング中
- エラー
- 成功
1. 初期状態
{ loading: boolean error?: undefined value?: undefined}
2. ローディング中
{ loading: true error?: Error | undefined value?: T}
3. エラー
{ loading: false error: Error value?: undefined}
4. 成功
{ loading: false error?: undefined value: T}
9. PromiseType
Promise<T>
型からT
型を抽出するだけの型。
export type PromiseType<P extends Promise<any>> = P extends Promise<infer T> ? T : never
10. AsyncFnReturn
非同期関数の戻り値の型とその状態を管理するための型。
この型は、非同期関数の実行結果とその状態(ローディング、成功、エラー)を一緒に返すために使用される。
export type AsyncFnReturn< T extends FunctionReturningPromise = FunctionReturningPromise> = [StateFromFunctionReturningPromise<T>, T]
StateFromFunctionReturningPromise
Promise<T>
を返す関数(もちろん非同期関数)の処理状態を表現する型。
type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> = AsyncState<PromiseType<ReturnType<T>>>
11. AsyncStateRetry
12. createContext
React標準のReact.createContext
を拡張して使いやすくしたもの、と考えるとわかりやすい。
type Options<ContextType extends any = any> = { strict?: boolean errorMessage?: string name?: string defaultValue?: ContextType}
type CreateContextReturn<T> = [React.Provider<T>, () => T, React.Context<T>]
export const createContext = <ContextType extends any = any>({ strict = true, errorMessage = 'useContext: `context` is undefined. Seems you forgot to wrap component within the Provider', name, defaultValue}: Options<ContextType> = {}) => { const Context = React.createContext<ContextType | undefined>(defaultValue)
Context.displayName = name
const useContext = () => { const context = React.useContext(Context)
if (!context && strict) { const error = new Error(errorMessage) error.name = 'ContextError' Error.captureStackTrace?.(error, useContext) throw error }
return context }
return [ Context.Provider, useContext, Context ] as CreateContextReturn<ContextType>}
13. useSafeLayoutEffect
SSR環境やdocument
オブジェクトが存在しない環境においてuseLayoutEffect
をuseEffect
にフォールバックしてくれるフック。
export const useSafeLayoutEffect = Boolean(globalThis?.document) ? React.useLayoutEffect : React.useEffect
14. useUnmountEffect
コンポーネントがアンマウントされた時に、特定のコールバック関数を実行するためのフック。
export const useUnmountEffect = (callback: () => void) => // eslint-disable-next-line react-hooks/exhaustive-deps React.useEffect(() => () => callback(), [])
15. useIsMounted
16. getValidChildren
引数に渡したReactコンポーネントの子要素のうち、「有効な」子要素のみを抽出する関数。
export const getValidChildren = ( children: React.ReactNode): React.ReactElement[] => React.Children.toArray(children).filter((child) => React.isValidElement(child) ) as React.ReactElement[]
「有効な」ってどういう意味?
ただ、先述の説明だけじゃこれだけじゃ何にもわからない。ちゃんとReactのドキュメントを読む。

このページを見る限り、React.isValidElement()
関数についてこう書かれている。
isValidElement は値が React 要素 (React element) であるかどうかを判定します。
なるほど、React的には「React element」であることが「有効」であるようだ。
では、「React要素」の具体的な定義はなんだろう?
もうちょっとこのページを読み進めると、こんな事が書いてある。
React 要素と見なされるのは、JSX タグと、createElement によって返されるオブジェクトだけです。例えば、42 のような数値は有効な React ノード (node) ではあります(コンポーネントから返すことができるため)が、有効な React 要素ではありません。配列や、createPortal で作成されたポータルも、React 要素とは見なされません。
なるほど、つまり「React要素」であるためには以下のいずれかの条件を満たしていなければならないらしい。
- JSXタグである(
<span>koralle</span>
や<MyComponent />
みたいな) createElement
関数で返されるオブジェクトである。
この関数が必要になるケースについても言及されていた。
isValidElement が必要となることは非常に稀です。これが主に役立つのは、要素のみを受け入れる他の API(例えば cloneElement がそうです)を呼び出しており、引数が React 要素でないことによるエラーを避けたい場合です。
17. isValidElement
画面に表示される要素であるかどうかを判定する関数。
これはReact.isValidElement()
関数を拡張して作られている。
export const isValidElement = (child: any): child is React.ReactNode => React.isValidElement(child) || isString(child) || isNumber(child)
18. findChildren
子要素の中から指定した型の子要素を探して抽出する関数。
export const findChildren = ( children: React.ReactElement< any, string | React.JSXElementConstructor<any> >[], ...types: (string | React.JSXElementConstructor<any>)[]): [React.ReactElement | undefined, ...React.ReactElement[]] => (children.find((child) => types.some((type) => child.type === type)) ? children.sort((a, b) => types.some((type) => a.type === type) ? -1 : types.some((type) => b.type === type) ? 1 : 0 ) : [undefined, ...children]) as [ React.ReactElement | undefined, ...React.ReactElement[] ]
このテストコードのように、指定した型の子要素の中で最初に見つかった子要素をタプルの第一要素に持ってきて、その他の子要素をタプルの残りの要素に持ってくる。
describe('findChildren', () => { test('should find children of specified types', () => { const children = [ <div key="1">Div</div>, <span key="2">Span</span>, <section key="3">Section</section> ] const [foundChild, ...rest] = findChildren(children, 'span') expect(foundChild?.type).toBe('span') expect(rest).toHaveLength(2) })})
19. includesChildren
指定した型の子要素を含むかどうかを判定する関数。
export const includesChildren = ( children: React.ReactElement< any, string | React.JSXElementConstructor<any> >[], ...types: (string | React.JSXElementConstructor<any>)[]): boolean => children.some((child) => { if (types.some((type) => child.type === type)) return true
const children = getValidChildren(child.props.children)
return children.length ? includesChildren(children, ...types) : false })
使い方はこんな感じ
describe('includesChildren', () => { test('should return true if children include specified types', () => { const children = [ <div key="1"> <span>Inside Div</span> </div>, <section key="2">Section</section> ] expect(includesChildren(children, 'span')).toBeTruthy() })
test('should return false if children do not include specified types', () => { const children = [ <div key="1">Div</div>, <section key="2">Section</section> ] expect(includesChildren(children, 'span')).toBeFalsy() })})
20. omitChildren
指定した型の子要素を排除する関数。
export const omitChildren = ( children: React.ReactElement< any, string | React.JSXElementConstructor<any> >[], ...types: (string | React.JSXElementConstructor<any>)[]): React.ReactElement[] => children.filter((child) => types.every((type) => child.type !== type))
使い方。
describe('omitChildren', () => { test('should omit children of specified types', () => { const children = [ <div key="1">Div</div>, <span key="2">Span</span>, <section key="3">Section</section> ] const omittedChildren = omitChildren(children, 'span') expect(omittedChildren).toHaveLength(2) expect(omittedChildren.some((child) => child.type === 'span')).toBeFalsy() })})
21. pickChildren
子要素の中から指定した型の子要素だけを抽出する関数。
条件を満たす最初の子要素を1つだけ抽出するfindChildren
とは異なり、こちらは条件を満たす子要素をすべて抽出する。
export const pickChildren = ( children: React.ReactElement< any, string | React.JSXElementConstructor<any> >[], ...types: (string | React.JSXElementConstructor<any>)[]): React.ReactElement[] =>
22. cx
配列形式で渡したクラス名を半角スペース区切りで結合して1つの文字列にする関数。
export const cx = (...classNames: (string | undefined)[]) => classNames.filter(Boolean).join(' ')
使い方。
describe('cx', () => { test('should concatenate class names', () => { const classNames = cx('class1', undefined, 'class2') expect(classNames).toBe('class1 class2') })})
23. isRefObject
ReactのRefオブジェクトかどうかを判定する関数。
export const isRefObject = (val: any): val is { current: any } => isObject(val) && 'current' in val
24. assignRef
Reactのrefを安全に設定できる関数。
export const assignRef = <T extends any = any>( ref: ReactRef<T> | undefined, value: T) => { if (ref == null) return
if (typeof ref === 'function') { ref(value)
return }
try { // @ts-ignore ref.current = value } catch (error) { throw new Error(`Cannot assign value '${value}' to ref '${ref}'`) }}
25. mergeRefs
26. useMergeRefs
27. useCallbackRef
簡単に言えば、useCallback
フックを拡張してパフォーマンスを向上させたもの。
再レンダリング時にコールバック関数の新しい参照を持つようにするのではなく、あくまで既に保持している参照を更新するようにすることでパフォーマンスを向上させている。
export const useCallbackRef = <T extends (...args: any[]) => any>( callback: T | undefined, deps: React.DependencyList = []) => { const callbackRef = React.useRef(callback)
React.useEffect(() => { callbackRef.current = callback })
// eslint-disable-next-line react-hooks/exhaustive-deps return React.useCallback( ((...args) => callbackRef.current?.(...args)) as T, deps )}
細かく分割しながら処理を見ていく
useRef
フックを使ってコールバック関数への参照を保持するRefObjectを作成する。
const callbackRef = React.useRef(callback)
再レンダリング時は、そのRefObjectを更新することで同じ参照を使い回す。
React.useEffect(() => { callbackRef.current = callback})
最終的な返り値は下のコードブロックを見て分かる通り、useCallback
の返り値と同じ。
return React.useCallback( ((...args) => callbackRef.current?.(...args)) as T, deps)
28. useUpdateEffect
初回レンダー時には実行されないが、依存配列が更新された時にのみコールバック関数を実行されるような副作用を定義するのに使用するフック。
export const useUpdateEffect = ( callback: React.EffectCallback, deps: React.DependencyList) => { const renderCycleRef = React.useRef(false) const effectCycleRef = React.useRef(false)
React.useEffect(() => { const mounted = renderCycleRef.current const run = mounted && effectCycleRef.current
if (run) return callback()
effectCycleRef.current = true // eslint-disable-next-line react-hooks/exhaustive-deps }, deps)
React.useEffect(() => { renderCycleRef.current = true
return () => { renderCycleRef.current = false } }, [])}
流れとしてはこんな感じ。
まず、renderCycleRef
とeffectCycleRef
という2つのRefオブジェクトを作成する。
これらのRefオブジェクトで管理するboolean値をtrue
にするタイミングを上手く制御するのがキモ。
const renderCycleRef = React.useRef(false)const effectCycleRef = React.useRef(false)
次に最初のuseEffect
。
React.useEffect(() => { const mounted = renderCycleRef.current const run = mounted && effectCycleRef.current
if (run) return callback()
effectCycleRef.current = true // eslint-disable-next-line react-hooks/exhaustive-deps}, deps)
このようなuseEffect
を定義することで、初回レンダー時の副作用の実行をスマートにスキップできる。
次に、2つ目のuseEffect
。このようにすることで2回目以降のレンダー時に副作用が更新されるようにしつつ、且つアンマウントされた場合には副作用が実行されないようにしている。
React.useEffect(() => { renderCycleRef.current = true
return () => { renderCycleRef.current = false }}, [])
使い方はこんな感じ。
import React, { useState } from 'react'import { useUpdateEffect } from '@yamada-ui/utils'
const MyComponent = () => { const [count, setCount] = useState(0)
useUpdateEffect(() => { console.log('Count has been updated:', count) }, [count])
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> )}
29. useAsync
30. useAsyncFunc
export const useAsyncFunc = <T extends FunctionReturningPromise>( func: T, deps: React.DependencyList = [], initialState: StateFromFunctionReturningPromise<T> = { loading: false }): AsyncFnReturn<T> => { const lastCallId = React.useRef(0) const [isMounted] = useIsMounted() const [state, setState] = React.useState<StateFromFunctionReturningPromise<T>>(initialState)
const callback = React.useCallback( (...args: Parameters<T>): ReturnType<T> => { const callId = ++lastCallId.current
if (!state.loading) setState((prevState) => ({ ...prevState, loading: true }))
return func(...args).then( (value) => { if (isMounted() && callId === lastCallId.current) setState({ value, loading: false })
return value }, (error) => { if (isMounted() && callId === lastCallId.current) setState({ error, loading: false })
return error } ) as ReturnType<T> }, // eslint-disable-next-line react-hooks/exhaustive-deps deps )
return [state, callback as unknown as T]}