エンジニアを目指す日常ブログ

日々勉強したことのメモ。独学ですので間違っていたらコメント等で教えてください。

yarnで古いバージョンのパッケージをインストールする方法メモ

記事の移行について

本記事の詳細は、こちらに移行しました。

bunsugi.com

はじめに

Reactのreact-router-domがメジャーバージョンアップした(バージョン6)とのことで、バージョン6の記法を勉強できていないためバージョン5をインストールしたい。

yarnを使って実施する方法のメモ。

結論

バージョンを指定してパッケージインストールする方法。

react-router-domのバージョン5、かつマイナーバージョンは最新のものをインストールしたい場合

$ yarn add. react-router-dom@5.x

react-router-domのバージョン5.2をインストールしたい場合

$ yarn add. react-router-dom@5.2

React × Typescriptでコンポーネントを書く時のテンプレートメモ

はじめに

今回の記事の目的

React × TypeScriptでコンポーネントを追加するときに毎回迷うので、基本となる要素をメモ。

基本のテンプレート

MyComponent.tsx ※ファイル名は大文字から開始

import { memo, ReactNode, VFC } from "react";

type Props = {
    children: ReactNode;    //childrenは「ReactNode」
    onClick: () => void;    // 関数のprops
    disabled?: boolean;     // 任意のpropsには ? をつける
    // ・・・その他propsの引数の型
};

export const MyComponent: VFC<Props> = memo((props) => {
    const { children, onClick, disabeld = false, …その他のprops… } = props;

    // ・・・処理

    return (
    // ・・・返却値
    );

});

VFC

関数型コンポーネントであることを表す。

任意のprops項目

任意のpropsを指定する場合は、分割代入する際にdisabled = falseといった形で初期値を指定する。

memoについて

memo関数で囲むことで「propsに変更がない限り、親コンポーネントが再レンダリングされてもこのコンポーネントは再レンダリングしない」ようにできる。 必要に応じて利用する。

おわりに

コンポーネントを作成するときに必要な要素をメモした。

ReactでCSSを適用する方法のメモ

はじめに

今回の記事の目的

ReactでCSSを当てる方法はいくつかある。覚えておけばよさそうなものだけ抜粋してメモ。

  • classNameでクラスを設定
  • インラインスタイル
  • styled-componentsモジュールを利用
  • コンポーネントライブラリ(chakra-uiやmaterial-ui)を利用

classNameでクラスを設定

ソースコード

App.tsx

function App() {
    return (
        <>
            {/* 方法1:classNameでクラスを設定 */}             
            <div className="red-area">
                <h1>方法1</h1>
                <p>
                    classNameというpropsにクラス名を渡す。
                    index.tsxで呼び出しているindex.clasNameというpropsにクラス名を渡す。
                    index.cssに通常のCSSを記載する。
                </p>
            </div>
        </>
    );
}
export default App;

index.css

.red-area {
    background-color: red;
    color: white;
}

インラインスタイル

ソースコード

App.tsx

const style = { container: { backgroundColor: "red", color: "white" } };

function App() {
    return (
        <>
            {/* 方法2:インラインスタイル */}
            <div style={style.container}>
                <h1>方法2</h1>
                <p>
                    インラインスタイル:styleというpropsに、オブジェクトを渡す。
                    オブジェクトの型はReact.CSSProperties。
                    ここではstyleオブジェクトの中に、
                    要素ごとのReact.CSSPropertiesオブジェクトを設定している。
                </p>
            </div>
        </>
    );
}

export default App;

styled-components

インストール

$ yarn add styled-components
$ yarn add @types/styled-components

@types/styled-componentsはtypescriptの場合に必要。

ソースコード

styled.div``で囲んだ中にはCSSをそのまま書ける。:hoverなども書ける。

import styled from "styled-components";

function App() {
    return (
        <>
            {/* 方法3:styled-componentsモジュールを利用 */}
            <SContainer>
                <h1>方法3</h1>
                <p>
                    styled-componentsをインポートする。
                    関数の外で、例えばdivをベースに独自のスタイルをつけたコンポーネント
                    SContainerを用意する。
                </p>
            </SContainer>
        </>
    );
}

