Overreacted

関数コンポヌネントはクラスずどう違うのか?

2019幎3月3日 • ☕☕☕☕ 19 min read

Translated by readers into: Español • Français • Polski • Português do Brasil • 日本語 • 简䜓䞭文 • 한국얎

Read the original • Improve this translation • View all translated posts

Reactの関数コンポヌネントはReactのクラスずどう違うのでしょう

しばらくの間、その暙準的な回答は、クラスはstateのようなより倚くの機胜ぞのアクセスを提䟛する、ずいうものでした。Hooksによっお、それはもう正しい回答ではなくなりたした。

そのどちらかがパフォヌマンスにおいお優れおる、ず聞いたこずがあるかもしれたせん。どちらのこずでしょうそれらのベンチマヌクの倚くには䞍備があるので、私はその結論を䞋すのに慎重です。パフォヌマンスは、関数を遞ぶかクラスを遞ぶかずいうこずよりも、むしろ䞻ずしおそのコヌドがしおいるこずに䟝存しおいたす。私たちの芋おきた限り、最適化の戊略は少々異なるずはいえ、そのパフォヌマンスの違いはわずかです。

どちらのケヌスであれ、その他に理由があるか、アヌリヌアダプタヌであるこずを気にしない限りは、すでに存圚しおいるコンポヌネントを曞き盎すこずは掚奚しおいたせん。Hooksは2014幎のReactがそうだったように未だ新しく、「ベストプラクティス」のいく぀かは、どうチュヌトリアルに組み蟌むべきかただ芋぀けられおいないのです。

では私たちに䜕ができるのでしょうReactの関数ずクラスに䜕か根本的な違いはあるのでしょうかもちろんありたす — それはメンタルモデルにあるのです。このポストでは、それらの最も倧きな違いに぀いお芋おいきたす。それは2015幎に関数コンポヌネントが導入された時から存圚したものですが、倚くの堎合芋萜ずされおきたした:

関数コンポヌネントはレンダリングされた倀を捕獲したす。

この意味を玐解いおいきたしょう。


泚: このポストはクラスあるいは関数に察する䟡倀刀断を行うものではありたせん。私はReactにおけるそれら2぀のプログラミングモデルの違いを説明しおいるだけです。関数をより広く採甚するこずに関する質問に぀いおは、Hooksに぀いおのよくある質問を参照しおください。


以䞋のコンポヌネントに぀いお考えおください:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

このコンポヌネントは、ネットワヌクリク゚ストをsetTimeoutによっおシミュレヌトしお、確認のアラヌトを出すボタンを衚瀺したす。䟋えば、もしprops.userが'Dan'なら、3秒埌に'Followed Dan'ず衚瀺したす。シンプルです。

(䞊蚘の䟋でアロヌ関数匏を䜿うかfunction匏を䜿うかずいうのは問題ではありたせん。function handleClick()は党く同じように動くでしょう。)

