'use client'; /* eslint-disable react-hooks/exhaustive-deps */ import * as React from 'react'; import { error } from '../utils/error'; export interface UseControlledProps { /** * Holds the component value when it's controlled. */ controlled: T | undefined; /** * The default value when uncontrolled. */ default: T | undefined; /** * The component name displayed in warnings. */ name: string; /** * The name of the state variable displayed in warnings. */ state?: string | undefined; } export function useControlled({ controlled, default: defaultProp, name, state = 'value', }: UseControlledProps): [T, (newValue: T | ((prevValue: T) => T)) => void] { // isControlled is ignored in the hook dependency lists as it should never change. const { current: isControlled } = React.useRef(controlled !== undefined); const [valueState, setValue] = React.useState(defaultProp); const value = isControlled ? controlled : valueState; // The two warnings below are development-only, but their hooks are called unconditionally so // that the Hook order stays identical on every render. The `process.env.NODE_ENV` guard lives // inside the effect bodies instead of around the hook calls. const { current: defaultValue } = React.useRef(defaultProp); React.useEffect(() => { if (process.env.NODE_ENV === 'production') { return; } if (isControlled !== (controlled !== undefined)) { error( [ `A component is changing the ${ isControlled ? '' : 'un' }controlled ${state} state of ${name} to be ${isControlled ? 'un' : ''}controlled.`, 'Elements should not switch from uncontrolled to controlled (or vice versa).', `Decide between using a controlled or uncontrolled ${name} ` + 'element for the lifetime of the component.', "The nature of the state is determined during the first render. It's considered controlled if the value is not `undefined`.", 'More info: https://fb.me/react-controlled-components', ].join('\n'), ); } }, [state, name, controlled]); React.useEffect(() => { if (process.env.NODE_ENV === 'production') { return; } if ( !isControlled && serializeToDevModeString(defaultValue) !== serializeToDevModeString(defaultProp) ) { error( [ `A component is changing the default ${state} state of an uncontrolled ${name} after being initialized. ` + `To suppress this warning opt to use a controlled ${name}.`, ].join('\n'), ); } }, [defaultProp, state, name]); const setValueIfUncontrolled = React.useCallback((newValue: React.SetStateAction) => { if (!isControlled) { setValue(newValue as T); } }, []); return [value as T, setValueIfUncontrolled]; } function serializeToDevModeString(input: unknown) { let nextId = 0; const seen = new WeakMap(); try { const result = JSON.stringify(input, function replacer(key, value) { if (key === '_owner' && this != null && typeof this === 'object' && '$$typeof' in this) { return undefined; } if (typeof value === 'bigint') { return `__bigint__:${value}`; } if (value !== null && typeof value === 'object') { const id = seen.get(value); if (id !== undefined) { return `__object__:${id}`; } seen.set(value, nextId); nextId += 1; } return value; }); return result ?? `__top__:${typeof input}`; } catch { return '__unserializable__'; } }