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

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

【React+Node.js/socket.io】チャットアプリに入室機能とユーザ名表示機能を追加する

はじめに

前回、ReactとNode.js Expressを使って、チャットアプリの超基礎となる部分を作成した。

tomiko0404.hatenablog.com

今回は、機能を追加し、さらにチャットアプリに近づける。

youtu.be

追加する機能

発言者名と発言時刻を表示する機能

発言者名と発言の時刻を取得して表示する機能。 発言者名は、入室機能によってブラウザごとに設定する。

発言者名、発言時刻表示機能イメージ
発言者名、発言時刻表示機能

入室機能(簡易)

チャット画面に入る前に、ユーザ名を設定する機能。

入室機能イメージ
入室機能

見た目の改善・コンポーネント

マージンを設定したり、ボタンのデザインを作成。

前提

前回の記事の続きとなります。

tomiko0404.hatenablog.com

最終的なソースコード

ソースコードはこちら。 github.com

修正内容の解説

発言時刻を表示する機能

メッセージ送信機能への変更

前回のApp.tsxのうち、画面サーバ(クライアント側)からWebサーバに送信する内容を増やす。

    // 「送る」ボタンを押したときの処理。サーバにmessageを送信する。
    //  messageContentオブジェクト(中身は名前/メッセージ/発言時刻)
    const onClickSend = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
        e.preventDefault();
        const date = new Date();
        const messageContent: MessageContent = {
            name: name,
            message: message,
            postat: date.toLocaleString("ja"),
        };

        socket.emit("chat message", messageContent);
        setMessage("");
    };

時刻を取得するのはJavasciptのDateクラス

const date = new Date();

で実施している。

Webサーバ側の変更

Webサーバ側(index.js)は、オブジェクトを受けとってブロードキャストする動作は変わらないので、特に変更なし。

メッセージ受信、表示機能への変更

あとは画面サーバ側で、ブロードキャストされた値をそれぞれ表示してあげるだけで良い。

    useEffect(() => {
        socket.on("chat", (messageContent) => {
            setMessageList((messageList) => [...messageList, messageContent]);
        });
    }, []);

の部分でサーバから送信されたmessageContentに必要な値が格納されている。

入室機能(簡易)

入室画面とメインチャット画面を作成する

これまで作成してきたメインのチャット画面に加え、入室画面を作成する。