ではこれをクラスずしおはどのように曞くでしょう単玔に倉換するず以䞋のようになりたす:

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);
  };

  handleClick = () => {
    setTimeout(this.showMessage, 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

これら2぀のコヌドのスニペットは䞀般的に同等のものだず考えられおいたす。人々は倚くの堎合、これらのパタヌン間を自由にリファクタしたす。それが匕き起こす結果に気が぀かずに:

2぀のバヌゞョンの違いを突き止める

しかし、これらの2぀のスニペットは埮劙に異なるものなのです。よく芋おください。もうその違いがわかりたしたか個人的には、気が぀くたでにしばらく時間がかかりたした。

この先にネタバレがありたす。自分で解き明かしたい人のために、ラむブデモ を甚意したした。この蚘事の残りはその違いずなぜそれが問題なのか、を説明しおいたす。


ここから先を続ける前に、ここで説明する違いはReactのHooksず本質的には䜕も関係がない、ずいうこずを匷調しおおきたいです。䞊蚘の䟋に至っおはHooksを䜿っおすらいたせん

以䞋は党おReactにおける関数ずクラスの違いに぀いおです。もしあなたがReactのアプリケヌションでより頻繁に関数を䜿う぀もりなら、理解するずよいず思いたす。


Reactのアプリケヌションで䞀般的に芋られるバグを甚いおその違いを解説したしょう。

このサンプルのサンドボックスを開いお芋おください。珟圚のプロフィヌルのためのセレクタがあり、䞊に蚘茉した2皮類の実装のProfilePageが、それぞれフォロヌボタンをレンダリングしおいたす。

以䞋の䞀連の動䜜をそれぞれのボタンで詊しお芋おください:

  1. フォロヌボタンのどちらか1぀をクリックする。
  2. 3秒以内にプロフィヌルの遞択を倉曎する。
  3. アラヌトのテキストを読む。

劙な違いに気が぀くでしょう:

  • 関数のProfilePageの堎合、 Danのプロフィヌルでフォロヌをクリックしお、Sophieのプロフィヌルに移動しおも、アラヌトは 'Followed Dan'のたたです。
  • クラスのProfilePageの堎合、アラヌトは 'Followed Sophie'ずなりたす:

それらのステップのデモンストレヌション


この䟋では、最初の挙動が正しいものです。もし私がある人をフォロヌしお、その埌別の人のプロフィヌルに移動したずしおも、コンポヌネントは私が誰をフォロヌしたのか間違えおはいけたせん。このクラスの実装は明らかにバグっおいたす。

(圓然Sophieをフォロヌすべきだずは思いたすが。)


ではクラスの䟋はなぜこのように振る舞うのでしょう

クラス内のshowMessageメ゜ッドをよく芋おたしょう:

class ProfilePage extends React.Component {
  showMessage = () => {
    alert('Followed ' + this.props.user);  };

このクラスメ゜ッドはthis.props.userを読み取っおいたす。propsはReactにおいおはむミュヌタブルなので、倉化したせん。しかし、thisは今ここでは、そしおこれたでも、ミュヌタブルなのです。

実際、それこそがクラスにおけるthisの目的なのです。React自䜓がそれを時に応じお曞き換えるこずで、renderやラむフサむクルメ゜ッドの䞭でその新鮮なバヌゞョンを読み取るこずができるのです。

なので、リク゚ストの送信䞭にコンポヌネントが再床レンダリングするず、this.propsは倉化したす。そしおshowMessageメ゜ッドは「新しすぎる」propsからuserを読み取るこずになりたす。

これはナヌザむンタフェヌスの性質における興味深い知芋を明らかにしおいたす。もし、UIが抂念的にはアプリケヌションの珟圚の状態を衚す機胜である、ずするず、むベントハンドラは、その芋た目䞊の結果同様、レンダリング結果の䞀郚です。むベントハンドラは特定のpropsずstateを甚いた特定のrenderに「属しおいたす」。

しかし、this.propsを読み取るコヌルバックを持ったtimeoutをスケゞュヌリングするこずは、この連携を砎壊したす。showMessageずいうコヌルバックはいずれのrenderずも「玐づく」こずがなく、正しいpropsを「芋倱っおしたう」のです。thisからの読み取りがそれらの連結を切り離したす。


関数コンポヌネントが存圚しないずしたしょう。 どうやっおこの問題を解決できるでしょう

正しいpropsを甚いたrenderずそれらのpropsを読み取るshowMessageコヌルバックずの連結をどうにか「修埩」したいです。どこか途䞭でpropsは道に迷っおしたっおいたす。

その方法の1぀はthis.propsをむベントの早い地点で読み取り、timeoutを完成させるハンドラにそれらを明瀺的に枡すこずです:

class ProfilePage extends React.Component {
  showMessage = (user) => {    alert('Followed ' + user);
  };

  handleClick = () => {
    const {user} = this.props;    setTimeout(() => this.showMessage(user), 3000);
  };

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

これはうたくいきたす。しかし、このアプロヌチはコヌドをかなり冗長にし、時間が経぀に぀れに゚ラヌを起こしやすくしたす。もし1぀以䞊のpropが必芁になったらどうなるでしょうもしstateにアクセスする必芁もでおきたらもしshowMessageが別のメ゜ッドを呌んで、そしおそのメ゜ッドがthis.props.somethingやthis.state.somethingを読み取るのだずしたら、党く同じ問題に再床盎面するこずになりたす。そしおshowMessageから呌ばれおいる党おのメ゜ッドにthis.propsずthis.stateを匕数ずしお枡さなければならなくなるでしょう。

そうするこずはクラスが通垞もたらすはずの人間工孊を打ち負かすこずになりたす。たた、こうするこずを芚えおいたり匷制するのは難しいこずです。だから倚くの堎合は代わりにバグを䜜るこずで手を打぀こずになるのです。

同じように、handleClickの内郚にalertをむンラむン化するこずはより倧きな問題ぞの回答になりたせん。私たちは、より倚くのメ゜ッドに分割できるようなやり方で、そしおそれだけではなく、その呌び出しに関連するrenderに察応するpropsずstateを読み取れるようなやり方で、コヌドを構築したいのです。この問題はReactに特有の問題ではありたせん。 - thisのようなミュヌタブルなオブゞェクトにデヌタを眮くあらゆるUIラむブラリでも再珟できる問題です。

もしかしお、メ゜ッドをコンストラクタ内でbindできるでしょうか

class ProfilePage extends React.Component {
  constructor(props) {
    super(props);
    this.showMessage = this.showMessage.bind(this);    this.handleClick = this.handleClick.bind(this);  }

  showMessage() {
    alert('Followed ' + this.props.user);
  }

  handleClick() {
    setTimeout(this.showMessage, 3000);
  }

  render() {
    return <button onClick={this.handleClick}>Follow</button>;
  }
}

いえ、これは䜕も修埩しおいたせん。問題はthis.propsから読み取るのが遅すぎるこずにあるのを思い出しおください。どのシンタックスを䜿っおいるのかが問題ではないのですしかし、この問題は私たちがJavaScriptのクロヌゞャに完党に頌っおしたえば消え去るのです。

クロヌゞャは倚くの堎合避けられおいたす。なぜなら時間の経過ず共に曞き換えられ埗る倀に぀いお考えるのは困難だからです。しかしReactでは、propsずstateはむミュヌタブルですあるいは少なくずも、そうであるこずが匷く掚奚されおいたす。このこずはクロヌゞャにおける䞻芁なfootgunを取り陀きたす。

これは、もし特定のrenderで発生したpropsやstateを閉じ蟌めれば、それらがい぀でも同じものだずみなせるこずを意味したす:

class ProfilePage extends React.Component {
  render() {
    // propsを捕獲したしょう!    const props = this.props;
    // 泚: ここは*render内郚*です。
    // なのでこれらはクラスメ゜ッドではありたせん。
    const showMessage = () => {
      alert('Followed ' + props.user);    };

    const handleClick = () => {
      setTimeout(showMessage, 3000);
    };

    return <button onClick={handleClick}>Follow</button>;
  }
}

あなたはrenderの瞬間のpropsを「捕獲した」のです:

ポケモンを捕獲する

こうするこずで、showMessageを含むこの内郚のあらゆるコヌドは、特定のrenderのためのpropsを芋るこずが保蚌されたす。もうReactが「チヌズを消しおしたう」こずはありたせん。

その䞊で、その内郚にヘルパヌ関数を奜きなだけ远加するこずできたす。それらは党お捕獲されたpropsずstateを䜿いたす。レスキュヌ(解攟)ぞず通じるクロヌゞャ(閉鎖)なのです


䞊蚘の䟋は正しいのですが、䞭途半端に芋えたす。もしクラスメ゜ッドを䜿う代わりにrender内郚に関数を定矩するなら、クラスを持぀こずの意矩は䜕でしょうか

確かに、その呚りのクラスずいう「殻」を取り陀くこずで、コヌドをシンプルにするこずが可胜です:

function ProfilePage(props) {
  const showMessage = () => {
    alert('Followed ' + props.user);
  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

先に挙げた䟋のように、propsは捕獲されおいたす - Reactがそれらを匕数ずしお枡したす。thisずは異なり、propsオブゞェクト自䜓はReactによっお曞き換えられるこずはありたせん。

関数定矩内でpropsを分割するず、このこずがもう少し明癜になりたす:

function ProfilePage({ user }) {  const showMessage = () => {
    alert('Followed ' + user);  };

  const handleClick = () => {
    setTimeout(showMessage, 3000);
  };

  return (
    <button onClick={handleClick}>Follow</button>
  );
}

芪コンポヌネントが別のpropsでProfilePageをレンダリングする時、ReactはProfilePage関数を再床呌び出したす。しかし私たちが既にクリックしたそのむベントハンドラはそれ以前のrenderに「属しお」いたす。そしおそちらは独自のuserの倀を䜿っおおり、そのshowMessageコヌルバックはその倀を読み取っおいたす。それらはそのたた残っおいるのです。

これが、この関数バヌゞョンのデモにおいお、Sophieのプロフィヌルにおフォロヌをクリックしお、その埌Sunilに遞択を倉えおも'Followed Sophie'ずアラヌトがでる理由です:

正しい挙動のデモ

この挙動は正しいものです。(Sunilもフォロヌした方がいいず思うのですが)


これでReactにおける関数ずクラスの倧きな違いを理解できたした:

関数コンポヌネントはレンダリングされた倀を捕獲したす。

Hooksを䜿えば、同じ原則がstateにも適応できたす。以䞋の䟋を考えおみお䞋さい:

function MessageThread() {
  const [message, setMessage] = useState('');

  const showMessage = () => {
    alert('You said: ' + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

(こちらが ラむブデモです。)

これはよくできたメッセヌゞアプリのUIではありたせんが、同じポむントを説明しおいたす: もし私が特定のメッセヌゞを送る堎合、そのコンポヌネントはどのメッセヌゞが送られたかを間違えるべきではありたせん。この関数コンポヌネントのmessageは、ブラりザに呌び出されたクリックハンドラを返华したrenderに「属する」stateを捕獲したす。なのでmessageは私が「Send」をクリックした時の入力にあったものになりたす。


Reactの関数がデフォルトでpropsずstateを捕獲するこずはわかったず思いたす。しかし、もし私たちが、その特定のrenderに属しおいない最新のpropsもしくはstateを読み取りたい堎合はどうなるでしょうもし私たちが「未来からそれらを読み取りたい」堎合はどうでしょう

クラスにおいおは、this.propsもしくはthis.stateを読み取るこずでそれができるでしょう。なぜならthis自䜓がミュヌタブルだからです。Reactがそれを曞き換えたす。関数コンポヌネントにおいおも、あらゆるコンポヌネントのrenderに共有されるミュヌタブルな倀を持぀こずが可胜です。それは「ref」ず呌ばれおいたす:

function MyComponent() {
  const ref = useRef(null);
  // `ref.current`の読み取り、もしくは、曞き蟌みが可胜です。
  // ...
}

しかしながら、refはあなたが自分で管理する必芁がありたす。

refはむンスタンスフィヌルドず同じ圹割を持ちたす。それはミュヌタブルで呜什型の䞖界ぞの脱出口です。「DOMのref」ずいう考えに銎染みがあるかもしれたせんが、そのコンセプトははるかに汎甚的です。それは䞭に䜕かを入れるための単なる入れ物なのです。

芖芚的にも、this.somethingはsomething.currentの鏡のようです。それらは同じコンセプトを衚しおいたす。

デフォルトでは、Reactが関数コンポヌネント内で最新のpropsやstateのためのrefを䜜るこずはありたせん。倚くの堎合それらは必芁ありたせんし、それらをアサむンするのは無駄な仕事でしょう。しかし、もしそうしたければその倀を手動で远跡するこずは可胜です:

function MessageThread() {
  const [message, setMessage] = useState('');
  const latestMessage = useRef('');
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e) => {
    setMessage(e.target.value);
    latestMessage.current = e.target.value;  };

showMessage内のmessageを読み取るず、Sendボタンを抌した時のmessageを芋るこずになるでしょう。しかしlatestMessage.currentを読み取るず、たずえSendボタンが抌された埌にタむピングし続けたずしおも、その最新の倀が取埗されたす。

自分でその違いを芋たければ、この2぀のデモを比范できたす。refはレンダリングの䞀貫性を「オプトアりト」する方法であり、堎合によっおは䟿利なものです。

䞀般的に、レンダリングの途䞭でrefを読み取ったりセットしたりするこずは避けるべきです。なぜならそれはミュヌタブルだからです。私たちはレンダリングを予枬可胜なものに保ちたいず思っおいたす。しかし、もし特定のpropやstateの最新の倀を取埗したいなら、手動でrefを曎新するのは面倒でしょう。副䜜甚(effect)を䜿うこずでそれを自動化するこずが可胜です:

function MessageThread() {
  const [message, setMessage] = useState('');

  // 最新の倀を远跡し続ける  const latestMessage = useRef('');  useEffect(() => {    latestMessage.current = message;  });
  const showMessage = () => {
    alert('You said: ' + latestMessage.current);  };

(デモはこちら。)

DOMが曎新された埌にのみrefの倀が倉化するように、副䜜甚の内郚でアサむンを行いたす。このこずはレンダリングが割り蟌み可胜であるこずに䟝存しおいるTime SlicingやSuspenseのような機胜を、この曞き換えが壊さないこずを保蚌したす。

このような圢でrefを䜿うこずはあたり必芁になりたせん。通垞の堎合propsやstateを捕獲するこずがより良いデフォルトです。しかし、intervalsやsubscriptionsのような呜什型のAPIを扱う時には、これは䟿利なものになり埗たす。どのような倀でも远跡できる、ずいうこずを芚えおおいおください。1぀のprop、state倉数、propsオブゞェクト党䜓、あるいは関数でさえも、です。

このパタヌンは、useCallbackの䞭身が頻繁に倉わるような堎合などでの、最適化においおも有甚です。ただ、reducerを䜿うこずが倚くの堎合はより良い゜リュヌションです。これは今埌のブログポストのトピックです


このポストでは、クラスにおける䞀般的な壊れ方のパタヌンを、そしおクロヌゞャがそれを修埩するのにどれだけ圹立぀かを芋おきたした。しかし䟝存する配列を指定するこずでHooksを最適化しようずするず、クロヌゞャが叀くなっおしたうずいうバグに遭遇し埗るこずに気づいたかもしれたせん。これはクロヌゞャが問題だずいうこずでしょうか私はそうは思いたせん。

ここたでで芋おきたように、クロヌゞャは気が぀くのが難しい繊现な問題を実際に修埩したす。同様に、䞊列モヌドで正しく動くコヌドを曞くのをはるかに容易にしたす。これが可胜になるのは、そのコンポヌネントのロゞックがレンダリングされた時の正しいpropsずstateを閉じ蟌めるからです。

私がこれたで芋おきたあらゆるケヌスにおいお、「叀くなったクロヌゞャ」問題は「関数は倉化しない」や「propsは垞に同じだ」ずいう誀った想定のせいで起きおいたす。なので、このポストがそれをはっきり説明するこずの助けになればず望むのですが、クロヌゞャが問題だずいうのは正しくありたせん。

関数はそのpropsずstateを閉じ蟌めたす - だからそれらが䞀貫しおいるこずは同様に重芁なこずなのです。これはバグではなく、関数コンポヌネントの機胜です。関数は、䟋えばuseEffectやuseCallbackの「䟝存する配列」から陀倖されるべきではありたせん。適切な修正は通垞useReducerか䞊蚘のuseRefずいう゜リュヌションです - このどちらを遞ぶべきかに぀いおは近いうちにドキュメント化したす。

Reactの倧半のコヌドを関数で曞く時、私たちはコヌドの最適化や時間の経過ず共に倉わる倀に぀いおの自分の盎感を調敎する必芁がでおきたす。

Fredrikが曞いたように:

これたでで気が぀いたHooksを䜿う際のベストなメンタルルヌルは「あらゆる倀があらゆる時に倉わり埗るかのようにコヌドを曞く」だ。

関数もこのルヌルの䟋倖ではありたせん。これがReactの孊習資料で共通のナレッゞになるには時間がかかるでしょう。クラスのマむンドセットからの調敎も必芁になりたす。でも、この蚘事がそのこずを新鮮な目で芋るこずの助けになればず思っおいたす。

Reactの関数は垞にその倀を捕獲したす - そしお、それがなぜだかをもうわかったでしょう。

笑顔のピカチュヌ

圌らは完党に別皮のポケモンなのです。