delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

【Reactの勉強】チュートリアルをやってみる(「フォームの操作とイベントハンドリング」)

概要

前回に引き続き、以下の日本大学文理学部情報科学科の教授の方がクリエイティブ・コモンズで公開してくださっているチュートリアルをやっていく。

zenn.dev

また、適宜、以下の本を参考にさせて頂きながら進める。(以下、『りあクト!』)

oukayuka.booth.pm

今回はデプロイを除けば最後のセクションである「フォームの操作とイベントハンドリング」をやった。

TypeScriptでやろうとすると型をちゃんと定義しないといけなくて結構大変だった。いろいろ調べながらやった。

バージョン

「Reactの勉強」で使用している主なソフトウェアのバージョンは以下の通り。

  • node: 15.5.0
  • react-create-app: 4.0.1
  • react: 17.0.1
  • react-dom: 17.0.1
  • typescript: 4.1.3
  • eslint: 7.17.0
  • prettier: 2.2.1
  • stylelint: 13.8.0

ポイント

以下、TypeScriptでやる場合にポイントだったところを書いていく。

「event」変数に型を定義する

元のコードには以下のような「handleSubmit」というsubmitイベントを受け取って処理をする関数がある。

function handleSubmit(event) {
// 省略
}

TypeScriptの場合は、まずはこの引数の「event」に型を定義する必要がある。

以下の記事を読んで、React.FormEvent<HTMLFormElement>を定義したら解決した。
qiita.com

※ 調べているときにジェネリクスに「HTMLInputElement」を渡すコードも出てきたが、今回のケースはformのsubmitイベントなので「HTMLFormElement」を渡すようである。

コードは以下

const handleSubmit = (event: React.FormEvent<HTMLFormElement>): void => {
// 省略
}

「event.target 」に型を定義する

同じく「handleSubmit」に関して、元のコードでは以下のように1行で「event.target.elements」を変数「breed」に分割代入している。

function handleSubmit(event) {
  // 省略
  const { breed } = event.target.elements;
  // 省略
}

この部分をTypeScriptで書くと、「event.target」は「elements」という属性を持っていないということで、ESLintでエラーになる。

f:id:kamatimaru:20210213050340p:plain

これを解消するためには、以下のように「event.target」を型アサーションで「HTMLFormElement」と型定義した上で、いったん変数に保存する必要があった。(React.FormEventのジェネリクスに渡している型と同じ型になる。)

const eventTarget = event.target as HTMLFormElement;
const { breed } = eventTarget.elements;

このように書くことで、「eventTarget」は「elements」という属性を持っているということをTypeScriptが認識してくれるようになる。

少し文脈は違うが、以下の記事を参考にさせて頂いて解決した。
qiita.com

elementsからフォームの入力値を取り出すときに「namedItem」を使う

同じ部分に関して、元のコードでは以下のようにオブジェクトを分割代入している。

const { breed } = eventTarget.elements;

この部分も、TypeScriptでは「elements」は「breed」という属性を持っていないということで、ESLintでエラーになる。

f:id:kamatimaru:20210213052646p:plain

VS Codeによると「elements」の型は「HTMLFormControlsCollection」とのことである。

developer.mozilla.org

公式ドキュメントを読むと、「HTMLFormControlsCollection」は「namedItem」というメソッドを持っており、これでFormの各項目のnameの文字列を引数に指定すると、対象のElementを取得できるようである。

ただし、以下の2点を対応する必要がある。

  • 「namedItem」のreturnの型は「Element | RadioNodeList | null」になっているので、型アサーションで「HTMLInputElement」を定義する必要がある。
  • 「namedItem」は項目が「elements」に存在しない場合にnullを返すので、代入する変数はnullを許可する必要がある。

上記を全て対応すると以下のようなコードになった。

const breed: HTMLInputElement | null = eventTarget.elements.namedItem(
      'breed',
    ) as HTMLInputElement;
if (breed != null) {
   props.onFormSubmit(breed.value);
}

