LoginSignup
0
0

More than 1 year has passed since last update.

第 8 章 何はなくともコンポーネント メモ

Posted at

コンポーネントのメンタルモデル

React ではコンポーネントをどう考えればいいか。これはよくいわれることだけど、 JavaScript の関数のようなものと考えるのが一番近い。
props を引数として受け取り、戻り値として React Elementsを返す関数
返されたReact Elementsがそのコンポーネントのレンダリング結果 になる。
ただしコンポーネントが通常の関数とちがうのは、個々に『状態』を持つことができるところ

関数コンポーネントも、 仮想 DOM の差分検出処理エンジンによって React Elements ごとに状態を保持する空間が用意される
そのコンポーネントが持つ状態のことを state と呼ぶ

props と state が同じである限りはその返す React Elements が変 わることはないんだけど、そのどちらか一方または両方が変わると返す React Elements も変わって くる。つまりコンポーネントのレンダリングに差分が発生する。

React の差 分検出処理エンジンは、仮想 DOM 内の React Elements すべてを監視していて、そのどれかの props またはその保持している state の値に差分を検出すると、そのコンポーネントのレンダリング処理を 再実行するようになってる。

コンポーネントと Props

state というのは極力コンポーネントに持たせるべきではないもの?

純粋関数とは、引数が同じなら必ず同じ戻り値を返す関数のこと。

React にとって理想的 なコンポーネントとは、props が同じなら必ずレンダリング結果が同じになるコンポーネント

props とはコンポーネントにとっての関数に対する引数のようなもの
React に特有の概念で『properties(プロパティ)』を短くして props と呼んでる
そして JSX が生成する React Elements を通してコールされたコンポーネント側では、それを { 属性名: 属性値 } の形式の props というオ ブジェクトとして受け取る。

受け取り方には 2 つ
コンポーネントの実装が関数だった場合はその関数の第 1 引数として渡される
クラスだった場合は初期化時にそのメンバーオブジェクト props として設定される

import { VFC } from 'react';
import CharacterList, { Character } from './CharacterList';
import './App.css';

const App: VFC = () => {
  const characters: Character[] = [
    {
      id: 1,
      name: '桜木花道',
      grade: 1,
      height: 189.2,
    },
    {
      id: 2,
      name: '流川 楓',
      grade: 1,
      height: 187,
    },
    {
      id: 3,
      name: '宮城リョータ',
      grade: 2,
      height: 168,
    },
    {
      id: 4,
      name: '三井 寿',
      grade: 3,
    },
    {
      id: 5,
      name: '赤木剛憲',
      grade: 3,
      height: 197,
    },
  ];

  return (
    <div className="container">
      <header>
        <h1>『SLAM DUNK』登場人物</h1>
      </header>
      <CharacterList school="湘北高校" characters={characters} />
    </div>
  );
};

export default App;

VFC
これは VoidFunctionComponent インターフェー スのエイリアスで、FunctionComponent の関数の props からそのコンポーネントの子要素が格納される children を除いたものなの

従来の FC で定義された関数コンポーネントだと、子要素の操作が必要ない場合でも暗黙の内に props の中に子要素のオブジェクトが渡されてた。

import { VFC } from 'react';
import { Header, Icon, Item } from 'semantic-ui-react';

export type Character = {
  id: number;
  name: string;
  grade: number;
  height?: number;
};

type Props = {
  school: string;
  characters: Character[];
};

const CharacterList: VFC<Props> = (props) => {
  const { school, characters } = props;

  return (
    <>
      <Header as="h2">{school}</Header>
      <Item.Group>
        {characters.map((character) => (
          <Item key={character.id}>
            <Icon name="user circle" size="huge" />
            <Item.Content>
              <Item.Header>{character.name}</Item.Header>
              <Item.Meta>{character.grade}年生</Item.Meta>
              <Item.Meta>
                {character.height ? character.height : '???'}
                cm
              </Item.Meta>
            </Item.Content>
          </Item>
        ))}
      </Item.Group>
    </>
  );
};

export default CharacterList;

Props という型エイリアスを定義しているところ。この型はコンポーネントを定義する関数宣言の型適用で使われてる
こうやって FC に型引数を渡すことで、そのコンポーネントの props の型を指定できる

props の型を設定することで、そのコンポーネントを JSX でマウントするときに必要な属性値と その型に縛りが発生する。だから App.tsx で をマウントするとき、school と chracter の属性値をそれぞれ Props で定義されている適正な型で記述しないと怒られてしまう

