From b8c495cb280fc0dfb365bf36404bd5f59f653aaa Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 29 Oct 2025 15:49:48 -0700 Subject: [PATCH 1/6] simplify useControllableState --- packages/ui/src/hooks/useControllableState.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/hooks/useControllableState.ts b/packages/ui/src/hooks/useControllableState.ts index de81ae1cd2f..a67b9946fbc 100644 --- a/packages/ui/src/hooks/useControllableState.ts +++ b/packages/ui/src/hooks/useControllableState.ts @@ -1,5 +1,5 @@ '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. @@ -23,9 +23,5 @@ export function useControllableState( setLocalValue(propValue) }, [propValue]) - const setValue = useCallback((value: ((prev: T) => T) | T) => { - setLocalValue(value) - }, []) - - return [localValue, setValue] + return [localValue, setLocalValue] } From a8ac82103614a388d091811bf6e769ab628a967a Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 29 Oct 2025 15:49:59 -0700 Subject: [PATCH 2/6] use in useconfig --- packages/ui/src/providers/Config/index.tsx | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) 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(() => { From 9cf88ecf5da3b49d596dfacb8f4e9db6f25f1820 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 29 Oct 2025 16:02:51 -0700 Subject: [PATCH 3/6] improve useControllableState type --- packages/ui/src/hooks/useControllableState.ts | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/hooks/useControllableState.ts b/packages/ui/src/hooks/useControllableState.ts index a67b9946fbc..79397590329 100644 --- a/packages/ui/src/hooks/useControllableState.ts +++ b/packages/ui/src/hooks/useControllableState.ts @@ -1,5 +1,5 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' /** * A hook for managing state that can be controlled by props but also overridden locally. @@ -9,9 +9,17 @@ import { useEffect, useRef, useState } from 'react' */ export function useControllableState( propValue: T, - defaultValue?: T, -): [T, (value: ((prev: T) => T) | T) => void] { - const [localValue, setLocalValue] = useState(propValue ?? defaultValue) + defaultValue: NonNullable, +): [NonNullable, (value: ((prev: T) => T) | T) => void] +export function useControllableState( + propValue: T, + defaultValue?: undefined, +): [T, (value: ((prev: T) => T) | T) => void] +export function useControllableState( + propValue: T, + defaultValue?: NonNullable, +): [NonNullable | T, (value: ((prev: T) => T) | T) => void] { + const [localValue, setLocalValue] = useState(propValue) const initialRenderRef = useRef(true) useEffect(() => { @@ -23,5 +31,12 @@ export function useControllableState( setLocalValue(propValue) }, [propValue]) - return [localValue, setLocalValue] + const setValue = useCallback((value: ((prev: T) => T) | T) => { + setLocalValue(value) + }, []) + + return [localValue ?? defaultValue, setValue] as [ + NonNullable | T, + (value: ((prev: T) => T) | T) => void, + ] } From 77da9681c3c708c1dae4bd2813670b79ca57a3ea Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 29 Oct 2025 16:03:33 -0700 Subject: [PATCH 4/6] simplify --- packages/ui/src/hooks/useControllableState.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/hooks/useControllableState.ts b/packages/ui/src/hooks/useControllableState.ts index 79397590329..b45c4344d12 100644 --- a/packages/ui/src/hooks/useControllableState.ts +++ b/packages/ui/src/hooks/useControllableState.ts @@ -1,5 +1,5 @@ '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. @@ -31,12 +31,5 @@ export function useControllableState( setLocalValue(propValue) }, [propValue]) - const setValue = useCallback((value: ((prev: T) => T) | T) => { - setLocalValue(value) - }, []) - - return [localValue ?? defaultValue, setValue] as [ - NonNullable | T, - (value: ((prev: T) => T) | T) => void, - ] + return [localValue ?? defaultValue, setLocalValue] } From b0a6be54f02d32256942858dac4f260311fab11c Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 29 Oct 2025 16:08:55 -0700 Subject: [PATCH 5/6] improve --- packages/ui/src/hooks/useControllableState.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/hooks/useControllableState.ts b/packages/ui/src/hooks/useControllableState.ts index b45c4344d12..ecbee40ad0c 100644 --- a/packages/ui/src/hooks/useControllableState.ts +++ b/packages/ui/src/hooks/useControllableState.ts @@ -7,18 +7,15 @@ import { useEffect, useRef, useState } from 'react' * * @internal - may change or be removed without a major version bump */ -export function useControllableState( +export function useControllableState( propValue: T, - defaultValue: NonNullable, -): [NonNullable, (value: ((prev: T) => T) | T) => void] -export function useControllableState( + defaultValue: 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?: undefined, -): [T, (value: ((prev: T) => T) | T) => void] -export function useControllableState( - propValue: T, - defaultValue?: NonNullable, -): [NonNullable | T, (value: ((prev: T) => T) | T) => void] { + defaultValue?: D, +): [T extends NonNullable ? T : D | NonNullable, (value: ((prev: T) => T) | T) => void] { const [localValue, setLocalValue] = useState(propValue) const initialRenderRef = useRef(true) @@ -31,5 +28,8 @@ export function useControllableState( setLocalValue(propValue) }, [propValue]) - return [localValue ?? defaultValue, setLocalValue] + return [localValue ?? defaultValue, setLocalValue] as [ + T extends NonNullable ? T : D | NonNullable, + (value: ((prev: T) => T) | T) => void, + ] } From df6aa1f88048f1c1f60f76a442047fe4f7429f94 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Wed, 29 Oct 2025 16:27:43 -0700 Subject: [PATCH 6/6] rename to fallbackvalue --- packages/ui/src/hooks/useControllableState.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/hooks/useControllableState.ts b/packages/ui/src/hooks/useControllableState.ts index ecbee40ad0c..1688d783b53 100644 --- a/packages/ui/src/hooks/useControllableState.ts +++ b/packages/ui/src/hooks/useControllableState.ts @@ -5,16 +5,19 @@ 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( propValue: T, - defaultValue: D, + 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?: D, + fallbackValue?: D, ): [T extends NonNullable ? T : D | NonNullable, (value: ((prev: T) => T) | T) => void] { const [localValue, setLocalValue] = useState(propValue) const initialRenderRef = useRef(true) @@ -28,7 +31,7 @@ export function useControllableState( setLocalValue(propValue) }, [propValue]) - return [localValue ?? defaultValue, setLocalValue] as [ + return [localValue ?? fallbackValue, setLocalValue] as [ T extends NonNullable ? T : D | NonNullable, (value: ((prev: T) => T) | T) => void, ]