「handleSubmit」関数におけるポイントは以上。

Formコンポーネントのpropsに渡す関数に型を定義する

次はFormコンポーネントに関して、元のコードでは以下のようにFormコンポーネントを実装している。

function Form(props) {
// 省略
  return (
  // 省略
  );
}

このpropsには、以下のように「reloadImages」という関数が渡ってくる。

const Main: FC = () => {
  // 省略
  const reloadImages = (breed: string) => {
  // 省略
  };

  return (
    <main>
      <!-- 省略 -->
          <Form onFormSubmit={reloadImages} />
        <!-- 省略 -->
    </main>
  );
};

TypeScriptでは、この関数の型も定義しないと、以下のようにESLintでエラーになってしまう。

f:id:kamatimaru:20210213055421p:plain


以下の記事を参考にさせて頂いて、関数の型を作成してpropsに定義したところ、解消できた。
qiita.com

コードは以下

type ReloadImagesFunction = (param: string) => void;

interface FormProps {
  onFormSubmit: ReloadImagesFunction;
}

const Form: FC<FormProps> = (props) => {
  // 省略
};

ハマったポイントは以上

成果物

これで実装に関しては全てのセクションが完了した。
柴犬と秋田犬の画像を選べるようになった。

f:id:kamatimaru:20210213060730p:plain

f:id:kamatimaru:20210213060833p:plain

以上

RabbitMQのチュートリアルをやってみる(その1)

概要

以下のRabbitMQのチュートリアルをやってみることにした。
www.rabbitmq.com

チュートリアルはいくつかの言語で公開されていて、Python版も公開されている。

背景

Python界隈ではCeleryというタスクキューのライブラリが使われているのをよく見かけるので、Celeryを勉強しようと思ってチュートリアルをやろうとしたのだが、
docs.celeryproject.org

CeleryはRabbitMQやAWS SQSなどのメッセージングシステムの使用を前提としている。

そもそも、メッセージングシステムというものをちゃんと理解していない気がしたので、まずはそこから勉強しようと思って、RabbitMQのチュートリアルをやってみることにした。

Intoroductionを読む。

まずは、以下のIntoroductionをざっと読んだ。
www.rabbitmq.com

メッセージブローカーとは、郵便システムにおける「郵便ポスト」であり「郵便局」であり「郵便配達員」であるという例えが分かりやすかった。

You can think about it as a post office: when you put the mail that you want posting in a post box, you can be sure that Mr. or Ms. Mailperson will eventually deliver the mail to your recipient. In this analogy, RabbitMQ is a post box, a post office and a postman.

RabbitMQのDockerコンテナを立てる。

公式がDockerコンテナを提供しているので、それを使用する。

hub.docker.com

コンテナは一つしか使わないが、起動しやすいようにDocker Composeにした。
最終的なdocker-compose.ymlは以下

version: "3"
services:
    rabbitmq_server:
        image: rabbitmq:3-management
        ports: 
            - "5672:5672"
            - "15672:15672"
        environment: 
            RABBITMQ_DEFAULT_USER: admin
            RABBITMQ_DEFAULT_PASS: password

注意点

いくつか知っておかないとハマるポイントがあった。

コンテナの種類が2種類ある。

RabbitMQのDockerコンテナは

  • RabbitMQ単体のもの
  • 管理画面がついているもの

の2種類が提供されている。

管理画面のイメージは以下

f:id:kamatimaru:20210211172636p:plain

コンテナのタグの命名規約が以下のように異なる。

  • RabbitMQ単体のもの: 「3」のように数字のみ
  • 管理画面がついているもの: 「3-management」のように後ろに「-management」がつく

チュートリアルをやるときは、プログラムを実行しながら管理画面でRabbitMQの状態を確認できるので、管理画面がついている方を使った方がいいと思う。

portは2つ指定する

「management」タグのコンテナを使用する場合は、

  • RabbitMQが起動するポート
  • 管理画面のポート