// SContainerの定義
const SContainer = styled.div`
    background-color: red;
    color: white;
`;
export default App;

div等HTMLのタグを装飾する場合は

const SContainer = styled.div`

とするが、自作のコンポーネントを装飾する際は

const SContainer2 = styled(SContainer)`

のように記載する。

コンポーネントライブラリ(chakra-uiやMUI)を利用

今回はchakra-uiを利用する。

chakra-ui.com

最も有名なのはMaterial UIらしい。 mui.com

インストール

$ yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4

ソースコード

import { ChakraProvider, Box, Text, Heading } from "@chakra-ui/react";

function App() {
    return (
        <>
            <ChakraProvider>
                <Box bg="red" color="white">
                    <Heading>方法4</Heading>
                    <Text>
                        コンポーネントライブラリのchakra-uiを利用。
                        divタグの代わりにBox、pタグの代わりにTextなど
                        便利に使えるコンポーネントがそろっている。
                    </Text>
                </Box>
            </ChakraProvider>
        </>
    );
}

export default App;

<ChakraProvider>タグで囲む必要があるが、親コンポーネントで囲んでおけば子コンポーネントでもchakra-uiが使える。

その他の方法

これ以外にも方法は様々あるらしい。

  • CSS Modulesを利用する方法。有名なモジュールであるnode-sassは非推奨になっている模様。
  • StyledJsxというモジュールを利用する方法。
  • emotionというモジュールを利用する方法。

表示内容

方法1~3の出力
方法1~3の出力

方法4の出力
方法4の出力

おわりに

方法3と4をうまく使えれば良さそう。

参考文献

zenn.dev

www.udemy.com

【React】レンダリングを制御するための関連知識メモ

はじめに

Reactではレンダリングの動作を理解することがとても重要で、レンダリングを制御するための機能が様々用意されている。use~系が多くて難しいので、基本的なところをメモしておく。対象は以下4つとする。

useEffect

副作用を制御するuseEffect。

レンダリングに関わる関数・メソッド

以下、メモ化三兄弟。

  • memo():コンポーネントのメモ化
  • useCallback():関数のメモ化
  • useMemo():変数(関数の処理結果)のメモ化

Reactで再レンダリングが行われる条件

そもそもReactでは、以下の条件で再レンダリングが行われる。

useEffect()

useEffectは以下2つの目的で使うものらしい。

  • ある処理を、特定の値が変化した時のみ動作させることができる
  • レンダリングが完了した後に処理を実行させることができる

公式ページには以下のように記載がある。

副作用を有する可能性のある命令型のコードを受け付けます。

DOM の書き換え、データの購読、タイマー、ロギング、あるいはその他の副作用を、関数コンポーネントの本体(React のレンダーフェーズ)で書くことはできません。それを行うと UI にまつわるややこしいバグや非整合性を引き起こします。

代わりに useEffect を使ってください。useEffect に渡された関数はレンダーの結果が画面に反映された後に動作します。副作用とは React の純粋に関数的な世界から命令型の世界への避難ハッチであると考えてください。

デフォルトでは副作用関数はレンダーが終了した後に毎回動作しますが、特定の値が変化した時のみ動作させるようにすることもできます。

フック API リファレンス – React

副作用を含む処理はuseEffectの中に書くように…と言っている。(難しい)

副作用とは、alertや、APIでのデータ取得や、console.logなどがある。

使い方

useEffect ( () => {処理}, [依存配列] )

依存配列にstateを設定すると、そのstateが更新されたときのみ第一引数の処理を実行する。依存配列に[]を設定した場合は、初期レンダリング完了時のみ処理を実行する。

利用例

特定の値が変化した時のみ動作させる目的での使い方を記載しておく。 関心を分離するために複数の副作用を使うという例が公式ページにも記載されている。

副作用フックの利用法 – React

