diff --git a/packages/ui/src/hooks/useControllableState.ts b/packages/ui/src/hooks/useControllableState.ts index de81ae1cd2f..1688d783b53 100644 --- a/packages/ui/src/hooks/useControllableState.ts +++ b/packages/ui/src/hooks/useControllableState.ts @@ -1,17 +1,25 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' /** * A hook for managing state that can be controlled by props but also overridden locally. * Props always take precedence if they change, but local state can override them temporarily. * + * @param propValue - The controlled value from props + * @param fallbackValue - Value to use when propValue is null or undefined + * * @internal - may change or be removed without a major version bump */ -export function useControllableState( +export function useControllableState( + propValue: T, + fallbackValue: D, +): [T extends NonNullable ? T : D | NonNullable, (value: ((prev: T) => T) | T) => void] +export function useControllableState(propValue: T): [T, (value: ((prev: T) => T) | T) => void] +export function useControllableState( propValue: T, - defaultValue?: T, -): [T, (value: ((prev: T) => T) | T) => void] { - const [localValue, setLocalValue] = useState(propValue ?? defaultValue) + fallbackValue?: D, +): [T extends NonNullable ? T : D | NonNullable, (value: ((prev: T) => T) | T) => void] { + const [localValue, setLocalValue] = useState(propValue) const initialRenderRef = useRef(true) useEffect(() => { @@ -23,9 +31,8 @@ export function useControllableState( setLocalValue(propValue) }, [propValue]) - const setValue = useCallback((value: ((prev: T) => T) | T) => { - setLocalValue(value) - }, []) - - return [localValue, setValue] + return [localValue ?? fallbackValue, setLocalValue] as [ + T extends NonNullable ? T : D | NonNullable, + (value: ((prev: T) => T) | T) => void, + ] } diff --git a/packages/ui/src/providers/Config/index.tsx b/packages/ui/src/providers/Config/index.tsx index 75a437a43b6..5de36315516 100644 --- a/packages/ui/src/providers/Config/index.tsx +++ b/packages/ui/src/providers/Config/index.tsx @@ -8,7 +8,9 @@ import type { GlobalSlug, } from 'payload' -import React, { createContext, use, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { createContext, use, useCallback, useEffect, useMemo } from 'react' + +import { useControllableState } from '../../hooks/useControllableState.js' type GetEntityConfigFn = { // Overload #1: collectionSlug only @@ -43,20 +45,10 @@ export const ConfigProvider: React.FC<{ readonly children: React.ReactNode readonly config: ClientConfig }> = ({ children, config: configFromProps }) => { - const [config, setConfig] = useState(configFromProps) - - const isFirstRenderRef = useRef(true) - // Need to update local config state if config from props changes, for HMR. // That way, config changes will be updated in the UI immediately without needing a refresh. - useEffect(() => { - if (isFirstRenderRef.current) { - isFirstRenderRef.current = false - return - } - - setConfig(configFromProps) - }, [configFromProps, setConfig]) + // useControllableState handles this for us. + const [config, setConfig] = useControllableState(configFromProps) // Build lookup maps for collections and globals so we can do O(1) lookups by slug const { collectionsBySlug, globalsBySlug } = useMemo(() => {