🤣

Reactについてざっくりまとめてみた

2022/03/22に公開

typescriptと関数コンポーネントをベースにReactの基本に触れていきます。
これからReactに入門する方に対して、実務でReactを使う上での下地をつくることが本記事の目的です。

したがって、細かい部分には触れず、概要を掴むことに重きを置きます。

※また、html・css・js(node.js)・typescriptを一定レベル理解している前提で説明いたします。その点ご了承ください。

Reactとは

Reactとは、UI構築のためのJSライブラリです。
大きく2点の特徴があります。

  • 宣言的なview
  • コンポーネントベース

宣言的なビューとは、状態に応じた処理やUIの表示内容を宣言できるviewということです。

従来、宣言的の反対である命令的という概念でUIが実装されていました。
命令的だと1回1回処理をステップごとに細かく記述する必要があり、直感的ではなく不便でした。
そこであらかじめ1回1回命令していた処理をまとめて宣言することで、直感的なUIの構築が可能になります。

宣言的なviewについて、以下の記事が参考になるかと思います。
https://zenn.dev/arei/articles/f59e263aa3edf2

コンポーネントベースとは、コンポーネントという単位を組み合わせてUIを開発することです。

コンポーネントは値を「状態(state)」として保持することもできます。(stateの扱い方については後述)
UIの機能や役割などに応じてDOMを分割することで、複雑なUIを構築しやすく安全な実装をすることができます。

コンポーネントベースについては、下記の記事が参考になるかと思います。
https://qiita.com/thim/items/c46bb888c864c0360e22

また、コンポーネントの種類に関してですが、クラスコンポーネントと関数コンポーネントの2種類があります。
現状では関数コンポーネントの方が主流なので、書き方を合わせましょう。

コンポーネントの書き方

基本的なコンポーネントの書き方を紹介致します。

コンポーネントは以下のような書き方で書けます。

const Child = () => {
  return (
    <div>
      <p>こんにちは</p>
    </div>
  );
}

呼び出す際もシンプルです。
Parentという親のコンポーネントから子のコンポーネントとしてChildを呼び出します。

const Parent = () => {
  return <Child />
}

データを親から子に渡したい時があるかと思います。
その際はバケツリレーで値を送ることができます。

// React.VC→Reactの関数コンポーネントの型
const Parent: React.VC = () => {
  const greeting = "こんにちは";
  return <Child greeting={greeting} />
}

type Props = {
  // optionalにすると親から値が渡らないことを許容できる
  greeting: string;
};

const Child: React.VC<Props> = ({ greeting }) => {
  return (
    <div>
      <p>{ greeting }</p>
    </div>
  );
}

さらにコンポーネントの中にコンポーネントを組み込みたいといった、Wrapperとしてのコンポーネントも実装することができます。

// React.VC→Reactの関数コンポーネントの型
const Parent: React.VC = () => {
  return (
    <Child>
      <p>こんにちは</p>
    </Child>
  );
}


const Child: React.VC = ({ children }) => {
  // 呼び出された際に挟んだコンポーネント、DOMが入ってくる
  return (
    <div>
      {children}
    </div>
  );
}

基本のhooks

今まで触れてきたコンポーネントの書き方をベースにReact固有の関数であるhooksについて触れていきます。

useState

先ほどコンポーネントは値を状態として保持することができると触れました。
useStateは状態であるstateの値を設定することができるhooksです。

isLoadingはstateの値で、setIsLoadingでstateの値を変更することができる関数です。

const [isLoading, setIsLoading] = useState<boolean>(false);

stateの値を使うことで、複雑なUIを実装することが可能です。

どういうことか。

そのためには、Reactのコンポーネントが再レンダリングするタイミングを知る必要があります。再レンダリングするタイミングは以下です。

  • stateが更新されたとき
  • コンポーネントにわたってきたpropsの値が変更されたとき
  • 親のコンポーネントがレンダリングしたとき

つまり、stateが変更されたら、該当のstateを保持しているコンポーネントでは再レンダリングが発生します。

以下のサンプルコードでは、必要なデータを取得できたかに応じてisLoadingの値とUIを変更しています。

const Component: React.FC = () => {
  const [isLoading, setIsLoading] = useState<boolean>(false);
  const [data, setData] = useState<Data>(initialvalue);

  // useEffectについては後で触れます。
  // とりあえず、データを取得してstateにセットして、loadingのstateを変更していることを想定してください。
  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true);
      const fetchedData = await fetchData();
      setData(fetchedData);
      setIsLoading(false);
    };
    fetchData();
  }, []);


  return (
    <>
      { // 三項演算子で表現する
        isLoading ? (
        // スピナーを表示
        <Spinner />
      ) : (
        // 通常のコンテンツを表示
        <Contents data={data} />
      )}
    </>
  );
};

このようにstateを管理することで、それに応じた動的なUIを比較的に直感的に構築できます。

useEffect

useEffectは関数の実行をレンダリングの後にまで遅らせることができます。

先ほどの例では非同期でデータを取得するために使っていました。他にもstateや変数に応じたDOMの変更などに使うことができます。

第一引数では実行する関数、第二引数では関数の実行に依存する値を配列の形でセットします。
空配列の場合、コンポーネントの初回レンダリングのみ関数を実行します。

useEffect(() => {}, []);

詳しくはuseEffectに関する記事をご覧ください。
https://qiita.com/seira/items/e62890f11e91f6b9653f

useContext

reactコンポーネントでは、親からpropsで子に値を送ることができますが、多くの場合コンポーネントは何重もの入れ子になるでしょう。
複雑に入れ子になるとバケツリレーが何回も行われるので、値を追う労力が増えます。
useContextを使うと、親からpropsで受け取らなくても値を受け取ることができます。

