20211009のReactに関する記事は12件です。

【備忘録】Docker + Rails6 + React + TypeScript で環境構築する

自分のアウトプット用のメモです。 Docker 上に Rails プロジェクトを作成する(MySQLは5.7) $ mkdir -p ~/project/myapp $ cd ~/project/myapp $ mkdir -p containers/nginx $ mkdir environments Gemfile作成 $ cd ~/project/myapp $ touch Gemfile.lock $ vim Gemfile # Gemfile source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails" Dockerfile作成 $ vim Dockerfile FROM ruby:2.7.2 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \ && apt-get update \ && echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update -qq && apt-get install -y build-essential \ libpq-dev \ nodejs \ yarn RUN mkdir /myapp ENV APP_ROOT /myapp WORKDIR $APP_ROOT ADD ./Gemfile $APP_ROOT/Gemfile ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock RUN bundle install ADD . $APP_ROOT docker-composeファイル作成 $ cd ~/project/myapp $ vim docker-compose.yml version: "3" services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: demo-db ports: - "3306:3306" volumes: - demo-db:/var/lib/mysql web: build: . command: rails s -p 3001 -b '0.0.0.0' volumes: - .:/myapp ports: - "3001:3001" links: - db volumes: demo-db: docker-compose run コマンドで Rails プロジェクトの作成。 $ cd ~/project/myapp $ docker-compose run web rails new . --force --no-deps --database=mysql Rails プロジェクトを作成できたら、build する。 $ docker-compose build データベースの作成 config/database.yml を編集する。 $ vim config/database.yml config/database.yml ## 編集した箇所のみ抜粋 default: &default password: password # docker-compose.yml の MYSQL_ROOT_PASSWORD: で設定したパスワード host: db # localhost → db に変更 データベースを作成する。 $ docker-compose run web rake db:create Docker の起動 $ docker-compose up http://0.0.0.0:3001 にアクセスすると、Rails の初期画面が表示される。 この時 shell に以下のようなエラーが出るときは、 shell Cannot render console from 172.20.0.1! Allowed networks: 127.0.0.0/127.255.255.255, ::1 config/environments/development.rb に config/environments/development.rb Rails.application.configure do config.web_console.permissions = '0.0.0.0/0' end を追加すればエラーログが消える。 React の導入 react-rail を使って react を rails プロジェクトに導入する。 Gemfile gem 'react-rails' 再度 build を実行する。 shell $ docker-compose run web rails webpacker:install # OR (on rails version < 5.0) rake webpacker:install $ docker-compose run web rails webpacker:install:react # OR (on rails version < 5.0) rake webpacker:install:react $ docker-compose run web rails generate react:install 実際に React が導入されているか確認する。 controller の作成。 shell $ docker-compose run web rails g controller sample index 以下のファイルが作成されていれば OK。 app/controllers/sample_controller.rb class SampleController < ApplicationController def index end end react コンポーネントファイルの作成。 shell $ docker-compose run web rails g react:component Sample app/javascript/components/Sample.tsx が作成されるので、以下のように修正する。 app/javascript/components/Sample.jsx import React from "react"; const Sample = () => { return <>this is react sample.</>; }; export default Sample; view の作成。 app/views/sample/index.html.erb を作成し react コンポーネントを読み込む。 app/views/sample/index.html.erb <%= react_component 'Sample' %> ルーティングを追加する。 config/routes.rb Rails.application.routes.draw do get "sample", to: "sample#index" end Docker を起動し、http://0.0.0.0:3001/sample にアクセスする。「this is react sample.」の文字が表示されていれば OK。 TypeScript の導入 React が導入できたので、以下コマンドで TypeScript 導入する。 shell $ docker-compose run web bundle exec rails webpacker:install:typescript $ docker-compose run web yarn add @types/react @types/react-dom 無事インスールが終えたら sample.jsx の拡張子を sample.tsx に修正する。 app/javascript/components/Sample.tsx import React from "react"; const Sample: React.FC = () => { return <>this is typescript sample.</>; }; export default Sample; ここまで修正したら再度 http://0.0.0.0:3001/sample にアクセスし、「this is typescript sample.」の文字が表示されていることを確認する。 JS の修正時にホットリロードさせる Docker の場合は $ bin/webpack-dev-server のコマンドを直接叩くとエラーがでるっぽいので docker-compose.yml を修正した。 docker-compose.yml version: "3" services: db: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: rails-front-demo-db ports: - "3306:3306" volumes: - rails-db:/var/lib/mysql web: &web build: . command: rails s -p 3001 -b '0.0.0.0' environment: WEBPACKER_DEV_SERVER_HOST: webpacker volumes: - .:/app_name ports: - "3001:3001" links: - db webpacker: <<: *web command: bundle exec bin/webpack-dev-server depends_on: [] environment: WEBPACKER_DEV_SERVER_HMR: "true" WEBPACKER_DEV_SERVER_HOST: 0.0.0.0 ports: - "3035:3035" volumes: rails-db: webpacker の service を追加。それに伴い web の service も修正。 $ docker-compose up で Docker を起動し、http://0.0.0.0:3001/sample にアクセスする。app/javascript/components/Sample.tsx のファイルを適当に書き換えて保存するとホットリロードすることがわかる。 以上です! こちらの方の記事をほとんどそのまま移させていただきました! 大変わかりやすかったです!!ありがとうございますm(_ _)m
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSを使ってReact・Django(DRF)のウェブアプリをデプロイしたい