これまで

  • メインチャット画面:/ (例えば開発環境ではhttp:localhost:8000

変更後

  • ログイン画面:/(例えば開発環境ではhttp:localhost:8000
  • メインチャット画面 /main (例えば開発環境ではhttp:localhost:8000/main
ルーティング

ルーティングはReact-router-domを利用した。

tomiko0404.hatenablog.com tomiko0404.hatenablog.com

App.tsx

import "./App.css";

import { ChakraProvider } from "@chakra-ui/react";
import { BrowserRouter } from "react-router-dom";
import { Router } from "./router/Router";

function App() {
    return (
        <ChakraProvider>
            <>
                <BrowserRouter>
                    <Router />
                </BrowserRouter>
            </>
        </ChakraProvider>
    );
}

export default App;

/router/Router.tsx

import { Route, Switch } from "react-router";
import { LoginPage } from "../components/pages/LoginPage";
import { MainChat } from "../components/pages/MainChat";
import { Page404 } from "../components/pages/Page404";


export const Router = () => {
    return (
        <Switch>
            <Route exact path="/" render={() => <LoginPage />} />
            <Route exact path="/main" render={() => <MainChat />} />
        </Switch>
    );
};

入室画面は/pages/LoginPage.tsxに、メイン画面は/pages/MainChat.tsxに切り出す。

入室画面

入室画面では、stateとしてnameを利用する。

<input>に入力されているvaluenameに常にnameに設定する。「入室」ボタンがクリックされたら、onClickJoin関数により/mainのURLに遷移する。その際、stateとして{name: name}が渡される。

tomiko0404.hatenablog.com

/pages/LoginPage.tsx

import { useState } from "react";
import { useHistory } from "react-router";
import { NameInputArea } from "../organisms/NameInputArea";

export const LoginPage = () => {
    // 名前
    const [name, setName] = useState("");
    const history = useHistory();

    // 名前エリアの文字が変更されたときの処理
    const onChangeName = (e: React.ChangeEvent<HTMLInputElement>) => {
        setName(e.target.value);
    };

    // 名前が登録されたときの処理
    const onClickJoin = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
        history.push("/main", { name: name });
    };

    return (
        <>
            <NameInputArea onChange={onChangeName} onClick={onClickJoin} value={name} />
        </>
    );
};

/components/organisms/NameInputArea.tsx

import { VFC } from "react";
import { InputArea } from "../molecules/InputArea";

type Props = {
    value: string; // インプットに表示されているテキスト
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
    onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
};
export const NameInputArea: VFC<Props> = (props) => {
    const { value, onChange, onClick } = props;
    return (
        <InputArea
            value={value}
            onChange={onChange}
            onClick={onClick}
            placeholder="名前を入力してください"
            buttonText="入室"
        />
    );
};

/components/molecules/InputArea.tsx

import { Input } from "@chakra-ui/input";
import { Flex } from "@chakra-ui/layout";
import { VFC } from "react";
import { PrimaryButton } from "../button/PrimaryButton";

type Props = {
    value: string;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
    placeholder?: string;
    onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
    buttonText: string;
};

export const InputArea: VFC<Props> = (props) => {
    const { value, onChange, placeholder, onClick, buttonText } = props;
    return (
        <Flex align="center" justifyContent="center">
            <Input
                placeholder={placeholder}
                value={value}
                onChange={onChange}
            />
            <PrimaryButton onClick={onClick} disabled={!value}>
                {buttonText}
            </PrimaryButton>
        </Flex>
    );
};
メインチャット画面

前回のApp.tsx/pages/MainChat.tsxに切り出す。

また、入室画面から遷移してきたときのstateオブジェクトを受け取る必要があるので、以下を追加する。

App.tsx

import { useLocation } from "react-router";

type LoginState = {
    name: string;
};

export const MainChat = () => {
    const { state } = useLocation<LoginState>();
    const { name } = state;
    .... 省略 ...
}

見た目の改善

ChakraUIの機能を使って諸々の見た目を改善。 また、Atomicデザインを意識してある程度コンポーネント化した。

ChakraUIの利用にはコツがいるので、以下サイト

Chakra UIの歩き方 & Tips集

等を参考にした。

テキスト入力&送信部を中央添えにする部分は苦労したが、justifyContentを使いこなす必要がありそう。

        <Flex align="center" justifyContent="center">
            <Input />
            <PrimaryButton></PrimaryButton>
        </Flex>

おわりに

少しチャットらしいアプリにすることができた。 誰でも入室できてしまうので、socket.io の room機能など使いこなす必要がありそう。

【CSS】箇条書きのマーカーをレベルによって変える方法

はじめに

箇条書きを入れ子にした際、マーカーを入れ子構造がわかりやすいように変更する方法のメモ。

見本
入れ子構造の時にマーカーを変更する

CSSの記載方法

/* 1段目 */
.entry-content ul {
    list-style-type: disc;
}
/* 2段目 */
.entry-content ul > li > ul {
    list-style-type: circle;
}
/* 3段目 */
.entry-content ul > li > ul > li > ul {
    list-style-type:square ;
}
/* 4段目 */
.entry-content ul > li > ul > li > ul > li > ul {
    list-style-type:"\1F337" ;
}

list-style-typeオプションについて

オプションで指定する方法

  • disc(黒丸)
  • circle(白丸)
  • square(四角)

等から選ぶことができる。

list-style-type - CSS: カスケーディングスタイルシート | MDN

文字コードでマーカーを指定する方法

4段目は、マーカーを文字コードで指定している。 文字コードは以下のサイトを参考にした。

gray-code.com

上記サイトの16進数の列に&#x1F337;とある絵文字を使うときは、

list-style-type:"\1F337" ;

と指定する。

参考:HTMLの中身

<ul>
    <li>1段目 
        <ul>
            <li>2段目
                <ul>
                    <li>3段目
                        <ul>
                            <li>4段目</li>
                        </ul>
                    </li>
               </ul>
            </li>
        </ul>
    </li>
</ul>

Typescriptの特殊な型定義のメモ

はじめに

Javascript、Reactを勉強するにあたり、型定義が必要なTypescriptを利用しているが、何を定義したらよいか難しい場合があるので、調べた内容を記載する。

型定義のメモ

children: ReactNode;

childrenはReactNode型を指定する。

利用例

import {ReactNode, VFC } from "react";

type Props = {
    children: ReactNode;
};

export const MyButton: VFC<Props> = (props) => {
    const { children} = props;
    return (
        // 省略
    );
};

onClick関数

onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;

onChange関数

 onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;

自分で型を定義する場合

typesフォルダを作成し、その中に大文字始まりの型名をファイル名としてファイル作成する。拡張子は.tsとする。

src
├── App.tsx
├── components
├── index.css
├── index.tsx
└── types
    └── MessageContent.ts

MessageContent.ts

export type MessageContent = {
    name: string;
    message : string;
    postat : string;
}

実装の例

onClickとonChangeの例

メッセージを入力し、「送る」ボタンを押したら送信するフォーム。

App.tsx

import { useState } from "react";
import { InputArea } from "./molecules/InputArea";
import "./styles.css";

export default function App() {
  // インプットエリアに入力するメッセージ
  const [message, setMessage] = useState("");

  // インプットエリアの文字が変更されたときの処理
  const onChangeMessage = (e: React.ChangeEvent<HTMLInputElement>) => {
    setMessage(e.target.value);
  };

  // 「送る」ボタンを押したときの処理。サーバにmessageを送信する。
  const onClickSend = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
    // メッセージを送る処理
    console.log(`messageを送る処理:${message}`);
    setMessage("");
  };

  return (
    <>
      <InputArea
        text={message}
        onChange={onChangeMessage}
        onClick={onClickSend}
      />
    </>
  );
}

/molecules/InputArea.tsx

import { VFC } from "react";

type Props = {
  text: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
};

export const InputArea: VFC<Props> = (props) => {
  const { text, onChange, onClick } = props;
  return (
    <>
      <input
        value={text}
        onChange={onChange}
        placeholder="メッセージを入力してください"
      />
      <button onClick={onClick}>送る</button>
    </>
  );
};

おわりに

今のところ出てきたのはこれくらいだが、 JavaScript特有の型が様々あるので、調べるごとにメモする予定。

react-router-domを使ったルーティング方法メモ

はじめに

react-router-domを利用して基本のページ遷移を実施する。

react-router-domは2021/11/3にバージョン6がリリースされたようだが、情報が少なくて独学が難しいのでバージョン5から始める。

react-router-dom@バージョン5

インストール

以下をインストールする。

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

超基本の形

基本的な使い方は以下。

期待する動作

  • localhost:8000にアクセスするとHomeページが表示される。
  • localhost:8000/mypageにアクセスするとMyPageが表示される。
  • リンクは常に表示される。

ディレクトリ構成

my-router
├── App.tsx
├── index.css
├── index.tsx
├── pages
│   ├── Home.tsx
│   └── MyPage.tsx
└── router
     └── Router.tsx

ソースコード

App.tsx

import { BrowserRouter, Link } from "react-router-dom";
import { Router } from "./router/Router";

function App() {
    return (
        <BrowserRouter>
            {/*  常に表示する部分 */}
            <Link to="/">Link to Home</Link>
            <br />
            <Link to="/mypage">Link to MyPage</Link>

            {/*  ルーティングする部分 */}
            <Router />
        </BrowserRouter>
    );
}

export default App;

Router.tsx

import { Switch, Route } from "react-router-dom";
import { Home } from "../pages/Home";
import { MyPage } from "../pages/MyPage";

export const Router = () => {
    return (
        <Switch>
            <Route exact path="/" render={() => <Home />} />
            <Route path="/mypage" render={() => <MyPage />} />
        </Switch>
    );
};

BrowserRouter

react-router-domを利用する合図のようなもの。全体をこれで囲む必要がある。

<a>タグの役割をするもの。

Swtich

ルーティング部分(URLごとに表示するコンポーネントを指定する部分)を囲む。

ネストする場合

期待する動作

  • localhost:8000にアクセスするとHomeページが表示される。
  • localhost:8000/mypageにアクセスするとMyPageが表示される。
  • localhost:8000/mypage/mapage-detailにアクセスするとMyPageDetailが表示される。
  • リンクは常に表示される。

ディレクトリ構成

pagesディレクトリに、MyPageの詳細ページを追加する。

├── pages
│   ├── Home.tsx
│   ├── MyPage.tsx
│   └── MyPageDetail.tsx

ソースコード

先ほどのApp.tsxにおいて

<Route path="/mypage" render={() => <MyPage />} />

の部分を、以下のように書き換える。

<Route
    path="/mypage"
    render={(props) => {
        const url = props.match.url;
        console.log(props)
        return (
            <Switch>
                <Route exact path={url} render={() => <MyPage />} />
                <Route path={`${url}/mypage-detail`} render={() => <MyPageDetail />} />
            </Switch>
        );
    }}
/>

注意点として、Router側をネストしているからといって<Link>には関係ないので、<Link>の中身はto = "/mypage/mypage-detail"と記載しなくてはならない。to="/mypage-detail"とするのはNG。

Mypage.tsx※MyPage内に、MyPageDetailへのリンクを表示

import { Link } from "react-router-dom";

export const MyPage = () => {
    return (
        <div>
            <h1>Mypage</h1>
            <Link to="/mypage/mypage-detail">link to MyPageDetail</Link>
        </div>
    );
};

url

renderに設定する無名関数にはpropsが渡されている。 ここで、props.match.urlにはRouteの引数に取ったpath="/mypage"の値が入っている。

propsの中身は、localhost:8000/mypageにアクセスしたときは以下で、

mypageのRouteのprops
mypageのRouteのprops

localhost:8000/mypage/mypage-detailにアクセスすると以下のようになっている。

"mypage/mypage-detailのRouteのprops"
mypage/mypage-detailのRouteのprops

locationは「今アクセスしているURL」に関わる情報が入っていそう。一方でmatchは、事前に<Route>で指定した値や、isExact(今アクセスしているURLと、事前に<Route>で指定したpathが等しいかどうか)等の比較に関わる値が入っていそう。

また、ネストが増えていくとコードが見にくくなっていくので、文のパラメータを別途配列で定義し、<Switch>文の中身をmap文で回すことも推奨されている。(参考文献参照)

<Link>タグではなくuseHistoryを使う方法

<a>タグではなく、ボタンをクリックした際などのイベントにページ遷移を紐づけたい場合は、useHistory()関数を利用する。

やり方は別記事に記載した。

tomiko0404.hatenablog.com

参考文献

www.udemy.com

react-router-dom@バージョン6

バージョン6のやり方を覚え次第追記予定。

おわりに

react-router-domを利用してルーティングを設定し、<Link>から遷移する基本のやり方をメモした。

関連記事

ページ遷移先に値を渡したい場合にやること

tomiko0404.hatenablog.com

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