関数コンポーネントでは、レンダリングしたい内容を戻り値として return で返す

<>...</>
これはフラグメントといって React.Fragment のシンタックスシュガー

でくくっても表示結果は同じなんだけど、そうすると HTML ソース に意味のない
階層ができてしまう。でもフラグメントにしておくとそれが避けられる
た だ、
とちがって必ず中身のノードが必要なので、処理の結果、中身がなくなる可能性のある ときは使っちゃダメ

Character 型の定義で height? と要素名にクエスチョンマーク
これは『※その要素は省略できます』ってこと
だから三井さんの height 値は設定されて ないのに型チェックで怒られてない。設定されてなければ参照値は undefined なので、三項演算子 で '???' という文字列が返ってる

JSX で要素 をループ処理によって記述する場合、各要素にユニークな値を key 属性として設定しなければならない
仮想 DOM の差分検出処理で再レンダリングを効率的にするために必要

クラスコンポーネントで学ぶ State

コンポーネントをクラスで表現する

今となってはクラスコンポーネントでしかできないことはほんのわずかしか残ってな いし、それも今後すべて関数コンポーネントでサポートされる予定になってる
Facebook の React 開発チームが公式に推奨してるのも関数コンポーネント

・this の挙動が不可解で、そのためにコードが冗長になりがち
・minify やホットリローディング、さらに今後導入を検討しているコンポーネントの AOT コンパイルなどにおいて、クラスは最適化が困難で動作も不安定
・ライフサイクルメソッドを用いると機能的に関連しているはずのコードがバラバラに記述され
ることになり、可読性が落ちる
・状態を分離するのが難しく、ロジックを再利用するのが難しい

クラスコンポーネントに State を持たせる

関数コンポーネントに state を持たせるためにはちょっとしたマジックが必要になるんだけど、ク ラスコンポーネントは簡単に state が持てる。props と同様、型引数に state の型を渡せば、メンバー 変数state からstateにアクセスできるようになる

import { Component, ReactElement } from 'react';
import { Button, Card, Statistic } from 'semantic-ui-react';
import './App.css';

type State = { count: number };

class App extends Component<unknown, State> {
  constructor(props: unknown) {
    super(props);
    this.state = { count: 0 };
  }

  reset(): void {
    this.setState({ count: 0 });
  }

  increment(): void {
    this.setState((state) => ({ count: state.count + 1 }));
  }

  render(): ReactElement {
    const { count } = this.state;

    return (
      <div className="container">
        <header>
          <h1>カウンター</h1>
        </header>
        <Card>
          <Statistic className="number-board">
            <Statistic.Label>count</Statistic.Label>
            <Statistic.Value>{count}</Statistic.Value>
          </Statistic>
          <Card.Content>
            <div className="ui two buttons">
              <Button color="red" onClick={() => this.reset()}>
                Reset
              </Button>
              <Button color="green" onClick={() => this.increment()}>
                +1
              </Button>
            </div>
          </Card.Content>
        </Card>
      </div>
    );
  }
}

export default App;

ひとつめは props の型で、このコンポーネントには props が必要ないので unknown を渡して
る。デフォルト値は空オブジェクト {} なんだけど、{} の型は TypeScript の解釈では『null 以外の あらゆるオブジェクト』になってしまうため、@typescript-eslint/ban-typesのルールで使用が禁じ られてるの。だからプロパティを持てないオブジェクトの型である unknown がここではよりふさわしい

Component に渡してる 2 つめの型引数だけど、これが state の型になる

お約束としてスーパークラスに props を渡すのを忘れないように

その下で定義しているメンバーメソッド reset と increment の中でその値を操作してる。 気をつけなきゃいけないのは、this.state の値を直接書き換えないこと。直接代入していいのはコ ンストラクタの中だけで、それ以外では値の設定には必ず setState メソッドを使うようにする

setState メソッドの使い方について説明しておくと、そ の引数には次の 2 種類が設定できるようになってる
i. state 内の変更したい要素名をキーに、値をその値にしたオブジェクト e.g. {count:0}
ii. (prevState,props?)=>newState形式の、以前のstate(必要であればpropsも)を引数として 受け取って新しい state を返す関数
e.g. (state,props)=>({foo:state.foo+props.bar})

React ではイベントハンドラには通常、そのイベントが起きたときに実行したい関数を設定する
この () => this.increment() というのは、引数を受け取らず increment メソッドを実行する無名関数