はじめに DVA 取得と、デプロイについての知識とそれを含めた開発スキルのレベルアップのために色々とやっているのでそれの途中経過をアウトプット。 前提として以下の記事の話があります。 React と DRF の SPA で JWT を Cookieで管理してみた話 もくもく会アウトプット:Django で JWT を Cookie で処理するやつを理解したい やったこと フロント側(React)を CodePipeline・CodeBuild・Route53・CroudFront・S3 の構成でデプロイ サーバー側(DRF)を EC2 インスタンス 1 台を使ってデプロイ 上記を Https でやり取りするようにして連携 ひとまず今回は Code シリーズを使った S3 静的 Web ホスティングでの React デプロイと同時にクロスオリジンでの SPA デプロイの体験を行うというのがテーマなので最小限の構成です。 サーバー側も Code シリーズや ElasticBeanStalk などを使ってやろうとしたのですが、フロントと比べると数倍は難しかったのでまずは 1 番基本的なデプロイを試してみて何をする必要があるのかを把握することに重きを置きました。 以下今回のサービス構成図です。 前述の通り、サーバー側は DB も証明書も EC2 インスタンス内においてあります。 当然あまりよろしくはないので、今後リファクタリングを進めたり実際に一定期間デプロイすることを考えるとプライベートサブネットを作って踏み台 Web サーバーを作り、インスタンスはプライベートサブネットに移して、AutoScaling グループにしてさらにそれを ALB で管理することになると思いますし、それに従って証明書は ALB で管理し、DB は RDS 等を用いることになると思います。 ただそうなると個人でやるには割とコストかかるなぁ……と感じますね、やはりインフラは重い…… 要点 ネットをうまく探せば大まかなところは見つかるので、見つけられなかったところとか分かりづらいところを載せておきます。 自動化ツールを使う場合、.env は基本的にはリソースに含まれないはずなので AWS System Manager の Parameter Store や CodeBuild の環境変数設定画面を用いて環境変数を設定する 上記で設定した環境変数を buildspec.yml などを使って、ビルドする際に.env として書き出す SPA、つまるところクロスオリジンにする場合、ローカルと違ってフロントからサーバーへの Request も Https で行わないと blocked mixed-content エラーになる よって独自ドメインはフロントとサーバーの 2 つで必要となり、サーバー側の独自ドメインは ElasticIP とで名前解決の紐付けを行う サーバーが Django の場合、SameSite 周りの設定が settigs.py 経由だとうまく行かない場合がある 同様に、if settings.DEBUGが本番環境だと動作しないことがある よって、開発環境でif settings.DEBUGで SameSite の設定を変えていた Middleware をsamesite='None'及びsecure=Trueで固定するように変更する 上記の点さえ把握してれば、あとはネットの海を漁れば以降のことは見なくてもできると思います。 今回は React、Django(DRF)でやってますが多分どの言語でもやることはだいたい同じだと思うので参考になれば幸いです。 フロント側 【AWS】S3+CloudFront+Route53+ACM で SSL 化(https)した静的 Web サイトを公開する React で作った Web アプリを GitHub で管理して S3 に自動デプロイする [CodeBuild]buildspec.yml での環境変数指定方法あれこれまとめ React+Django+AWS でペットの成長をサポートするアプリを開発したので Tips をまとめた 上記サイトを参考にさせていただいてなんとかなりました。 特に下 2 つのサイトのおかげで.env をビルドの自動化のときにどう作ればいいのかというところを解決できたので本当に助かりました。 以下手順をセクションごとに。 準備 リポジトリの用意(今回は Github) ASM で環境変数の定義 buildspec.yml の作成 独自ドメインの取得(今回は freenom) なお ASM の ParameterStore は KMS を使って暗号化することもできる。 buildspec.yml は CodeBuild で使う、Build の手順書みたいなもの。 version: 0.2 env: #  ASMで作成した環境変数の指定 parameter-store: buildspec.yml内での呼び出し方:ASM設定した環境変数のKey名(ex.REACT_APP_API_URL:https://localhost:8000の場合はREACT_APP_API_URL) key: "REACT_APP_API_URL" phases: install: runtime-versions: nodejs: 14 pre_build: commands: - if [ -e /tmp/node_modules.tar ]; then tar xf /tmp/node_modules.tar; fi - npm install build: commands: #コマンドの実行結果としてBuild Sequence……を出力する - echo "Build Sequence……" # .envを作成して、そこにREACT_APP_API_URL:https://localhost:8000を定義して出力する - echo "REACT_APP_API_URL=$key" >> .env - npm run build post_build: commands: - tar cf /tmp/node_modules.tar node_modules artifacts: files: - '**/*' base-directory: build cache: paths: - /tmp/node_modules.tar phase 部分が実際のビルド時の動作についての指示。 ビルド前、ビルド中、ビルド後とそれぞれコマンドを設定できる。 作った buildspec.yml はプロジェクトルートにおいてリポジトリへ push しておく。 S3 バケットの作成 静的 Web ホスティングができるような設定で作成しておく。 不慣れであるなら、この段階ではバケットポリシーはパブリック読み取りアクセスを許可しておき、ブロックパブリックアクセスもオフにしておくと後でデプロイがうまく行っているか確認ができる。 CodePipeline と CodeBuild、CodeDeploy の設定 CodePipeline からパイプラインを作成するとそのまま一連の設定ができる。 CodeCommit にあたる部分はソースプロパイダで CodeCommit 他用意したリポジトリを設定する。(今回は Github) すると連携してくれと言われるので画面の通り進めていってリポジトリとビルドに使うブランチを指定する。 CodeBuild プロジェクトを作成するからビルドプロジェクトを作成する、CodeBuild で既に作ってあるならばそれを指定。 また環境変数はここでも設定できる。 するとこんな感じでビルドに使うマシンの設定になるので OS を選んで任意の設定をする。 特に指定等なければ最新のイメージ、最新のバージョンを選択しておく。 CodeDeploy 任意のデプロイ先を選択、今回は S3 を選択して、作ったバケットを選択する。 これでデプロイが始まる。 デプロイできたかの確認はこの段階であれば静的 Web ホスティングの設定画面にあるバケットウェブサイトエンドポイントの URL を叩いてみるとわかる。 サイトが表示されれば OK。 CloudFront コンテンツ配信ではこれを使うと便利なので S3 とは基本的にセットで使っていくことが多い。 CDN としての役割の他に証明書を持たせて SSL での通信も行えるようにすることもできる。 この段階ではディストリビューションを作るだけ。 OAI を使ったアクセスにすることでバケットへの直接アクセスを避け、必ず CloudFront を経由したアクセスになる。 今回は S3 は静的 Web ホスティング用途で使っているだけで、インターネットからバケットにアクセスしてデータをどうのこうのとはしないので、なので AWS が作ってくれた OAI に基づいたバケットポリシーで問題ない。 Route53 と Certificate Manager ここで SSL 通信を行うための設定を行う。 用意した独自ドメインを使って、ホストゾーンを作る。 できたホストゾーンの NS レコードの値をすべてドメインの取得先の DNS 設定(freenom だと NameServer)で登録する。 Certificate Manager では取得したドメインに対して SSL 証明書を作成する。 手順は以下の通り。 パブリック証明書のリクエストを行い、先程取得したドメイン名または*.ドメイン名の形式で登録する。 証明書をリクエストできたら、証明書の画面に行き CNAME レコードを作成。作成手順は以下の 2 種 A. DNS 設定ファイルをエクスポートして Route53 から任意のホストゾーンでその設定ファイルの値に基づいて作成 B. 証明書の画面からドメイン欄をクリックすると Route53 でのレコードを作成というボタンが出るのでそこから作成 CNAME レコードが作成されてしばらくすると証明書が検証実行中のステータスから発行済になるのでそうなったら完了。 CloudFront に戻り、以下の設定を行う。 代替ドメイン名の欄には取得したドメインを登録する。 SSL 証明書を作る際に*.ドメイン名と設定しておくと、画像の様にサブドメインも設定できる。 ここで気をつけておかないといけないことは必ずデフォルトルートオブジェクトオプションに index.html を指定しておくこと、でないと 403 エラーになる。 しかし、実は SPA でwindow.loactionを使ったページ遷移をしている場合これを設定しても 403 になる。 なのでその場合は CloudFront で公開した SPA でページ遷移を行う時に、403エラーが返ってくる時の対処法 以上を参考に設定をしておく。 ここまで終わったら Route53 で A レコードを代替ドメイン名欄で指定した値を CloudFront のディストリビューションのエイリアスと紐つけて作成すればフロント側は完了。 サーバー側 EC2 インスタンス起動 冒頭に書いた通り、今回は API サーバー 1 台だけなので普通にインスタンスを起動します。 OS は Ubuntu の方がスムーズに行くかと思います。(特に postgres 周りのインストールは LinuxOS だと躓くポイントがいくつかある) インスタンスを起動したら 任意のクライアントでキーペアを使って SSL 接続でインスタンスに接続(AWS コンソール上でもできるはず) sudo apt-get updateでアップデート、Linux とかだとここはsudo suしてからyum update -yとかだと思います。 必要なものをインストール 今回インストールしたのは以下の通り。 sudo apt-get install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx python3-venv DB の設定 インストールが終わったら DB の設定を行います。 今回は Postgres なので以下の通り。 # postgres起動 sudo -u postgres psql # DB名は任意 CREATE DATABASE ~; # USER名、PASSWORDは任意 CREATE USER ~ WITH PASSWORD '~' # 初期設定 ALTER ROLE ~ SET client_encoding TO 'utf8'; ALTER ROLE ~ SET default_transaction_isolation TO 'read committed'; ALTER ROLE ~ SET timezone TO 'UTC+9'; # 作ったUSERに作成したDBの権限をすべて付与。 GRANT ALL PRIVILEGES ON DATABASE ~db TO jira; # uuidが使えるようにしておく CREATE EXTENSION "uuid-ossp"; 仮想環境の作成 Python は 3 以降から venv で仮想環境を構築することが推奨されているのでそれで行う。 # 仮想環境作成 python3 -m venv 仮想環境名 # 仮想環境をアクティブ source 仮想環境名/bin/activate # ちなみに無効化は以下 deactive Django のセットアップ sudo -H pip3 install --upgrade pipで pip のインストール 仮想環境に入ってgit cloneでリポジトリをクローンする nano .envで環境変数を作成 pip install -r requirements.txtで依存関係すべてインストール 予め、pip freeze > requirements.txtを Django のプロジェクトルート(通常であれば manage.py があるルート)で実行して書き出したものを push したリポジトリを用意しておくと楽。 .env 部分は SECRET_KEY= ~ DEBUG=False DATABASE_URL=postgres://ユーザー名:パスワード@localhost:/DB名 ALLOWED_HOSTS=EC2インスタンスのElasticIP,サーバー側に用意した独自ドメイン と定義して作成。 また settings.py 部分はdjango-environを使って env = environ.Env() env.read_env(os.path.join(BASE_DIR, '.env')) SECRET_KEY = env('SECRET_KEY') DEBUG = env('DEBUG') ALLOWED_HOSTS = env.list('ALLOWED_HOSTS') # django-cors-headers3.4.0の場合 CORS_ORIGIN_WHITELIST = [ "http://localhost:3000", "http://localhost:3000", "https://127.0.0.1:3000", "https://127.0.0.1:3000", "http://localhost:8000", "http://127.0.0.1:8000", "https://サーバー側の独自ドメイン" ] # django-cors-headers3.5.0の場合 # CORS_ALLOWED_ORIGINS = [ # "http://localhost:3000" # ] CSRF_TRUSTED_ORIGINS = ['localhost:3000', '127.0.0.1', 'サーバー側の独自ドメイン'] SESSION_COOKIE_SAMESITE = 'None' SESSION_COOKIE_SECURE = True といったようにしておく、あとから Route53 の設定をしたあとにやってもよし。 また Middleware も開発時には以下のように DEBUG を見て samesite 周りの変更を行っていたところを class SameSiteMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) from config import settings for key in response.cookies.keys(): response.cookies[key]['samesite'] = 'Lax' if settings.DEBUG else 'None' response.cookies[key]['secure'] = not settings.DEBUG return response このように常にsamesite='none'かつsecure=Trueにするようにしておく。 class SameSiteMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) from config import settings for key in response.cookies.keys(): response.cookies[key]['samesite'] = 'None' response.cookies[key]['secure'] = True return response if settings.DEBUGがうまく動作しない問題は [Django]settings に DEBUG=True を定義してても、テスト実行すると DEBUG=False になってしまう罠 上記のような記事でも報告されてたりする、Django はこうイマイチそこでトラブル起きる?ってところで詰まることが多い気がする。 そもそも settings.py で指定してるのでそれで動いて欲しいものなのだけども…… 閑話休題、.env ファイルの作成以外の変更は予めリポジトリを作成するときに一緒にやってしまっても構わないと思う。 nano エディタで変更するのはつかれるので。 あとは python3 manage.py migrate python3 manage.py collectstatic python3 manage.py createsuperuser とやっていく。 DB 周りの設定をやっていない migrate でエラーが出るので先に済ましたということになる。 ネットワークの設定 Route53 フロント同じ要領でサーバー側の独自ドメインの設定を行う。 ALB を使うと証明書もここで解決できるはずだが、今回はパス。 Gunicorn sudo nano /etc/systemd/system/gunicorn.service ファイルは以下の通り。 [Unit] Description=gunicorn daemon After=network.target [Service] User=ubuntu Group=www-data WorkingDirectory=/home/ubuntu/jira_api ExecStart=/home/ubuntu/仮想環境名/bin/gunicorn --access-logfile - --workers 3 --bind unix:/home/ubuntu/プロジェクトフォルダ名(以下A)/config(wsgiが入っているフォルダ名).sock config.wsgi:application [Install] WantedBy=multi-user.target ここで気をつけないと行けない部分はプロジェクトフォルダ名と wsgi が入っているフォルダ名の部分。 リポジトリから Clone した場合、リポジトリ名がプロジェクトフォルダ名にあたることになるはずである。 いずれにせよ、プロジェクトのルートディレクトリになるフォルダ名を指定すること。 wsgi が入っているフォルダ名はdjango-admin startproject mysiteで作成したフォルダ、つまり特に弄ってなければ settings.py が入っているフォルダになる。 そのフォルダに wsgi も一緒に入っているのでそのフォルダ名を指定すること。 このあたりは個々人で違ってくると思う部分なのに間違えると通信できなくなるので注意、この辺の関係が解説によってまばらだったりしたのでかなり沼りました。 nginx の設定 sudo nano /etc/nginx/sites-available/jira_api 一先ずは以下の通りにする。 server { listen 80; server_name サーバー側の独自ドメイン名; location = /favicon.ico {access_log off; log_not_found off;} location /static/ { root /home/ubuntu/Djangoのstaticフォルダがあるルート; } location / { include proxy_params; proxy_pass http://unix:/home/ubuntu/プロジェクトフォルダ名/wsgiがあるフォルダ名.sock; } } ポート番号を見ればわかるが Http 通信を行うための基本的なテンプレートのようなものらしい。 設定したらsudo ln -s /etc/nginx/sites-available/jira_api /etc/nginx/sites-enabled/でエイリアスを作っておく。 あとはsudo ufw allow 'Nginx Full'。 SSL 通信を可能にする ここまでしたら一応 Gunicorn と nginx を再起動する。 sudo systemctl restart gunicorn sudo systemctl enable gunicorn sudo systemctl restart nginx きちんとデプロイできて、ここまでの設定がうまくいっていれば、ElasticIP/admin で URL で叩けば Django の管理ページが出るはずである。 しかし、このままだと SSL(Https)通信ができないのでそのための設定を行う。 今回は EC2 内部で発行管理するので Certbot で試してみる。 Certbot 参考: EC2 上の Django アプリを独自ドメイン、SSL 対応する 参考記事中の DNS の設定は Route53 で A レコードを同じ要領で作成すればよし。 Certbot の指示通り導入を行って、sudo certbot --nginx行い、さらに指示に従ってうまく作成できればsudo nano /etc/nginx/sites-available/jira_apiで作成したファイルが server { server_name サーバー側の独自ドメイン名; location = /favicon.ico {access_log off; log_not_found off;} location /static/ { root /home/ubuntu/Djangoのstaticフォルダがあるルート; } location / { include proxy_params; proxy_pass http://unix:/home/ubuntu/プロジェクトフォルダ名/wsgiがあるフォルダ名.sock; } listen 443 ssl; # managed by Certbot ssl_certificate /etc/letsencrypt/live/challengejiradjango.tk/fullchain.pem; # manage> ssl_certificate_key /etc/letsencrypt/live/challengejiradjango.tk/privkey.pem; # mana> include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot } server { if ($host = サーバー側の独自ドメイン名) { return 301 https://$host$request_uri; } # managed by Certbot listen 80; server_name サーバー側の独自ドメイン名; return 404; # managed by Certbot } 以上のような形に変更され、SSL 通信が可能となっているはずである。 あとはセキュリティグループで Http、SSL、HTTPS を許可しているか確認をし、再度 nginx→gunicorn の順でリセットを行えば全行程が終了、お疲れ様でした。 ちなみに、Nginx でエラーが出ている場合はsudo tail -f /var/log/nginx/error.logでログを確認してみる。 最後に リファクタリングの余地はかなりあるが、これでクロスオリジン前提での React(というか JS フレームワーク)と Django(DRF)のデプロイをするためには最低限何をすればいいのかということが把握できた。 主にリファクタリングの余地はサーバー側にたくさんあるはずで、いま思いつく限りでも デプロイの自動化 EC2 ではなく、ALB を使った SSL 証明書の管理と SSL 通信 サブネットを分けて踏み台サーバーを作る AutoScaling を導入し、マルチ AZ にする DB は RDS にする などがある。 ただ今回やったサーバー側の処理はおそらく API GateWay と Lambda で置き換えることができて、しかもその方が都合が良さそうだなとは感じた。 Django 側で複雑なデータ処理を行う……という工程があればまた別なのかもしれないけども、その場合でも FastAPI の方が適していそうな気がするし、プログラミングの世界は本当に終わりがないんだなということをしみじみと感じました。 もし今回の記事で間違っているところや補足の解説、ベストプラクティスやリファクタリング案などあればぜひ教えて頂けると嬉しいです。 とりあえずは、DVA 取得に向けて頑張ろうと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React メモ