たとえば値をカウントアップしていって、値が10で割り切れる数になった際にカウント2(10の位をカウントアップするプログラムを作ってみる。

カウントアップのイメージ
カウントアップのイメージ

App.tsx

import { useEffect, useState } from "react";

export const App = () => {
    const [count1, setCount1] = useState(0);
    const [count2, setCount2] = useState(0);

// count1が更新された場合のみ以下の処理を実行する
    useEffect(() => {
        if (count1 > 0 && count1 % 10 === 0) {
            setCount2((count2) => count2 + 1);
        }
    }, [count1]);

    const onClickCountUp = (e: React.MouseEvent<HTMLButtonElement>) => {
        setCount1((count1) => count1 + 1);
    };

    return (
        <>
            <button onClick={onClickCountUp}>カウントアップ</button>
            <p>{`カウント:${count1}  カウントの10の位:${count2}`}</p>
        </>
    );
};

もしuseEffectを使わなかった場合、以下のエラーが出ることになる。

Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.

count1が10になった際、count2がカウントアップされて1になる。count2が変更されたので再レンダリングされる。count1は10なのでまたcount2がカウントアップされる。count2が変更されたので・・・と、count2のカウントアップが繰り返されるためである。

memo()

コンポーネントをメモ化してくれる関数。コンポーネントに対して設定する。親コンポーネントが再レンダリングされても、propsに変更がない限り再レンダリングされないようにする

使い方

コンポーネント側の関数をmemo()で囲む。(記載例はtype-script。)

Child.tsx

// インポートが必要
import { memo, VFC } from "react";

type Props = {
    text: string;
};

// 関数全体をmemoで囲む
export const Child: VFC<Props> = memo((props) => {
    const { text } = props;
    return (
        <div>{text}</div>
    );
});

このときChildコンポーネントは、呼び出す側の親コンポーネントが再レンダリングされたとしても、propsであるtextが更新されない限りは再レンダリングされなくなる。

useCallback()

第一引数に渡している関数をメモ化してくれる。 第二引数で、関数を再定義する条件を指定する(useEffectと同様)。

どんなときに使うか

基本的には、子コンポーネントのpropsとして関数を渡している場合に利用する。

memo()で子コンポーネントをmemo化しても、propsに関数がある際、親コンポーネントが再レンダリングされた際に関数が再定義されることで、結局propsが変更された扱いとなり、子コンポーネントが再レンダリングされてしまう。

そうなるのを防ぐために、子コンポーネントにpropsで渡している関数をuseCallback()でメモ化しておく必要がある。

逆に、子コンポーネントをmemo化していないと意味が無いので注意。

利用例

コンポーネント

App.tsx

import { useCallback, useState } from "react";
import { Child} from "./Child";

export const App = () => {

    const [text, setText] = useState("");
    const [count, setCount] = useState(100);
    const onClickInit = useCallback(() => setCount(0), []);

    return (
        <>
            <p>{`カウント:${count}`}</p>
            <Child onClick={onClickInit} />
        </>
    );
};

コンポーネント

Child.tsx

import { memo, VFC } from "react";

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

// memo化しているのでonClickが再定義されなければ再レンダリングされない
export const Child: VFC<Props> = memo((props) => {
    const { onClick } = props;
    return (
        <>
            <button onClick={onClick}>カウント初期化</button>
        </>
    );
});

useMemo()

変数をメモ化してくれる関数。 実践で利用したことはまだ無い。

使い方

変数の導出処理が重い場合、再レンダリングするたびに計算すると大変なので、初回レンダリング時にのみ計算するよう設定する。

第二引数の設定は、useEffectやuseCallbackと同じルール。

 const value = useMemo(() => (1+2+3+4+…重い計算処理…) , []);

実際に使ってみた例は以下となる。

import { useMemo, useState } from "react";

export const App = () => {
    const calc = () => {
        let num = 0;
        for (let i = 1; i < 100000000; i++) {
            num += i;
        }
        console.log("重い計算処理calc()を実行");
        return num;
    };

    // 変数valueの計算が重い場合
    const value = useMemo(() => calc(), []);

    // stateを更新するボタン
    const [state, setState] = useState(0);
    const onClickState = () => setState((state) => state + 1);

    return (
        <>
            <button onClick={onClickState}>stateを更新するボタン</button>
            <p>{`State:${state}   value:${value}`}</p>
        </>
    );
};

重要なのは

const value = useMemo(() => calc(), []);

の部分で、このように書くことでvalueの計算が初回レンダリング時のみで済む。

普通に

const value = calc();

と書くと、ボタンを押してstateを変更するたびにcalc()が実行されてしまうことがコンソールログから確認できる。

おわりに

ひとまずメモ化三兄弟を整理できた。useEffectは奥が深く理解しきれていないので、理解し次第追記する。

AWS Amplify がビルドに失敗したときの原因をメモ

 はじめに

AWS AmplifyでReactアプリを作成しようとしたところ、ビルド実行時にエラーとなったので原因をメモ。

ビルド失敗の絵

前提

  • ソースコードはCodeCommitに保管する。
  • AWS Amplifyの「Host web App」を利用してアプリを作成する。

エラーの内容

エラーは以下。リポジトリ名、アプリ名はay-s-○○○○で伏せます。

2021-11-07T03:30:28.392Z [INFO]: # Cloning repository: https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/ay-s-○○○○
2021-11-07T03:30:28.847Z [INFO]: Cloning into 'ay-s-○○○○'...
2021-11-07T03:30:38.226Z [INFO]: fatal: unable to access 'https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/ay-s-○○○○/': The requested URL returned error: 403
2021-11-07T03:30:38.229Z [ERROR]: !!! CustomerError: Unable to clone repository due to user error code: 128
2021-11-07T03:30:38.230Z [INFO]: # Starting environment caching...
2021-11-07T03:30:38.290Z [INFO]: # Environment caching completed
Terminating logging...

エラー画面
エラー画面

要するに、

fatal: unable to access 'https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/ay-s-○○○○/': The requested URL returned error: 403

CodeCommitのリポジトリからcloneしようと思ったけどアクセスができません!!ということのようです。

エラーの原因

Amplify側に設定したサービスロールの内容が間違っていた。

以下サイトに同じ事例があった。
Unable to clone repo (403) at Build step · Issue #572 · aws-amplify/amplify-console · GitHub

サービスロールの設定箇所

Amplifyで「new app」を作成する画面で、サービスロールを選択する場所がある。

おそらく、ここで「new service role」のほうを選んでいれば問題ないと思うが、既存のロールを選んでしまったと思われる。

サービスロールを設定する箇所
サービスロールを設定する箇所

Amplifyに設定されているサービスロールの確認

「アプリの設定」→「全般」で確認できる。

サービスロール確認
サービスロール確認

サービスロールに適用されているポリシーの確認

IAMのコンソールからロールに設定されたポリシーの中身を確認すると、「Resource」に記載されているCodeCommitのARN名が間違っていた。(既に削除した、昔のリポジトリのものが入っていた)。

IAMポリシー
IAMポリシー

ここを修正した。

IAMポリシーに設定すべきCodeCommirのARN名確認方法

ARN名は「CodeCommit」の該当リポジトリ→「設定」で確認できる。

CodeCommirのARN名
CodeCommirのARN名

おわりに

IAMポリシーを修正し、無事デプロイが完了した。

デプロイ完了
デプロイ完了

関連記事

CodeCommitについて詳細

CodeCommitの使い方を記載した記事。 tomiko0404.hatenablog.com

重要だけど忘れそうなJavaScriptの記法メモ

はじめに

Reactを勉強する中でJavaScriptの記法を利用している。

その中でよく出てきた、超基本の重要なJavaScript記法をメモしておく。今後、何か月か勉強をお休みすることがあると忘れそうなため。

前提

以下の例に記載してるものは、参考環境の説明章で述べるindex.jsに記載しているもの。

アロー関数

こんな文法で無名関数がつくれる。

( 引数 ) => { 処理 }

非常に重要かつ、私自身が罠にはまったので、別記事にまとめている。

tomiko0404.hatenablog.com

テンプレート文字列

文字列と変数を結合して出力したいときの書き方。

古い書き方だと、文字列はダブルクォーテーション""でくくり、変数はそのまま記載したものを+で結合する。

これだと読みにくいのでテンプレート文字列という書き方がある。

  • 全体を``(バッククォート)で囲む。
  • 変数を使いたいときは${変数名}と記載する。

index.js

const math = 90;
const japanese = 60;
const english = 85;

// 古い書き方
console.log(
    "算数:" + math + "点、国語:" + japanese + "点、英語:" + english + "点です。平均は" + (math + japanese + english) / 3 + "点です。"
);

// テンプレート文字列を利用した書き方
console.log(
    `算数:${math}点、国語:${japanese}点、英語:${english}点です。平均は${(math + japanese + english) / 3}点です。`
);

出力結果は以下。

算数:90点、国語:60点、英語:85点です。平均は78.33333333333333点です。
算数:90点、国語:60点、英語:85点です。平均は78.33333333333333点です。

分割代入

オブジェクトや配列の中身を変数に代入する方法。

オブジェクトの場合

オブジェクトの項目名に合わせた変数名で取得できる。

const point = {
    math: 90,
    japanese: 60,
    english: 85,
    science: 95,
    social: 55,
};
// オブジェクトの分割代入
const {math, japanese, english} = point;

console.log(
    `(point)算数:${math}点、国語:${japanese}点、英語:${english}点です。`
);

配列の場合

配列の場合は以下の通り、順番通りに分割代入できる。 代入の際にはオブジェクトの場合と異なり[]で囲む。

また、配列の前半に変数に代入する必要がないレコードがある場合は、以下のように変数名を入れなくてもよさそう。

const pointArray = [90, 60, 85, 95, 55];

// 配列の分割代入
const [, , , science, social] = pointArray;
console.log(`(pointArray)理科:${science}点、社会:${social}点です。`);

出力結果はオブジェクト分割代入のコードと合わせて以下の通り。

(point)算数:90点、国語:60点、英語:85点です。
(pointArray)理科:95点、社会:55点です。

上記の例のように使うシーンがあるかは不明だが、Reactでは

[num, setNum]=useState(0);

というように、公式の関数から値や関数を取り出すときにたくさん利用する。

スプレッド構文

配列の中身を1個ずつ取り出す構文。

...[配列名]

正確な記載ではないのだが、array1 = [1,2,3,4,5];に対して、...array1と書くと、1,2,3,4,5という扱いになる。とざっくり思っている。

配列の一番後ろに要素を足す(よく使うやり方)

let messages = [
    "こんにちは",
    "元気ですか",
    "はい元気です",
    "テストどうでしたか",
];

messages.forEach((i) => console.log(i));
console.log("------------------------")

// スプレッド構文でmessagesを展開してから結合
messages = [...messages, "まあまあです"];

messages.forEach((i) => console.log(i));

出力結果は以下の通り。

こんにちは
元気ですか
はい元気です
テストどうでしたか
------------------------
こんにちは
元気ですか
はい元気です
テストどうでしたか
まあまあです

オブジェクトのの一番後ろに要素を足す

オブジェクトでも同様に要素を足すことができる。

let point = {
    math: 90,
    japanese: 60,
    english: 85,
    science: 95,
    social: 55,
};

console.log(point);

// スプレッド構文でオブジェクトの要素を追加
point = { ...point, music: 100 };
console.log(point);

出力結果は以下の通り。

出力結果
出力結果

関数の引数にも利用できる

あまり利用シーンは無いように感じられるが、配列から1要素ずつ取り出すので以下のようなこともできる。

const pointArray = [90, 60, 85, 95, 55];
const pointAvg = (p1, p2, p3, p4, p5) => (p1 + p2 + p3 + p4 + p5) / 5;

// スプレッド構文で90, 60, 85, 95, 55を引数に入れる
console.log(`5教科の平均点は${pointAvg(...pointArray)}点です。`);

出力結果は以下の通り。

5教科の平均点は77点です。

map構文

配列の1個1個の要素に対して処理を記載できる。再頻出でかなり重要。

mapの引数には関数を入れる。この関数は、第一引数に配列の各要素、第二引数に配列のインデックスが設定された関数である。

let messages = [
    "こんにちは",
    "元気ですか",
    "はい元気です",
    "テストどうでしたか",
];

// map関数でmessagesの各要素に対して処理
messages.map((data, index) => {
    console.log(index + ":" + data);
});

ここで利用しているアロー関数では、先ほど述べた罠にはまらないように注意。

出力結果は以下の通り。

0:こんにちは
1:元気ですか
2:はい元気です
3:テストどうでしたか

参考までに、以下の記事ではHTMLの<li>要素に各要素を入れて返却したい箇所で利用している。

<ul>
    {messageList.map((data, index) => {
        return <li key={index}>{data}</li>;
    })}
</ul>

tomiko0404.hatenablog.com

参考:環境の説明

プロジェクトフォルダを作成し、その中に

  • index.html
  • index.js

の2ファイルを作成する。

index.htmlをブラウザで表示し、ボディ部でindex.jsを呼び出す。

index.html

<!DOCTYPE html>
<html lang="jp">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Javascriptのメモ</title>
    </head>
    <body>
        <!-- ★★bodyタグの中にscriptタグを記載 ★★-->
        <script type="text/javascript" src="index.js"></script>
    </body>
</html>

おわりに

Reactの勉強を進めることで、JavaScriptの文法はだいぶ頭に入ってきた。 忘れたときにはこの記事が参考になると良いと思います。

【React/ useState】setState関数の書き方が下手で大量レンダリングが起こった話

はじめに

Reactを利用してアプリを作っていたところ、予期せぬ大量レンダリングが発生して画面描画が遅くなってしまった。

いろいろ試した結果、useStateのset関数の使い方が悪いことがわかったのでメモ。

ひとこと結論

setState関数の引数を「関数にする」

自分自身のstateを使う際は、setState関数の引数には「値」ではなく「関数」を入れる。 そして

全体を「useEffectで囲む」

実施内容

背景

Reactを利用して簡易チャットアプリを作成した。詳細は以下の記事を参照。

作成したチャットアプリ tomiko0404.hatenablog.com

その中で、以下のsetMessageList部分の処理で大量のレンダリングが走っていた。

    // メッセージリスト(全員が送ったメッセージの一覧)をStateとして定義
    const [messageList, setMessageList] = useState<string[]>([]);

    // サーバから、誰かからのメッセージが送信されたときの処理
    // messageListの一番後ろに、サーバから受け取ったメッセージを追加する。
    socket.on("chat", (msg) => {
        setMessageList([...messageList, msg]);
        console.log("setMessageListを実行:" + messageList);
    });

処理を簡単に説明すると、

  • 画面側で、メッセージ一覧画面に表示するためのメッセージリストmessageListをStateとして管理している。
  • 誰かがメッセージを送信したら、サーバがそれを受け取り、全員の画面に向けてメッセージを発信する。
  • メッセージを受け取った画面側は、messageListにの一番後ろに今受け取ったメッセージを追加することで、画面に表示する。

という内容である。

失敗① 単純にsetMessageListを実行した場合

背景に記載したコードをそのまま実行した場合。

「1回目」「2回目」・・・「5回目」までメッセージを送信したところ、5回目にはかなり描画が遅くなっていた。(2秒ほど)

コンソールログを見てみる。わかりやすいように、メッセージを送信するたびに1行の空白行を入れている。 5回目のメッセージを送信した際は、2,098回ものsetMessageList処理が走っていることがわかる。

大量のsetMessageList実行ログ
失敗①

元々のイメージでは、例えば4回目のメッセージを送ったとき、messageListの値は[1回目, 2回目, 3回目] から [1回目, 2回目, 3回目, 4回目]に変更されると思っていた。しかし、ログからは[1回目][1回目, 2回目, 4回目]などすべてのパターン一通り通ってから[1回目, 2回目, 3回目, 4回目]に落ち着いているように見える…。

注)ちなみに表示されているmessageListとログのmessageListが合っていない(画面では5回目まで表示されているのにログには4回目までしか入っていない)のは、console.logの位置の問題だと思われるが、詳細は不明。

