3
\$\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');
    }
}
\$\endgroup\$
0
\$\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');
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.