フロント知識ほぼゼロエンジニアがReactをお勉強する
Reactのコンセプトをメインに見ていく.
(文法など,細かいことはいったんスルーする)
目標
- Reactアプリケーション構築の基礎を完全に理解したい
- コンポーネント,ライフサイクル,etc
const element = <h1>Hello, world!</h1>;
JSX
と呼ばれるJavascriptの拡張構文.
Reactではこれを使って要素を構成していく.
以下は等価で,
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
React.createElement()
が以下のようなオブジェクトを生成.
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
これをReact要素と呼び,Reactはこれらオブジェクトを読み取りDOMを構成.
DOMとは
DOM はドキュメントオブジェクトモデルという名前の通り、 HTML や XML のドキュメントに含まれる要素や要素に含まれるテキストのデータをオブジェクトとして扱います。そしてドキュメントをオブジェクトが階層的に組み合わされたものとして識別します。 そして DOM では JavaScript など色々なプログラミング言語などから、オブジェクトを扱うための API を提供しています。
Javascriptなどで要素や属性を取得できるのはこういうことだ
Reactでの”要素”とは,
要素とは React アプリケーションの最小単位の構成ブロックです。
これも要素
const element = <h1>Hello, world</h1>;
React DOMがReact要素に合致するようにDOMを更新してくれる.
Reactで構築されたアプリは通常ルートDOMノードをひとつ持つ.
このDOMノードにレンダーする方法は以下.
<div id="root">
<!-- This div's content will be managed by React. -->
</div>
const element = <h1>Hello, world</h1>;
const root = ReactDOM.createRoot(
document.getElementById('root')
);
root.render(element);
React DOMは内部で最適化されている.
React DOM は要素とその子要素を以前のものと比較し、DOM を望ましい状態へと変えるのに必要なだけの DOM の更新を行います。
例として,以下のコードは「毎秒UIツリー全体を表す要素を作成している」が,実際はテキストノードのみがReact DOMで更新してくれる.
// 秒刻みで動く時計の例
const root = ReactDOM.createRoot(
document.getElementById('root')
);
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);
}
setInterval(tick, 1000);
コンポーネント
コンポーネントにより UI を独立した再利用できる部品に分割し、部品それぞれを分離して考えることができるようになります。
概念的にはJavaScriptの関数と似ており,"props"(プロパティの意味)と呼ばれる入力を受け取り画面上に表示すべきものを記述するReact要素を返す.
つまり,コンポーネント定義の最もシンプルな方法は以下.
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
propsというオブジェクトを引数として受け取りReact要素を返すのでReactコンポーネントと呼べる.
このような,JavaScriptの関数で定義されたコンポーネントを関数コンポーネント(function component)という.
コンポーネントのレンダー
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(element);
これはページ上に”Hello, Sara”を表示する.
Reactがユーザ定義のコンポーネントを見つけた場合,JSXに書かれている属性と子要素(この例でいうと`name="Sara"の部分)を単一のオブジェクト"props"としてコンポーネントに引き渡す.
おさらい(この例でのフロー):
-
<Welcome name="Sara" />
という要素を引数としてroot.render()
呼び出し - ReactはWelcomeコンポーネントを呼び出し,propsとして
{name: 'Sara'}
を渡す -
Welcome
コンポーネントは出力として<h1>Hello, Sara</h1>
要素を返す - React DOMは
<h1>Hello, Sara</h1>
に一致するようDOMを効率的に更新
コンポーネントの組み合わせ
コンポーネントは組み合わせ可能.
例:Welcomeコンポーネントを何回もレンダーするAppコンポーネントを作成
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
コンポーネントはより小さな単位(コンポーネント)に分割していくことを心がける.
(ネストしすぎると,変更にコストがかかりすぎる,内部の部品の再利用も困難になる)
一例として以下を見ていく.
function Comment(props) {
return (
<div className="Comment">
<div className="UserInfo">
<img className="Avatar"
src={props.author.avatarUrl}
alt={props.author.name}
/>
<div className="UserInfo-name">
{props.author.name}
</div>
</div>
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
ネストが深すぎるので,Avatar
コンポーネントとUserInfo
コンポーネントを作りシンプル化.
function Avatar(props) {
return (
<img className="Avatar"
src={props.user.avatarUrl}
alt={props.user.name}
/>
);
}
function UserInfo(props) {
return (
<div className="UserInfo">
<Avatar user={props.user} />
<div className="UserInfo-name">
{props.user.name}
</div>
</div>
);
}
function Comment(props) {
return (
<div className="Comment">
<UserInfo user={props.author} />
<div className="Comment-text">
{props.text}
</div>
<div className="Comment-date">
{formatDate(props.date)}
</div>
</div>
);
}
時計を毎秒更新する上記の例では,root.render()
を毎秒呼び出す形で実現していたが,
一般的にはroot.render()
は一度しか呼び出さない.
そこで,この毎秒更新する時計をstateとライフサイクルを導入して書き換える.
関数をクラスへ変換
関数として定義していたものをClockというクラスとして定義し直す.
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
render
メソッドは更新が発生した際に毎回呼ばれるようになるが,
同一のDOMノード内で<Clock />
をレンダーしている限りClockクラスのインスタンスは1つだけ使われる.
=ローカルstateやライフサイクルメソッドの機能を利用可能.
クラスにローカルstate追加
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
-
this.props.date
をthis.state.date
に変更 -
this.state
の初期状態を設定するクラスコンストラクタを追加 - 必ず
props
を引数として親クラスのコンストラクタをコールしなければならない
これでローカルstateの導入が完了.
クラスにライフサイクルメソッドを追加
多くのコンポーネントを有するアプリケーションでは,
コンポーネントが破棄された場合にそのコンポーネントが占有していたリソースの解放が重要.
時計の例でいくと,
- タイマーを設定したい:最初に
Clock
がDOMとして描画されるとき.マウントと呼ぶ. - タイマーをクリアしたい:
Clock
が生成したDOMが削除されるとき.アンマウントと呼ぶ.
以下のようにコンポーネントクラスで特別なメソッドを宣言することで,
コンポーネントがマウント・アンマウントした際のコードを書くことが可能.
(ライフサイクルメソッドと呼ぶ)
-
componentDidMount()
:マウント時 -
componentWillUnmount()
:アンマウント時
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
おさらい(上記例のフロー):
-
<Clock />
がroot.render()
に渡されると,ReactはClock
コンポーネントのコンストラクタを呼び出す - Reactは
Clock
コンポーネントのrender()
を呼び出すことでDOMをClock
のレンダー出力と一致するよう更新 -
Clock
出力がDOMに挿入されるとcomponentDidMount()
メソッドを呼び出す(ここではtick()
メソッドを毎秒呼び出すようブラウザに要求) -
tick()
内でsetState()
を呼び出すことでReactはstateの変更を検知できrender()
を再呼び出し,ReactがDOMを更新 -
Clock
コンポーネントがDOMから削除されたらcomponentWillUnmount()
を呼び出し
stateにて注意すべき点
setState()
を使用
変更は必ず// Wrong
this.state.commnet = 'Hello';
// Correct
this.setState({commnet: 'Hello'});
this.state
に直接代入OKなのはコンストラクタのみ.
stateの更新は非同期なことがある
Reactではパフォーマンス観点から複数のsetState()
呼び出しを一度の更新にまとめることがある.
this.props
とthis.state
は非同期に更新されるため,次のstateを求める際にそれらの値に依存すべきでない.
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
上記関数は前のstateを最初の引数として受け取り,更新が適用される時点でのpropsを第二引数として受け取る.
stateの更新はマージされる
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
上記のように,いくつかのstateを持っていても別々のsetState()
呼び出しで
それら変数を独立して更新することが可能.
ここで復習,
「stateはその名の通りステータスを持つ,propsはread only(プロパティ)」
stateのリフトアップ
複数のコンポーネント間で同一の変数を反映・参照したい場合がある.
このとき,それぞれのコンポーネントでローカルstateを使うのではなく,
最も近い共通の祖先コンポーネント(親コンポーネントなど)にstateを
持たせることで実現可能になる.これをリフトアップという.
異なるコンポーネント間で state を同期しようとする代わりに、トップダウン型のデータフローの力を借りるべきです。(一部略)効果としてバグを発見して切り出す作業が少なく済むようになります。あらゆる state はいずれかのコンポーネント内に “存在” し、そのコンポーネントのみがその state を変更できるので、バグが潜む範囲は大幅に削減されます。加えて、ユーザ入力を拒否したり変換したりする任意の独自ロジックを実装することもできます。
コンポジション
Reactは強力なコンポジションモデルを備えているため,
継承よりもコンポジションを利用することが推奨されている.
Facebook では、何千というコンポーネントで React を使用していますが、コンポーネント継承による階層構造が推奨されるケースは全く見つかっていません。
props とコンポジションにより、コンポーネントの見た目と振る舞いを明示的かつ安全にカスタマイズするのに十分な柔軟性が得られます。コンポーネントはどのような props でも受け付けることができ、それはプリミティブ値でも、React 要素でも、あるいは関数であってもよい、ということに留意してください。
強力な例として,以下のような受け取った子要素をそのまま出力するようなことが可能.
(props.children
を呼ぶことで任意の子要素すべてを出力可能)
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
アプリケーション構築Tips
- ひとつのコンポーネントはひとつのことだけをするべきだ
- 単一責任の原則(single responsibility principle)
- 肥大化してきたら分割チャンス!
- データモデルとUI(=コンポーネント)はきれいにマッピングできるはずだ
- できなければデータモデルが正しく構築できていないかも
- どのデータをstateで管理すべきかよく考え,stateの最小構成を明確化しよう
- 親から props を通じて与えられたデータでしょうか? もしそうなら、それは state ではありません
- 時間経過で変化しないままでいるデータでしょうか? もしそうなら、それは state ではありません
- コンポーネント内にある他の props や state を使って算出可能なデータでしょうか? もしそうなら、それは state ではありません
- propsは親から子へデータを渡すための手段,stateはユーザ操作・時間経過などで動的に変化するデータを扱うための手段
- 描画だけの静的なアプリケーションならpropsだけで十分なはず
- どのコンポーネントがstateを所有するのか明確化しよう
- Reactは単一方向のデータフローで成り立つため,共通の祖先コンポーネントで配置させる
- 見つからなければstateを保持するだけの新しい共通親コンポーネントを作る
- 逆方向のデータフローを追加しよう
子コンポーネントで親コンポーネントのstateを更新するには,
親コンポーネントで定義したコールバックを子コンポーネントに渡せば実現可能になる.