DEV Community

Kelley van Evert
Kelley van Evert

Posted on

2

An uncontrollabilizer for React

I just wanted to share my take on making a quick & easy, and hopefully reasonably TypeScript-proof version of jquense's uncontrollable, but written with the sensibility of Yury's stackoverflow answer.

The use-case? You have a React component that houses one, or maybe even a number, of stately values. You would want to both be able to delegate control over to a controlling component, but also want to be able to take the reins yourself if the surrounding component does not want to control you.

1/2 As a HOC

The idea: just write your component as if it's fully controlled, and then use a dead simple HOC wrapper that fills in any necessary state management. The only hard part really, is getting the types right. (Sadly, Exclude<string, keyof P> is just string again, so that doesn't actually work.)

Here's a CodeSandbox example.

And here's the code:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// A "type function" that computes an uncontrolled version
//  of controlled component's props type, given
//  a value key, an onchange key (both currently part
//  of the props type), and a default value key (which should
//  be freshly chosen)
type Uncontrolled<
  P, // controlled component's props
  VK extends keyof P, // value key
  OCK extends keyof P, // onchange key
  DK extends Exclude<string, keyof P> // default value key
> = Omit<P, VK | OCK> & { [k in DK]: P[VK] };

// Turns a controlled component into a component that can both
//  be used in a controlled fashion or an uncontrolled fashion
function uncontrollabilize<
  P,
  VK extends keyof P,
  OCK extends keyof P,
  DK extends Exclude<string, keyof P>
>(
  C: React.FunctionComponent<P>,
  valueKey: VK,
  onChangeKey: OCK,
  defaultKey: DK
): React.FunctionComponent<P | Uncontrolled<P, VK, OCK, DK>> {
  return function Wrapped(props: P | Uncontrolled<P, VK, OCK, DK>) {
    // Using a flag which is only set once, to disable switching between
    //  controlled and uncontrolled usage of the same instance.
    const isControlled = useRef<boolean>(valueKey in props).current;

    // It would be theoretically more correct, and type-check,
    //  if this state initialization only occurs in the
    //  else branch below. But then it's less clear that
    //  the hook is always, or always-not, called.
    // So, stability first.
    const defaultValue = (props as any)[defaultKey];
    const [value, set_value] = useState<P[VK]>(defaultValue);

    if (isControlled) {
      return <C {...props as P} />;
    } else {
      const controllerProps = {
        [valueKey]: value,
        [onChangeKey]: set_value
      };
      return (
        // @ts-ignore
        <C {...props as Uncontrolled<P, VK, OCK, DK>} {...controllerProps} />
      );
    }
  };
}

2/2 As a Hook

And of course there's a hook version of that, which is way shorter and nicer to the eye :D But it does have a slight loss of appeal in not allowing you to type your component's props rigorously. That is, you need to make all three props (value, onChange, defaultValue) optional.

Here's a CodeSandbox example.

function useUncontrollizable<T>(
  val?: T,
  set_val?: (newVal: T) => void,
  default_val?: T
): [T, (newVal: T) => void] {
  const isControlled =
    typeof val !== "undefined" && typeof set_val !== "undefined";
  const control = useState<T>(default_val);
  return isControlled ? [val, set_val] : control;
}

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →