Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions packages/ui/src/hooks/useControllableState.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
export function useControllableState<T, D>(
propValue: T,
fallbackValue: D,
): [T extends NonNullable<T> ? T : D | NonNullable<T>, (value: ((prev: T) => T) | T) => void]
export function useControllableState<T>(propValue: T): [T, (value: ((prev: T) => T) | T) => void]
export function useControllableState<T, D>(
propValue: T,
defaultValue?: T,
): [T, (value: ((prev: T) => T) | T) => void] {
const [localValue, setLocalValue] = useState<T>(propValue ?? defaultValue)
fallbackValue?: D,
): [T extends NonNullable<T> ? T : D | NonNullable<T>, (value: ((prev: T) => T) | T) => void] {
const [localValue, setLocalValue] = useState<T>(propValue)
const initialRenderRef = useRef(true)

useEffect(() => {
Expand All @@ -23,9 +31,8 @@ export function useControllableState<T>(
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> ? T : D | NonNullable<T>,
(value: ((prev: T) => T) | T) => void,
]
}
18 changes: 5 additions & 13 deletions packages/ui/src/providers/Config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -43,20 +45,10 @@ export const ConfigProvider: React.FC<{
readonly children: React.ReactNode
readonly config: ClientConfig
}> = ({ children, config: configFromProps }) => {
const [config, setConfig] = useState<ClientConfig>(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<ClientConfig>(configFromProps)

// Build lookup maps for collections and globals so we can do O(1) lookups by slug
const { collectionsBySlug, globalsBySlug } = useMemo(() => {
Expand Down
Loading