reset = (e: SyntheticEvent) => { 
 e.preventDefault(); 
 this.setState({ count: 0 });
};

increment = (e: SyntheticEvent) => {
 e.preventDefault();
 this.setState((state) => ({ count: state.count + 1 }));
};

React が提供している SyntheticEvent という型で定義されるイベントオブジェクト
イベントハンドラのコールバックに引数として渡されるオブジェクトの型

たとえばこれが a要素だったりするとクリックで ページ移動が起きてしまうので、それをキャンセルするためにこういう記述が必要になる

他にも実用的な使い方だと、 要素で選択した値を受け取りたい場合は、その onChange 値に設定した関数内で同様にイベントハンドラ e を引数に定義しておけば、e.target.value から参照できたりする

コンポーネントのライフサイクル

コン ポーネントにおけるライフサイクルとは、まずマウントして初期化され、次にレンダリングされた後、 何らかのきっかけで再レンダリングされ、最後にアンマウントされるまでの過程をいう

クラスコンポーネントにはライフサイクルの各フェーズに対応したライフサイクルメソッド (lifecycle methods)というものがあり、そこに必要な処理を登録しておける。
1. Mounting フェーズ ...... コンポーネントが初期化され、仮想 DOM にマウントされるまでの フェーズ。このフェーズで初めてコンポーネントがレンダリングされる
2. Updating フェーズ ...... 差分検出処理エンジンが変更を検知してコンポーネントが再レン ダリングされるフェーズ
3. Unmounting フェーズ ...... コンポーネントが仮想 DOM から削除されるフェーズ
4. Error Handling フェーズ ...... 子孫コンポーネントのエラーを検知、捕捉するフェーズ

import { Component, ReactElement } from 'react';
import { Button, Card, Icon, Statistic } from 'semantic-ui-react';
import './App.css';

const LIMIT = 60;
type State = { timeLeft: number };

class App extends Component<unknown, State> {
  timerId: NodeJS.Timer | null = null;

  constructor(props: unknown) {
    super(props);
    this.state = { timeLeft: LIMIT };
  }

  componentDidMount = (): void => {
    this.timerId = setInterval(this.tick, 1000);
  };

  componentDidUpdate = (): void => {
    const { timeLeft } = this.state;
    if (timeLeft === 0) this.reset();
  };

  componentWillUnmount = (): void => {
    if (this.timerId) clearInterval(this.timerId);
  };

  tick = (): void =>
    this.setState((prevState) => ({ timeLeft: prevState.timeLeft - 1 }));

  reset = (): void => this.setState({ timeLeft: LIMIT });

  render = (): ReactElement => {
    const { timeLeft } = this.state;

    return (
      <div className="container">
        <header>
          <h1>タイマー</h1>
        </header>
        <Card>
          <Statistic className="number-board">
            <Statistic.Label>time</Statistic.Label>
            <Statistic.Value>{timeLeft}</Statistic.Value>
          </Statistic>
          <Card.Content>
            <Button color="red" fluid onClick={this.reset}>
              <Icon name="redo" />
              Reset
            </Button>
          </Card.Content>
        </Card>
      </div>
    );
  };
}

export default App;

setInterval()というのは JavaScript の組み込み関数で、第 1 引数の関数を第 2 引数のミリ秒ご と延々と実行し続けるようにするもの

ライフサイクルメソッドはたくさんあるけど、使う機会があるのはほぼ下記
componentDidMount, shouldComponentUpdate, componentDidUpdate, componentWillUnmount の 4 つくらい

Presentational Component と Container Component

ひとつのコンポーネントを『Presentational Component』お よび『Container Component』というものの 2 種類に分割しようというデザインパターンがある
presentational component というのは『presentational(表現に関する)』の名前のとおり、純粋に見た目だけを責務とするコンポーネントのこと。
container component というのはそれをコンテナのごとく抱合してロジックを追加するためのコンポーネントのこと

React の公式ドキュメントには『React の流儀(Thinking in React)』という章があって、そこで 公式が推奨するコンポーネントの正しい作り方が説明されてる
1. デザインモックから始め、その UI をコンポーネントの構造に分解して落とし込む
2. ロジックを除外した、静的に動作するバージョンを作成する
3. UI を表現するために最低限必要な「状態」を特定する
4. 3 の「状態」をどこに配置すべきかを決める
5. 階層構造を逆のぼって考え、データが上階層から流れてくるようにする

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0