@ryvora/react-use-controllable-state
Version:
🎮🔄 Controlled/uncontrolled state hook for React. Build flexible, predictable components!
8 lines (7 loc) • 10.6 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/use-controllable-state.tsx", "../src/use-controllable-state-reducer.tsx"],
"sourcesContent": ["import * as React from 'react';\nimport { useLayoutEffect } from '@ryvora/react-use-layout-effect';\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n (React as any)[' useInsertionEffect '.trim().toString()] || useLayoutEffect;\n\ntype ChangeHandler<T> = (state: T) => void;\ntype SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>;\n\ninterface UseControllableStateParams<T> {\n prop?: T | undefined;\n defaultProp: T;\n onChange?: ChangeHandler<T>;\n caller?: string;\n}\n\nexport function useControllableState<T>({\n prop,\n defaultProp,\n onChange = () => {},\n caller,\n}: UseControllableStateParams<T>): [T, SetStateFn<T>] {\n const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({\n defaultProp,\n onChange,\n });\n const isControlled = prop !== undefined;\n const value = isControlled ? prop : uncontrolledProp;\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(prop !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n const setValue = React.useCallback<SetStateFn<T>>(\n (nextValue) => {\n if (isControlled) {\n const value = isFunction(nextValue) ? nextValue(prop) : nextValue;\n if (value !== prop) {\n onChangeRef.current?.(value);\n }\n } else {\n setUncontrolledProp(nextValue);\n }\n },\n [isControlled, prop, setUncontrolledProp, onChangeRef]\n );\n\n return [value, setValue];\n}\n\nfunction useUncontrolledState<T>({\n defaultProp,\n onChange,\n}: Omit<UseControllableStateParams<T>, 'prop'>): [\n Value: T,\n setValue: React.Dispatch<React.SetStateAction<T>>,\n OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,\n] {\n const [value, setValue] = React.useState(defaultProp);\n const prevValueRef = React.useRef(value);\n\n const onChangeRef = React.useRef(onChange);\n useInsertionEffect(() => {\n onChangeRef.current = onChange;\n }, [onChange]);\n\n React.useEffect(() => {\n if (prevValueRef.current !== value) {\n onChangeRef.current?.(value);\n prevValueRef.current = value;\n }\n }, [value, prevValueRef]);\n\n return [value, setValue, onChangeRef];\n}\n\nfunction isFunction(value: unknown): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "import * as React from 'react';\nimport { useEffectEvent } from '@ryvora/react-use-effect-event';\n\ntype ChangeHandler<T> = (state: T) => void;\n\ninterface UseControllableStateParams<T> {\n prop: T | undefined;\n defaultProp: T;\n onChange: ChangeHandler<T> | undefined;\n caller: string;\n}\n\ninterface AnyAction {\n type: string;\n}\n\nconst SYNC_STATE = Symbol('RYVORA:SYNC_STATE');\n\ninterface SyncStateAction<T> {\n type: typeof SYNC_STATE;\n state: T;\n}\n\nexport function useControllableStateReducer<T, S extends {}, A extends AnyAction>(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams<T>,\n initialState: S\n): [S & { state: T }, React.Dispatch<A>];\n\nexport function useControllableStateReducer<T, S extends {}, I, A extends AnyAction>(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams<T>,\n initialArg: I,\n init: (i: I & { state: T }) => S\n): [S & { state: T }, React.Dispatch<A>];\n\nexport function useControllableStateReducer<T, S extends {}, A extends AnyAction>(\n reducer: (prevState: S & { state: T }, action: A) => S & { state: T },\n userArgs: UseControllableStateParams<T>,\n initialArg: any,\n init?: (i: any) => Omit<S, 'state'>\n): [S & { state: T }, React.Dispatch<A>] {\n const { prop: controlledState, defaultProp, onChange: onChangeProp, caller } = userArgs;\n const isControlled = controlledState !== undefined;\n\n const onChange = useEffectEvent(onChangeProp);\n\n // OK to disable conditionally calling hooks here because they will always run\n // consistently in the same environment. Bundlers should be able to remove the\n // code block entirely in production.\n /* eslint-disable react-hooks/rules-of-hooks */\n if (process.env.NODE_ENV !== 'production') {\n const isControlledRef = React.useRef(controlledState !== undefined);\n React.useEffect(() => {\n const wasControlled = isControlledRef.current;\n if (wasControlled !== isControlled) {\n const from = wasControlled ? 'controlled' : 'uncontrolled';\n const to = isControlled ? 'controlled' : 'uncontrolled';\n console.warn(\n `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n );\n }\n isControlledRef.current = isControlled;\n }, [isControlled, caller]);\n }\n /* eslint-enable react-hooks/rules-of-hooks */\n\n type InternalState = S & { state: T };\n const args: [InternalState] = [{ ...initialArg, state: defaultProp }];\n if (init) {\n // @ts-expect-error\n args.push(init);\n }\n\n const [internalState, dispatch] = React.useReducer(\n (state: InternalState, action: A | SyncStateAction<T>): InternalState => {\n if (action.type === SYNC_STATE) {\n return { ...state, state: action.state };\n }\n\n const next = reducer(state, action);\n if (isControlled && !Object.is(next.state, state.state)) {\n onChange(next.state);\n }\n return next;\n },\n ...args\n );\n\n const uncontrolledState = internalState.state;\n const prevValueRef = React.useRef(uncontrolledState);\n React.useEffect(() => {\n if (prevValueRef.current !== uncontrolledState) {\n prevValueRef.current = uncontrolledState;\n if (!isControlled) {\n onChange(uncontrolledState);\n }\n }\n }, [onChange, uncontrolledState, prevValueRef, isControlled]);\n\n const state = React.useMemo(() => {\n const isControlled = controlledState !== undefined;\n if (isControlled) {\n return { ...internalState, state: controlledState };\n }\n\n return internalState;\n }, [internalState, controlledState]);\n\n React.useEffect(() => {\n // Sync internal state for controlled components so that reducer is called\n // with the correct state values\n if (isControlled && !Object.is(controlledState, internalState.state)) {\n dispatch({ type: SYNC_STATE, state: controlledState });\n }\n }, [controlledState, internalState.state, isControlled]);\n\n return [state, dispatch as React.Dispatch<A>];\n}\n"],
"mappings": "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",
"names": ["value", "React", "state", "isControlled"]
}