目次 コマンド JSX props 条件分岐と繰り返し処理 Stateとイベント処理 ルーティング 1. コマンド # create-react-appをインストール $ npm install -g create-react-app # 新規作成 $ npx create-react-app [プロジェクト名] # TSX(JSXのTypeScript版)で作りたい時 # --template typescriptをつける $ npx create-react-app --template typescript [プロジェクト名] # 開発サーバー起動 $ npm start # ビルドしてHTML/CSS/JSファイルを生成 $ npm run build 2. JSX classはclassName, forはhtmlForで書く {変数名} の形式で埋め込むことができる index.js(最初に呼び出されるファイル) //React関連のライブラリをインポート import React from 'react'; import ReactDOM from 'react-dom'; //アプリ固有のファイルをインポート import './index.css'; import App from './App'; //キャッシュのためのサービスをインポート import reportWebVitals from './reportWebVitals'; //Appコンポーネントを実行 //render(comp, target) //comp: 出力するコンポーネント //target: 出力先の要素 ReactDOM.render( <App />, document.getElementById('root') ); //サービスを有効化 registerServiceWorker(); コンポーネントファイル //Reactの基本ライブラリをインポート import React, { Component } from 'react'; //ロゴ画像/スタイルシートをインポート import logo from './log.svg'; import './App.css'; //Appコンポーネントを定義 class App extends Component { //描画内容を準備 render() { return ( //class → classNameにすること <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } } //Appクラスをエクスポート export default App; React.Fragment(ダミーの要素) //React.Fragmentは最終的な出力には含まれない ReactDOM.render( <React.Fragment> <p>あいうえお</p> <p>かきくけこ</p> </React.Fragment> , document.getElementById('root'); ); 属性の設定 //属性値前後のクォートは付けない //href="{url}"だとダメ const url = 'https://wings.msn.to/'; <a href={url}>Webサイト</a> //style属性の指定にはオブジェクトを利用する //オブジェクトリテラルではcamelCase記法に直す const style = { color: 'Red', backgroundColor: 'Yellow' }; <p style={style}>あいうえお</p> //属性をまとめて設定 //オブジェクトリテラルと「...」演算子を利用する const attrs = { src: 'http://test/image/aiueo.jpg', alt: 'あいうえお', title: 'テストあいうえお' }; <img {...attrs} /> コンポーネントを入れ子にする import React, { Component } from 'react'; //子コンポーネントをインポート import MyCover from './MyCover'; export default class MyBook extends Component { render() { return ( <div> {/* MyCoverコンポーネントを呼び出し */} <MyCover title={this.props.info.title} /> </div> ) } } 3. props propsとは? 単一のオブジェクトとしてこのコンポーネントに渡すオブジェクトのこと。 JSX内で使用する属性情報、子要素を渡すことができる。 //コンポーネント側 import React, { Component } from 'react'; export default class MyHello extends Component { //プロパティを元に出力を生成 render(){ return <div>こんにちは、{this.props.name}さん</div>; } } //index.js import MyHello from './MyHello'; ... ReactDOM.render( //MyHelloコンポーネントにプロパティを引き渡す //この場合は「name」を渡す <MyHello name="鈴木" />, document.getElementById('root'); ); 「...」演算子を使用することで、オブジェクトの内容を個々の属性にばらすことも可能。 const data = { name: '鈴木', age: 18, sex: '男' }; ReactDOM.render( <MyClass {...data} />, document.getElementById('root') ); 4. 条件分岐と繰り返し処理 条件分岐 「?:」演算子、「&&」演算子 「?:」演算子は、三項演算子のような使い方 「&&」演算子は、trueの時だけ出力される //「?:」演算子 { this.props.isNew ? <MyNew /> : null } //「&&」演算子 { this.props.isNew && <MyNew /> } 即時関数 //関数を使う //見栄えが悪くなるので、なるべく先に名前付きで関数を作って、それを利用するようにする {(() => { if(this.props.isNew){ return <MyNew /> } })()} 繰り返し処理 Array#mapメソッドを利用する const data = [ { name: '鈴木', age: 18, sex: '男', userid=1 }, { name: '佐藤', age: 20, sex: '女', userid=2 }, }; //index.js ReactDOM.render( //配列dataを使っての繰り返し処理 <div> {data.map((x) => <MyClass {...x} />)} </div> ) //keyプロパティを持たないことによる警告対処のため //以下のようにkey属性を持つことを推奨 //主キーを持たない場合はkey={index}とするのもOK //<MyClass {...x} key={x.userid} /> 5. Stateとイベント処理 Stateとは? コンポーネントの状態(state)を管理するオブジェクト。 stateの値を更新する場合は、setStateを使用する export default class MyState extends Component { constructor(props) { super(props); //Stateの初期値を設定 this.state = { current: new Date() } } //1000ミリ秒おきにStateを更新 //setStateを使用する setInterval(() => { this.setState({ current: new Date() }); }, 1000) render() { //currentの値をページに反映 <div>{this.state.current.toLocaleString()}</div> } } イベント処理 HTMLではonchange(全て小文字)だが、JSXではonChange(cameCase形式)で書く this.関数名.bindでイベント実行時の関数を指定(bindを付けないとエラーになる) //onchangeイベントを起こす <input id="txtName" type="text" onChange={this.show.bind(this)} /> //アロー関数で指定する場合 <input id="txtName" type="text" onChange={(e) => this.show(e)} //引数を渡す場合 show(end, e) { console.log(`${e.target.value}${end}`); } ... //元々のイベントオブジェクトは、引数リストの末尾に渡される //end => 'さん' //e => this //がそれぞれわたされる <input id="txtName" type="text" onChange={this.show.bind(this, 'さん')} /> ライフサイクルメソッド 後で書く 6. ルーティング ルーティング機能用のライブラリとしてReact Routerがある # React Routerをインストール $ npm install --save react-router-dom ルーティングのサンプル ルーティング設定は<Route>で定義する <Switch>で括っているのは、最初に合致した要素だけ出力する、という意味 exact属性は、完全一致で判定するという意味 ルーティング経由でのページ遷移は、<Link>で定義する import React, { Component } from 'react'; //ルーティング関連の機能をインポート import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom'; //ルーティングで利用するコンポーネントをインポート import MyTop from './MyTop'; import MyHello from './MyHello'; import MyArticle from './MyArticle'; export default class App extends Component { render() { //Router(BrowserRouter)でルーティング機能を有効化 return( <Router> <div> {/* ルーティング経由のリンクリストを準備 */} <ul> <li><Link to="/">トップ</Link></li> <li><Link to="/hello">Hello</Link></li> <li><Link to="/article">公開記事</Link></li> </ul> <hr /> {/* ルーティング設定(条件にマッチしたコンポーネントを出力) */} <Switch> <Route exact path="/" component={MyTop} /> <Route path="/hello" component={MyHello}/> <Route path="/article" component={MyArticle}/> </Switch> </div> </Router> ) } } ルートパラメータ //リンク <Link to="/article/13">記事13</Link> //ルートパラメーターを含んだルート <Route path="/article/:id" component={MyArticle} /> //コンポーネント側でルートパラメータを受け取る //this.props.match.params.名前で受け取ることが可能 const id = this.props.match.params.id; 補足 <Route>にてpathを書かない場合、全てのURLにマッチする、という書き方ができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript] URLからwindow.location.XXXで色々クエリを取得する。