失敗② useEffectを利用した場合

Reactでは、再レンダリングする条件として、何らかのステートが更新された場合という条件が存在する。

参考資料: 【React】再レンダリングの仕組みと最適化

今回、messageList以外にもstateを用意していたので、それらの値が変化するたびに実行されてしまうのは問題がありそう。 ということで、useEffect()を利用することにした。

    useEffect(() => {
        socket.on("chat", (msg) => {
            setMessageList([...messageList, msg]);
            console.log("setMessageListを実行:" + messageList);
        });
    }, []);

結果、以下のように最新のメッセージしか表示されなくなってしまった。

最新のメッセージのみ表示
最新のメッセージのみ表示

この理由は明白で、useEffectの第二引数を空配列[]にしていることである。 第二引数を空配列にすると、この関数は初回レンダリング時にしか実行されなくなる。

結果、サーバからメッセージを受け取った際に行う処理(関数)の定義を初回レンダリング時にしか行なわない。つまりこの関数は、「空配列(messageListの初期値)の後ろに受け取ったメッセージを付け加える」という関数となる。

失敗③ useEffectの第二引数にmessageListを加えた場合

失敗②では、関数が定義され直さないのがいけなかったということで、messageListの値が変更されたときだけ関数を定義しなおしてもらおうということで、useEffectの第二引数にmessageListを入れてみた。

    useEffect(() => {
        socket.on("chat", (msg) => {
            setMessageList([...messageList, msg]);
            console.log("setMessageListを実行:" + messageList);
        });
    }, [messageList]);

