SetStateは、状態の更新が同期的であると誤解させる新参者に悪名高い評判があります。 setState調整アルゴリズムを介して状態を計算するのに非常に高速ですが、それでも非同期操作であるため、不適切に使用すると、Reactコンポーネントで複雑な状態を管理するのが非常に困難になる可能性があります。 この記事では、setState'sあまり知られていない機能シグネチャについて学びます。これによりアトミック状態の更新が保証されます。

SetStateClassic™

Reactをしばらく使用している場合は、次のような状況に遭遇した可能性があります。

/*
The initial value of
this.state.count = 0
*/

// multiple calls
this.setState(count: this.state.count + 1);
this.setState(count: this.state.count + 1);
console.log(this.state.count); // 1

// for-loop
for (let i = 0; i < 10; i++) {
  this.setState({count: this.state.count + 1});
}
console.log(this.state.count); // 1

// if-statement
this.setState({count: this.state.count + 1});
if (this.state.count === 0) {
  console.log(this.state.count);  // 0
}

SetStateは、Reactコミュニティへの新規参入者に多くの混乱を引き起こしました。 キーワード「reactsetstate」を使用してStackOverflowをすばやく検索すると、setStateが非同期か同期かについてまだ混乱しているがあります。 Eric Elliotは、 setStateをすべて一緒に回避し、Reduxにすべての状態の更新を処理させることさえ提案しました。

そして、不満は部分的に正当化されます。 Reactの公式ドキュメントでは、setStateオブジェクトリテラルを渡すこの古典的な方法では、修正が難しい「競合状態」のバグが発生する傾向があります。 コンポーネントに管理するstateがたくさんある場合、この問題はさらに複雑になります。

Silly state illustration

機能的なSetState

ドキュメントであまり目立つように宣伝されていないsetStateを使用する別の方法があります。

this.setState((prevState) => {
  return {count: prevState.count + 1};
})
this.setState((prevState) => {
  return {count: prevState.count + 1};
})
this.setState((prevState) => {
  return {count: prevState.count + 1};
})
this.setState((prevState) => {
  console.log(prevState.count);  // 3
  return {count: prevState.count + 1};
})

これまで見たことがなければ、私がこれを作っていると思うのは間違いないでしょうが、それは本物のだと確信しています。 (使い慣れたオブジェクトリテラルの代わりに)関数を渡すことにより、現在の状態ツリーを最初の引数として取得し、後続のsetState呼び出しに制御を渡す前に、独自の任意の計算を実行する機会があります。 つまり、これらの関数はアトミック状態の更新を受け取ることが保証されています。

「関数を使用してsetStateを複数回呼び出すのは安全です。 更新はキューに入れられ、後で呼び出された順序で実行されます。」 –ダン・アブラモフ

この使い方setState Reactで状態を操作するための予測可能で信頼性の高い方法を提供します。 より冗長ですが、状態を更新するために大規模な状態ツリーや高度なロジックを使用している場合は不可欠になります。

両方のアプローチを比較できるように、実際の例を見てみましょう。

password box illustration

パスワードバリデーターの構築

ユーザーがパスワードをリセットできるようにするフォームを作成していると想像してください。 いくつかの状態を保持する必要があります。 そのほとんどは、パスワード強度を検証するためのものです。

class PasswordForm extends Component {

  constructor(props) {
    super(props);
    this.state = {
      password: '',
      hasEnoughChars: false,
      hasUpperAndLowercaseChars: false,
      hasSpecialChars: false,
      isPasswordValid: false
    };
  }

  render() {
    return (
      <div>

        /*input*/
        <input onChange={this.handleInput} type="password" value={this.state.password}/>

        /*visual prompts*/
        <div>
          <span style={bgColor(this.state.hasEnoughChars)}>
            Minimum 8 characters
          </span>
          <span style={bgColor(this.state.hasUpperAndLowercaseChars)}>
            Include 1 uppercase and lowercase letter
          </span>
          <span style={bgColor(this.state.hasSpecialChars)}>
            Minimum 1 special character
          </span>
        </div>

        /*button*/
        <button disabled={!this.state.isPasswordValid}>
          Submit
        </button>

      </div>
    );
  }

  bgColor(condition) {
    return {backgroundColor: condition ? 'green' : 'red'};
  }

  // Object literal & Callback
  handleInput(e) {

    this.setState({
      password: e.target.value,
      hasEnoughChars: e.target.value.length >= 8,
      hasUpperAndLowercaseChars: /[a-z]/.test(e.target.value) && /[A-Z]/.test(e.target.value),
      hasSpecialChars: /[!"#$%&'()*+,-./:;<=>[email protected][\]^_`{|}~]/.test(e.target.value)
    }, () => {

      if (this.state.hasEnoughChars && this.state.hasUpperAndLowercaseChars && this.state.hasSpecialChars) {
        this.setState({isPasswordValid: true});
      }
      else {
        this.setState({isPasswordValid: false});
      }
    });
  }

  // Functions
  handleInput(e) {

    this.setState({password: e.target.value});

    this.setState((prevState) => ({
      hasEnoughChars: prevState.password.length >= 8,
      hasUpperAndLowercaseChars: /[a-z]/.test(prevState.password) && /[A-Z]/.test(prevState.password),
      hasSpecialChars: /[!"#$%&'()*+,-./:;<=>[email protected][\]^_`{|}~]/.test(prevState.password)
    }));

    this.setState((prevState) => ({
      isPasswordValid: prevState.hasEnoughChars
        && prevState.hasUpperAndLowercaseChars
        && prevState.hasSpecialChars
    }));
  }
}

コールバックの使用は、関数をネストすることによって物事の順序を管理するという考え方に私たちを置きます。 今のところそれほど悪くはありませんが、this.setState({isPasswordValid})の後にコールバックを追加する必要がある場合は、すぐに醜くなります。

機能の使用setStateは、1つの関心領域に合わせて調整されています。 このため、状態を遷移するために必要なすべての関連情報が最初の引数prevStateで提供されるため、これらの関数定義内で状態更新の順序を「ハードコーディング」する必要はありません。

まとめ

setStateにこの関数シグネチャを使用すると、最新の状態ツリーを直接操作できます。 この予測可能性により、問題についてより効果的に推論し、自信を持って状態が豊富なReactコンポーネントを構築できます。

このパターンを試してみて、気に入ったかどうかを確認してください。 このsetStateの使用方法が事実上になる可能性があります。 ツイートで、Dan Abramovは、「将来、Reactは最終的にこのパターンをより直接的にサポートする可能性が高い」と示唆しました。

👉パスワードフォームのCodePenをチェックしてください