特定のクエリが変わった時だけレンダリングを行うという処理を書こうと思った時に、window.location.XXXのXXXの部分の指定に色々あるということを学んだので、メモ。 window.location.XXXの色々 URLがhttps://higu.com:8080/higu/scraps?status=openv#hereの場合 window.location.href // -> "https://higu.com:8080/higu/scraps?status=open#here" window.location.protocol // -> "https:" window.location.host // -> "higu.com:8080" window.location.hostname // -> "higu.com" window.location.port // -> "8080" window.location.pathname // -> "/higu/scraps" window.location.search // -> "?status=open" window.location.hash // -> "#here" window.location.origin // -> "https://higu.com:8080" その他の便利な機能 URLがhttps://higu.com:8080/higu/scraps?status=open#hereの場合 ?の後のクエリだけ欲しい window.location.search.substring(1) // -> "status=openv#here" 特定の場所のクエリが欲しい ver query = window.location.href; // /higuの前 query.split("/higu")[0]; // -> "https://higu.com:8080" // /higuのの後 query.split("/higu")[1]; // -> "/scraps?status=openv#here" // status=openだけ var tmp = query.split("/scraps?")[1]; tmp.split("#here")[0]; // -> "status=open" 私が実際にwindow.location.XXXを用いたコード クエリが変わった時に、useEffect内が走る const query = window.location.href; const updateQuery = query.split("/mypage")[0]; useEffect(() => { ~略~ }, [updateQuery]); 参考:こちら
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを始めたい人へ。今日だけでできるTODO #19 useMemo

useMemoとは 関数による計算結果をメモ化するためのフックです。 useMemo()を利用する場合はimportでuseMemoを読み込みます。 import React, { useMemo } from 'react'; 基本構文は下記の通りになります。 useMemo(() => 値を計算する関数の呼び出し,[値の計算に必要な要素の配列]); useMemoとuseCallbackの違い useCallbackは関数をメモ化しますが、useMemoは関数の結果をメモ化します。 App.js import React, { useState, useMemo } from 'react'; import { Counter } from './Counter'; const initialCount = 0; const square = (params) => { const data = [...Array(100).keys()]; data.forEach(() => { console.log( `B+1のボタンがクリック、square関数実行中、 ループ処理を${data.length}回実行中…` ); }); return params * params; } export default function App() { const [countA, setCountA] = useState(initialCount); const [countB, setCountB] = useState(initialCount); const countResultA = () => { setCountA((prevCountA) => prevCountA + 1); console.log('Aボタンがクリックされました'); } const countResultB = () => { setCountB((prevCountB) => prevCountB + 1); console.log('Bボタンがクリックされました'); } const squareArea = useMemo(() => square(countB), [countB]); return ( <> <Counter text='Aボタン' count={countA} countIncrement={countResultA} /> <Counter text='Bボタン' count={countB} countIncrement={countResultB} /> <p>計算結果:{ squareArea }</p> </> ); }; Counter.js import React from 'react'; export const Counter = React.memo(({ text, count, countIncrement, countDecrement, countReset, initialCount }) => { return ( <> <p>現在のカウント数:{count}</p> <button onClick={countIncrement}>increment</button> {/* <button onClick={countDecrement}>decrement</button> <button onClick={countReset}>reset</button> */} </> ); }); 下記の画像は、AとBのボタンをそれぞれ2回ずつクリックした時の結果です。 Aをクリックした時はcountResultAが実行され、コンポーネントが再レンダリングされたタイミングではsquare関数は実行されません。 Bをクリックした時のみsquare関数が実行されます。 useMemo()を利用しなかった場合どうなるか確認してみます。 Aのボタンがクリックされたタイミングでも計算結果の値が変わらないのにsquare関数が実行されています。 useMemo()を利用することでパフォーマンスのチューニングができることが理解できたかと思います。 前回までのReact.memoやuseCallbackも含めて機能と使い所を覚えておきましょう。 React.memoの記事 useCallbackの記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

これからReact始めたい人のための今日だけでできるTODO#19 useMemo

useMemoとは 関数による計算結果をメモ化するためのフックです。 useMemo()を利用する場合はimportでuseMemoを読み込みます。 import React, { useMemo } from 'react'; 基本構文は下記の通りになります。 useMemo(() => 値を計算する関数の呼び出し,[値の計算に必要な要素の配列]); useMemoとuseCallbackの違い useCallbackは関数をメモ化しますが、useMemoは関数の結果をメモ化します。 App.js import React, { useState, useMemo } from 'react'; import { Counter } from './Counter'; const initialCount = 0; const square = (params) => { const data = [...Array(100).keys()]; data.forEach(() => { console.log( `B+1のボタンがクリック、square関数実行中、 ループ処理を${data.length}回実行中…` ); }); return params * params; } export default function App() { const [countA, setCountA] = useState(initialCount); const [countB, setCountB] = useState(initialCount); const countResultA = () => { setCountA((prevCountA) => prevCountA + 1); console.log('Aボタンがクリックされました'); } const countResultB = () => { setCountB((prevCountB) => prevCountB + 1); console.log('Bボタンがクリックされました'); } const squareArea = useMemo(() => square(countB), [countB]); return ( <> <Counter text='Aボタン' count={countA} countIncrement={countResultA} /> <Counter text='Bボタン' count={countB} countIncrement={countResultB} /> <p>計算結果:{ squareArea }</p> </> ); }; Counter.js import React from 'react'; export const Counter = React.memo(({ text, count, countIncrement, countDecrement, countReset, initialCount }) => { return ( <> <p>現在のカウント数:{count}</p> <button onClick={countIncrement}>increment</button> {/* <button onClick={countDecrement}>decrement</button> <button onClick={countReset}>reset</button> */} </> ); }); 下記の画像は、AとBのボタンをそれぞれ2回ずつクリックした時の結果です。 Aをクリックした時はcountResultAが実行され、コンポーネントが再レンダリングされたタイミングではsquare関数は実行されません。 Bをクリックした時のみsquare関数が実行されます。 useMemo()を利用しなかった場合どうなるか確認してみます。 Aのボタンがクリックされたタイミングでも計算結果の値が変わらないのにsquare関数が実行されています。 useMemo()を利用することでパフォーマンスのチューニングができることが理解できたかと思います。 前回までのReact.memoやuseCallbackも含めて機能と使い所を覚えておきましょう。 React.memoの記事 useCallbackの記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでアプリを作成しました【6】【Web フォーム】【随時更新】

