Skip to content

Yamada UIで頻出のユーティリティを見ていく

packages/utils/src/react.tsx

1. DOMAttributes

2. PropGetter

3. RequiredPropGetter

4. MaybeRenderProp

5. UseIsMountedProps

6. UseIsMountedReturn

7. FunctionReturningPromise

これは関数がPromiseを返すことを表現する型。

packages/utils/src/react.tsx
export type FunctionReturningPromise = (...args: any[]) => Promise<any>

8. AsyncState

非同期処理の状態を表現する方。

packages/utils/src/react.tsx
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. 初期状態
  2. ローディング中
  3. エラー
  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型を抽出するだけの型。

packages/utils/src/react.tsx
export type PromiseType<P extends Promise<any>> =
P extends Promise<infer T> ? T : never

10. AsyncFnReturn

非同期関数の戻り値の型とその状態を管理するための型。
この型は、非同期関数の実行結果とその状態(ローディング、成功、エラー)を一緒に返すために使用される。

packages/utils/src/react.tsx
export type AsyncFnReturn<
T extends FunctionReturningPromise = FunctionReturningPromise
> = [StateFromFunctionReturningPromise<T>, T]

StateFromFunctionReturningPromise

Promise<T>を返す関数(もちろん非同期関数)の処理状態を表現する型。

packages/utils/src/react.tsx
type StateFromFunctionReturningPromise<T extends FunctionReturningPromise> =
AsyncState<PromiseType<ReturnType<T>>>

11. AsyncStateRetry

12. createContext

React標準のReact.createContextを拡張して使いやすくしたもの、と考えるとわかりやすい。

packages/utils/src/react.tsx
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オブジェクトが存在しない環境においてuseLayoutEffectuseEffectにフォールバックしてくれるフック。

packages/utils/src/react.tsx
export const useSafeLayoutEffect = Boolean(globalThis?.document)
? React.useLayoutEffect
: React.useEffect

14. useUnmountEffect

コンポーネントがアンマウントされた時に、特定のコールバック関数を実行するためのフック。

packages/utils/src/react.tsx
export const useUnmountEffect = (callback: () => void) =>
// eslint-disable-next-line react-hooks/exhaustive-deps
React.useEffect(() => () => callback(), [])

15. useIsMounted

16. getValidChildren

引数に渡したReactコンポーネントの子要素のうち、「有効な」子要素のみを抽出する関数。

packages/utils/src/react.tsx
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要素」であるためには以下のいずれかの条件を満たしていなければならないらしい。

  1. JSXタグである(<span>koralle</span><MyComponent />みたいな)
  2. createElement関数で返されるオブジェクトである。

この関数が必要になるケースについても言及されていた。

isValidElement が必要となることは非常に稀です。これが主に役立つのは、要素のみを受け入れる他の API(例えば cloneElement がそうです)を呼び出しており、引数が React 要素でないことによるエラーを避けたい場合です。

17. isValidElement

画面に表示される要素であるかどうかを判定する関数。
これはReact.isValidElement()関数を拡張して作られている。

packages/utils/src/react.tsx
export const isValidElement = (child: any): child is React.ReactNode =>
React.isValidElement(child) || isString(child) || isNumber(child)

18. findChildren

子要素の中から指定した型の子要素を探して抽出する関数。

packages/utils/src/react.tsx
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[]
]

このテストコードのように、指定した型の子要素の中で最初に見つかった子要素をタプルの第一要素に持ってきて、その他の子要素をタプルの残りの要素に持ってくる。

packages/utils/tests/react.test.tsx
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

指定した型の子要素を含むかどうかを判定する関数。

packages/utils/src/react.tsx
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
})

使い方はこんな感じ

packages/utils/tests/react.test.tsx
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

指定した型の子要素を排除する関数。

packages/utils/src/react.tsx
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とは異なり、こちらは条件を満たす子要素をすべて抽出する。

packages/utils/src/react.tsx
export const pickChildren = (
children: React.ReactElement<
any,
string | React.JSXElementConstructor<any>
>[],
...types: (string | React.JSXElementConstructor<any>)[]
): React.ReactElement[] =>

22. cx

配列形式で渡したクラス名を半角スペース区切りで結合して1つの文字列にする関数。

packages/utils/src/react.tsx
export const cx = (...classNames: (string | undefined)[]) =>
classNames.filter(Boolean).join(' ')

使い方。

packages/utils/tests/react.test.tsx
describe('cx', () => {
test('should concatenate class names', () => {
const classNames = cx('class1', undefined, 'class2')
expect(classNames).toBe('class1 class2')
})
})

23. isRefObject

ReactのRefオブジェクトかどうかを判定する関数。

packages/utils/src/react.tsx
export const isRefObject = (val: any): val is { current: any } =>
isObject(val) && 'current' in val

24. assignRef

Reactのrefを安全に設定できる関数。

packages/utils/src/react.tsx
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フックを拡張してパフォーマンスを向上させたもの。
再レンダリング時にコールバック関数の新しい参照を持つようにするのではなく、あくまで既に保持している参照を更新するようにすることでパフォーマンスを向上させている。

packages/utils/src/react.tsx
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

初回レンダー時には実行されないが、依存配列が更新された時にのみコールバック関数を実行されるような副作用を定義するのに使用するフック。

packages/utils/src/react.tsx
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
}
}, [])
}

流れとしてはこんな感じ。

まず、renderCycleRefeffectCycleRefという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

packages/utils/src/react.tsx
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]
}

31. useAsyncRetry

32. createId