でコンテナ起動時にポートを2種類指定する必要がある。

以下、該当箇所。(5672が本体で15672が管理画面)

ports: 
    - "5672:5672"
    - "15672:15672"
管理画面のログイン情報を設定する(任意)

管理画面にはuser/passwordが設定されている。
デフォルトでは「guest/guest」らしいが、慣れているadmin/passwordに変更する。

以下、該当箇所。

environment: 
    RABBITMQ_DEFAULT_USER: admin
    RABBITMQ_DEFAULT_PASS: password

コンテナを立ち上げることができたので、次回からチュートリアルをやっていく。

isortのblack互換の設定にprofileオプションを使う

概要

Pythonプロジェクトのフォーマッターにisortとblackを使う場合、両者にはフォーマットルールが異なる部分があるので、isort側でblackと競合しないように設定する必要がある。

これまでは、blackの公式ドキュメントに紹介されているisortの設定をコピーして、pyproject.tomlやtox.iniに貼り付けていた。
github.com

ところが、最近、isortの設定にprofileというオプショングループのようなものがあり、black互換用のprofileも提供されていることを知った。

pycqa.github.io

従って、以下のように1行設定するだけで、black互換の各種オプションをコピペするのと同じ効果が得られる。
※ 具体的に設定されるオプション値もドキュメントに記載されている。

profile = black

この書き方の方が保守性が高そうだなと思った。

知ったきっかけ

FastAPIってフォーマッターは何を使っているのかな?と気になって、リポジトリを眺めていたら、pyproject.tomlに

[tool.isort]
profile = "black"

とあったので、このオプションなんだろう?と思って調べてみた。
github.com

MySQL8系ではデフォルトで文字コードがutf8mb4に設定されている

MySQL8系では、文字コードがデフォルトでutf8mb4に設定されていることを知った。

mysql> show variables like '%char%';
+--------------------------+--------------------------------+
| Variable_name            | Value                          |
+--------------------------+--------------------------------+
| character_set_client     | utf8mb4                        |
| character_set_connection | utf8mb4                        |
| character_set_database   | utf8mb4                        |
| character_set_filesystem | binary                         |
| character_set_results    | utf8mb4                        |
| character_set_server     | utf8mb4                        |
| character_set_system     | utf8                           |
| character_sets_dir       | /usr/share/mysql-8.0/charsets/ |
+--------------------------+--------------------------------+
8 rows in set (0.01 sec)

MySQL5系では、デフォルトの文字コードがlatin1だったので、日本語を扱う場合は以下のように、my.cnfの設定で文字コードをUTF8に設定しなければならなかった。

[mysqld]
character-set-server=utf8

[mysql]
default-character-set=utf8

[client]
default-character-set=utf8

[mysqldump]
default-character-set=utf8

なので、公式のMySQL5系のDockerコンテナを拡張した独自のDockerコンテナを作ったりしていた。
github.com

MySQL8系からはこのような作業は不要ということになる。
嬉しい。

自作アプリ(Django製)にGitHub ActionsによるCIを導入してみた

概要

GitHub Actionsを覚えたかったので、以前にDjangoの勉強用に作った自作アプリに導入してみることにした。

github.com

割とスムーズにできた。

やったこと

結論

.github/workflow配下に以下のようにYAMLを配置する。

name: CVE Rooster CI
on: push

jobs:
    unittest:
        name: Check Code Format And Unit Test
        runs-on: ubuntu-latest
        steps:
            - name: Checkout
              uses: actions/checkout@v2
            - name: Set Up Python3
              uses: actions/setup-python@v2
              with:
                python-version: '3.7'
                architecture: x64
            - name: Install Python Packages
              run: pip3 install -r requirements.txt
            - name: Start Docker Compose
              run: docker-compose up -d
            - name: Check Code Format
              run: tox -e flake8
            - name: Unit Test
              run: tox -e testapp

Djangoユニットテストはtoxの中で実行している。

説明

いくつかポイントだと思った箇所を説明する。