React Hook Formとは React Hook Form 『React Hook Form』 とは「高性能で柔軟かつ拡張可能な使いやすいフォームバリデーションライブラリ」を掲げた入力フォームの管理に特化した React 向けのライブラリ。 状態管理を DOM で行う非制御コンポーネントでフォームの値を扱うことで他よりも高速なフォームライブラリを可能にする。 Material-UI とは React Stepper component - Material-UI Google の Material デザインをベースに開発された、UI コンポーネントライブラリです。 お手軽に Material デザインを取り入れられることに加えて、コンポーネントの種類が豊富に用意されているため、それらを組み合わせるだけでも見栄えの良いものを作ることができます。 Create React App でアプリの雛形を作成 $ npx create-react-app <アプリ名> $ cd <アプリ名> 事前準備 Material-UI 及び React Hook Form v7 を利用する為、事前にライブラリをインストールする。 $ npm install @material-ui/core $ npm install react-hook-form $ git diff -p : snip : diff --git a/package.json b/package.json index f61e3cb..9c49442 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "version": "0.1.0", "private": true, "dependencies": { + "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-hook-form": "^7.12.1", "react-scripts": "4.0.3", "web-vitals": "^1.1.2" }, パッケージがインストールされ、package.json に追加されたのを確認する。 「ファイルタイプ別にグループ化する」方法を用いてソースを管理 components ディレクトリを src ディレクトリ配下に配置する $ mkdir src/components ベースとなる画面構成を作成 自動生成された App.js を、Header コンポーネントと Content コンポーネントで構成するためにテキストエディターで下記のように編集する。 App.js import './App.css'; import { Grid } from '@material-ui/core'; import Header from './components/Header'; import Content from './components/Content'; function App() { return ( <Grid container direction="column"> <Header /> <div style={{ padding: 30 }}> <Content /> </div> </Grid> ); } export default App; Header コンポーネントと Content コンポーネントを定義し、ファイルを用意する。 $ touch src/components/Header.js $ touch src/components/Content.js Header.js は下記のとおりに編集する。 Header.js import React from "react"; import { AppBar, Toolbar } from "@material-ui/core"; const Header = () => { return ( <AppBar position="static" style={{ backgroundColor: "#000000" }}> <Toolbar> <img src="https://classmethod.jp/wp-content/themes/classmethod/img/common/logo_classmethod.svg" alt="クラスメソッド株式会社"></img> </Toolbar> </AppBar> ); }; export default Header; Content.js は下記の通りに編集する。 Content.js import { Grid } from '@material-ui/core' function Content() { return ( <Grid container> <Grid sm={2}/> <Grid lg={8} sm={8} spacing={10}> コンテンツ </Grid> </Grid> ) } export default Content 参考サイト React 初心者が Material-UI で今どきの Web フォームを作ってみた(Stepper編) React Hook Form(V7)を使って簡単にバリデーションを実装しよう!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでアプリを作成しました【6】【Web フォーム】

