Lo que me faltaba en los componentes funcionales de React.js

En los últimos años, quizás los perezosos no hayan escrito sobre los ganchos de React. Yo también tomé una decisión.





Recuerdo la primera impresión: el efecto WOW. No tienes que escribir clases. No es necesario que describa el tipo de estado, inicialice los estados en el constructor, inserte todo el estado en un objeto, recuerde cómo setState



fusionar el nuevo estado con el anterior. Ya no tiene que forzar métodos componentDidMount



y componentWillUnmount



lógica confusa de inicialización y liberación de recursos.





Aquí hay un componente simple: un cuadro de texto controlable y un contador que aumenta en 1 en un temporizador y disminuye en 10 con el clic de un botón;





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);
    }
}

      
      



:





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>;
}
      
      



, : , , . .





. , input



button



Input



Button



, 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);
    }
}

      
      



¿Qué hacer? En casos difíciles, ¿volver a los componentes de la clase? Bueno, no, simplemente nos encantan las posibilidades que brindan los ganchos.





Propongo mover los controladores que abarrotan el código en el objeto de clase junto con las dependencias. ¿No es mejor eso?





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);
        }
    };
}

      
      



El código del componente es nuevamente simple y elegante. Los controladores de eventos, junto con las dependencias, se apiñan pacíficamente en la clase.





El gancho useMembers



y la clase base son triviales:





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;
}

      
      



Código en Github








All Articles