React コンポーネントをリファクタリングする方法

Reactコンポーネントのリファクタリング

Reactアプリケーションに適用されるリファクタリングに関する一般的な問題について解説します。

クラスコンポーネントから学習してしまい、フックに移行する前に少し混乱する可能性があります。

関数コンポーネントが好まれる理由と、クラスコンポーネントよりも関数コンポーネントを使用する利点があるかどうかを詳しく学んで下さい。

なぜクラスではなくフックなのか?

Reactでクラスを使用する利点は、状態がいつ変化したかを識別し、this.stateキーワードを使用してグローバル状態またはコンポーネント状態を更新するライフサイクルメソッドがクラスに含まれていることです。

対照的に、フックはReact 関数コンポーネントで使用され、クラスを必要とせずにコンポーネントの状態やその他の機能を関数コンポーネントに持たせることができます。

クラスコンポーネントは、関数コンポーネントに比べてテストが困難です。

フックを使用すると、コードがよりクリーンになり、読みやすく、テストしやすくなります。

柔軟性が高く、特に複数のコンポーネントでカスタム フックを再利用できます。

クラスコンポーネントは冗長で面倒です、フックでは、ライフサイクルメソッドを使用する必要はありません、副作用は単一の関数で処理できます。

そして、フックではクリックイベント用の関数をバインドしたり、コンポーネントやグローバルStateの値にアクセスするために、thisのようなものは必要がない事です。

ですが、React フックを使用するようにアプリケーションをリファクタリングしようとすると、最初に直面する問題は、他の課題が発生する根本的な問題になります。

機能を壊さずに、クラスコンポーネントを関数コンポーネントリファクタリングするにはどうすればいいのか?

遭遇する最も一般的なユースケースのいくつかを、最も簡単なものから見ていきましょう。

フックを使用してReactでアプリケーションを開発する前に、必ず従うべき厳守されているルールがいくつかあります。

それらルールは下記で解説しておりますので参照下さい。

dev-k.hatenablog.com

状態がない場合

下記は、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フックは下記で詳しく解説しております。

dev-k.hatenablog.com

//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を渡します。

ライフサイクルの簡素化

コンポーネントのライフサイクルメソッドである componentDidMountcomponentWillUnmountcomponentDidUpdateのロジックをリファクタリングすることもよくある課題の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()を置き換えますが、注意点が依存配列を忘れない事です。

そしてthisrender関数は必要ありません。

空の依存関係配列を省略した場合は、APIに何千回もヒットする無限ループが発生します。

依存配列を使用すると、満たされたときにuseEffect に再レンダリングを実行するように指示する条件を指定できます。

2番目の引数に空の配列を提供することで、componentDidMount()の機能と同様に、useEffectをマウント時に1 回だけ実行するように指示します、このステップを忘れることは、駆け出しのReactプログラマーが犯す最も一般的なエラーの 1 つです。

空の配列を渡すと、コンポーネントがマウントされたときにのみエフェクト関数が実行され、アンマウントされたときにクリーンアップされます。

これは、コンポーネントのマウント時にデータを追跡またはフェッチする場合に最適です。

下記は、依存関係配列に値を渡す例です。

import { useEffect, useState } from 'react' ;

const [name, setName] = useState("Taro")

useEffect ( () => {     

}, [ name ]) //値を持つ配列引数 

この場合コンポーネントがマウントされ、name変数の値が変更されるたびに、効果関数が呼び出されることです。

ReactフックのuseEffectは下記で詳しく解説しておりますので参照下さい。

dev-k.hatenablog.com

dev-k.hatenablog.com

dev-k.hatenablog.com

dev-k.hatenablog.com

では、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.


componentWillUnmountclearIntervalを呼び出してタイマーを削除し、コンポーネントがアンマウントされたときにタイマーを削除するようにします。

これを関数コンポーネントのフックで同じことをするには、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.


現在の日付とタイマーオブジェクトをそれぞれ保持するdatetimerの状態があります。

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.countprevProps.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を使用しコンポーネントリファクタリングすることを選択した場合は、当記事があなたにとって素晴らしいヒントとして見つけられて頂ければ幸いです。

本日は以上となります。

最後まで読んで頂きありがとうございます。

プライバシーポリシー

© 2023 Icons8 LLC. All rights reserved.

© [deve.K], 2023. React logo is a trademark of Facebook, Inc. JavaScript is a trademark of Oracle Corporation and/or its affiliates. jQuery and the jQuery logo are trademarks of the JS Foundation. TypeScript and the TypeScript logo are trademarks of the Microsoft Corporation. Next.js and the Next.js logo are trademarks of Vercel, Inc. Firebase and the Firebase logo are trademarks of Google LLC. All logos edited by [deve.K].