React Hook Formとは React Hook Form 『React Hook Form』 とは「高性能で柔軟かつ拡張可能な使いやすいフォームバリデーションライブラリ」を掲げた入力フォームの管理に特化した React 向けのライブラリ。 状態管理を DOM で行う非制御コンポーネントでフォームの値を扱うことで他よりも高速なフォームライブラリを可能にする。 Material-UI とは React Stepper component - Material-UI Google の Material デザインをベースに開発された、UI コンポーネントライブラリです。 お手軽に Material デザインを取り入れられることに加えて、コンポーネントの種類が豊富に用意されているため、それらを組み合わせるだけでも見栄えの良いものを作ることができます。 Create React App でアプリの雛形を作成 $ npx create-react-app <アプリ名> $ cd <アプリ名> 事前準備 Material-UI 及び React Hook Form v7 を利用する為、事前にライブラリをインストールする。 $ npm install @material-ui/core $ npm install react-hook-form $ git diff -p : snip : diff --git a/package.json b/package.json index f61e3cb..9c49442 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,13 @@ "version": "0.1.0", "private": true, "dependencies": { + "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-hook-form": "^7.12.1", "react-scripts": "4.0.3", "web-vitals": "^1.1.2" }, パッケージがインストールされ、package.json に追加されたのを確認する。 「ファイルタイプ別にグループ化する」方法を用いてソースを管理 components ディレクトリを src ディレクトリ配下に配置する $ mkdir src/components ベースとなる画面構成を作成 自動生成された App.js を、Header コンポーネントと Content コンポーネントで構成するためにテキストエディターで下記のように編集する。 App.js import './App.css'; import { Grid } from '@material-ui/core'; import Header from './components/Header'; import Content from './components/Content'; function App() { return ( <Grid container direction="column"> <Header /> <div style={{ padding: 30 }}> <Content /> </div> </Grid> ); } export default App; Header コンポーネントと Content コンポーネントを定義し、ファイルを用意する。 $ touch src/components/Header.js $ touch src/components/Content.js Header.js は下記のとおりに編集する。 Header.js import React from "react"; import { AppBar, Toolbar } from "@material-ui/core"; const Header = () => { return ( <AppBar position="static" style={{ backgroundColor: "#000000" }}> <Toolbar> <img src="https://classmethod.jp/wp-content/themes/classmethod/img/common/logo_classmethod.svg" alt="クラスメソッド株式会社"></img> </Toolbar> </AppBar> ); }; export default Header; Content.js は下記の通りに編集する。 Content.js import { Grid } from '@material-ui/core' function Content() { return ( <Grid container> <Grid sm={2}/> <Grid lg={8} sm={8} spacing={10}> コンテンツ </Grid> </Grid> ) } export default Content 次のステップで、Content.js に Stepper コンポーネントを配置する。 テキストエディターの Content.js を編集する。 ※ 内容は、Material-UI 公式ドキュメントに掲載されているサンプルのソースコードをベースにする。 Content.js を説明する。 Stepper コンポーネントの数字が表示されたステップアイコン下部に表示されるラベルは、getSteps 関数で定義される React のステートとして、Stepper コンポーネントのインデックス番号(activeStep)を管理 ボタンクリックに応じて、setActiveStep でインデックス番号(activeStep)を増減させる インデックス番号(activeStep)に応じて、getStepContent 関数で表示するコンテンツを切り替える 今回のサンプルでは、Material-UI 公式ドキュメントに掲載されているサンプルのソースコードをベースとしているため 3ステップの Web フォーム(のベースとなる画面)を作成。 必要に応じて Content.js に定義されている getSteps 関数及び getStepContent 関数へラベルとコンテンツを追加することにより 4ステップ以上の Web フォームも気軽に作成できる。 ※Header.js の弊社企業ロゴを差し替えたり、試しに 4ステップ以上の構成に変更してみるなどオリジナルの Web フォーム(のベースとなる画面)を自身でカスタマイズして、作ってみる。 Content.js をテキストエディターで編集 getStepContent 関数は、React が管理している Stepper コンポーネントのインデックス番号(activeStep)に応じたコンテンツを取得する処理を記述しているため、各インデックス番号に応じたコンテンツ(コンポーネント)を下記のように編集する。 Content.js : snip : import Basic from "./Basic"; import Optional from "./Optional"; import Confirm from "./Confirm"; function getSteps() { return [ '基本項目', '任意項目', '入力確認' ]; } function getStepContent(stepIndex) { switch (stepIndex) { case 0: return <Basic handleNext={handleNext} />; case 1: return <Optional handleNext={handleNext} handleBack={handleBack} />; case 2: return <Confirm handleBack={handleBack} />; default: return 'Unknown stepIndex'; } } Content.js を編集後、各コンテンツのコンポーネントに応じたソースファイルを、用意する。 $ touch src/components/Basic.js $ touch src/components/Optional.js $ touch src/components/Confirm.js 各コンポーネントの実装 Basic.js 基本項目として、3つのアイテム(チェックボックス、テキストフィールド、プルダウンリスト)を用意する。 react-hook-form v7 と Material-UI を組み合わせた利用方法については、React Hook Form の公式ドキュメントにサンプルコード付きで掲載されている。 Get Started | React Hook Form - Integrating with UI libraries Basic.js の全文ソースコードを転記 Basic.js import { Grid } from '@material-ui/core' import { useForm, Controller } from "react-hook-form"; import TextField from "@material-ui/core/TextField"; import { Button, MenuItem } from "@material-ui/core"; import FormControlLabel from '@material-ui/core/FormControlLabel'; import Checkbox from '@material-ui/core/Checkbox'; function Basic(props) { const { control, handleSubmit } = useForm({ defaultValues: { checkBox: false, textBox: "", pullDown: "", }, }); const onSubmit = () => { props.handleNext(); }; return ( <Grid container> <Grid sm={2}/> <Grid lg={8} sm={8} spacing={10}> <form onSubmit={handleSubmit(onSubmit)}> <Controller control={control} name="checkBox" render={({ field: { value, onChange } }) => ( <FormControlLabel control={ <Checkbox checked={value} onChange={onChange} color='primary' /> } label="チェックボックス" /> )} /> <Controller control={control} name="textBox" render={({ field }) => ( <TextField {...field} label="テキストフィールド" fullWidth margin="normal" placeholder="プレースホルダー" /> )} /> <Controller control={control} name="pullDown" render={({ field }) => ( <TextField {...field} label="プルダウンリスト" fullWidth margin="normal" id="select" select > <MenuItem value="one">選択肢1</MenuItem> <MenuItem value="two">選択肢2</MenuItem> <MenuItem value="three">選択肢3</MenuItem> </TextField> )} /> <Button variant="contained" color="primary" type="submit" > 次へ </Button> </form> </Grid> </Grid> ) } export default Basic ブラウザで、下記の表示を確認 テキストフィールドにマウスカーソルを合わせて、入力待ちの状態になるとプレースホルダーが表示 プルダウンリストを選択すると選択項目が表示 いずれも、Material-UI が提供している部品を活用している。 この Web フォーム(React アプリ)のポイントは、下記の3点。 フォーム(項目)への入力や入力内容は、react-hook-form が管理してくれる フォームに配置された各アイテムのデザインや入力体験は、Material-UI が面倒を見てくれる 基本項目フォームの「次へ」ボタンは、Content.js に定義された handleNext() 関数をプロパティ経由で受け取って呼び出すことで、次のフォームへ遷移させている(Stepper コンポーネントのインデックス番号を1つ繰り上げている) Optional.jsを編集 Optional.js では、Tooltip を追加。 また、Basic.js とは異なり、「戻る」ボタンと「次へ」ボタンが表示。各ボタンの onClick イベントで呼ばれる onSubmit() 関数に "back" や "next" といったアクション(プロパティ)を渡しており、onSubmit() 関数内で、各アクションに応じた処理(インデックス番号の増減)を行っている。 Optional.js import { Grid } from '@material-ui/core' import { useForm, Controller } from "react-hook-form"; import TextField from "@material-ui/core/TextField"; import { Button } from "@material-ui/core"; import Tooltip from '@material-ui/core/Tooltip'; function Optional(props) { const { control, handleSubmit } = useForm({ defaultValues: { multilineText: "", }, }); const onSubmit = (action) => { if(action === 'back') { props.handleBack(); } else if (action === 'next') { props.handleNext(); } }; return ( <Grid container> <Grid sm={2}/> <Grid lg={8} sm={8} spacing={10}> <form onSubmit={handleSubmit(onSubmit)}> <Controller control={control} name="multilineText" render={({ field }) => ( <Tooltip title="自由に記入することができます" placement="top-start" arrow > <TextField {...field} label="備考欄" fullWidth margin="normal" rows={4} multiline variant="outlined" placeholder="その他ご要望等あれば、ご記入ください" /> </Tooltip> )} /> <Button variant="contained" color="primary" onClick={() => onSubmit("back")} > 戻る </Button> <Button variant="contained" color="primary" onClick={() => onSubmit("next")} > 次へ </Button> </form> </Grid> </Grid> ) } export default Optional 各ステップ毎の入力情報を 『保存』 及び 『取得』 する ここまでの実装で基本項目や任意項目のフォーム(コンテンツ)は用意できたが、各フォームに入力された入力情報は各コンポーネントの react-hook-form がステートを保持したままの状態であり各ステップ毎の入力情報は、どこかに保存して管理しておく必要がある。そこで、React の『コンテクスト』を利用して各コンポーネントのデータを管理する。 Reactを基本からまとめてみた【13】【React Context】 Content.js や Basic.js および Optional.js を編集する。 Content.js : snip : export const UserInputData = React.createContext(); function Content() { const [currentState, setCurrentState] = React.useState({}); const value = { currentState, setCurrentState }; : snip : return ( <Grid container> <Grid sm={2}/> <Grid lg={8} sm={8} spacing={10}> <Stepper activeStep={activeStep} alternativeLabel> {steps.map((label) => ( <Step key={label}> <StepLabel>{label}</StepLabel> </Step> ))} </Stepper> <UserInputData.Provider value={value}> { getStepContent(activeStep, handleNext, handleBack)} </UserInputData.Provider> </Grid> </Grid> ) } export default Content UserInputData というコンテクストオブジェクトを、createContext() で作成 各ステップのフォームで入力された情報を保持するための currentState を useState() で作成 UserInputData コンテクストオブジェクトのプロバイダコンポーネントに currentState と setCurrentState を含めた value プロパティ経由で、各ステップのコンテンツ(フォーム)コンポーネントに渡す Basic.js : snip : import React, { useContext } from "react"; import { UserInputData } from "./Content"; function Basic(props) { : snip : const { currentState, setCurrentState } = useContext(UserInputData); const onSubmit = (data) => { props.handleNext(); setCurrentState({...currentState, "Basic": data }); }; : snip : Content.js から UserInputData を import useContext() を利用して UserInputData コンテクストオブジェクトのプロバイダコンポーネント経由で currentState と setCurrentState を受け取る onClick イベントで呼ばれる onSubmit() 関数内で、react-hook-form から受け取った入力データを setCurrentState を利用して currentState へ追加 Optional.js : snip : import React, { useContext } from "react"; import { UserInputData } from "./Content"; function Optional(props) { const { control, handleSubmit, getValues } = useForm({ defaultValues: { multilineText: "", }, }); const { currentState, setCurrentState } = useContext(UserInputData); const onSubmit = (action) => { if(action === 'back') { props.handleBack(); } else { props.handleNext(); } const data = getValues(); setCurrentState({...currentState, "Optional": data }); }; return ( <Grid container> : snip : <Button variant="contained" color="primary" onClick={() => onSubmit("back")} > 戻る </Button> <Button variant="contained" color="primary" type="submit" > 次へ </Button> Basic.js と少し異なる点として「戻る」ボタンがクリックされた時も、 フォームの入力情報を保存するように追加で実装している。 React のステートとして、react-hook-form で入力された内容がボタンクリック後に保存されているかを確認するために、Chrome ブラウザと、React Developer Tools を利用して確認する。 Confirm.js import { Grid } from '@material-ui/core' import React, { useContext } from "react"; import { Button } from "@material-ui/core"; import { UserInputData } from "./Content"; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; var item = { 'checkBox': 'チェックボックス', 'textBox': 'テキストボックス', 'pullDown': 'プルダウン', 'multilineText': 'マルチラインテキスト' } function Confirm(props) { const { currentState } = useContext(UserInputData); const onSubmit = () => { alert(JSON.stringify(currentState)); }; const inputDataLists = []; var id = 0; for ( var k in currentState) { for ( var v in currentState[k]) { var value = '' if (currentState[k][v] === true) { value = 'チェックしました'; } else if (currentState[k][v] === false) { value = 'チェックしていません'; } else if (currentState[k][v] === '') { value = '未入力'; } else { value = currentState[k][v]; } inputDataLists.push( { "id": id, "name": item[v], "value": value } ); id++; } } return ( <Grid container> <TableContainer component={Paper}> <Table aria-label="Customer Input Data"> <TableHead> <TableRow> <TableCell>項目</TableCell> <TableCell>入力内容</TableCell> </TableRow> </TableHead> <TableBody> { inputDataLists.map(function(elem) { return ( <TableRow key={elem.id}> <TableCell>{elem.name}</TableCell> { elem.value ? <TableCell>{elem.value}</TableCell> : <TableCell>None</TableCell> } </TableRow> ) }) } </TableBody> </Table> </TableContainer> <Button variant="contained" color="primary" onClick={props.handleBack}> 戻る </Button> <Button variant="contained" color="primary" onClick={onSubmit}> 送信 </Button> </Grid> ) } export default Confirm ブラウザから、基本項目および任意項目を記入し入力確認画面を確認する。 各フォーム内で入力されたデータを、Material-UI のテーブルとして表示させる 基本項目および任意項目で入力された情報は、currentState から取得する 「次へ」ボタンの代わりに、「送信」ボタンを設置 バリデーション機能を追加 Yup をプロジェクトにインストールする。 $ npm install @hookform/resolvers yup $ git diff -p : snip : diff --git a/package.json b/package.json index f61e3cb..c8b846f 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,17 @@ "version": "0.1.0", "private": true, "dependencies": { + "@hookform/resolvers": "^2.6.1", + "@material-ui/core": "^4.12.2", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-hook-form": "^7.12.1", "react-scripts": "4.0.3", - "web-vitals": "^1.1.2" + "web-vitals": "^1.1.2", + "yup": "^0.32.9" パッケージがインストールされ、package.json に追加したので、Basic.js のチェックボックス及び、テキストフィールドに入力を必須とするバリデーション機能を追加する。 Basic.js : snip : import FormControl from '@material-ui/core/FormControl'; import FormHelperText from "@material-ui/core/FormHelperText"; import { yupResolver } from '@hookform/resolvers/yup'; import * as Yup from 'yup'; function Basic(props) { const basicSchema = Yup.object().shape({ checkBox: Yup.boolean() .oneOf([true], 'チェックが必要です'), textBox: Yup.string() .required('必須項目です') pullDown: Yup.string() .oneOf(['one', 'two', 'three'], 'いずれかを選択してください'), }); const { control, handleSubmit, formState:{ errors } } = useForm({ mode: 'onBlur', defaultValues: { checkBox: false, textBox: "", pullDown: "", }, resolver: yupResolver(basicSchema) }); : snip : return ( <Grid container> <Grid sm={2}/> <Grid lg={8} sm={8} spacing={10}> <form onSubmit={handleSubmit(onSubmit)}> <Controller control={control} name="checkBox" render={({ field: { value, onChange } }) => ( <FormControl error> <FormControlLabel control={ <Checkbox checked={value} onChange={onChange} color='primary' /> } label="チェックボックス" /> <FormHelperText> { errors.checkBox?.message } </FormHelperText> </FormControl> )} /> <Controller control={control} name="textBox" render={({ field }) => ( <TextField {...field} label="テキストフィールド" error={errors.textBox ? true : false} helperText={errors.textBox?.message} fullWidth margin="normal" placeholder="プレースホルダー" /> )} /> <Controller control={control} name="pullDown" render={({ field }) => ( <TextField {...field} label="プルダウンリスト" error={errors.pullDown ? true : false} helperText={errors.pullDown?.message} fullWidth margin="normal" id="select" select > <MenuItem value="one">選択肢1</MenuItem> <MenuItem value="two">選択肢2</MenuItem> <MenuItem value="three">選択肢3</MenuItem> </TextField> )} /> ブラウザから基本項目のフォームを表示させ、『次へ』ボタンをクリックする。『次へ』のボタンをクリックしたにも関わらず、入力が必須化されているため任意項目のフォームへ遷移することができない。ソースコードの定義としては、下記の実装部分が該当する。oneOf() の第2引数や、required() の引数がエラーメッセージになる。 ※ basicSchema を react-hook-form で利用するために useForm() の resolver に設定することで、外部の検証用ライブラリを利用することができる。 Basic.js const basicSchema = Yup.object().shape({ checkBox: Yup.boolean() .oneOf([true], 'チェックが必要です'), textBox: Yup.string() .required('必須項目です') pullDown: Yup.string() .oneOf(['one', 'two', 'three'], 'いずれかを選択してください'), }); * エラーメッセージを表示する部分は、下記のように実装する。 Basic.js const { control, handleSubmit, formState:{ errors } } = useForm({ mode: 'onBlur', defaultValues: { checkBox: false, textBox: "", pullDown: "", }, resolver: yupResolver(basicSchema) }); ※チェックボックス側は、FormControl コンポーネントと FormHelperText コンポーネントを利用する。 Basic.js <Controller control={control} name="checkBox" render={({ field: { value, onChange } }) => ( <FormControl error> <FormControlLabel control={ <Checkbox checked={value} onChange={onChange} color='primary' /> } label="チェックボックス" /> <FormHelperText> { errors.checkBox?.message } </FormHelperText> </FormControl> )} /> 各 API の詳細については、Material-UI の公式ドキュメントを参照すること FormControl API FormHelperText API ※チェックボックスとは異なり、テキストフィールドやプルダウンリストは2行(error/helperText プロパティ)追加するだけで対応できる。 Basic.js <Controller control={control} name="textBox" render={({ field }) => ( <TextField {...field} label="テキストフィールド" error={errors.textBox ? true : false} helperText={errors.textBox?.message} fullWidth margin="normal" placeholder="プレースホルダー" /> )} /> <Controller control={control} name="pullDown" render={({ field }) => ( <TextField {...field} label="プルダウンリスト" error={errors.pullDown ? true : false} helperText={errors.pullDown?.message} fullWidth margin="normal" id="select" select > <MenuItem value="one">選択肢1</MenuItem> <MenuItem value="two">選択肢2</MenuItem> <MenuItem value="three">選択肢3</MenuItem> </TextField> )} /> 各 API の詳細については、Material-UI の公式ドキュメントを参照すること TextField API テキストフィールドはいくつか制限を設ける。入力内容を「半角英数字記号」に制限する。 Basic.js const basicSchema = Yup.object().shape({ : snip : textBox: Yup.string() .required('必須項目です') .matches(/^[a-zA-Z0-9!-/:-@¥[-`{-~ ]*$/, "半角英数字記号以外は使用できません") : snip : }); matches() の第1引数で指定した正規表現にマッチする必要があるという制限を設けたため、全角文字を入力した場合など、にエラーとして扱われる。 文字数制限も追加する。 Basic.js const basicSchema = Yup.object().shape({ checkBox: Yup.boolean() .oneOf([true], 'チェックが必要です'), textBox: Yup.string() .required('必須項目です') .max(10, '10文字以内で入力してください') .matches(/^[a-zA-Z0-9!-/:-@¥[-`{-~ ]*$/, "半角英数字記号以外は使用できません") : snip : }); max() の第1引数で指定した数値以内の文字数でない場合、エラーとして扱われる。 入力確認画面の送信ボタンを実装する Web フォームとして利用するために、入力確認画面の送信ボタンをクリックした際に外部の API エンドポイントへ入力された内容(JSON 情報)を POST できるようにする。 具体的には、Confirm.js の onSubmit() に手を加える。 変更前 Confirm.js const onSubmit = () => { alert(JSON.stringify(currentState)); }; 入力確認画面の送信ボタンをクリックした際の動作は currentState の JSON データをアラートで表示しているだけを下記のように修正する。 変更後 Confirm.js const onSubmit = () => { postData(); }; async function postData() { const res = await fetch( 'https://example.com/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(currentState) } ); } 修正後は、入力確認画面の送信ボタンをクリックした際に currentState の JSON データを https://example.com/api という架空の API エンドポイントへ POST リクエストを行う非同期処理に変更する。本番利用する際は、実際に入力された情報を処理するためのシステム等で利用される HTTP エンドポイント(URL)へ置き換える必要がある。 データ送信に失敗した際のエラー通知を追加 エラー通知は、react-hot-toast というライブラリを利用する。 react-hot-toast react-hot-toast パッケージをインストールする。 $ npm install react-hot-toast Confirm.js を編集する。 Confirm.js : snip : import toast, { Toaster } from 'react-hot-toast'; : snip : function Confirm(props) { const { currentState } = useContext(UserInputData); const notifyError = () => toast.error('データの送信に失敗しました。少し待ってからリトライしてください'); const onSubmit = () => { postData() .then(data => { console.log(JSON.stringify(data)); }) .catch(err => { notifyError(); console.log(err); }); }; async function postData() { const res = await fetch( 'https://example.com/api', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(currentState) } ); const data = await res.json(); return data; } : snip : return ( <Grid container> <Toaster position="top-right" duration="4000" /> <TableContainer component={Paper}> 変更点は、下記の5つ react-hot-toast を import toast() API を呼び出す notifyError 関数を定義 postData 関数で、fetch したレスポンスデータを JSON 形式で返却 onSubmit 関数内で、postData() 呼び出しがエラーの場合に notifyError() を実行 Toaster コンポーネントを配置 Confirm.js を編集で、エラー通知が実装できた。 toast() API 等の詳細については、公式ドキュメントを確認する。 toast() API - react-hot-toast データ送信が成功した際のサンクスページを追加 ソースファイルを作成する。 $ touch src/components/Thanks.js Thanks.js import { Grid } from '@material-ui/core' import Typography from '@material-ui/core/Typography'; function Thanks() { return ( <Grid container alignItems="center" justifyContent="center"> <Typography variant="h4"> ありがとうございました </Typography> </Grid> ) } export default Thanks Confirm コンポーネントのプロパティとして handleNext を渡す。 Content.js function getStepContent(stepIndex, handleNext, handleBack) { switch (stepIndex) { case 0: return <Basic handleNext={handleNext} />; case 1: return <Optional handleNext={handleNext} handleBack={handleBack} />; case 2: return <Confirm handleNext={handleNext} handleBack={handleBack} />; default: return 'Unknown stepIndex'; } } postData() が成功した際に、props.handleNext() を呼び出す。 Confirm.js function Confirm(props) { : const onSubmit = () => { postData() .then(data => { console.log(JSON.stringify(data)); props.handleNext(); }) activeStep と steps.length が同じ場合に Thanks コンポーネントを表示させるように条件分岐を追加する。 Content.js : snip : import Thanks from "./Thanks"; : snip : return ( <Grid container> <Grid sm={2}/> <Grid lg={8} sm={8} spacing={10}> <Stepper activeStep={activeStep} alternativeLabel> {steps.map((label) => ( <Step key={label}> <StepLabel>{label}</StepLabel> </Step> ))} </Stepper> {activeStep === steps.length ? ( <Thanks /> ) : ( <UserInputData.Provider value={value}> { getStepContent(activeStep, handleNext, handleBack)} </UserInputData.Provider> )} </Grid> </Grid> ) 参考サイト React 初心者が Material-UI で今どきの Web フォームを作ってみた(Stepper編) React Hook Form(V7)を使って簡単にバリデーションを実装しよう! React 初心者が Material-UI で今どきの Web フォームを作ってみた(react-hook-form編) React 初心者が Material-UI で今どきの Web フォームを作ってみた(yup編) React Hook Form - スキーマバリデーション
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】値の受け渡し Propsとchildren

はじめに Reactでは、値の受け渡しにPropsがあります。そして、propsの特別な受け渡し方法としてchildrenがあります。Propsとchildrenについてまとめていきます。 Props 下記のコードでは、Propsを使って、ColoredMessageコンポーネントに色とメッセージを受け渡しをしています。 App.jsx import { ColoredMessage } from "./components/ColoredMessage"; export const App = () => { return ( <> <ColoredMessage color="blue" message="おげんきですか?" /> <ColoredMessage color="pink" message="元気です!" /> </> ); }; ColoredMessage.jsx export const ColoredMessage = (props) => { const contentStyle = { color: props.color, fontSize: "10px", }; return <p style={contentStyle}>{props.message}</p>; }; children 下記のコードでは、<ColoredMessage></ColoredMessage>で囲んだコードをColoredMessageコンポーネントで、props.childrenで受け取ってます。 App.jsx import { ColoredMessage } from "./components/ColoredMessage"; export const App = () => { return ( <> <ColoredMessage color="blue">おげんきですか?</ColoredMessage> <ColoredMessage color="pink">元気です!</ColoredMessage> </> ); }; ColoredMessage.jsx export const ColoredMessage = (props) => { const contentStyle = { color: props.color, fontSize: "20px", }; return <p style={contentStyle}>{props.children}</p>; }; 分割代入を使うと短く書ける ColoredMessage.jsx export const ColoredMessage = (props) => { const { color, children } = props; //propsの分割代入 const contentStyle = { color, //propsを省略できる。そして、color:colorの省略記法が使える fontSize: "10px", }; return <p style={contentStyle}>{children}</p>; //propsを省略できる };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【最新2021年版】webpackでReact、TypeScriptの環境構築【脱初心者】

【最新2021年版】webpackでReact、TypeScriptの環境構築【脱初心者】 こんにちは。ゆうじろうです。 今日はwebpackを使ったReact、TypeScriptの環境構築を進めていきます。 では、何がともあれプロジェクトを作成しましょう。 mkdir sample-project cd sample-project 作成したプロジェクトで、初期化 npm init -y 上記のようにpackage.jsonができていればOKです。 Reactのプロジェクトなので、Reactをインストールしていきましょう。 npm install --save react react-dom webpackを使用するので、同様にwebpackもインストール npm install --save-dev @types/react-dom @types/webpack @types/webpack-dev-server ts-loader ts-node typescript webpack webpack-cli webpack-dev-server 次にwebpackの設定ファイルを作成しましょう。 touch webpack.config.ts webpackの中身はこんな感じ。 import path from 'path'; import { Configuration } from 'webpack'; const config: Configuration = { context: path.join(__dirname, 'src'), entry: './index.tsx', output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js', publicPath: '/assets', }, module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', }, ], }, mode: "development", resolve: { extensions: ['.ts', '.tsx', '.js', '.jsx'], }, devtool: "inline-source-map", devServer: { static: { directory: path.join(__dirname, 'public'), }, compress: true, port: 3000, }, }; export default config; 次にTypeScriptの設定ファイルを作成します。 touch tsconfig.json その中身がこちら { "compilerOptions": { "sourceMap": true, "baseUrl": "./", "target": "es5", "strict": true, "module": "commonJs", "jsx": "react", "lib": [ "ES5", "ES6", "DOM" ], "allowSyntheticDefaultImports": true, "esModuleInterop": true, "isolatedModules": true, } } 次にエントリーファイルであるsrc/index.tsxを作成します。 mkdir src && touch src/index.tsx index.tsxの中身がこちら。 import React from 'react'; import ReactDOM from 'react-dom'; ReactDOM.render(<h1>Hello World!</h1>, document.getElementById('app')); 次にhtmlファイルを作成していきます。 mkdir puclic && touch public/index.html public/index.htmlの中身がこちら <!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <div id="app"></div> <script src="/assets/bundle.js"></script> </body> </html> ここまできたら、もう何も怖いものはありません。 webpackでbundleしてやりましょう! npx webpack-dev-server この表示になったら、 http://localhost:3000 にアクセスしてみましょう! Hello worldと記述されていればOKです! お疲れ様でした! 投稿者のサイトもあるから、是非遊びにきてね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでMaterialUIが使えない時の対策

Next.jsでMaterialUIが使えない時の対策です。 現象 MaterialUI公式の、ボタンを表示するソースコードをコピペしてもエラーが出る。 環境 ・Next.js ・Mac OS Big Sur ver11.6 ・Docker Desktop4.1.0 ・npm install @mui/material コマンドでインストール済み エラー内容: エラー内容:Module not found: Can't resolve '@emotion/react' @emotion/reactをインストールをしても、 今度は@emotion/styledのエラーが出ます。 エラー内容:Module not found: Can't resolve '@emotion/styled' ソースコード(テスト用) import Head from 'next/head' import Image from 'next/image' import styles from '../styles/Home.module.css' // add MaterialUI import * as React from 'react'; import ReactDOM from 'react-dom'; import Button from '@mui/material/Button'; export default function Home() { return ( <Button variant="contained">Hello World</Button> ) } 対策 @emotion/reactと@emotion/styledをインストールし、Dockerをbuild後、再起動します。 インストールコマンド yarn add @emotion/react @emotion/styled 参考 npm公式 https://www.npmjs.com/package/@emotion/styled Material UI公式 https://mui.com/getting-started/usage/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Storybook上でEmotionが動作しないエラーの対処

エラー内容 Storybook上でEmotionを使ったスタイル指定が効かない問題に直面した。例えば、スタイルを次のようにEmotionのcssを使って書いてみる。 sample.jsx import React from 'react'; import {css} from "@emotion/react"; const myStyle = css` color: red; `; function RedText() { return ( <div css={myStyle}>'redtext' </div> ) } export default RedText; RedTextは赤い「redtext」という文字を表示してほしい。ところが、これをStorybook上で確認すると、文字は黒のままでスタイルが効いてくれない。 コンソールで確認すると、redtextのdiv要素に次のようなメッセージが出ていた。 <div css="You have tried to stringify object returned from css function. It isn't supposed to be used directly (e.g. as value of the className prop), but rather handed to emotion so it can handle it (e.g. as value of css prop)."> 環境 react 17.0.2 @storybook/react 6.3.9 @emotion/react 11.4.1 @emotion/babel-preset-css-prop 11.2.0 解決方法 yarn add @emotion/babel-preset-css-prop を実行する .storybook/main.jsを次のように編集する .storybook/main.js module.exports = { "stories": [ //自由に編集 ], "addons": [ //自由に編集 ], webpackFinal: (config) => { config.module.rules.push({ test: /\.(js|jsx)$/, loader: require.resolve('babel-loader'), options: { cacheDirectory: true, presets: [require.resolve('@emotion/babel-preset-css-prop')], }, }); return config; }, }; 3. Storybookを起動 解説 Storybook Emotion Reactなどで検索すると、日本語記事ではReact + TypeScript + emotion の環境に Storybook を導入するがヒットする。これは、webpack.config.jsを.storybookに追加してオーバーライドする解決策。しかし、オーバーライドのためのカスタマイズがされているため採用しづらかった。 また、Githubでは、Emotionのissueであるbabel-preset-css-prop does not work with Storybookと、StorybookのissueであるCan't get emotion's css prop working inside storybookが見つかった。 これらを読むと、.storybook/main.jsにオプションを追加することでwebpack.config.jsを追記せずともwebpackの設定ができるとのこと。ところが、次のような設定では動かなかった(参考)。 うまくいかなかったmain_1.js module.exports = { //stories,addonsは省略 babel: async (options) => ({ ...options, presets: [...options.presets, '@emotion/babel-preset-css-prop'], }), } 同様に、次のように設定しても動かなかった。(参考) うまくいかなかったmain_2.js module.exports = { //stories,addonsは省略 babel: (config) => { config.presets.push(require.resolve("@emotion/babel-preset-css-prop")); return config; }, } 最終的に、ここを参考にすることで解決した。 うまくいったmain.js module.exports = { //stories,addonsは省略 webpackFinal: (config) => { config.module.rules.push({ test: /\.(js|jsx)$/, loader: require.resolve('babel-loader'), options: { cacheDirectory: true, presets: [require.resolve('@emotion/babel-preset-css-prop')], }, }); return config; }, }; 一応動くようにはなったが、環境次第なところがありそう。 ここで参考にした2つのissueには他にも多くの対処法が載っているので、また動かなくなったらそれらを試すのがいいと思う。特に、Storybookの方のissueは2021/10/9時点ではまだOpenなので、今後も進展がありそう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む