React コンポーネントをリファクタリングする方法
Reactアプリケーションに適用されるリファクタリングに関する一般的な問題について解説します。
クラスコンポーネントから学習してしまい、フックに移行する前に少し混乱する可能性があります。
関数コンポーネントが好まれる理由と、クラスコンポーネントよりも関数コンポーネントを使用する利点があるかどうかを詳しく学んで下さい。
なぜクラスではなくフックなのか?
Reactでクラスを使用する利点は、状態がいつ変化したかを識別し、this.state
キーワードを使用してグローバル状態またはコンポーネント状態を更新するライフサイクルメソッドがクラスに含まれていることです。
対照的に、フックはReact 関数コンポーネントで使用され、クラスを必要とせずにコンポーネントの状態やその他の機能を関数コンポーネントに持たせることができます。
クラスコンポーネントは、関数コンポーネントに比べてテストが困難です。
フックを使用すると、コードがよりクリーンになり、読みやすく、テストしやすくなります。
柔軟性が高く、特に複数のコンポーネントでカスタム フックを再利用できます。
クラスコンポーネントは冗長で面倒です、フックでは、ライフサイクルメソッドを使用する必要はありません、副作用は単一の関数で処理できます。
そして、フックではクリックイベント用の関数をバインドしたり、コンポーネントやグローバルStateの値にアクセスするために、this
のようなものは必要がない事です。
ですが、React フックを使用するようにアプリケーションをリファクタリングしようとすると、最初に直面する問題は、他の課題が発生する根本的な問題になります。
機能を壊さずに、クラスコンポーネントを関数コンポーネントにリファクタリングするにはどうすればいいのか?
遭遇する最も一般的なユースケースのいくつかを、最も簡単なものから見ていきましょう。
フックを使用してReactでアプリケーションを開発する前に、必ず従うべき厳守されているルールがいくつかあります。
それらルールは下記で解説しておりますので参照下さい。
状態がない場合
下記は、JSXをレンダリングするだけのクラスコンポーネントである、最も基本的な例を示しております。
//before // クラスコンポーネント class App extends React.Component { handleClick = () => { alert("Hello!") } render() { return ( <div> Hello World <button onClick={this.handleClick}> Click me! </button> </div> ) } } export default App;
See the Pen React クラスコンポーネントのリファクタリング before by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記でのリファクタリングでは非常に簡単となります。
まず、関数コンポーネントではrender()
、this
は使用しません。
//after //関数コンポーネント const App = () => { const handleClick = () => { alert("Hello!") } return ( <div> Hello World <button onClick={handleClick}>Click me!</button> </div> ) } export default App;
See the Pen React リファクタリング後 関数コンポーネント after by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
render()
関数を使用する代わりに、親コンポーネントであるApp()関数を介して直接返します。
そして関数コンポーネントでは、this
キーワードを使用しませんので代わりに、関数スコープ内の JavaScript値に置き換えます。
//before // クラスコンポーネント class App extends React.Component { render() { return ( <h1>Hello!</h1> ) } }
//after //関数コンポーネント const App = () => { return ( <h1>Hello!</h1> ) } export default App;
このように構文に違いがあります。
クラスコンポーネントは、Reactコンポーネントを継承し、renderメソッドが付属しています。
一方、関数型コンポーネントはJavaScriptの関数です。
つまり、関数型コンポーネントは冗長ではないという事です。
状態を持つコンポーネント
カウンターでの例で見ていきましょう。
下記では、クラスコンポーネントがどのように状態を処理するかを示しています。
//before // クラスコンポーネント class App extends React.Component { constructor(props) { super(props); this.state = { counter: 0 }; } render() { return ( <div> <h1>Update Count</h1> <p>count: {this.state.counter}</p> <button onClick={() => this.setState({ counter: this.state.counter + 1 })}>Click me</button> </div> ); } }
See the Pen React クラスコンポーネントのカウンター before by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記の場合、イベントハンドラーでアロー関数を使用していますので、呼び出す関数内のthisを現在のクラスインスタンスにもバインドされます。
本来であれば、コンストラクタ内にバインドする必要があります。
クラスコンポーネントでは、状態を初期化する場合、コンストラクタを実装し、super(props)を呼び出す必要があります。
これをしないと、State変数が「未定義」になるなどのバグが発生する可能性があります。
変数にアクセスする場合は、thisキーワードでアクセスします。
最後に、状態を設定するには、setStateメソッドを呼び出します。
これを、関数コンポーネントとして状態を扱います。
useStateフックを使用します。
useStateフックは下記で詳しく解説しております。
//after //関数コンポーネント const App = () => { const [count, setCount] = useState(0); return ( <div> <h1>Update Count</h1> <p>count: {count}</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div> ); }
See the Pen React 関数コンポーネントのカウンター after by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
関数コンポーネントでは、this
のインスタンスがないため、thisのバインドを気にする必要性はありません。
useStateフックを使用する事により、this.setState()
とstate = {}
を置き換えて、ステートレスから状態を保持するステートフルコンポーネントとして機能します。
下記では、インクリメントとデクリメント機能のリファクタリングとなります。
//before // クラスコンポーネント class App extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } increment = () => { this.setState({ count: this.state.count + 1, }); } decrement = () => { this.setState({ count: this.state.count - 1, }); } render() { return ( <div className="counter"> <h1>Counter</h1> <div> <button onClick={this.increment}>Increment</button> <button onClick={this.decrement}>Decrement</button> </div> <p>{this.state.count}</p> </div> ); } }
See the Pen React クラスコンポーネントのカウンター③ before by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
//after //関数コンポーネント const App = () => { const [count, setCount]= useState(0); const handleIncrement = () => { setCount(count + 1) } const handleDecrement = () => { setCount(count - 1) } return ( <div className="counter"> <h1>Counter</h1> <div> <button data-testid="increment" onClick={handleIncrement}>Increment</button> <button data-testid="decrement" onClick={handleDecrement}>Decrement</button> </div> <p data-testid="count">{count}</p> </div> ); }
See the Pen React 関数コンポーネントのカウンター ③ after by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
これらのコードスニペットは、コードの可読性が向上したことを示しています。
複雑さが取り除かれただけでなく、関数コンポーネントが 1 つのことだけを実行できるようになりました。
それはカウンターアプリケーションのレンダリングです。
this.state
を使うのではなく、useState()フックを使って状態を初期化することができます。
useStateフックは、this.state
を使ったクラスコンポーネントのように、アプリケーション内の他のコンポーネントとステートを共有する機能も持っています。
Props
クラスコンポーネントでは、thisを追加しPropsを参照します。
//before // クラスコンポーネント class App extends React.Component { render() { return <h1>Hi, {this.props.name}</h1>; }
//after //関数コンポーネント const App = (props) => { return <h1>Hi, {props.name}</h1>; }
関数コンポーネントを使用している間、関数の引数としてPropsを渡します。
ライフサイクルの簡素化
コンポーネントのライフサイクルメソッドである componentDidMount
、componentWillUnmount
、componentDidUpdate
のロジックをリファクタリングすることもよくある課題の1つです。
Reactクラスを使用してロジックを構築するには、マウントから更新、そして最後にアンマウントまで、各フェーズに含まれるライフサイクル機能の観点からコンポーネントを設計する必要があります。
それら機能を壊さずに関数コンポーネントでリファクタリングしていく必要があります。
componentDidMount
メソッドからいきましょう。
状態とフェッチ呼び出しを備えた、なるべく必要最小限のクラスコンポーネントは次のようになります。
//before // クラスコンポーネント class App extends React.Component { state = { users: [] } componentDidMount() { fetch('https://jsonplaceholder.typicode.com/users') .then((response) => response.json()) .then((usersList) => { this.setState({ users: usersList }); }); } render() { return ( <ul> {this.state.users.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ) } } export default App;
See the Pen React クラスコンポーネントのライフサイクル before by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
Reactの2 つのフックであるuseEffect()とuseState()を使用し、関数コンポーネントを使用してもまったく同じ結果を得ることができます。
useEffectフックは、このロジックを抽出するのに最適です。
デフォルトでは、useEffect内のエフェクト関数は、すべてのレンダリング後に実行されます。
フックに精通している場合、これは一般的な知識です。
//after //関数コンポーネント import { useEffect, useState } from 'react' const App = () => { const [user, setUser] = useState([]) useEffect( () => { fetch('https://jsonplaceholder.typicode.com/users') .then(res => res.json()) .then(data => setUser(data)) }, []) const useUsers = user.map((user, index)=>{ return <div key={index}> <li>{user.name}</li> </div> }) return ( <div> {useUsers} </div> ); } export default App;
See the Pen React クラスのライフサイクルのリファクタリング before by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
useStateはstate = {}
およびthis.setState()
を置き換えます。
useEffect は componentDidMount()
を置き換えますが、注意点が依存配列を忘れない事です。
そしてthis
、render
関数は必要ありません。
空の依存関係配列を省略した場合は、APIに何千回もヒットする無限ループが発生します。
依存配列を使用すると、満たされたときにuseEffect に再レンダリングを実行するように指示する条件を指定できます。
2番目の引数に空の配列を提供することで、componentDidMount()
の機能と同様に、useEffectをマウント時に1 回だけ実行するように指示します、このステップを忘れることは、駆け出しのReactプログラマーが犯す最も一般的なエラーの 1 つです。
空の配列を渡すと、コンポーネントがマウントされたときにのみエフェクト関数が実行され、アンマウントされたときにクリーンアップされます。
これは、コンポーネントのマウント時にデータを追跡またはフェッチする場合に最適です。
下記は、依存関係配列に値を渡す例です。
import { useEffect, useState } from 'react' ; const [name, setName] = useState("Taro") useEffect ( () => { }, [ name ]) //値を持つ配列引数
この場合コンポーネントがマウントされ、name変数の値が変更されるたびに、効果関数が呼び出されることです。
ReactフックのuseEffect
は下記で詳しく解説しておりますので参照下さい。
では、componentWillUnmount()
コンポーネントが React 仮想DOM から削除されるときに読み込まれる、クラスベースのコンポーネントのライフサイクル メソッドです。
下記のように、コンポーネントのアンマウント時にいくつかのコードを実行できます。
//before // クラスコンポーネント class App extends React.Component { constructor() { super(); this.state = { date: new Date() }; } componentDidMount() { const timer = setInterval(() => { this.setState({ date: new Date() }); }, 1000); this.setState({ timer }); } componentWillUnmount() { clearInterval(this.state.timer); } render() { return <div>{this.state.date.toString()}</div>; } } export default App;
See the Pen React クラスコンポーネントのタイマー before by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
componentWillUnmount
でclearInterval
を呼び出してタイマーを削除し、コンポーネントがアンマウントされたときにタイマーを削除するようにします。
これを関数コンポーネントのフックで同じことをするには、useEffect
フックを使用します。
コンポーネントがアンロードされる前にコードを実行するコールバックを返します。
//after //関数コンポーネント import React, { useState, useEffect } from "react"; const App = () => { const [date, setDate] = useState(new Date()); const [timer, setTimer] = useState(); useEffect(() => { const timer = setInterval(() => { setDate(new Date()); }, 1000); setTimer(timer); return () => { //クリーンアップ clearInterval(timer); }; }, []); return <div>{date.toString()}</div>; }
See the Pen React 関数コンポーネントのタイマー after by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
現在の日付とタイマーオブジェクトをそれぞれ保持するdate
とtimer
の状態があります。
useEffectフックの第2引数に空の配列があるように、コンポーネントのロード時にsetInterval
でタイマーを生成しています。
useEffectに渡されたコールバックでは、クリーンアップコードを実行する関数を返しています。
そのため、その関数内にclearInterval
を用意し、コンポーネントがアンマウントされたときにタイマーを削除するようにしています。
では、componentDidUpdate
メソッドです。
カウンターの例で見てみましょう。
//before // クラスコンポーネント class ChildCounter extends React.Component { constructor() { super(); this.state = { superCount: 0 }; } componentDidUpdate(prevProps) { if (this.props.count !== prevProps.count) { this.setState({ superCount: this.props.count * 5 }); } } render() { return <p>{this.state.superCount}</p>; } } class App extends React.Component { constructor() { super(); this.state = { count: 0 }; } render() { return ( <div> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Increment </button> <p> <ChildCounter count={this.state.count} /> </p> </div> ); } }
See the Pen React クラスコンポーネントのカウンター② before by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
上記は単純です。
this.props.count
がprevProps.count
と異なるかどうかをif文でチェックします。
もし違うなら、superCount
の状態をthis.props.count * 5
で更新します。
この機能を、フックに相当するもので書く場合は下記のようにします。
これは、useEffectフックと同等であり置き換えます。
//after //関数コンポーネント import { useState, useEffect } from "react"; const ChildCounter = ({ count }) => { const [superCount, setSuperCount] = useState(0); useEffect(() => { setSuperCount(count * 5); }, [count]); return <p>{superCount}</p>; }; const App = () => { const [count, setCount] = useState(0); return ( <div> <button onClick={() => setCount(count => count + 1)}>Increment</button> <p> <ChildCounter count={count} /> </p> </div> ); }
See the Pen React 関数コンポーネントのカウンター② after by dev.K | Webアプリ開発者 (@enjinia_f) on CodePen.
子のコンポーネントであるChildCounter
コンポーネントにcountのProps
を持たせます。
そして、useEffectフックsetSuperCount
で、コールバックを呼び出しています。
第2引数には、count変数を格納した配列を渡しています。
これは、count変数の値の変化を監視していることを意味しており、値が変化(変更)されるとコールバックが実行されます。
このように、以前の古いcomponentDidUpdate
ライフサイクルメソッドよりもずっとシンプルになります。
useStateは、フックの氷山の一角にすぎません。
一部のライフサイクルメソッドをuseEffectに置き換えたり、 useContextを使用してコンテキストにサブスクライブしたり、独自のフックを実装したりして、コンポーネント間でステートフルロジックを再利用し、クラスを使用せずにコードをクリーンで読みやすい状態に保つことができます。
これから
すべての新しいコードは、フックを使用して関数コンポーネントとして作成する必要があります。
既存のコードは、頻繁に変更される場合にのみ書き直すべきです。
たとえば、バグを修正したり機能を追加したりする場合は、時間をかけてコンポーネントをフックに交換してください。
場合によっては、フックを使用してクラスコンポーネントを関数コンポーネントにリファクタリングすることは、膨大な量の作業になる可能性があります。
既存のプロジェクトをリファクタリングするのはあまり意味がありませんが、Reactフックを使い始めるのは理にかなっていると言えます。
コードベース内の既存のコードを理解したい場合は、クラスコンポーネントも学習する必要があります。
Reactを使用する一部の企業では、1日あたり何万人ものユーザーが使用するコード行が1,000行を超えるクラスコンポーネントがある場合があります。
また、面接でReactについて質問する企業は、依然としてクラスについて質問することもある場合がございます。
クラスコンポーネント内でフックを使用することはできません。
ただし、HOC(Higher-Order Components)パターンを使用して、既存のクラスコンポーネント内でフック ロジックを使用できます。
私の知るかぎりでは、プロの環境ではメインコンポーネントにはクラスコンポーネントを使用し、より小さな特定のコンポーネントには関数コンポーネントを使用します。
ただし、プロジェクトによってはそうではない場合もあります。
これは、クラス部品と関数部品のどちらを使用するかはプロジェクトの状況によって異なります。
最後に
フックを使用するために既存のプロジェクトを無理にリファクタリングする必要はありません。
自分自身とチームのためにさまざまなオプションを比較検討する必要があります。
新しいコンポーネントを作成するたびに、フックと関数を使用するだけです。
クラスコンポーネントを扱っていて、時間がある場合は、それらをリファクタリングします。
Reactは今後もクラスベースのコンポーネントをサポートし続けることに注意してください。
ですので、クラスベースのコンポーネントは引き続き関数ベースのコンポーネントと連携して動作するため、保持することを選択も可能です。
ですが、フック APIを使用しコンポーネントをリファクタリングすることを選択した場合は、当記事があなたにとって素晴らしいヒントとして見つけられて頂ければ幸いです。
本日は以上となります。
最後まで読んで頂きありがとうございます。