Create React UI component with uncontrollable

If you’re writing a UI library in React, a package you really should know is uncontrollable.

What does it do you may ask? It does a lot but to explain it’s quite messy so let’s start with an example instead.

Cheatsheet

This is uncontrollable in action:

import { SetStateAction } from "react";
import { useUncontrolled } from "uncontrollable";

type UIInputProps = {
    defaultValue?: string;
    value?: string;
    onChange?: SetStateAction<string>;
};

function UIInput(props: UIInputProps) {
    
    /*


        PROPS


     */

    const {
        value, 
        onChange,

        ... rest
    } = useUncontrollable(props, {
            value: "onChange"
        });


    return (
        <input 
            value={value}
            onChange={(e) => {
                onChange(e.target.value);
            }}
            {...rest} />
    );
}

export default UIInput;

This doesn’t seem much? Look better. This allows you to expose 3 optional props in your component value, defaultValue, onChange and not worry about them.

Just because you’re having useUncontrollable you covered 8 cases basically, from all undefined to all set. Not bad at all.

So what’s the original problem?

Let’s say you’re writing your UI component library and you want to provide a bunch of props that may or may not be used.

An example would be a UIInput component with defaultValue, value and onChange in all sorts of combinations.

// Case: defaultValue only 
<UIInput 
  name="email"
  defaultValue={user.email}
  onChange={(email) => ...} />

// Case: value only 
<UIInput 
  name="email"
  value={value}
  onChange={(email) => setValue(email)} />

Now, to support these cases you may have code like this:

function UIInput(props) {
    
    /*


        PROPS


     */

    const {
        value, 
        defaultValue = '', 
        onChange,

        ... rest
    } = props;
  

    /*


        INTERNAL


     */

    const isControlled = value !== undefined;


    /*


        STATES


     */

    const [
        internalValue, 
        setInternalValue
    ] = useState(defaultValue);


    /*


        EFFECTS


     */

    useEffect(() => {
        if (isControlled) {
            setInternalValue(value);
        }
    }, [value, isControlled]);


    /*


        FUNCTIONS


     */

    const handleChange = (e) => {
        const newValue = e.target.value;
        
        if (!isControlled) {
            setInternalValue(newValue);
        }
        
        onChange?.(newValue);
    };

    return (
        <input 
            value={isControlled ? value : internalValue}
            onChange={handleChange}
            {...rest} />
    );
}

export default UIInput;

You see how much trouble you have with props, useState, useEffect to keep internal and external states in sync? This can be quite noisy in a way. Well ,that’s what uncontrollable does, it saves you from all that boilerplate.

What’s under the hood?

Ok, apart from what it does for you, the other nice thing is that uncontrollable is 92 lines of code which makes it quite accessible to understand.

So let’s dive in. There are two main functions/hooks in it: useUncontrolledProp and useUncontrolled.

Let’s start with useUncontrolledProp.

// Reindented and removed some types for brevity 
function useUncontrolledProp(
    propValue: TProp | undefined,
    defaultValue: TProp | undefined,
    handler?: THandler
) {
  
    const wasPropRef = useRef<boolean>(propValue !== undefined);
 
    //
    // 1. create the internal state
    //    and init to default value
    const [stateValue, setState] = useState<TProp | undefined>(defaultValue); 
 
    const isProp = propValue !== undefined;
    const wasProp = wasPropRef.current;
 
    wasPropRef.current = isProp;
 
    /**
     * If a prop switches from controlled to Uncontrolled
     * reset its value to the defaultValue
     */
    if (!isProp && wasProp && stateValue !== defaultValue) {
        setState(defaultValue);
    }
 
    return [
        isProp ? propValue : stateValue,
        useCallback(
            (...args: Parameters<THandler>): ReturnType<THandler> | void => {
                const [value, ...rest] = args;
 
                //
                // 2. if external setState pass value 
                //    and keep internal state in sync
                let returnValue = handler?.(value, ...rest); << if external 
                setState(value);
                return returnValue;
            },
            [handler]
        ),
   ] as const;
}

The other function is useUncontrolled which is:

// Again reindented and removed the types for brevity 

export function useUncontrolled(props: TProps, config: ConfigMap<TProps>): Omit<TProps, TDefaults> {
    //
    // assuming you have 
    //    const config = { value, "onChange"};
    //    const {
    //        value, 
    //        onChange,
    //
    //        ... rest
    //    } = useUncontrollable(props, config);
    //
    return 
        Object
            .keys(config)  // << each key, in this case value
                .reduce((result: TProps, fieldName: string) => {
                    // for value this is
                    //
                    // const {
                    //     defaultValue: defaultValue,
                    //     value: propsValue
                    //     ...rest << these are the rest of the initial props
                    // } = result as any;
                    // 
                    // This is because 
                    //
                    // defaultKey(fieldName) >> defaultValue 
                    // fieldName >> value 
 
                    const {
                        [defaultKey(fieldName)]: defaultValue, 
                        [fieldName]: propsValue,
                        ...rest
                    } = result as any;

                    //    onChange    = config["value"];
                    const handlerName = config[fieldName]; 
    
                    // we saw before the internal of useUncontrolledProp
                    const [value, handler] = useUncontrolledProp(
                        propsValue,
                        defaultValue,
                        props[handlerName] as Handler
                    );

                    // re-merge value and onChange
                    return {
                        ...rest,
                        [fieldName]: value,
                        [handlerName]: handler,
                    };
                }, props);

Summary

uncontrollable is a package with millions of downloads used by packages like react-widget and react-bootstrap.

If you’re writing a UI component library in React, uncontrollable helps you dealing with the boilerplates React force you into to manage internal and external states.

A symbolic case is when you expose defaultValue, value and onChange or a show, onToggle on your component.

If you are interested in the use case and you want to see more, you can see react-widgets/src/Autocomplete.tsx for useUncontrolledProps and react-bootstrap/src/Nav.tsx for useUncontrolled as and extra example.