Closed14

フロント知識ほぼゼロエンジニアがReactをお勉強する

ピン留めされたアイテム
comchacomcha

Reactのコンセプトをメインに見ていく.
(文法など,細かいことはいったんスルーする)
https://ja.reactjs.org/docs/hello-world.html

目標

  • Reactアプリケーション構築の基礎を完全に理解したい
    • コンポーネント,ライフサイクル,etc
comchacomcha
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などで要素や属性を取得できるのはこういうことだ

comchacomcha

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);
comchacomcha

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);
comchacomcha

コンポーネント

コンポーネントにより 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"としてコンポーネントに引き渡す.

おさらい(この例でのフロー):

  1. <Welcome name="Sara" />という要素を引数としてroot.render()呼び出し
  2. ReactはWelcomeコンポーネントを呼び出し,propsとして{name: 'Sara'}を渡す
  3. Welcomeコンポーネントは出力として<h1>Hello, Sara</h1>要素を返す
  4. 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>
  );
}
comchacomcha
comchacomcha

コンポーネントはより小さな単位(コンポーネント)に分割していくことを心がける.
(ネストしすぎると,変更にコストがかかりすぎる,内部の部品の再利用も困難になる)

一例として以下を見ていく.

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>
  );
}
comchacomcha

https://zenn.dev/link/comments/36d29e221d5d59

時計を毎秒更新する上記の例では,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.datethis.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 />);

おさらい(上記例のフロー):

  1. <Clock />root.render()に渡されると,ReactはClockコンポーネントのコンストラクタを呼び出す
  2. ReactはClockコンポーネントのrender()を呼び出すことでDOMをClockのレンダー出力と一致するよう更新
  3. Clock出力がDOMに挿入されるとcomponentDidMount()メソッドを呼び出す(ここではtick()メソッドを毎秒呼び出すようブラウザに要求)
  4. tick()内でsetState()を呼び出すことでReactはstateの変更を検知できrender()を再呼び出し,ReactがDOMを更新
  5. ClockコンポーネントがDOMから削除されたらcomponentWillUnmount()を呼び出し
comchacomcha

stateにて注意すべき点

変更は必ずsetState()を使用

// Wrong
this.state.commnet = 'Hello';

// Correct
this.setState({commnet: 'Hello'});

this.stateに直接代入OKなのはコンストラクタのみ.

stateの更新は非同期なことがある

Reactではパフォーマンス観点から複数のsetState()呼び出しを一度の更新にまとめることがある.
this.propsthis.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()呼び出しで
それら変数を独立して更新することが可能.

comchacomcha
comchacomcha

ここで復習,
「stateはその名の通りステータスを持つ,propsはread only(プロパティ)」

comchacomcha

stateのリフトアップ

複数のコンポーネント間で同一の変数を反映・参照したい場合がある.
このとき,それぞれのコンポーネントでローカルstateを使うのではなく,
最も近い共通の祖先コンポーネント(親コンポーネントなど)にstateを
持たせることで実現可能になる.これをリフトアップという.

異なるコンポーネント間で state を同期しようとする代わりに、トップダウン型のデータフローの力を借りるべきです。(一部略)効果としてバグを発見して切り出す作業が少なく済むようになります。あらゆる state はいずれかのコンポーネント内に “存在” し、そのコンポーネントのみがその state を変更できるので、バグが潜む範囲は大幅に削減されます。加えて、ユーザ入力を拒否したり変換したりする任意の独自ロジックを実装することもできます。

comchacomcha

コンポジション

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>
  );
}
comchacomcha

アプリケーション構築Tips

  • ひとつのコンポーネントはひとつのことだけをするべきだ
    • 単一責任の原則(single responsibility principle)
    • 肥大化してきたら分割チャンス!
  • データモデルとUI(=コンポーネント)はきれいにマッピングできるはずだ
    • できなければデータモデルが正しく構築できていないかも
  • どのデータをstateで管理すべきかよく考え,stateの最小構成を明確化しよう
    • 親から props を通じて与えられたデータでしょうか? もしそうなら、それは state ではありません
    • 時間経過で変化しないままでいるデータでしょうか? もしそうなら、それは state ではありません
    • コンポーネント内にある他の props や state を使って算出可能なデータでしょうか? もしそうなら、それは state ではありません
    • propsは親から子へデータを渡すための手段,stateはユーザ操作・時間経過などで動的に変化するデータを扱うための手段
    • 描画だけの静的なアプリケーションならpropsだけで十分なはず
  • どのコンポーネントがstateを所有するのか明確化しよう
    • Reactは単一方向のデータフローで成り立つため,共通の祖先コンポーネントで配置させる
    • 見つからなければstateを保持するだけの新しい共通親コンポーネントを作る
  • 逆方向のデータフローを追加しよう
    子コンポーネントで親コンポーネントのstateを更新するには,
    親コンポーネントで定義したコールバックを子コンポーネントに渡せば実現可能になる.
このスクラップは2022/05/16にクローズされました