はじめに

  • 「TypeScript と React/Next.js でつくる 実践 Web アプリケーション開発」の本を読んだ

React の表示

react は主に jsx で描かれる。この読み込み順は以下である

  • Jsx のコードはブラウザで読み込めないので webpack により javascript に変換される
  • Javascript のコードが読み込まれ web にて描画される。
    • react の index.html が読み込まれる
    • javascript が読み込まれる
    • react が実行され render されていく
  • Javascript のコードからブラウザの表示内容を書き換えるには DOM にアクセスする。その前に React は仮装 DOM を内部で作り上げる。更新作業はこの DOM と差分を確認して描画されるようになっており高層化される。

基礎

React の型

element

import React from "react";

const Name = () => {
  //ここの型はChangeEventがdefaultでevent関連のobjectの型が入っており、TのシグネチャにInputElementが来ている
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e);
  };

  return (
    //1つ目の{}は文字列以外のものが入るときに使う。2個目はobjectが入るので使っている。
    <div style={{ padding: "16px", backgroundColor: "gray" }}>
      <label htmlFor="name">name</label>
      <input id="name" className="input-name" type="text" onChange={onChange} />
    </div>
  );
};

親要素を子要素に

import React from "react";
type ContainerProps = {
  title: string,
  //親要素のDOMを子要素に入れるときはReact.ReactNodeで型を宣言しchildrenで入れる。
  children: React.ReactNode
};

const Container = (props: ContainerProps) => {
  const { title, children } = props;
  return (
    <div style={{ background: "red" }}>
      <span>{title}</span>
      <div> {children}</div>
    </div>
  );
};

const Parent = (): JSX.Element => {
  return (
    <Container title="title">
      <p>ここの部分が子要素である</p>
    </Container>
  );
};

export default Parent;

children に関してはこちらを参照。FC は基本非推奨と考えて良さそう。props を引数に入れてしまえば同じ。

context

props の経由を楽にしてくれるもの。provider で宣言して consumer で受け取る

import React from "react";

//providerしたい子コンポーネントを定義
const TitleContext = React.createContext("");

const Title = () => {
  return (
    //タグでくくって親コンポーネントで渡したいpropsの変数を呼ぶことで利用できるようになる。
    <TitleContext.Consumer>
      {title => {
        return <h1>{title}</h1>;
      }}
    </TitleContext.Consumer>
  );
};

const Header = () => {
  return (
    <div>
      <Title />
    </div>
  );
};

const Page = () => {
  const title = "React";
  return (
    //宣言した以下の配下でtitleを利用できるようになる。
    <TitleContext.Provider value={title}>
      <Header />
    </TitleContext.Provider>
  );
};

export default Page;

React Hooks

関数コンポーネント中で状態やライフサイクルを扱うための機能。公式で提供しているものは 10 種類ある。これに加えて独自に組み合わせてできる独自フックがある。

useState と useReducer の状態フック

useState は省略。

useReducer は状態を扱うためのもの。配列やオブジェクトなどの複数のデータをまとめたものを扱う。複雑性がある場合はこちらを使う。

import { useReducer } from "react";

type Action = "DECREMENT" | "INCREMENT" | "DOUBLE" | "RESET";

const reducer = (currentCount: number, action: Action) => {
  switch (action) {
    case "DECREMENT":
      return currentCount - 1;
    case "INCREMENT":
      return currentCount + 1;
    case "DOUBLE":
      return currentCount * 2;
    case "RESET":
      return 0;
    default:
      return currentCount;
  }
};

type CounterProps = {
  initialValue: number
};

const Counter = (props: CounterProps) => {
  const { initialValue } = props;
  //dispatchとして受け付けて実行できるようになる。状態はcountが持つ
  const [count, dispatch] = useReducer(reducer, initialValue);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch("DECREMENT")}>-</button>
      <button onClick={() => dispatch("INCREMENT")}>+</button>
      <button onClick={() => dispatch("DOUBLE")}>*2</button>
      <button onClick={() => dispatch("RESET")}>Reset</button>
    </div>
  );
};

