I use React with material-ui.com and I love them both, but I'm tired with writing the boilerplate handlers like
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.currentTarget.value)}
again and again. Especially when stopPropagation
is needed (AFAICT it's always either needed or harmless), it gets pretty verbose.
So I tried to extend useState
so that it provides everything I need, so I can write things like:
const [email, setEmail, emailAgg] = useStateEx('');
...
onChange={emailAgg.changeHandler}
or
const [showPassword, , showPasswordAgg] = useStateEx(false);
...
onClick={showPasswordAgg.toggleHandler}
and similar.
My approach follows. I'd like it to be reviewed in general, especially for better typing (I had to use @ts-ignore
) and improvements.
I case you hate semicolons, please ignore them as my eslint is set up to require them. ;)
import { useState, Dispatch, SetStateAction } from 'react';
function booleanAgg(value: boolean, setter: (value: boolean) => void, makeSetter: (value: boolean) => ((event?: any) => void)) {
return {
value,
setter,
falseHandler: makeSetter(false),
trueHandler: makeSetter(true),
toggleHandler: makeSetter(!value),
};
}
function stringAgg(value: string, setter: (value: string) => void) {
return {
value,
setter,
changeHandler: (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => {
e.stopPropagation();
setter(e.currentTarget.value);
}
};
}
function numberAgg(value: number, setter: (value: number) => void) {
return {
value,
setter,
changeHandler: (e: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => {
e.stopPropagation();
setter(+e.currentTarget.value);
}
};
}
/**
* Works like useState and adds a third element containing boolean-specific handlers directly useable in buttons and checkboxes.
*/
export function useStateEx(initialState: boolean) : [boolean, Dispatch<SetStateAction<boolean>>, ReturnType<typeof booleanAgg>];
/**
* Works like useState and adds a third element containing string-specific handlers directly useable in text fields.
*/
export function useStateEx(initialState: string) : [string, Dispatch<SetStateAction<string>>, ReturnType<typeof stringAgg>];
/**
* Works like useState and adds a third element containing number-specific handlers directly useable in text fields.
*/
export function useStateEx(initialState: number) : [number, Dispatch<SetStateAction<number>>, ReturnType<typeof numberAgg>];
/**
* Prohibits use with any type not handled in the above overloads.
*/
export function useStateEx(initialState: any) : never;
export function useStateEx<S extends boolean|string|number>(initialState: S) : unknown {
const [value, setter] = useState(initialState);
function makeSetter(value: S) {
return function(e: any) {
if (typeof e?.stopPropagation === 'function') e.stopPropagation();
setter(value);
};
}
if (typeof value === 'boolean') {
// @ts-ignore
return [value, setter, booleanAgg(value, setter, makeSetter)]
} else if (typeof value === 'string') {
// @ts-ignore
return [value, setter, stringAgg(value, setter)];
} else if (typeof value === 'number') {
// @ts-ignore
return [value, setter, numberAgg(value, setter)];
} else {
throw new Error('Only boolean, string and number is supported');
}
}