React.jsの機能コンポーネントに欠けていたもの

近年、おそらく怠惰な人はReactフックについて書いていません。私も決心しました。





第一印象-WOW効果を覚えています。クラスを書く必要はありません。状態のタイプを記述したり、コンストラクターで状態を初期化したり、状態全体を1つのオブジェクトにプッシュsetState



したり、新しい状態を古い状態とマージする方法を覚えたりする必要はありませんリソースの初期化と解放のメソッドcomponentDidMount



componentWillUnmount



混乱するロジックを強制する必要がなくなりました





簡単なコンポーネントは次のとおりです。制御可能なテキストボックスと、タイマーで1ずつ増加し、ボタンをクリックすると10ずつ減少するカウンター。





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



機能コンポーネントは2倍短いだけでなく、より明確です。機能は1つの画面に収まり、すべてが目の前にあり、構造は簡潔で明確です。美しさ。





しかし、現実の世界では、すべてのコンポーネントがそれほど単純なわけではありません。のは、数値と文字列、および要素を変更するには、親の可能性、当社のコンポーネント信号を追加してみましょう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);
    }
}

      
      



何をすべきか?難しい場合は、クラスのコンポーネントに戻りますか?いいえ、フックがもたらす可能性が大好きです。





コードを乱雑にするハンドラーを、依存関係とともにクラスオブジェクトに移動することを提案します。それは良くないですか?





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

      
      



コンポーネントコードもシンプルでエレガントです。イベントハンドラーは、依存関係とともに、クラス内で平和的に集まります。





フックuseMembers



と基本クラスは簡単です。





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

      
      



Githubのコード








All Articles