サーバー上にGitリポジトリをcloneする

以下の2行が該当する。
※ 「name」はやることに名前をつけているだけなので、何でもよいし、なくてもよい。

- name: Checkout
  uses: actions/checkout@v2

前提として、Github Actionsでは、Github Actionsが実行されるサーバー上にGitリポジトリをcloneしたり、プログラミング言語をインストールする場合には以下の公式が提供しているリポジトリを使うことができる。

github.com

サーバー上にGitリポジトリをcloneする場合は、actions/checkoutというリポジトリを使用する。

この行では、

ということをやってくれている。

@の後ろの部分に関しては、バージョンを必ず指定しなければならない。
現在の最新版は2系なので、バージョン2を指定している。

※ プロダクションコードで運用する場合は、マイナーバージョンまで固定した方が良いのだろうが、今回は対象が自作のおもちゃアプリなのでメジャーバージョンのみ指定している。

Pythonをセットアップする

以下の部分がPythonをセットアップしている部分である。

- name: Set Up Python3
  uses: actions/setup-python@v2
  with:
    python-version: '3.7'
    architecture: x64

こちらも、公式がPythonセットアップ用のリポジトリを提供してくれているので、それを使用する。

github.com

使い方は公式ドキュメントに記載されている。

setup-python@v2は「setup-python」のバージョンであって、Pythonのバージョンが2系という意味ではない。

Docker Composeを起動する。

Docker Composeはサーバー上にコマンドが既にインストールされているので、以下のように、実行したいコマンドを書くだけでよい。

- name: Start Docker Compose
  run: docker-compose up -d

ポイントは以上

CIがpassするまでPRをマージできないようにする

CIを導入したので、CIがpassするまではGitHub上でPRをマージするボタンを押せないようにしたい。
※ マージ先がmain(master)ブランチの前提

これは、GitHubの画面上で設定することができる。

①以下の通り遷移する。
Settings > Branches > Add rule

②以下のように入力してruleを作成する。
f:id:kamatimaru:20210206230612p:plain

これだけでOK。

参考にさせていただいた本

以下の本がとても分かりやすかった。

booth.pm

JSとPythonでは空のlist(Array)の真偽値が異なる

まとめ

経緯

JSで以下のようなコードを書いたら、arrが空のArrayの場合でもif文の中に入ってこないことに気づいた。

# arrはArray
if (!arr) {
  console.log("arr is empty.");
}

Pythonでは空のlistはFalseになるので、検証してみたところ、やはりJSはTrueだった。

・JS

$ node
Welcome to Node.js v15.5.0.
Type ".help" for more information.
> Boolean([])
true

Python

