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');
}
}
1 Answer 1
I must admit that my experience with typescript and react.js is quite limited so my review will be limited to basic syntax points. This code seems straight-forward. It makes good use of arrow functions and destruction assignment. There are just a couple suggestions I will make below.
if (typeof e?.stopPropagation === 'function') e.stopPropagation();
It is best to include brackets around the block, even if it is all on one line:
if (typeof e?.stopPropagation === 'function') { e.stopPropagation(); }
Some believe such blocks should never be on one line. If you are going to do it on one line, you could use short-circuiting:
typeof e?.stopPropagation === 'function' && e.stopPropagation();
This block can be simplified somewhat:
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'); }
The else
keywords can be avoided because preceding blocks have return
statements.'
if (typeof value === 'boolean') {
// @ts-ignore
return [value, setter, booleanAgg(value, setter, makeSetter)]
}
if (typeof value === 'string') {
// @ts-ignore
return [value, setter, stringAgg(value, setter)];
}
if (typeof value === 'number') {
// @ts-ignore
return [value, setter, numberAgg(value, setter)];
}
throw new Error('Only boolean, string and number is supported');