質問やフィードバックがありましたら、フォームからお願いします
本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください
この文章では、さまざまな React Hooks の API を詳細に紹介するのではなく、デザインの観点からその背後にある理由を探求します。主にいくつかの段落に分かれて説明します:
- Function Component と Class Component の違い
- コンポーネント内で類似のロジックを再利用する方法
- 高階コンポーネント、レンダープロップ、ミックスインについての軽い考察
Function Component と Class Component
React の Function Component と Class Component の違いは何でしょうか?機能的な観点からいくつかのポイントにまとめることができます:
- Function Component は state を持たない(hook を使用しない場合)
- Function Component は
thisを持たない(これは重要な点です) - Class Component はインスタンスメソッドを宣言できる
1. Function Component は state を持たない(hook を使用しない場合)
以前は Function Component を作成する際、コンポーネントを操作するために props を渡す方法しかありませんでしたが、それ自体には何の問題もありません。
このように書くことはテストが容易ですが、Function Component は Class Component を完全に置き換えることはできません。なぜなら、Class Component はさまざまなライフサイクルメソッドを使用してコンポーネントをより細かく制御できるため、内部の実装においても柔軟性を持ち、state のメカニズムを通じてより複雑なロジックを実現することができます。
2. Function Component は this を持たない
Function Component は関数であるため this を持たず、言い換えれば、this が指すオブジェクトはコンポーネントそのものではありません。これにより、いくつかの利点が生まれます。第一に、構文が簡潔になり、this.state.xxx のような冗長な構文を書く必要がなくなります。また、イベントハンドラーを渡す際に、使用シーンに応じて bind(this) を心配する必要もありません。
公式ドキュメントでは、state を直接操作せず、常に setState メソッドを使用して状態を更新するように明記されていますが、実際の開発では、ドキュメントを読まずに不適切な方法で state を操作するエンジニアがいることもあります。例えば、以下のようなコードを書くことです:
this.state.verified = true;このように書くと、実際には React の更新メカニズムをトリガーすることができず、他のエンジニアに誤解を招く可能性が高まります。このようなコードが散見されると、リファクタリングや書き換えが非常に困難になります。Function Component は、構文上でこの問題を回避できます。とはいえ、保守性の低いコードを書く方法が見つからないわけではありませんが…。
特に注意が必要なのは、Class Component をライフサイクルを使用せずに Function Component に変換できる場合もありますが、両者には依然として違いがあります。最大の違いは this の制御にあります。
props と state はどちらも不変であるため、Class Component を使用する際、React の内部実装が this をコンポーネントに指向させてくれます。つまり、this は可変です。
ほとんどの場合、これは問題ありませんが、setTimeout や setInterval のような非同期処理に関わる場合には特に注意が必要です:
function Profile({ userId }) {
setTimeout(() => {
fetchUserProfile(userId).then(alert);
}, 2000);
return ...
}
class Profile extends React.Component {
componentDidUpdate() {
setTimeout(() => {
getUserProfile(this.props.userId);
}, 2000);
}
getProfile() {
fetchUserProfile(this.props.userId);
}
render() {
return ...
}
}この2秒間に userId が変更された場合、何が起こるでしょうか?
気づきましたか?Class Component は this が変化するため、この2秒間に userId が変更されると、Class Component は呼び出されたときの props を使用しますが、Function Component はボタンがクリックされたときの props を使用します。
this.props を使用する際、コンテキストはすでに変更されており、正しい結果を得ることができません。this.props を変数に保存することはできますが、それでも根本的な問題を回避することはできません。setTimeout の中で他の props を呼び出すメソッドがある場合、どうなるでしょうか?
対照的に、Function Component は this を持たないため、setTimeout や setInterval を安心して使用できます。
ただし、Class である以上、インスタンスメソッドを実装して外部にいくつかのメソッドを公開することができます。
これは必ずしも良いことではありません(多くの場合、むしろ悪いことです)。後でこのメソッドを変更したり、名前を変更したりする場合、他の場所やコンポーネント内でこのメソッドが直接呼び出されているかどうかを考慮する必要があり、コンポーネントの変更やリファクタリングがより面倒になることがあります。
初期の設計段階でこのメソッドの汎用性を保証できる場合を除いて、後でリファクタリングを行う際に、より多くの労力がかかることがあります。
また、Class Component では、ロジックが似たようなコードを共有することが非常に難しいです。
たとえば、ユーザーのログイン状態に基づいて表示内容を判定したいとします。この isLoggedIn 状態に基づいて表示する必要があるコンポーネントがすべてこの状態に直接アクセスできるようにしたい場合、一般的にはどのように設計するでしょうか?
最初の方法として、isLoggedIn のロジックと実装をコンテキストに組み入れ、コンテキストにアクセスするたびに Consumer を使用してコンポーネントを包むことが考えられます。
これにより、全体の実装が冗長になり、コンポーネントの実装とは無関係なコードが増えてしまいます(例えば、Consumer を追加することなど)。
おおよそ次のようになります:
const {Provider, Consumer} = createContext(null);
export default class UserContext extends React.Component {
state = {
isLoggedIn: false,
};
componentDidMount() {
fetchUser()
.then(res => this.setState({
isLoggedIn: true
}))
}
render() {
return (
<Provider value={this.state}>
{this.props.children}
</Provider>
);
}
}
export const UserConsumer = Consumer;App.js
const App = () => (
<UserContext>
<Profile />
</UserContext>
);Profile.js
import { Consumer } from 'UserContext';
class Profile extends React.Component {
render() {
return (
<Consumer>
{({ isLoggedIn }) => isLoggedIn ? showProfile() : null}
</Consumer>
);
}
}簡便性と再利用性を保つために、その当時人気があった解決策には高階コンポーネントやレンダープロップ、さらには古いミックスインがありました。
Mixin
まず、ミックスインについてですが、ミックスインが廃止された理由は非常にシンプルです。コンポーネントの内部実装に影響を与えやすく、変更が難しいからです。コンポーネントがミックスイン内で定義されたメソッドに依存する可能性があり、後の変更が非常に困難になることがあります。また、1つのミックスインが他のミックスインに依存することもあります。詳細についてはこちらのリンクを参照してください。
もちろん、ミックスインが必ずしも悪いパターンであるわけではなく、React にはあまり適していないかもしれません。
高階コンポーネント
高階コンポーネントは、関数を定義し、React コンポーネントを引数として受け取り、ラップされたコンポーネントを返すものです。
最も典型的な使用例は、Redux の connect です:
const MyProfile = ({ profile }) => {
};
export default connect(state => ({
profile: state.profile,
}))(MyProfile);この方法を使用することで、関数内でパラメータを自由に定義でき、全体の実装がよりエレガントになり、コンポーネント内部の実装を変更する必要もありません。唯一の依存関係は props の受け取りですが、mapStateToProps を使うことで、簡単に props をコンポーネントが必要とする形に変更できます。
この関数は React コンポーネントを返しますが、通常その実装は次のようになります:
const withWindowSize = (WrappedComponent) => class WindowComponent extends React.Component {
state = {
screenSize: window.innerWidth,
}
setScreenSize = () => this.setState({ screenSize: window.innerWidth });
componentDidMount() {
window.addEventListener('resize', this.setScreenSize);
}
componentWillUnmount() {
window.removeEventListener('resize', this.setScreenSize);
}
render() {
return <WrappedComponent {...this.props} windowSize={this.state.screenSize} />
}
}しかし、この方法でも、ある程度は背後の実装を理解しておく必要があります。例えば:
このようにラッピングされた場合、実際には withWindowSize が自動的に windowSize というプロパティを追加してくれることを知っておく必要があります。
見た目にはエレガントですが、実際には関数内でいくつかの作業を行う必要があり、さらに関数内でコンポーネントを定義したり、displayName を変更したりする必要もあります。
他の関数と組み合わせて使用しようとすると、非常に冗長になってしまいます。compose を使用してこの関数を簡略化することもできますが、使いにくさは否めません。
withWindowSize(withScroll(withRouter(connect(...これはかなり良い解決策ですが、初心者が学ぶ際にはそれなりのハードルとなってしまいます。
レンダープロップ
レンダープロップは、パラメータを関数の引数として渡し、children に公開する方法です。これにより、渡されたパラメータが何かを明確に把握でき、使用するかどうかを自由に選択できます。
React のコンテキストの Consumer は、レンダープロップの方式で利用されています。
const ListContainer = ({ list }) => (
<InfiniteList>
{this.props.children(list)}
</InfiniteList>
)レンダープロップにもいくつかの欠点があります。渡されたパラメータが何かを確認するためにコンポーネント内に入る必要があり、相手が関数ではなく一般的なコンポーネントを渡してきた場合にどう対応するかを考慮する必要があります。
これらの選択肢はどれも良いものですが、どこか欠けている部分があるようです。そのため、React Hooks の概念と実装が発表されると、すぐに React コミュニティで盛り上がりました。
Hooks の概念は、Function Component の元々の制約を取り除くことにあります。Function Component 内で state を使用したり、サイドエフェクトを宣言したり、ref や context などを利用できるようになります。これまで Class Component でしか使用できなかったものが、すべて Function Component で置き換え可能になったのです。
これにより、開発者はより軽量でシンプルな方法でコードを整理できるようになり、Hook はただの関数であるため、自分自身でロジックをカプセル化することもできます。詳細については、Dan が書いた Making sense of react hooks を参照してください。
ここで、最も簡単な useState と useEffect を例として挙げます:
function useWindowSize() {
const [windowSize, setWindowSize] = useState(window.innerWidth);
useEffect(() => {
const setSize = () => {
setWindowSize(window.innerWidth);
};
window.addEventListener('resize', setSize);
return () => window.removeEventListener('resize', setSize);
}, []);
return windowSize;
}コンポーネント内で、useWindowSize を呼び出して現在の window.innerWidth を取得できます。
const Layout = () => {
const windowSize = useWindowSize();
return ...
};これにより、windowSize の出所が明確になり、実装も非常に直感的です。高階コンポーネントやレンダープロップのように、他の状況を考慮する必要はありません。
ただし、Hooks にはすべてが良いわけではなく、Hooks の呼び出し順序を一貫して保つ必要があり、正しいレンダリング結果を得るために新たに eslint-plugin-react-hooks が導入されました。
現在でも、Hooks では Class Component を完全に置き換えられない部分があります。たとえば、componentDidCatch や getSnapshotBeforeUpdate などのメソッドは、レンダープロップを使用する際に、親が DOM 構造などでラッピングされている場合、Hook で解決することはできません。
また、Function Component は外部にメソッドを公開することができず、パラメータを渡す方法で制御するしかないため、時にはそれが不便になることもあります。
結論
React コミュニティは非常に広大で、これによりコアチームがこれらの機能の開発に集中できる環境が整っています。また、React のミックスイン、高階コンポーネントなど、さまざまな試行錯誤を経て、Hooks という解決策にたどり着いたのかもしれません。
解決策が次々と登場するように見えますが、その背後にある原理は似通っています。特定の問題を解決するために派生した結果としての製品なのです。
この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨
☕Buy me a coffee