export default Counter;

注意すべき点:

  • React は主にパフォーマンスのために同一のイベント処理が全て終了した後にコンポーネントを一度に再レンダリングする。よって関数の中で set を呼びだされる場合、更新されるのはその全ての処理が終わった後に更新される

useCallback と useMemo

useCallback と useMemo はメモ化用のフック(メモ化とは計算された値をメモリに保存し瞬時に返す最適化手法のことである)。これを使うために react が描画されるメソッドについておさらいしておく

  • props や内部状態が更新されたとき
    • 内部状態 →api 接続して state の値が変わったりする処理のことをさす
  • コンポーネント内で参照している context の値が更新されたとき
  • 親コンポーネントが再描画されたとき

上位コンポーネントが再描画されるとコンポーネントで再描画が発生する。以下 memo 化することで再描画を防ぐことができるようになる。

import { memo, useState } from "react";

type FizzProps = {
  isFizz: boolean
};
const Fizz = (props: FizzProps) => {
  const { isFizz } = props;
  console.log(`Fizzが再描画されました, isFizz=${isFizz}`);
  return <span>{isFizz ? "Fizz" : ""}</span>;
};

type BuzzProps = {
  isBuzz: boolean
};
const Buzz =
  memo <
  BuzzProps >
  (props => {
    const { isBuzz } = props;
    console.log(`Buzzが再描画されました, isBuzz=${isBuzz}`);
    return <span>{isBuzz ? "Buzz" : ""}</span>;
  });

const Parent = () => {
  const [count, setCount] = useState(1);
  const isFizz = count % 3 === 0;
  const isBuzz = count % 5 === 0;

  console.log(`Parentが再描画されました, count = ${count}`);
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <p>現在のカウント:{count}</p>
      <p>
        <Fizz isFizz={isFizz} />
        <Buzz isBuzz={isBuzz} />
      </p>
    </div>
  );
};

export default Parent;

// Parentが再描画されました, count = 1
// memoSample.tsx:8 Fizzが再描画されました, isFizz=false
// memoSample.tsx:17 Buzzが再描画されました, isBuzz=false
// memoSample.tsx:26 Parentが再描画されました, count = 2
// memoSample.tsx:8 Fizzが再描画されました, isFizz=false
// memoSample.tsx:26 Parentが再描画されました, count = 3
// memoSample.tsx:8 Fizzが再描画されました, isFizz=true
// memoSample.tsx:26 Parentが再描画されました, count = 4
// memoSample.tsx:8 Fizzが再描画されました, isFizz=false
// memoSample.tsx:26 Parentが再描画されました, count = 5
// memoSample.tsx:8 Fizzが再描画されました, isFizz=false
// memoSample.tsx:17 Buzzが再描画されました, isBuzz=true

ただし、上記では関数を渡したときや配列、オブジェクトを渡したときなどの値は再描画が発生する原因となる。そこで、useCallback や useMemo が利用される

import { useCallback, useState } from "react";
import React from "react";

type ButtonProps = {
  onClick: () => void
};

//memo化しておくことで再描画されなくなる。
const DoubleButton = React.memo((props: ButtonProps) => {
  const { onClick } = props;
  console.log("DoubleButtonが再描画されました");

  return <button onClick={onClick}>Double</button>;
});

const Parent = () => {
  const [count, setCount] = useState(0);
  //callbackを定義
  const double = useCallback(() => {
    setCount(c => c * 2);
  }, []);

  console.log("Parentが再描画されました");

  return (
    <div>
      現在のcount: {count}
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <DoubleButton onClick={double} />
    </div>
  );
};

export default Parent;

useMemo は省略。

useEffect と useLayoutEffect

useEffect は useMemo や useCallback と同じで第一引数にコールバック関数、第二引数に更新処理が入る場合に値を入れる。useEffect は api 接続や最初の描画時にのみ処理されるものが記載される。