結果、失敗①と全く同じ結果になった。。。

成功例

以下の記事を見つけた。

qiita.com

状態更新関数の引数に与えるものは、プリミティブに限りません。関数を与えることで、prev state を参照出来ます。これにより、初期レンダリング時のみに handleClick関数定義を抑止し、正しく動かすことが出来ます。 useState で生成された関数は、同時に生成された状態を参照しない方が良さそう、という話でした。

つまり、setMessageList()に値ではなく関数を与えれば、定義自体は初回レンダリング時にしか行なわれず(useEffectの効果)、さらに引数には最新の値を使ってくれる(関数を与えたことの効果)らしい。

setMssageListの定義を書き換える。

// setMessageList([...messageList, msg]);                             // 変更前
setMessageList((messageList) => [...messageList, msg]);    // 変更後

全体はこのようになる。useEffectの第二引数は、[](空配列)に戻す。

    useEffect(() => {
        socket.on("chat", (msg) => {
            setMessageList((messageList) => [...messageList, msg]);
            console.log("setMessageListを実行:" + messageList);
        });
    }, []);

結果として、画面描画速度が大幅に向上した。

コンソールログ上は、messageListの値が初期値のままになってしまっているが、これはイベント時の処理の定義が初回レンダリング時にしかされていないためである。

わかりにくいので画面描画時にもログを出力してみると、メッセージリストの更新前と更新後の値が出ているのみとなった。問題なさそう。

成功例の結果
成功例の結果

公式ドキュメントの記載

よく見ると公式ドキュメントにも記載があった。

ja.reactjs.org

新しい state が前の state に基づいて計算される場合は、setState に関数を渡すことができます。この関数は前回の state の値を受け取り、更新された値を返します。以下は、setState の両方の形式を用いたカウンタコンポーネントの例です。

おわりに

参考文献が少なかったり古かったりでかなり苦しんだが、チャットアプリの根幹にかかわる部分だったので解決できてよかった。