例として、GrandParent -> Parent -> Childといった入れ子のコンポーネントで、トップから最下層にデータを送りたい場合を想定します。

type Props = {
  username: string;
}

const GrandParent: React.FC = () => {
  const username = "hoge";
  return <Parent username={username} />;
};

// ただ中継しているだけ
const Parent: React.FC<Props> = ({ username }) => {
  return <Child username={username} />;
};

const Child: React.FC<Props> = ({ username }) => {
  return <p>{username}</p>;
};

useContextを使うとこうなります。(別ファイルに分割したい時は脳内で置き換えて考えてください)


export const UsernameContext = createContext<string>("");

const GrandParent: React.FC = () => {
  const username = "hoge";
  return (
    // トップコンポーネントでcreateContextで作成したプロバイダーでラップする
    <UsernameContext.Provider value={username}>
      <Parent />
    </UsernameContext.Provider>
  );
};

const Parent: React.FC = () => {
  return <Child />;
};

const Child: React.FC = () => {
  // プロバイダーでラップした時に設定した値を、useContextで取得する
  const username = useContext(UsernameContext);
  return <p>{username}</p>;
};

このようにuseContextを正しく使うことで、下層コンポーネントで値を呼び出すことが容易になります。

useReducer

useReducerは状態管理のためのhooksでuseStateと似ています。

const [state, dispatch] = useReducer(reducer,initialState)という構文で、stateとそれを更新するdispatchで構成されています。
reducerにはtypeに応じたswitch文を書いて、stateの更新処理を書きます。

以下のサンプルでご確認ください。


type Action = {
  type: "UPDATE_NAME";
  payload: string;
};

const Component: React.FC = () => {
  const user = {
    name: "",
    age: 0,
  };

  // typeに応じてstateを変更するために、switch文を書く
  const reducer = (userdata: { name: string; age: number }, action: Action) => {
    switch (action.type) {
      case "UPDATE_NAME":
        return {
          ...userdata,
          name: action.payload,
        };
      default:
        return userdata;
    }
  };

  const [state, dispatch] = useReducer(reducer, user);

  return (
    // inputで名前を変更することを想定
    <input
      onChange={(e) =>
        dispatch({
          type: "UPDATE_NAME",
          payload: e.currentTarget.value,
        })
      }
    />
  );
};

export default Component;

パフォーマンスを良くするには

reactでパフォーマンスをより良くする際にはメモ化をします。
メモ化とはざっくりキャッシュすることだと捉えてください。

このセクションでは、さまざまなメモ化の方法をシェアします。

React.memo

React.memoはコンポーネントをメモ化します。
再レンダリングする際に前後で変化があるかを検証して、差分がなければレンダリングしないようにする表現です。

レンダリングされるタイミングはいつか?

改めてコンポーネントがレンダリングする条件についておさらいしましょう。

  • stateが更新されたとき
  • コンポーネントにわたってきたpropsの値が変更されたとき
  • 親のコンポーネントがレンダリングしたとき

stateが変化した場合とpropsに渡ってきた値が変化した場合は該当コンポーネントに関係あるので、レンダリングをスキップすることはできません。しかし、親のコンポーネントがレンダリングした際は該当コンポーネント自体に本質的な変化はないはずです。
その際にReact.memoを使うことで、子のレンダリングをしないようにすることができます。

しかし、以下のような関数を親から子に渡している際はメモ化が正しく機能しません。
というのも、親のコンポーネントがレンダリングされるた際に関数を新たに宣言されます。関数の内容が同じだとしても別の関数として扱われるため、メモ化が働きません。

// 親のコンポーネントにて関数を子コンポーネントに渡す。
const alertFunc = (): void => {
    alert("~~~~~");
};
<Child func={() => alertFunc()} /> // 子コンポーネントはReact.memoにWrapされているとする

useCallback

上記のような関数をメモ化したい時に使えます。
React.memoと併用することでコンポーネントのレンダリングを少なくすることができます。

useEffectと似ていて、第一引数にメモ化したいコールバック関数、第二引数に依存配列を設定します。

const alertClick = useCallback(() => {
    alert("click");
}, []);

useMemo

useMemoは値の再計算をメモ化することができます。
値にかなりの計算がかかるような変数に使うと非常に有効です。

例えば、numを二倍にするような計算をメモ化した例が下記です。
他のhooksと同じように第二引数にある依存配列が変化したとき、それに応じた再計算を行わせることができます。

const doubledNum: number = useMemo(() => double(num), []);

パフォーマンス改善に関する記事を参照してみるといいかもしれません。
https://qiita.com/soarflat/items/b9d3d17b8ab1f5dbfed2

次に勉強するべきこと

次に勉強することがきりがないですが、ピックアップしてみます。

説明したhooks以外にもしばしば使うhooksがあります。どんなhooksがあるのか調べてみるといいでしょう。
https://ja.reactjs.org/docs/hooks-reference.html

また、関わっているプロジェクトで使っている状態管理ライブラリを勉強してみるのもいいかもしれません。
大きなプロダクトだとuseStateなどのhooksだけでは、stateの管理が難しいことがほとんどです。
そのため、いずれreduxrecoil、それらを学ぶ必要が出てくるでしょう。

加えて、Typescriptに自信がなければ勉強しましょう。
type-challengesという型問題集をやってTSの表現を覚えていくことをお勧めします。

まとめ

長々と書きましたが、実装力やフロントエンドの理解力を上げるためには自分で何か作ってみることが何よりも大切です。
他にもさまざまな表現やpackageがあるので開発する中で学んでいきましょう。

※ 理解に重きを置いたつもりなので、一部説明を割愛・省略している部分があります。ご了承ください。

参考文献

GitHubで編集を提案

Discussion