$ python3
Python 3.7.3 (default, Apr  7 2020, 14:06:47)
[Clang 11.0.3 (clang-1103.0.32.59)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> bool([])
False

従って、JSの場合は以下のように書かないといけない。(arrがnullを許可していない前提)

# arrはArray
if (arr.length === 0) {
    console.log("arr is empty.");
}

言われてみれば、昔JQueryとか書いてたときに、Arrayのemptyチェックは上のように書いていた気がする。

ここ1年くらいPythonばっかりやってたから、感覚がPythonで上書きされていたのかもしれない。

言語によって真偽値の仕様って微妙に異なるんだなと勉強になった。

【Reactの勉強】チュートリアルをやってみる(「サーバーからのデータ取得」・「状態の変更」)

概要

前回に引き続き、以下の日本大学文理学部情報科学科の教授の方がクリエイティブ・コモンズで公開してくださっているチュートリアルをやっていく。

zenn.dev

また、適宜、以下の本を参考にさせて頂きながら進める。(以下、『りあクト!』)

oukayuka.booth.pm

今回は前回に引き続き「サーバーからのデータ取得」と「状態の変更」のセクションをやる。

Promiseで外部のAPIからデータを取得するところをTypeScriptではどう書けばよいのかでけっこう苦しんだ。

ここはもっといい書き方があるはずなので、後で戻ってきたい。

バージョン

「Reactの勉強」で使用している主なソフトウェアのバージョンは以下の通り。

  • node: 15.5.0
  • react-create-app: 4.0.1
  • react: 17.0.1
  • react-dom: 17.0.1
  • typescript: 4.1.3
  • eslint: 7.17.0
  • prettier: 2.2.1
  • stylelint: 13.8.0

書いたコード(暫定の結論)

いったん以下のように書いてみたところ、とりあえずアプリも動いており、型も定義できている。

ただ、「型アサーション」を使っているところが無理やり感があって、あまりいい書き方ではないのではないかという気がしている。

[api.ts]

type DogAPIRandomResponse = {
  message: string[];
};

const fetchImages = async (breed: string): Promise<string[]> => {
  const response = await fetch(
    `https://dog.ceo/api/breed/${breed}/images/random/12`,
  );

  const data: Promise<DogAPIRandomResponse> = (await response.json()) as Promise<DogAPIRandomResponse>;

  return (await data).message;
};
export default fetchImages;

[App.tsx] ※ 必要な箇所のみ抜粋

const Main: FC = () => {
  const [urls, setUrls] = useState<string[]>([]);
  useEffect(() => {
    void fetchImages('shiba').then((dogImageUrls) => {
      setUrls(dogImageUrls);
    });
  }, []);

  return (
    <main>
      <section className="section">
        <div className="container">
          <Gallery urls={urls} />
        </div>
      </section>
    </main>
  );
};

「サーバーからのデータ取得」の詳細

「サーバーからのデータ取得」のセクションをやった上でつまづいたことを書いていく。
やることとしては、チュートリアルのコードを元にして、TypeScriptを使っているので、型を定義するコードに書き替えていく。

「fetchImages」関数の戻り値の型を定義する。

まずは「fetchImages」関数の戻り値の型を定義する。

「fetchImages」関数は犬の画像のurlのリストを返すので、string[]を定義する。

加えて、「fetchImages」関数はPromiseなので、Promise型で定義する必要がある。
この時点でのコードは以下のようになる。

const fetchImages = async (breed: string): Promise<string[]> => {
  const response = await fetch(
    `https://dog.ceo/api/breed/${breed}/images/random/12`,
  );

  const data = await response.json();

  return data.message;
};

ESLintのエラーを確認する。

関数の戻り値の型は定義したが、以下のコードでそれぞれESLintのエラーが出てしまう。

const data = await response.json();

return data.message;

ESLintのエラーは以下。

f:id:kamatimaru:20210128234639p:plain

f:id:kamatimaru:20210128234651p:plain

エラーメッセージによると以下の2つが原因のようである。

  • await response.json();の戻り値の型はPromise<any>である。
  • 「data」変数はTypeScriptにはany型で認識されているので、「message」というフィールドを持っていることが認識されない。

従って、これらを解消する必要がある。

APIのレスポンスの型を定義する。

原因の一つは、APIのレスポンスの型が定義されていないことなので、api.tsに以下のようにAPIのレスポンスの型を定義する。

type DogAPIRandomResponse = {
  message: string[];
};

※ 実際に返ってくるJSONスキーマは以下のURLをブラウザで開くと分かる。
https://dog.ceo/api/breed/shiba/images/random/12

「data」変数に型を定義する。

上で作成したAPIのレスポンスの型を「data」変数に定義する。
ここで、以下のように単に型を定義するだけだと、ESLintでエラーになってしまう。

const data: Promise<DogAPIRandomResponse> = (await response.json());

ESLintのエラーは以下。

f:id:kamatimaru:20210129004430p:plain

どうすればよいのだろうと思ったが、以下のように『りあクト!』の4章で紹介されている「型アサーション」という手法を使うとESLintのエラーが解消した。

const data: Promise<DogAPIRandomResponse> = (await response.json()) as Promise<DogAPIRandomResponse>;

ただ、本書によると「型アサーション」は最終手段とのことなので、もっとよい方法がないか引き続き調べたいと思う。

「data.message」をreturnする。

上記の対応をしても、まだ以下の行でESLintのエラーが出続けている。

return data.message;

ESLintのエラーは以下。

f:id:kamatimaru:20210129005222p:plain

これに関しては、VS CodeがQuick Fixを提案してくれた。

f:id:kamatimaru:20210129005538p:plain

任せて「Add 'await'」を選択すると、以下のようにコードを修正してくれて、ESLintのエラーも消えた。

return (await data).message;

await dataを括弧で括るのがポイントのようである。
await data.message;と書くとダメである。恐らくawaitが「message」にかかってしまうからだと思う。

これで、ESLintのエラーは全て消えたし、型も定義することができた。
「サーバーからのデータ取得」のセクションに関しては以上。

「状態の変更」の詳細

「状態の変更」セクションでは、上で作成した「fetchImages」関数をApp.tsx側で呼び出して、画像を表示する。
以下に書いたように、何箇所か元のチュートリアルのコードから修正したが、このセクションはそんなに苦労しなかった。

「useState」・「useEffect」が出てきた。

このセクションのコードには「useState」・「useEffect」という関数が登場する。

これらはReactにおいて「Hooks」という重要な機能らしく、『りあクト!』でも9章で1章を割いて説明されている。

ただ、本書の解説も読みながら進めたら、何となくどんな機能なのかは理解できた。

「useState」に型を定義する。

元のコードとの違いとして、TypeScriptでは「useState」に型を渡せるので、以下のように「string[]」と定義する。

const [urls, setUrls] = useState<string[]>(null);

すると、元のコードでは初期値にnullを代入しているが、「string[]」型にはnullを代入できないので、以下のようにESLintでエラーが出る。

f:id:kamatimaru:20210129011708p:plain

ここでの対応として、以下の2つがある。

  1. 空のlistを初期値として渡すように変更する。
  2. nullを代入できるようにする。

2の場合は、『りあクト!』でも紹介されているように、以下のように型をstring[] | null書くと実現できる。

const [urls, setUrls] = useState<string[] | null>(null);

ただ、nullを許容しない方がベターかなと思って、今回は1の方針で対応した。
その場合は、以下のように初期値をnullから空のlistに変更すればよい。

const [urls, setUrls] = useState<string[]>([]);

変数名が重複しているので変える。

元のコードでは、以下のようにuseStateの戻り値を格納する変数と、fetchImagesのコールバック関数の引数の変数の両方で「urls」という変数名を使用している。

const [urls, setUrls] = useState(null);
useEffect(() => {
     fetchImages("shiba").then((urls) => {
       setUrls(urls);
     });
}, []);

コールバック関数の引数の方は仮引数なので、挙動上は問題ないはずだが、ESLintではエラーになってしまう。

f:id:kamatimaru:20210129012805p:plain

従って、以下のように片方の変数名を「dogImageUrls」に変える。

useEffect(() => {
    void fetchImages('shiba').then((dogImageUrls) => {
      setUrls(dogImageUrls);
    });
  }, []);

これでESLintのエラーは全て消えた。

「Loading」を表示する条件を修正する。

「条件分岐」のセクションで以下のように、結果を取得中の状態の場合は、Loadingコンポーネントを表示するようにしていた。

const Gallery: FC<GalleryProps> = (props) => {
  const { urls } = props;
  if (urls.length == null) {
    return <Loading />;
  }

今回、urlsの初期値を空のlistに変更したので、以下のようにlistが空の場合にLoadingコンポーネントを表示するように修正する。

if (urls.length.length === 0) {
  return <Loading />;
}

「状態の変更」のセクションに関しては以上。


成果物

セクションがここまで進むと、毎回APIからランダムに柴犬の画像のURLを取得するようになるので、リロードする度に異なる画像が表示されるようになる。

f:id:kamatimaru:20210129015132p:plain

f:id:kamatimaru:20210129015943p:plain

以上