What I was missing in React.js functional components

In recent years, perhaps the lazy hasn't written about React hooks. I also made up my mind.





I remember the first impression - the WOW effect. You don't have to write classes. You don't need to describe the type of state, initialize the states in the constructor, push the entire state into one object, remember how to setState



merge the new state with the old one. You no longer have to force methods componentDidMount



and componentWillUnmount



confusing logic of initialization and release of resources.





Here's a simple component: a controllable text box and a counter that increments by 1 on a timer and decrements by 10 on the click of a button;





import * as React from "react";

interface IState {
    numValue: number;
    strValue: string;
}

export class SomeComponent extends React.PureComponent<{}, IState> {
    
    private intervalHandle?: number;

    constructor() {
        super({});
        this.state = { numValue: 0, strValue: "" };
    }

    render() {
        const { numValue, strValue } = this.state;
        return <div>
            <span>{numValue}</span>
            <input type="text" onChange={this.onTextChanged} value={strValue} />
            <button onClick={this.onBtnClick}>-10</button>
        </div>;
    }

    private onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) => 
				this.setState({ strValue: e.target.value });

    private onBtnClick = () => this.setState(({ numValue }) => ({ numValue: numValue - 10 }));

    componentDidMount() {
        this.intervalHandle = setInterval(
            () => this.setState(({ numValue }) => ({ numValue: numValue + 1 })),
            1000
        );
    }

    componentWillUnmount() {
        clearInterval(this.intervalHandle);
    }
}

      
      



becomes even simpler:





import * as React from "react";

export function SomeComponent() {
    const [numValue, setNumValue] = React.useState(0);
    const [strValue, setStrValue] = React.useState("");

    React.useEffect(() => {
        const intervalHandle = setInterval(() => setNumValue(v => v - 10), 1000);
        return () => clearInterval(intervalHandle);
    }, []);

    const onBtnClick = () => setNumValue(v => v - 10);
    const onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) => setStrValue(e.target.value);

    return <div>
        <span>{numValue}</span>
        <input type="text" onChange={onTextChanged} value={strValue} />
        <button onClick={onBtnClick}>-10</button>
    </div>;
}
      
      



The functional component is not only two times shorter, it is clearer: the function fits into one screen, everything is in front of your eyes, the designs are laconic and clear. Beauty.





But in the real world, not all components are so simple. Let's add to our component the ability to signal the parent about a number and string change, input



and button



replace the elements and with components Input



and Button



, which will require wrapping event handlers with a hook useCallback



.





interface IProps {
    numChanged?: (sum: number) => void;
    stringChanged?: (concatRezult: string) => void;
}

export function SomeComponent(props: IProps) {
    const { numChanged, stringChanged } = props;
    const [numValue, setNumValue] = React.useState(0);
    const [strValue, setStrValue] = React.useState("");

    const setNumValueAndCall = React.useCallback((diff: number) => {
        const newValue = numValue + diff;
        setNumValue(newValue);
        if (numChanged) {
            numChanged(newValue);
        }
    }, [numValue, numChanged]);

    React.useEffect(() => {
        const intervalHandle = setInterval(() => setNumValueAndCall(1), 1000);
        return () => clearInterval(intervalHandle);
    }, [setNumValueAndCall]);

    const onBtnClick = React.useCallback(
        () => setNumValueAndCall(- 10),
        [setNumValueAndCall]);

    const onTextChanged = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
        setStrValue(e.target.value);
        if (stringChanged) {
            stringChanged(e.target.value);
        }
    }, [stringChanged]);

    return <div>
        <span>{numValue}</span>
        <Input type="text" onChange={onTextChanged} value={strValue} />
        <Button onClick={onBtnClick}>-10</Button>
    </div>;
}

      
      



: useCallback



, . onBtnClick



useEffect



setNumValueAndCall



, useCallback



, (setNumValueAndCall



) . , - , onBtnClick



useEffect



setNumValueAndCall



.





. , .





.





export class SomeComponent extends React.PureComponent<IProps, IState> {
    private intervalHandle?: number;
    constructor() {
        super({});
        this.state = { numValue: 0, strValue: "" };
    }

    render() {
        const { numValue, strValue } = this.state;
        return <div>
            <span>{numValue}</span>
            <Input type="text" onChange={this.onTextChanged} value={strValue} />
            <Button onClick={this.onBtnClick}>-10</Button>
        </div>;
    }

    private onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
        this.setState({ strValue: e.target.value });
        const { stringChanged } = this.props;
        if (stringChanged) {
            stringChanged(e.target.value);
        }
    }

    private onBtnClick = () => this.setNumValueAndCall(- 10);

    private setNumValueAndCall(diff: number) {
        const newValue = this.state.numValue + diff;
        this.setState({ numValue: newValue });
        const { numChanged } = this.props;
        if (numChanged) {
            numChanged(newValue);
        }
    }

    componentDidMount() {
        this.intervalHandle = setInterval(
            () => this.setNumValueAndCall(1),
            1000
        );
    }

    componentWillUnmount() {
        clearInterval(this.intervalHandle);
    }
}

      
      



What to do? In difficult cases, returning to class components? Well, no, we just love the possibilities that hooks bring.





I propose to move the handlers cluttering the code into the class object along with the dependencies. Isn't that better?





export function SomeComponent(props: IProps) {
    const [numValue, setNumValue] = React.useState(0);
    const [strValue, setStrValue] = React.useState("");
    const { onTextChanged, onBtnClick, intervalEffect } = 
          useMembers(Members, { props, numValue, setNumValue, setStrValue });

    React.useEffect(intervalEffect, []);

    return <div>
        <span>{numValue}</span>
        <Input type="text" onChange={onTextChanged} value={strValue} />
        <Button onClick={onBtnClick}>-10</Button>
    </div>;
}

type SetState<T> = React.Dispatch<React.SetStateAction<T>>;

interface IDeps {
    props: IProps;
    numValue: number;
    setNumValue: SetState<number>;
    setStrValue: SetState<string>;
}

class Members extends MembersBase<IDeps> {

    intervalEffect = () => {
        const intervalHandle = setInterval(() => this.setNumValueAndCall(1), 1000);
        return () => clearInterval(intervalHandle);
    };

    onBtnClick = () => this.setNumValueAndCall(- 10);

    onTextChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { props: { stringChanged }, setStrValue } = this.deps;
        setStrValue(e.target.value);
        if (stringChanged) {
            stringChanged(e.target.value);
        }
    };

    private setNumValueAndCall(diff: number) {
        const { props: { numChanged }, numValue, setNumValue } = this.deps;
        const newValue = numValue + diff;
        setNumValue(newValue);
        if (numChanged) {
            numChanged(newValue);
        }
    };
}

      
      



The component code is again simple and elegant. Event handlers, along with dependencies, peacefully huddle in the class.





Hook useMembers



and base class are trivial:





export class MembersBase<T> {
    protected deps: T;
    setDeps(d: T) {
        this.deps = d;
    }
}

export function useMembers<D, T extends MembersBase<D>>(ctor: (new () => T), deps:  (T extends MembersBase<infer D> ? D : never)): T {
    const ref = useRef<T>();
    if (!ref.current) {
        ref.current = new ctor();
    }
    const rv = ref.current;
    rv.setDeps(deps);
    return rv;
}

      
      



Code on Github








All Articles