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.)
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.
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;
}
Top comments (0)