Was mir in den Funktionskomponenten von React.j gefehlt hat

In den letzten Jahren hat der Faule vielleicht nicht über React Hooks geschrieben. Ich habe mich auch entschieden.





Ich erinnere mich an den ersten Eindruck - den WOW-Effekt. Sie müssen keine Klassen schreiben. Sie müssen den Zustandstyp nicht beschreiben, die Zustände im Konstruktor initialisieren, den gesamten Zustand in ein Objekt verschieben und sich daran erinnern, wie setState



der neue Zustand mit dem alten zusammengeführt wird. Sie müssen keine Methoden componentDidMount



und keine componentWillUnmount



verwirrende Logik für die Initialisierung und Freigabe von Ressourcen mehr erzwingen .





Hier ist eine einfache Komponente: ein steuerbares Textfeld und ein Zähler, der auf einem Timer um 1 erhöht und auf Knopfdruck um 10 verringert wird;





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

      
      



wird noch einfacher:





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



Die Funktionskomponente ist nicht nur zweimal kürzer, sie ist auch klarer: Die Funktion passt in einen Bildschirm, alles ist vor Ihren Augen, die Konstruktionen sind lakonisch und klar. Schönheit.





In der realen Welt sind jedoch nicht alle Komponenten so einfach. Lassen Sie uns unser Komponentensignal die Möglichkeit eines Elternteils fügen Sie die Zahlen und Strings zu ändern, und die Elemente input



, und button



Austausch von Komponenten Input



und Button



das wird Wrap - Handler Haken Ereignisse erfordern 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);
    }
}

      
      



Was zu tun ist? In schwierigen Fällen zu Klassenkomponenten zurückkehren? Nein, wir lieben die Möglichkeiten, die Haken bieten.





Ich schlage vor, die Handler, die den Code überladen, zusammen mit den Abhängigkeiten in das Klassenobjekt zu verschieben. Ist das nicht besser





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

      
      



Der Komponentencode ist wieder einfach und elegant. Event-Handler und Abhängigkeiten drängen sich friedlich in der Klasse zusammen.





Hook useMembers



und Basisklasse sind 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 auf Github








All Articles