【React+Node.js/socket.io】チャットアプリに入室機能とユーザ名表示機能を追加する
はじめに
前回、ReactとNode.js Expressを使って、チャットアプリの超基礎となる部分を作成した。
今回は、機能を追加し、さらにチャットアプリに近づける。
追加する機能
発言者名と発言時刻を表示する機能
発言者名と発言の時刻を取得して表示する機能。 発言者名は、入室機能によってブラウザごとに設定する。
入室機能(簡易)
チャット画面に入る前に、ユーザ名を設定する機能。
見た目の改善・コンポーネント化
マージンを設定したり、ボタンのデザインを作成。
前提
前回の記事の続きとなります。
最終的なソースコード
ソースコードはこちら。 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>
に入力されているvalue
をname
に常にname
に設定する。「入室」ボタンがクリックされたら、onClickJoin
関数により/main
のURLに遷移する。その際、stateとして{name: name}
が渡される。
/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の利用にはコツがいるので、以下サイト
等を参考にした。
テキスト入力&送信部を中央添えにする部分は苦労したが、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段目は、マーカーを文字コードで指定している。 文字コードは以下のサイトを参考にした。
上記サイトの16進数の列に🌷
とある絵文字を使うときは、
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を利用する合図のようなもの。全体をこれで囲む必要がある。
Link
<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
にアクセスしたときは以下で、
localhost:8000/mypage/mypage-detail
にアクセスすると以下のようになっている。
location
は「今アクセスしているURL」に関わる情報が入っていそう。一方でmatch
は、事前に<Route>
で指定した値や、isExact
(今アクセスしているURLと、事前に<Route>
で指定したpathが等しいかどうか)等の比較に関わる値が入っていそう。
また、ネストが増えていくとコードが見にくくなっていくので、<Switch>
文の中身をmap
文で回すことも推奨されている。(参考文献参照)
<Link>
タグではなくuseHistoryを使う方法
<a>
タグではなく、ボタンをクリックした際などのイベントにページ遷移を紐づけたい場合は、useHistory()
関数を利用する。
やり方は別記事に記載した。
参考文献
react-router-dom@バージョン6
バージョン6のやり方を覚え次第追記予定。
おわりに
react-router-domを利用してルーティングを設定し、<Link>
から遷移する基本のやり方をメモした。
関連記事
ページ遷移先に値を渡したい場合にやること
yarnで古いバージョンのパッケージをインストールする方法メモ
記事の移行について
本記事の詳細は、こちらに移行しました。
はじめに
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やMUI)を利用
- その他の方法
- 表示内容
- おわりに
- 参考文献
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を利用する。
最も有名なのは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というモジュールを利用する方法。
表示内容
おわりに
方法3と4をうまく使えれば良さそう。