useEffect は以下の順番で実行される

  • 描画関数が実行され
  • DOM が更新される
  • 画面に描画される
  • useEffect が実行される

という流れをとる。したがって、思い api 通信を行ったときは描画されたあとでデータが入る感じになる。それを防ぎたい場合はいくつかの方法があるが、Hooksh ではuseLayoutEffectが存在する。 これは「DOM が更新される」と「画面に描画される」前の間で実行されるもの。したがって、チラツキを消すことができる。

useContext

useContext は Context から値を参照するためのフック。useContext の引数に Context を渡すことで Context の値を取得できる

import React, { useContext } from "react";
type User = {
  id: number,
  name: string
};

const UserContext = (React.createContext < User) | (null > null);

const GrandChild = () => {
  //親コンポーネントにproviderがある場合、useContextを利用することで値を取得することができる。hookを使わない場合はタグでconsumerを使って囲む必要がある。
  const user = useContext(UserContext);
  return user !== null ? <p>Hello, {user.name}</p> : null;
};

const Child = () => {
  const now = new Date();

  return (
    <div>
      <p>Current: {now.toLocaleDateString()}</p>
      <GrandChild />
    </div>
  );
};

const Parent = () => {
  const user: User = {
    id: 1,
    name: "HogeUser"
  };

  return (
    <UserContext.Provider value={user}>
      <Child />
    </UserContext.Provider>
  );
};

useRef と useImperativeHandle

ref には大きく分けて 2 つの使い方がある

  • データの保持
  • DOM の参照

データの保持は useState や useReducer が存在するが、これらとの違いは再描画が発生するかどうかの違いがある。useState の方で値が変わる場合は DOM が更新される。useRef はされない。

DOM を参照することができ、<input>要素などにマウントするとその DOM を所持することができるようになる

import React, { useState, useRef } from "react";

const ImageUploader = () => {
  const inputImageRef = (useRef < HTMLInputElement) | (null > null);
  const fileRef = (useRef < File) | (null > null);
  const [message, setMessage] = (useState < string) | (null > "");

  const onClickText = () => {
    if (inputImageRef.current !== null) {
      inputImageRef.current.click();
    }
  };

  const onChangeImage = (e: React.ChangeEvent<HTMLInputElement>) => {
    const files = e.target.files;
    console.log(files);
    if (files !== null && files.length > 0) {
      fileRef.current = files[0];
    }
  };

  return (
    <div>
      // ここではp要素でクリックしてinputのonChange処理を実行している。
      <p style={{ textDecoration: "underline" }} onClick={onClickText}>
        画像をuploadする
      </p>
      <input
        type="file"
        ref={inputImageRef}
        accept="image/*"
        onChange={onChangeImage}
        style={{ visibility: "hidden" }}
      />
    </div>
  );
};

export default ImageUploader;

useImperativeHandle は親要素から小要素のイベントを明示的に呼び出すことができるもの。ただ、あまり使われることはない。親要素と小要素が密な結合となるため使われるのがそこまで多くない

カスタムフックとデバッグ

カスタムフックはこれまで上げてきた hook をつなぎ合わせたものを作ることができるもの。メソッドとして定義する。

import React, { useState, useCallback, useDebugValue } from "react";

//カスタムフック
const useInput = (defaultText: string = "") => {
  const [state, setState] = useState(defaultText);
  const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    setState(e.target.value);
  }, []);

  useDebugValue(`Input: ${state}`);
  return [state, onChange] as const;
};

const Input = () => {
  const [text, onChangeText] = useInput();
  return (
    <div>
      <input type="text" value={text} onChange={onChangeText} />
    </div>
  );
};

これを利用することで、各コンポーネントでメソッドを記載する必要がなくなった。useDebugValueはデバッグ用の hook である。react developer tools というブラウザの拡張機能を利用することで中身を見ることができるようになる。

プロフィール
Kobasan
現在都内のWeb会社で働いている人です.主にこれまで,Web関連及び機械学習周りのことをやってきました.このブログではそれらの内容を含む,ちょっとした私の備忘録を記載するものです.