4
\$\begingroup\$

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');
 }
}
Sᴀᴍ Onᴇᴌᴀ
29.5k16 gold badges45 silver badges201 bronze badges
asked Apr 13, 2020 at 15:37
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

I must admit that my experience with and 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');
answered Apr 22, 2020 at 20:10
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.