import * as React from 'react'; function isFactoryDispatch(newState) { return typeof newState === 'function'; } /** * @internal * * A [`useState`](https://reactjs.org/docs/hooks-reference.html#usestate)-like hook * to manage a value that could be either `controlled` or `uncontrolled`, * such as a checked state or text input string. * * @see https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components for more details on `controlled`/`uncontrolled` * * @returns an array of the current value and an updater (dispatcher) function. * The updater function is referentially stable (won't change during the component's lifecycle). * It can take either a new value, or a function which is passed the previous value and returns the new value. * * ❗️❗️ Calls to the dispatcher will only modify the state if the state is `uncontrolled`. * Meaning that if a state is `controlled`, calls to the dispatcher do not modify the state. * */ export const useControllableState = (options)=>{ 'use no memo'; if (process.env.NODE_ENV !== 'production') { if (options.state !== undefined && options.defaultState !== undefined) { // eslint-disable-next-line no-console console.error(`@fluentui/react-utilities [useControllableState]: A component must be either controlled or uncontrolled (specify either the state or the defaultState, but not both). Decide between using a controlled or uncontrolled component and remove one of this props. More info: https://reactjs.org/link/controlled-components ${new Error().stack}`); } } const [internalState, setInternalState] = React.useState(()=>{ if (options.defaultState === undefined) { return options.initialState; } return isInitializer(options.defaultState) ? options.defaultState() : options.defaultState; }); // Heads up! // This part is specific for controlled mode and mocks behavior of React dispatcher function. const stateValueRef = React.useRef(options.state); React.useEffect(()=>{ stateValueRef.current = options.state; }, [ options.state ]); const setControlledState = React.useCallback((newState)=>{ if (isFactoryDispatch(newState)) { newState(stateValueRef.current); } }, []); return useIsControlled(options.state) ? [ options.state, setControlledState ] : [ internalState, setInternalState ]; }; function isInitializer(value) { return typeof value === 'function'; } /** * Helper hook to handle previous comparison of controlled/uncontrolled * Prints an error when isControlled value switches between subsequent renders * @returns - whether the value is controlled */ const useIsControlled = (controlledValue)=>{ 'use no memo'; const [isControlled] = React.useState(()=>controlledValue !== undefined); if (process.env.NODE_ENV !== 'production') { // We don't want these warnings in production even though it is against native behaviour // eslint-disable-next-line react-hooks/rules-of-hooks React.useEffect(()=>{ if (isControlled !== (controlledValue !== undefined)) { const error = new Error(); const controlWarning = isControlled ? 'a controlled value to be uncontrolled' : 'an uncontrolled value to be controlled'; const undefinedWarning = isControlled ? 'defined to an undefined' : 'undefined to a defined'; // eslint-disable-next-line no-console console.error(`@fluentui/react-utilities [useControllableState]: A component is changing ${controlWarning}. This is likely caused by the value changing from ${undefinedWarning} value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components ${error.stack}`); } }, [ isControlled, controlledValue ]); } return isControlled; };