- 投稿日:2019-12-09T23:46:44+09:00
Firebase + Next.js (React.js + SSR) で遭遇したトラブルの話 & 書籍検索サービスの紹介
この記事は ドワンゴ Advent Calendar 2019 の13日目の記事です。
はじめに
私は趣味で 書籍検索サービス(β) というサービスの開発のお手伝いをしております。
このサービスの機能としては
- 検索ワードで書籍の検索ができる
- バーコードリーダーにより、書籍に印刷されたバーコードより書籍情報を取得できる
- 複数のストアの販売ページへのリンクが有る
- 現在はAmazonとBOOK☆WALKERのみ。他サイトも追加予定。
- OpenBDによる書籍紹介目的で自由に利用できる画像が取得できる
- Twitterに画像として書籍情報を送信するための画像が生成できる
というものがあります。
https://search.riel.live/detail/9784040729558/
このサービスでは、このような画像を生成することができます。
書籍を紹介する際に、ぜひ使っていただけると幸いです。技術面の紹介
このサービスの、技術面での特徴は以下のとおりです。
- Firebase Hostingでホスティングされている
- Cloud FunctionsとNext.jsでSSRされている
- APIサーバもすべてNext.js上に実装されている
- CORS実行できない外部APIがあるので、全ての外部通信をNext.jsでプロキシしている
- おすすめ書籍はContentfulで管理されている
Next.jsは開発開始当時の最新である9系列を利用し、最新機能であるAPIサーバ機能を利用して作っております。
Next.js をFirebaseでホスティングした際に遭遇したトラブル
以上の構成で、起こったトラブルの一覧です。
9.0.7 以降の Next.js において(examlpeも含めて)壊れる
Firebase example: couldn't find a
pages
directory · Issue #8893 · zeit/next.js · GitHub具体的にどのような問題かというと、Firebase+Next.jsの公式サンプルを使ってアプリケーションを作った場合ですら
couldn't find a pages directory
というエラーを出して、ホスティングに失敗するという不具合です。私はこれで(9.0.7リリース直後に)休日を1日潰しました。
いくら不具合っぽいところを直しても治らなく、ローカルのdevサーバだけは動くという状況になりました。この件に関してはIssueがあり、執筆時点(2019/12/07現在)でもまだクローズされていません。
そのため、現時点では9.0.6にバージョンを固定してNext.jsを使っております。SSR時に react-hooks が使えない
9.0.6に起因する問題なのかもしれませんが、SSR時にreact-hooksを使うとSSRでのビルドに失敗します。
この問題を回避するためにはDynamic Importを使用してhooksを使いたいコンポーネントを分割する方法と、react-hooksを諦めてHoC版のコンポーネントを使う方法の2種類があります。
いまはその2つの手法を併用しております。できればSSRを使用しておきたい箇所には使えないので困っています。
いい解決法があれば教えて下さい。
next/config
がうまく引き回されない
publicRuntimeConfig
にdotenv経由で取り出したContentfulのAPIキーを書いているのですが、他ページから開いて遷移したときにエラーページになります。
これも壊れたままになっているのですが、良い直し方がわからずまだ放置しています。まとめ
個人的には、残念ながら「ReactでどうしてもSSRがしたい!」というとき以外に使うと罠に落ちるイメージになりました。
Reactは非常に良いプロダクトですし、このサービスを作ることによってReactへの理解が深まった気がします。
みなさんもぜひReactでプロダクトを作ってみてください。あと、皆さん本を買って読みましょう!世界にはまだまだ面白い物語が待っていますよ!
以下は、デモを兼ねたこのサービスで生成した画像による自分のおすすめ作品です。
https://search.riel.live/detail/9784041028209/
https://search.riel.live/detail/9784049126631/
(ドワンゴポーカー部推薦図書です)
- 投稿日:2019-12-09T23:43:01+09:00
Next.jsの公式チュートリアルをやってみた(1/3)
概要
業務でNext.jsを触る機会を得たため、まずは下記チュートリアルをやってみました。
チュートリアルの章ごとの備忘録をまとめます。
https://nextjs.org/learn/basics/getting-startedGetting Started
Introduction
Next.js良いぞ!と機能紹介しているページ。一旦読み飛ばす
Setup
作業用の
hello-next
ディレクトリを作成し、以降ここで作業。
まずは、npmコマンドで必要なライブラリをインストールし、
ビュー用のファイルを置くことになるpagesディレクトリを作成します。$ mkdir hello-next $ cd hello-next $ npm init -y $ npm install --save react react-dom next $ mkdir pages
- 作業後のディレクトリ構成
$ ll drwxr-xr-x 524 aota staff 16K 12 9 21:00 node_modules/ -rw-r--r-- 1 aota staff 263K 12 9 21:00 package-lock.json -rw-r--r-- 1 aota staff 324B 12 9 21:00 package.json drwxr-xr-x 2 aota staff 64B 12 9 21:08 pages/
- package.jsonの中身
$ cat package.json { "name": "hello-next", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "next": "^9.1.4", "react": "^16.12.0", "react-dom": "^16.12.0" } }上記package.jsonのscriptsを下記に書き換えるよう指示されるので置換する。
"scripts": { "dev": "next", "build": "next build", "start": "next start" }
- 置換後のpackage.jsonの中身
$ cat package.json { "name": "hello-next", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "next", "build": "next build", "start": "next start" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "next": "^9.1.4", "react": "^16.12.0", "react-dom": "^16.12.0" } }あとは、下記コマンド実行で準備完了!
$ npm run devブラウザで
http://localhost:3000
にアクセスしてみる
何が表示された?と聞かれるので、404だよーと選択し次へ。404 Page
404表示されてるかい?それでOKだ
Creating Our First Page
先程作ったpagesディレクトリ配下にindex.jsを作る
$ vi pages/index.js
- pages/index.jsの中身
const Index = () => ( <div> <p>Hello Next.js</p> </div> ); export default Index;ブラウザで先程開いていた
http://localhost:3000
をリロードしてみる
Hello Next.jsと表示されてるのでOK!ここで、シンタックスエラーを起こしてみよう。
タグの閉じタグを消してみる
- pages/index.jsの中身
const Index = () => ( <div> <p>Hello Next.js </div> ); export default Index;ブラウザで再度
http://localhost:3000
を開いてみる
何が表示された?と聞かれるので、Syntaxエラーがでたよと選択し次へ
Handling Errors
Next.jsではデフォルトでブラウザにエラーを出力するよ。
エラーを直したらリロードしなくてもページが再レンダリングされるんだ。You are Awesome
ファーストステップはこれでおしまい!
Navigate Between Pages
単体のページは、できたので今度は複数のページを作ろう。
先程作ったpagesディレクトリ配下にabout.jsを作る
$ vi pages/about.js
- pages/about.jsの中身
export default function About() { return ( <div> <p>This is the about page</p> </div> ); }ブラウザで
http://localhost:3000/about
にアクセスしてみる
うむ、ちゃんと表示された。次は、これらのページをつなげてみる
ただ<a>
タグで書いちゃうと、サーバーとの通信が発生するよね。
クライアントサイドでナビゲーションしたいから、<Link>
タグを使うよ。
使い方は次回。Using Link
作っていた
pages
配下のindex.js
を<Link>
タグを使うように書き換える。$ vi pages/index.js
- pages/index.jsの中身
// This is the Link API import Link from 'next/link'; const Index = () => ( <div> <Link href="/about"> <a>About Page</a> </Link> <p>Hello Next.js</p> </div> ); export default Index;ブラウザで
http://localhost:3000/about
にアクセスしてみる
About Page
をクリックしてaboutページに遷移後、ブラウザの戻るボタンで戻っても
正常にindexページに遷移することが確認できる。
(サーバーにリクエストがいっていないのは、Chromeの開発者モードのNetworkタブから確認できる)Client-Side History Support
先程、ブラウザの戻るボタンで戻れたのは、
location.history
を<Link>
タグがハンドリングしてたからなんだ。Adding Link Props
属性値を追加したいときあるよね。
title属性を追加したい場合は、こうするはずだ。
- pages/index.jsの中身
// This is the Link API import Link from 'next/link'; const Index = () => ( <div> <Link href="/about"> <a title="About Page">About Page</a> </Link> <p>Hello Next.js</p> </div> ); export default Index;これは正常にレンダリングされる。では下記の場合はどうか。
- pages/index.jsの中身
// This is the Link API import Link from 'next/link'; const Index = () => ( <div> <Link href="/about" title="About Page"> <a>About Page</a> </Link> <p>Hello Next.js</p> </div> ); export default Index;ブラウザで
http://localhost:3000/about
にアクセスしてみる
Chromeの開発者モードのElementsタブを確認してもtitle属性は、<a>
タグに追加されていない。
また、Chromeの開発者モードのConsoleタブを確認するとエラーがでている。
何が表示された?と聞かれるので、title属性はないしConsoleでエラーがでたよと選択し次へLink is Just a Higher Order Component (HOC)
実際、title属性を追加しても
<Link>
タグには、なんの影響もないんだ。
なぜなら、hrefか近い属性しか<Link>
タグは許容していないからだ。
(<Link>
タグもといnext/link
がHOCというもので、通常のコンポーネントではないということ。)属性を追加したい場合は、その下のコンポーネント、今回であれば
<a>
タグに渡せば良い。
また<Link>
タグの子コンポーネントとして必要なことは、onClick属性が使えることだけだ。Link is Simple, but Powerful
これまで基本的な
<Link>
タグの使い方をみてきたけど、
今後のレッスンではもっと面白い使い方を紹介するよ。
それまでは、Next.js Routing documentationをみてみよう。Using Shared Components
Introduction
共通のHeaderコンポーネントを作って、複数のページで読み込んでみよう
Create the Header Component
componentsディレクトリを作成し、配下にHeader.jsを作成する。
$ mkdir components $ vi components/Header.js
- components/Header.jsの中身
import Link from 'next/link'; const linkStyle = { marginRight: 15 }; const Header = () => ( <div> <Link href="/"> <a style={linkStyle}>Home</a> </Link> <Link href="/about"> <a style={linkStyle}>About</a> </Link> </div> ); export default Header;Using the Header Component
pages/index.jsとpages/about.jsを編集し、先程作成したHeaderコンポーネントを読みこむ。
- pages/index.jsの中身
import Header from '../components/Header'; export default function Index() { return ( <div> <Header /> <p>Hello Next.js</p> </div> ); }
- pages/about.jsの中身
import Header from '../components/Header'; export default function About() { return ( <div> <Header /> <p>This is the about page</p> </div> ); }ブラウザで
http://localhost:3000
にアクセスしてみる
Headerコンポーネントが読み込まれ、Home、Aboutそれぞれで遷移することが確認できる。ここでちょっとした変更をしてみよう。
1.$ npm run dev
で起動したアプリケーションをSTOPする。(Ctrl
+C
)
2. componentsディレクトリをcompsにリネームしてみる。
3. pages/index.jsおよびpages/about.jsで、Headerコンポーネントの読み込み先を../comps/Header
に変更する。
4. アプリケーションを再起動する($ npm run dev
)
- pages/index.jsの中身
import Header from '../comps/Header'; export default function Index() { return ( <div> <Header /> <p>Hello Next.js</p> </div> ); }
- pages/about.jsの中身
import Header from '../comps/Header'; export default function About() { return ( <div> <Header /> <p>This is the about page</p> </div> ); }何が表示された?と聞かれるので、問題なく動いたと選択し次へ
The Component Directory
ちゃんと動くよね。コンポーネントを置くディレクトリは好きな名前にしていいんだ。
特別な名前が必要なのは、/pages
ディレクトリと、/public
ディレクトリだけなんだ。
もちろん、コンポーネントは/pages
ディレクトリ配下にも作成できる。
ただ今回そうしなかったのは、Headerコンポーネントへの直接のリンクが不要だからだ。
(http://localhost:3000/Header
にアクセスするのは不要。)※ ここでcompsは、componentsにリネームして戻しておく。アプリケーションの再起動も忘れずに
The Layout Component
共通のCSSスタイルをあてたいときあるよね。
共通のレイアウトコンポーネントを作っていくよ。まず、componentsディレクトリ配下にMyLayout.jsを作成するよ。
- components/MyLayout.jsの中身
import Header from './Header'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const Layout = props => ( <div style={layoutStyle}> <Header /> {props.children} </div> ); export default Layout;そして、このレイアウトコンポーネントを使うためにpages/index.jsとpages/about.jsを編集する。
- pages/index.jsの中身
import Layout from '../components/MyLayout'; export default function Index() { return ( <Layout> <p>Hello Next.js</p> </Layout> ); }
- pages/about.jsの中身
import Layout from '../components/MyLayout'; export default function About() { return ( <Layout> <p>This is the about page</p> </Layout> ); }ブラウザで
http://localhost:3000/about
にアクセスしてみる
レイアウトコンポーネントで指定したborder: '1px solid #DDD'
の枠線があることが確認できた。次に、components/MyLayout.jsから
{props.children}
を削除してみよう。
- components/MyLayout.jsの中身
import Header from './Header'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const Layout = props => ( <div style={layoutStyle}> <Header /> </div> ); export default Layout;ブラウザで
http://localhost:3000/about
にアクセスしてみる
何が表示された?と聞かれるので、コンテンツが消えたと選択し次へRendering Child Components
{props.children}
を削除すると、Layoutコンポーネントの中においたコンテンツはレンダリングされないんだ。
Layoutコンポーネントの作成方法をひとつ紹介したけど、他にも作成する方法があるよ。Method 1 - Layout as a Higher Order Component
// components/MyLayout.js import Header from './Header'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const withLayout = Page => { return () => ( <div style={layoutStyle}> <Header /> <Page /> </div> ); }; export default withLayout;// pages/index.js import withLayout from '../components/MyLayout'; const Page = () => <p>Hello Next.js</p>; export default withLayout(Page);// pages/about.js import withLayout from '../components/MyLayout'; const Page = () => <p>This is the about page</p>; export default withLayout(Page);Method 2 - Page content as a prop
// components/MyLayout.js import Header from './Header'; const layoutStyle = { margin: 20, padding: 20, border: '1px solid #DDD' }; const Layout = props => ( <div style={layoutStyle}> <Header /> {props.content} </div> ); export default Layout;// pages/index.js import Layout from '../components/MyLayout.js'; const indexPageContent = <p>Hello Next.js</p>; export default function Index() { return <Layout content={indexPageContent} />; }// pages/about.js import Layout from '../components/MyLayout.js'; const aboutPageContent = <p>This is the about page</p>; export default function About() { return <Layout content={aboutPageContent} />; }Using Components
今回は、2つの方法について紹介したよ。
1. 共通のHeaderコンポーネントとして
2. Layoutコンポーネントとしてコンポーネントは、スタイル適用やページのLayoutなどに使えるんだ。
また、NPMモジュールから読み込む方法もあるよ。
- 投稿日:2019-12-09T23:40:50+09:00
FunctionalComponent と React Hooks 関数の自分なりの解釈と説明用文書
はじめに
以前は React コンポーネントはクラスで実装するのが常識だったが、シンプルな実装として SFC (Stateless Functional Component) という関数での実装の手法も存在した。
そして、 React Hooks が利用できるようになって、関数コンポーネントでも state を持つことができるようになり、 FC (Functinal Component) による実装が可能になった。ここでは FC と React Hooks の組み合わせについて自分なりの解釈と説明をします。
これまでの SFC について
SFC は上位のコンポーネントから渡した props の値が変更された時に実行され、レンダリングされます。
props の値が変化しない場合は再レンダリングはされません。
そのため、状態は常に SFC の利用側でコントロールする必要がありました。const ExampleSFC = (props: { count: number, // onIncrementClick のハンドラを外側で実装して、インクリメントした値を count props で渡す感じ onIncrementClick: () => void }) => { return ( <div> <div>count: {count}</div> <button onClick={onIncrementClick}>+1</button> </div> ); }FC について
React Hooks を使うことで、関数コンポーネントで State (状態) を持つことができるようになります。
ここでは Hooks のうち、 useState と useCallback を使います。const ExampleFC = (props: { initialCount: number = 0 }) => { // コンポーネント内で count を持つ const [count, setCount] = useState(initialCount); // +1 がクリックされたら count をインクリメントするコールバックを定義する const onIncrementClick = useCallback(() => { setCount(count => count + 1); }, []); return ( <div> <div>count: {count}</div> <button onClick={onIncrementClick}>+1</button> </div> ); }FC の実行とレンダリングのタイミング
SFC は props が変化したら実行 (=レンダリング) されるということを SFC の項目で説明したが、 FC は props に加えて state と、 consume している context の値が変化した際も実行される。
ここでいう state は useState によるもの、 context は useContext によるものを指す。useState について
React Hooks で提供されている useState は、それを実行したコンポーネントでのみ持つ "状態" である。
const [value, setValue] = useState();この場合、
setValue()
を実行してvalue
を更新した際に、その FC が実行される。useContext について
React Hooks で提供されている useContext は、その親 (先祖) コンポーネントで作成されたコンテキスト (
createContext()
) で提供されている値を監視&参照する事ができる。
ざっくりconst value = useContext(context)
の様な形で context の値を consume することができる。
ここで consume している値が更新された際に FC が実行される。useContext を扱うことは少ないかもしれないが、 react-redux で提供されている useSelector も同様の仕組みとなっている。 (と認識している)
useCallback について
上記のように、 props, state, context の値が更新されると FC が実行されるが、イベントハンドラを以下のように実装するとパフォーマンス的な問題が発生する。
const ExampleFC = (props: { initialCount: number = 0 }) => { // コンポーネント内で count を持つ const [count, setCount] = useState(initialCount); return ( <div> <div>count: {count}</div> <button onClick={() => setState(count => count + 1)}>+1</button> </div> ); }ここでの問題は、 button の onClick に設定している
() => setState(count => count + 1)
この関数の定義が FC が実行される度に再生成されるという問題である。この ExampleFC では initialCount props と count state のみ参照しているため、実行される頻度は高くないため問題が顕在化しにくいが、多数の props, state, context を参照し、多数の callback を持つ FC になると問題が顕著になると思う。
前述した
FC について
の項目で説明した Example は以下のように useCallback を使ってコールバックを生成している。const onIncrementClick = useCallback(() => { setCount(count => count + 1); }, []);useCallback を使うことによって、何度 FC が実行されてもハンドラ関数の定義は一度のみに限定することが可能となる。
これはメモ化という仕組みによるものだが、メモ化については割愛する。useCallback の第2引数は依存する値 (dependencies) となっており、指定した値が更新された際にコールバックが再生成される。
コールバック内で使用可能な変数は、コールバックが生成された際の値となっているため、基本的にはコールバック内で使う変数を第2引数で指定する。(ちょっと複雑だが、例で記載している setCount は第2引数で指定していない。 useState で生成した set 関数は常に一意となるため、第2引数で指定する必要はない。)
useEffect について
useEffect はコンポーネントのライフサイクルを管理する上で非常に重要になる Hooks 関数である。
クラスコンポーネントでいう componentDidMount, componentDidUpdate, componentDidUnmount などの副作用を実現するために useEffect を利用する。useEffect は以下のように利用する。
const ExampleFC = (props: { initialCount: number = 0 }) => { // コンポーネント内で count を持つ const [count, setCount] = useState(initialCount); // +1 がクリックされたら count をインクリメントするコールバックを定義する const onIncrementClick = useCallback(() => { setCount(count => count + 1); }, []); useEffect(() => { console.log(`count が更新されました。新しい値は ${count} です。`); }, [count]); return ( <div> <div>count: {count}</div> <button onClick={onIncrementClick}>+1</button> </div> ); }前述したとおり、 FC は参照する props, state, context が変化した度に実行される。
例は count しか変化する変数がないため若干わかりにくい例かもしれないが、 count が更新される度に ExampleFC が実行され、上から下まで一連の実装が処理される。useEffect も例外ではなく毎回通って実行されるが、メモ化という仕組みにより、第2引数で指定した依存変数が変化した場合にのみ第1引数で指定したコールバックが実行される。
なので、更新を感知して実行したい処理を第1引数に書き、どの変数が変化したら第1引数で指定した処理を実行したいのかを第2引数で指定する形となる。最後に
拙い文章でアレだが、こんな主張である。
- 投稿日:2019-12-09T23:40:19+09:00
create-react-appを使わずにReactの環境構築をして周辺ツールとかを理解する
Reactに入門したのはいいもののcreate-react-appでプロジェクトを作成すると無駄なファイルが多かったり、環境構築を丸投げしてるのでちょっと色々まずいなと思い、
create-react-appを使わずに環境構築をしていこうと思います。ドキュメントを見ると学習用やちょっと試したいときに最適である的なことが書いてありますね。
以下はこの記事での手順をまとめたスクリプトです。npmが入っている前提ですが、実行ディレクトリにプロジェクトが生成されると思います。
最終的なスクリプト
PROJECT_NAME=myapp echo $PROJECT_NAME mkdir $PROJECT_NAME cd $PROJECT_NAME cat <<EOF > package.json { "scripts": { "dev": "webpack-dev-server --open" } } EOF npm i react react-dom webpack webpack-cli npm i -D typescript ts-loader webpack-dev-server @types/{react,react-dom} mkdir dist mkdir src cat <<EOF > dist/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title> my-app </title> </head> <body> <div id="root"></div> <script src="bundle.js"></script> </body> </html> EOF cat <<EOF > src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; const App = () => <div> hello word </div> ReactDOM.render(<App/>, document.getElementById("root")) EOF cat <<EOF > tsconfig.json { "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true, "allowSyntheticDefaultImports" :true } } EOF cat <<EOF > webpack.config.js const path = require('path'); module.exports = { mode: "development", entry: './src/index.tsx', module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: [ '.tsx', '.ts', '.js' ], }, devtool: 'inline-source-map', devServer: { contentBase: './dist', }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), } }; EOF
実行後は以下のようになると思います。
├── dist │ └── index.html ├── package-lock.json ├── package.json ├── src │ └── index.tsx ├── tsconfig.json └── webpack.config.js必要なモジュール
- React/ReactDOM
- webpack
- ソースコードを1つのファイルにまとめるツール。v4 から cli も必須になった。
- webpack-dev-server
- Hotリロード機能。
- トランスパイラ
- babel, ts-loaderとか
- classとかJSXとかトランスパイルしてJavaScriptコードを生成する。
npmでいれる。
npm i react react-dom webpack webpack-clitypescriptを使う場合は型定義とかも
npm i -D typescript ts-loader webpack-dev-server @types/{react,react-dom}作成するファイル
- index.html
- index.js
- package.json
- ts-config.json
- webpack.config.js
index.html
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>my-app</title> </head> <body> <div id="root"></div> <script src="bundle.js"></script> </body> </html>htmlファイル。webpackでbundleしたファイルを読み込む。
index.ts
src/index.tsx
import React from "react" import ReactDOM from "react-dom" const App = () => <div> hello word </div> ReactDOM.render(<App />, document.getElementById("root"))エントリーファイル。Hello Worldだけ。
tsconfig.json
tsconfig.json
{ "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true, "allowSyntheticDefaultImports": true } }とりあえず公式のと同じです。適宜追加していきます。
webpack.config.js
webpackの設定ファイルです。初見ででかいconfigファイルを見るとビビってしまいますが、最低限必要なのはそこまで多くないです。
https://webpack.js.org/guides/typescript/
https://webpack.js.org/concepts/
webpack.config.js
const path = require("path") module.exports = { mode: "development", entry: "./src/index.tsx", module: { rules: [ { test: /\.tsx?$/, use: "ts-loader", exclude: /node_modules/, }, ], }, resolve: { extensions: [".tsx", ".ts", ".js"], }, devtool: "inline-source-map", devServer: { contentBase: "./dist", }, output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, }mode(dev or prod)と入出力パスとloaderとresolveなんで簡単ですね。
この部分はwebpack-dev-serverの設定です。
devtool: "inline-source-map", devServer: { contentBase: "./dist", },https://webpack.js.org/guides/development/#using-webpack-dev-server
package.json
npm init -y
みたいにすると雛形が生成されますが、最低限ファイルがあればいいようです。echo "{}" > package.jsonnpm i react, react-domあとオプションが微妙ですね。
--save-dev | -D
でdevDependencyに追加
--save | -S
で両方とかですかね??npxはローカルにインストールしたパッケージを実行できる(正確ではないかも)
npx webpack-dev-server --open
npm script
に追記して{ "scripts": { "start": "webpack-dev-server --open" } }npm run startで実行できるようになる。
あとは適当にテストとかlinterとかいれるだけ。
...用語とか言葉とかあやふやなところ適宜修正します。
その他
git
echo "node_modules" > .gitignore git init git add . git commit -m "create project"styled-components
npm i styled-components npm i @types/styled-components
- 投稿日:2019-12-09T23:19:07+09:00
Next.jsで画像の表示
- 投稿日:2019-12-09T22:56:44+09:00
初心を思い出して好き勝手にアプリを開発した
そもそもクソアプリじゃないアプリとは?
サービスを開発する時にクソじゃないアプリにするために考えることと言えば下記のようなことになるかと思います。
- 見た目がちゃんとしている
- 致命的な不具合がほぼ無い
- ちょっとした不具合も少ない
- サービスを成すためにちゃんと必要な機能が整えられている
- 利用規約やプライバシーポリシーがある
- マネタイズ方法を考えている
今回のアプリの方針
でもクソじゃないアプリを作るというのは疲れます。後半のリリース前は利用規約や色々な説明文の調整など、やりたくないことばかりをやるハメになります。
ということで今回はクソアプリということなので何も気にせず好き勝手に作ろうと思い立ちました。元々アイデアとしてはあったけど、普段だったら絶対作らないだろうなぁ…というものをいい機会ですので作り進めてみました。具体的には下記のような方針です。
- 見た目上変なところがあってもいい
- 不具合の確認とかしないので最初の方に作った機能とか壊れている可能性がある
- 作りたいところを優先的に作っているので使いづらくていも良い
- マネタイズもアクセス解析も利用規約もなにもない
他者無視でとにかく自己中心的に作り進めていくだけのクソアプリです。
どういうアプリか
Web上で気軽に音を入力して再生できるアプリです。
ふと曲がコード進行含め頭に思い浮かぶことってよくありますよね。でもメモるのも面倒だしと思って放置していると100%忘れて二度と思い出せません。そんなときのためにWebで手軽に曲をメモしておけるツールです。記事を書き終わってToneEverybodyというわけのわからない名前でデプロイし終わってから思いましたが「キョクメモ」というサービス名だったら最高ですね! もしくは曲メモ帳。
リアルタイムでデータ保存
FirebaseのRealtime Databaseを使っており、データはどんどん保存されていきます。Realtime Databaseなので、他の人が編集したものもどんどんリアルタイムで共有されていきます。ですので例えばチャンネル1は自分が、チャンネル2は友達が入力、みたいにしてその場で曲作りをしていくことも出来ます。
音を入力したら進む
メトロノームに合わせて音を正確に入力していくのはなかなか難しいため、一つ音を入力したら次の時間に進む、という形にしています。そのため楽器を弾くのに慣れていなくてもゆっくり一音ずつ正確に入力していくことが出来ます。
入力方式
PCの場合はキーボードの横並びであるz~m、r~pがドレミファソラシになっており、その上はピアノと同様シャープ音を利用することが出来ます。短いタイミングで押せば同じタイミングの和音にすることが出来ます。録音時は画面上にキーボードも出現するため、スマホの場合でも入力できます(この場合は同じチャンネル内の和音にすることは出来ません)。
バックスペースで戻る&削除、スペースキーで音を入力せずに次の時間に進むことが出来ます。スマホの場合も同様のボタンを用意しています。
再生の仕組み
入力後、もちろん実際に再生して音を鳴らして確認することが出来ます。元々ぼんやりとMIDIを使って再生することを考えていたのですが、よく使われているっぽいライブラリが大量にmp3ファイルを使っていて重すぎたりしてちょっと厳しいなぁ、と思っている時にTone.jsというフレームワークを発見しました。
Tone.jsはW3Cの草案であるWeb Audio APIというものを利用する形で作られています。そのため多分WebKitやFirefoxあたりでしか動作しませんが、スマホを含めたブラウザ上で動作するすごいやつです。Tone.jsの公式サイトにサンプルもたくさんありますので、この記事を読み終わった後にでも見てみてください。
具体的な再生方法
色々やり方があるのであくまで1例です。今回はチャンネルを利用したかったのでその形です。
まず再生のタイミングでPolySynthというものをチャンネルごとに作成します。これは音作りなどが出来る機能ですので一応チャンネル毎に音を変えることが出来ます(が今回は時間が足りず実装していません)。
const synthes = music.channels.map(_channel => new Tone.PolySynth().toMaster() )その後、実際に音を入れていきます。再生のタイミングではなく、楽譜のように音を鳴らすタイミングを指定するだけです。triggerAttackReleaseは鍵盤を押して離す、という流れを指定できます。
channel.playNotes.forEach(playNote => { Tone.Transport.schedule(function(_time) { synth.triggerAttackRelease(playNote.note.name, 0.5) }, playNote.time * 0.5) })あとはstartすれば再生されます。
Tone.Transport.start()開発方法
今回はReactを使いました。React Hooksでちゃんと一から何かしらを作ったことがなかったので試してみました。ちなみにReact Hooksというのはクラスではなく関数コンポーネントを利用する方法です。ちゃちゃっと適当に開発しようと思ったのでReduxを入れていないのですが、やはりプロパティやコールバックを橋渡しするのが増えてきてしんどくなってきたため、だんだん後悔しています。
プロジェクトの作成方法
1ページの簡単なアプリを作るだけだったので、Next.jsなども使わずcreate-react-appで作成しています。今ってもうTypeScriptも含めて初期化出来るみたいで簡単でした。
npx create-react-app my-app --typescript
ハマったところ
キーボード入力
PCではキーボード入力を可能にしているため、コンポーネントが作成された最初のタイミングで
document.addEventListener
でキー入力イベントを設定する必要があります。クラスコンポーネントであればcomponentDidMount内でやればいいだけなのですが、関数の場合はそれがありません。そのためuseEffectという機能を利用する必要があります。useEffect(() => { document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) })再描画の時にreturnしている部分が呼び出されるため、ここでリスナーを破棄しています。
ただ、本来であれば下記のようにするべきです。
useEffect(() => { document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) }, [])最後に空配列を入れています。これは、この中身が変わったらuseEffect内も生成し直してくれ、という指定です。空配列を指定することで中身が変わることがないため、ページが最初に表示された時以外は再生成されなくなります。
ではなぜそうせずに何度も生成し直しているのかというところですが、これは関数コンポーネントの基本的な仕組みが関係しています。
コンポーネント内の値は固定される
なんとなくuseStateを使ってステートを変更していると、簡単に値を変更できて楽だなぁというイメージがあるのではないかと思います。しかし実際にはそういうわけではなく、ステートが変わる毎に値の束縛が行われており、あくまでも再生成される時に最新の値で構築されているだけになります。
そのため普段は意識することがないかもしれませんが、今回のようにdocumentのキーボードイベントを取得したりすると、この場合だけ中のstateが全く変わっていないということがわかります。実際に、画面上の鍵盤で入力すると正しく音を登録できるのですが、キーボードで入力すると正常に音を登録できない、ということが発生しました。
そのため、ちょっと実装的にはアレかもですので他にやり方があるのかもしれませんが、キーボードイベントも毎回登録し直すような形にしています。
useRefというのもある
useRefというものがあり、ステートとは違い、参照を保存しておくことが出来るものです。
const musicRef = useRef<Music>()こんな感じで指定して
channels.current
に例えば今回はDOMの参照を入れておきます。実は今回キーボードでは和音を入力できるようにするため、キー入力後にsetTimeoutを利用し、ちょっと時間が経って入力が無かったら次の時間軸に進む、ということを行っています。
これも先程のdocumentイベントのように曲者で、コンポーネント外から色々しようとするので正しくコンポーネント内の値を取得、更新できません。
そのため、useRefを使ってデータの参照を行って色々と処理してあげる必要があります。ちなみに、この場合
createRef
も正常に動作しないため、ちょっと面倒な手順を用意する必要もあったりします。1このように、画面上のDOMからのイベント以外の色々なイベントを駆使しようとすると結構複雑になります。ハマりどころ且つ、処理が複雑になりきれいに書きにくくなると思いますので、可能であれば予め気をつけておいた方が良いような気がします。
まとめ
ということで完成させる気のないというクソアプリの紹介でした。下記はやろうと思っていたけど出来ていません。他に色々とやることがあるので多分もう作りません。
- テンポを変える
- 音色を変える
- 一時的に倍速入力できるようにする
- スマホでも1チャンネルに和音を入力できるようにする
- 登録時に削除せずに戻るだけの機能
- 再生時に超かっこいいアニメーション表示
- 自分の曲をロックして他の人が編集できないようにする
- 録音再開時はチャンネルの最後からスタート
- mp3でダウンロード
- 簡単タップで5000兆円手に入る機能
下記にアップしてありますのでぜひ遊んでみてください。使い方の説明ももちろんありませんので雰囲気でどうぞ! ちなみに誰かがもうぐちゃぐちゃにしちゃっていてもう無いかもしれませんがmayという昔作ったノスタルジックな曲をサンプルでちょっとだけ入れています。
Tone Everybody
https://tone-everybody.netlify.com/ちょっとでも良いなと思うところがあったら是非いいねをお願いします!
(曲を登録したら続きを作りたくなってしまう不思議……)
- 投稿日:2019-12-09T22:56:44+09:00
初心を思い出して好き勝手に開発した曲メモアプリ
そもそもクソアプリじゃないアプリとは?
サービスを開発する時にクソじゃないアプリにするために考えることと言えば下記のようなことになるかと思います。
- 見た目がちゃんとしている
- 致命的な不具合がほぼ無い
- ちょっとした不具合も少ない
- サービスを成すためにちゃんと必要な機能が整えられている
- 利用規約やプライバシーポリシーがある
- マネタイズ方法を考えている
今回のアプリの方針
でもクソじゃないアプリを作るというのは疲れます。後半のリリース前は利用規約や色々な説明文の調整など、やりたくないことばかりをやるハメになります。
ということで今回はクソアプリということなので何も気にせず好き勝手に作ろうと思い立ちました。元々アイデアとしてはあったけど、普段だったら絶対作らないだろうなぁ…というものをいい機会ですので作り進めてみました。具体的には下記のような方針です。
- 見た目上変なところがあってもいい
- 不具合の確認とかしないので最初の方に作った機能とか壊れている可能性がある
- 作りたいところを優先的に作っているので使いづらくていも良い
- マネタイズもアクセス解析も利用規約もなにもない
他者無視でとにかく自己中心的に作り進めていくだけのクソアプリです。
どういうアプリか
Web上で気軽に音を入力して再生できるアプリです。
ふと曲がコード進行含め頭に思い浮かぶことってよくありますよね。でもメモるのも面倒だしと思って放置していると100%忘れて二度と思い出せません。そんなときのためにWebで手軽に曲をメモしておけるツールです。記事を書き終わってToneEverybodyというわけのわからない名前でデプロイし終わってから思いましたが「キョクメモ」というサービス名だったら最高ですね! もしくは曲メモ帳。
リアルタイムでデータ保存
FirebaseのRealtime Databaseを使っており、データはどんどん保存されていきます。Realtime Databaseなので、他の人が編集したものもどんどんリアルタイムで共有されていきます。ですので例えばチャンネル1は自分が、チャンネル2は友達が入力、みたいにしてその場でジャムセッションみたいな感じで曲作りをしていくことも出来ます。
音を入力したら進む
メトロノームに合わせて音を正確に入力していくのはなかなか難しいため、一つ音を入力したら次の時間に進む、という形にしています。そのため楽器を弾くのに慣れていなくてもゆっくり一音ずつ正確に入力していくことが出来ます。
入力方式
PCの場合はキーボードの横並びであるz~m、r~pがドレミファソラシになっており、その上はピアノと同様シャープ音を利用することが出来ます。短いタイミングで押せば同じタイミングの和音にすることが出来ます。録音時は画面上にキーボードも出現するため、スマホの場合でも入力できます(この場合は同じチャンネル内の和音にすることは出来ません)。
バックスペースで戻る&削除、スペースキーで音を入力せずに次の時間に進むことが出来ます。スマホの場合も同様のボタンを用意しています。
再生の仕組み
入力後、もちろん実際に再生して音を鳴らして確認することが出来ます。元々ぼんやりとMIDIを使って再生することを考えていたのですが、よく使われているっぽいライブラリが大量にmp3ファイルを使っていて重すぎたりしてちょっと厳しいなぁ、と思っている時にTone.jsというフレームワークを発見しました。
Tone.jsはW3Cの草案であるWeb Audio APIというものを利用する形で作られています。そのため多分WebKitやFirefoxあたりでしか動作しませんが、スマホを含めたブラウザ上で動作するすごいやつです。Tone.jsの公式サイトにサンプルもたくさんありますので、この記事を読み終わった後にでも見てみてください。
具体的な再生方法
色々やり方があるのであくまで1例です。今回はチャンネルを利用したかったのでその形です。
まず再生のタイミングでPolySynthというものをチャンネルごとに作成します。これは音作りなどが出来る機能ですので一応チャンネル毎に音を変えることが出来ます(が今回は時間が足りず実装していません)。
const synthes = music.channels.map(_channel => new Tone.PolySynth().toMaster() )その後、実際に音を入れていきます。再生のタイミングではなく、楽譜のように音を鳴らすタイミングを指定するだけです。triggerAttackReleaseは鍵盤を押して離す、という流れを指定できます。
channel.playNotes.forEach(playNote => { Tone.Transport.schedule(function(_time) { synth.triggerAttackRelease(playNote.note.name, 0.5) }, playNote.time * 0.5) })あとはstartすれば再生されます。
Tone.Transport.start()開発方法
今回はReactを使いました。React Hooksでちゃんと一から何かしらを作ったことがなかったので試してみました。ちなみにReact Hooksというのはクラスではなく関数コンポーネントを利用する方法です。ちゃちゃっと適当に開発しようと思ったのでReduxを入れていないのですが、やはりプロパティやコールバックを橋渡しするのが増えてきてしんどくなってきたため、だんだん後悔しています。
プロジェクトの作成方法
1ページの簡単なアプリを作るだけだったので、Next.jsなども使わずcreate-react-appで作成しています。今ってもうTypeScriptも含めて初期化出来るみたいで簡単でした。
npx create-react-app my-app --typescript
ハマったところ
キーボード入力
PCではキーボード入力を可能にしているため、コンポーネントが作成された最初のタイミングで
document.addEventListener
でキー入力イベントを設定する必要があります。クラスコンポーネントであればcomponentDidMount内でやればいいだけなのですが、関数の場合はそれがありません。そのためuseEffectという機能を利用する必要があります。useEffect(() => { document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) })再描画の時にreturnしている部分が呼び出されるため、ここでリスナーを破棄しています。
ただ、本来であれば下記のようにするべきです。
useEffect(() => { document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) }, [])最後に空配列を入れています。これは、この中身が変わったらuseEffect内も生成し直してくれ、という指定です。空配列を指定することで中身が変わることがないため、ページが最初に表示された時以外は再生成されなくなります。
ではなぜそうせずに何度も生成し直しているのかというところですが、これは関数コンポーネントの基本的な仕組みが関係しています。
コンポーネント内の値は固定される
なんとなくuseStateを使ってステートを変更していると、簡単に値を変更できて楽だなぁというイメージがあるのではないかと思います。しかし実際にはそういうわけではなく、ステートが変わる毎に値の束縛が行われており、あくまでも再生成される時に最新の値で構築されているだけになります。
そのため普段は意識することがないかもしれませんが、今回のようにdocumentのキーボードイベントを取得したりすると、この場合だけ中のstateが全く変わっていないということがわかります。実際に、画面上の鍵盤で入力すると正しく音を登録できるのですが、キーボードで入力すると正常に音を登録できない、ということが発生しました。
そのため、ちょっと実装的にはアレかもですので他にやり方があるのかもしれませんが、キーボードイベントも毎回登録し直すような形にしています。
useRefというのもある
useRefというものがあり、ステートとは違い、参照を保存しておくことが出来るものです。
const musicRef = useRef<Music>()こんな感じで指定して
channels.current
に例えば今回はDOMの参照を入れておきます。実は今回キーボードでは和音を入力できるようにするため、キー入力後にsetTimeoutを利用し、ちょっと時間が経って入力が無かったら次の時間軸に進む、ということを行っています。
これも先程のdocumentイベントのように曲者で、コンポーネント外から色々しようとするので正しくコンポーネント内の値を取得、更新できません。
そのため、useRefを使ってデータの参照を行って色々と処理してあげる必要があります。ちなみに、この場合
createRef
も正常に動作しないため、ちょっと面倒な手順を用意する必要もあったりします。1このように、画面上のDOMからのイベント以外の色々なイベントを駆使しようとすると結構複雑になります。ハマりどころ且つ、処理が複雑になりきれいに書きにくくなると思いますので、可能であれば予め気をつけておいた方が良いような気がします。
まとめ
ということで完成させる気のないというクソアプリの紹介でした。下記はやろうと思っていたけど出来ていません。他に色々とやることがあるので多分もう作りません。
- テンポを変える
- 音色を変える
- 一時的に倍速入力できるようにする
- スマホでも1チャンネルに和音を入力できるようにする
- 登録時に削除せずに戻るだけの機能
- 再生時に超かっこいいアニメーション表示
- 自分の曲をロックして他の人が編集できないようにする
- 録音再開時はチャンネルの最後からスタート
- mp3でダウンロード
- 簡単タップで5000兆円手に入る機能
下記にアップしてありますのでぜひ遊んでみてください。使い方の説明ももちろんありませんので雰囲気でどうぞ! ちなみに誰かがもうぐちゃぐちゃにしちゃっていてもう無いかもしれませんがmayという昔作ったノスタルジックな曲をサンプルでちょっとだけ入れています。
Tone Everybody
https://tone-everybody.netlify.com/ちょっとでも良いなと思うところがあったら是非いいねをお願いします!
(曲を登録したら続きを作りたくなってしまう不思議……)
- 投稿日:2019-12-09T21:00:05+09:00
React初心者がHookを使ってTo Doリストを作ったらこうなった
目次
- 初めに
- 完成品
- 前提条件
- 各コンポーネントについて
- Appコンポーネント
- Headerコンポーネント
- Formコンポーネント
- ToDoListコンポーネント
- 最終的なコード
- 参考
初めに
React Hooks Advent Calendar 2019 10日目の記事を書かせていただきました。
まだReactを勉強し始めて間もないですが、やはりTo Doリストは一度作っておくべきだろうと思って色々Qiitaを漁っていてあることに気が付きました。
「React Hookを使って実装している人、ほとんどいないな...」と(私が見てないだけだと思います)。
そこで私はReact Hookの一つ、useStateを使ってTo Doリストを作りました。変数名とか結構ガバガバだと思います。
React初心者ですがお叱りなどあったら甘んじて受け入れる所存です。よろしくお願いします。※React Hooks Advent Calendar 2019 7日目の@daishiさんの記事では私とは別のやり方でTo Doリスト作ってますね。
完成品
完成品がこちらです。Netlifyでデプロイもしてます。→ To Do List Used By Hooks
私はブラウザはなんとなくVivaldi使っているのでVivaldiでアクセスした画面になります。スタイルはほぼ全部
Bootstrap 4
に投げました。コードはGitHubに投げました。
https://github.com/koralle/todo-usedby-hook
前提条件
- Windows 10
- node.js v12.13.1
- yarn 1.19.2
- Bootstrap 4
- create-react-app 3.2.0
各コンポーネントについて
App
を親コンポーネントにして、画面上からHeader
、Form
、ToDoList
で構成しました。Appコンポーネント
return
の中の不自然に見える<div>
タグはBootstrap
であれこれするためにこうなってます...App.jsimport React, {useState} from 'react'; import './App.css'; import Header from './Header'; import Form from './Form' import ToDoList from './ToDoList' const App = () => { // ToDoリストのStateをtoDoListと定義 const [toDoList, setToDoList] = useState([]); // toDoListに項目を追加 const addToDoList = (Title, Content) => { setToDoList(toDoList.concat({"title": Title, "content": Content})); } // toDoListの項目を削除 const deleteToDoList = (index) => { setToDoList(toDoList.filter(item => toDoList[index] !== item)); } return ( <div> <Header headerTitle="To Do List Used By Hooks"/> <div> <div> <Form add={addToDoList}/> <ToDoList list={toDoList} delete={deleteToDoList}/> </div> </div> </div> ); } export default App;Headerコンポーネント
header
タグをrenderしているだけなので,あまり書くことないです。Header.jsimport React from 'react'; import './Header.css' const Header = (props) => { return ( <header> <h1>{props.headerTitle}</h1> </header> ); } export default Header;Formコンポーネント
このコンポーネント、できればステートレスにしたかったんですけど上手くいきませんでした。
AppコンポーネントからaddToDoList()
をpropsとして引き渡してます。Form.jsimport React, {useState} from 'react'; import './Form.css' const Form = (props) => { const [toDoTitle, setToDoTitle] = useState(""); const [toDoContent, setToDoContent] = useState(""); // Titleフォームの状態の制御 const handleToDoTitleInputChange = (e) => { setToDoTitle(e.target.value); } // Contentフォームの状態の制御 const handleToDoContentInputChange = (e) => { setToDoContent(e.target.value); } // 入力フォームのクリア const resetInputField = () => { setToDoTitle(""); setToDoContent(""); } // 項目の追加を確定 const callAddToDoList = (e) => { e.preventDefault(); props.add(toDoTitle,toDoContent); resetInputField(); } return ( <form> <div> <div> <span>Title</span> </div> <input type="text" onChange={handleToDoTitleInputChange} value={toDoTitle} /> </div> <div> <div> <span>Detail</span> </div> <textarea onChange={handleToDoContentInputChange} value={toDoContent} ></textarea> </div> <div> <button type="submit" onClick={callAddToDoList} > ADD </button> </div> </form> ); } export default Form;ToDoListコンポーネント
AppコンポーネントのStateである
toDoList
と項目を削除する関数deleteToDoList()
をpropsとして引き渡しています。ToDoList.jsimport React from 'react'; import './ToDoList.css' const ToDoList = (props) => { // AppコンポーネントのStateであるtoDoListをpropsとして受け取って // mapでループする const toDoListItems = props.list.map( (item, i) => { return ( <div key={i} > <div> <h5>Title: {item.title}</h5> <p>Content: {item.content}</p> <button onClick={() => props.delete(i)} > Delete </button> </div> </div> ); } ); return ( <div> <h1>Your Tasks: {props.list.length}</h1> <div> {toDoListItems} </div> </div> ); } export default ToDoList;最終的なコード
上のコードに
Bootstrap4
でスタイルを付ける為にclassNameをつけ足していった結果が以下のコードになります。
(CSSファイルは割愛しました。)./public/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <title>To Do List used by Hooks</title> </head> <body> <div id="root"></div> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> </body> </html>App.jsimport React, {useState} from 'react'; import './App.css'; import Header from './Header'; import Form from './Form' import ToDoList from './ToDoList' const App = () => { const [toDoList, setToDoList] = useState([]); const addToDoList = (Title, Content) => { setToDoList(toDoList.concat({"title": Title, "content": Content})); } const deleteToDoList = (index) => { setToDoList(toDoList.filter(item => toDoList[index] !== item)); } return ( <div className="toDo-app"> <Header headerTitle="To Do List Used By Hooks"/> <div className="toDo-app-body container"> <div className="toDo-main"> <Form add={addToDoList}/> <ToDoList list={toDoList} delete={deleteToDoList}/> </div> </div> </div> ); } export default App;Header.jsimport React from 'react'; import './Header.css' const Header = (props) => { return ( <header className="toDo-header"> <h1>{props.headerTitle}</h1> </header> ); } export default Header;Form.jsimport React, {useState} from 'react'; import './Form.css' const Form = (props) => { const [toDoTitle, setToDoTitle] = useState(""); const [toDoContent, setToDoContent] = useState(""); const handleToDoTitleInputChange = (e) => { setToDoTitle(e.target.value); } const handleToDoContentInputChange = (e) => { setToDoContent(e.target.value); } const resetInputField = () => { setToDoTitle(""); setToDoContent(""); } const callAddToDoList = (e) => { e.preventDefault(); props.add(toDoTitle,toDoContent); resetInputField(); } return ( <form className="toDo-form"> <div className="toDo-form-title input-group"> <div className="input-group-prepend"> <span className="input-group-text">Title</span> </div> <input type="text" className="form-control shadow" onChange={handleToDoTitleInputChange} value={toDoTitle} /> </div> <div className="toDo-form-detail input-group"> <div className="input-group-prepend"> <span className="input-group-text">Detail</span> </div> <textarea className="form-control shadow" aria-label="Detail" onChange={handleToDoContentInputChange} value={toDoContent} ></textarea> </div> <div className="toDo-form-add"> <button className="btn btn-success" type="submit" onClick={callAddToDoList} > ADD </button> </div> </form> ); } export default Form;ToDoList.jsimport React from 'react'; import './ToDoList.css' const ToDoList = (props) => { const toDoListItems = props.list.map( (item, i) => { return ( <div key={i} className="card toDo-item"> <div className="card-body"> <h5 className="card-title">Title: {item.title}</h5> <p className="card-text">Content: {item.content}</p> <button className="btn btn-danger" onClick={() => props.delete(i)} > Delete </button> </div> </div> ); } ); return ( <div className="toDo-List"> <h1>Your Tasks: {props.list.length}</h1> <div> {toDoListItems} </div> </div> ); } export default ToDoList;参考
To Doリストのベースはこちらの記事で学ばせてもらいました。
Formコンポーネントを作る際にかなり参考にしました。
(2020年のフロントエンドマスターになりたければこの9プロジェクトを作れで紹介されていました)。
- 投稿日:2019-12-09T18:26:36+09:00
(React)親要素から受け取ったデータの型は使いたいものだけ使えば良い
型でいつも怒られているTypeScriptに何故か怒られなかったので共有します。
MainComponentはSubComponentをimportして使用しています。
MainComponentではTestType
という型を定義して、その型が持っている要素に'comp1'
と'comp2'
という文字列を代入しています。そして、それをSubComponentに渡しています。MainComponent.tsximport { SubComponent } from './SubComponent'; const MainComponent =>{ type TestType = { comp1: string; comp2: string; }; const components: TestType = { comp1: 'comp1', comp2: 'comp2', }; return( <SubComponent {...components} /> ); }SubComponentはMainComponentから受け取ったpropsからcomp1というキーの値をdiv要素に入れて返しています。
ここで、SubComponentでもTestPropsという型を定義しています。この型はMainComponentで定義したTestTypeという型からcomp2というキーを削除したものになります。
SubComponent.tsxtype TestProps = { comp1: string; }; const SubComponent = (props: TestProps) => { console.log(props); // {comp1: "comp1", comp2: "comp2"} return ( <div>{props.comp1}</div> // <div>{props.comp2}</div> // コメントアウトを外すとエラーになる ) }; export default SubComponent;上の例はコンパイル成功しますが、SubComponentの中で
props.comp2
を使用しようとすると、エラーになってしまいます。これはprops
の型がTestProps
と定義しているからです(TestProps
はcomp2という要素を持っていない)。しかし、
props
の中身を見るとcomp2
の要素も入っています。これはTypeScriptがデータは受け取るが、型宣言に入っていないものは弾くようによしなにやってくれてるんだと思います(調べてもよく分からなかったので分かる人教えてください)。
確かにデータ受け取っても宣言してないもの以外は使用できないようにすれば問題なさそうなので、上手く活用していきたいと思います。
- 投稿日:2019-12-09T16:16:10+09:00
ReactFire v2 alphaを試してみる(Firestore編)
はじめに
前回の記事では、react-firebase-hooksを試したのですが、12月6日のReact Day Berlinでは、ReactFireのプレゼンがありました。こちらはとても先進的で期待できます。
本記事では、Firestoreの部分を移植してみたいと思います。
ReactFireとは
ReactFire v1は2016年5月のリリースを最後に、その後deprecateされ、Firebase JS SDKを直接使うようアナウンスされています。それが2019年7月よりv2として再度開発しているようです。
https://github.com/FirebaseExtended/reactfire
現在、masterブランチがv2になっています。
READMEには、
Status: Alpha. ReactFire is meant for React Concurrent Mode, which is only available in experimental React builds.
と書いてあるのですが、ざっとコードを見たところ、Suspenseを使っているだけで、Concurrent Modeでなくても動きそうです。
コーディング
import React, { Suspense, useState } from "react"; import ReactDOM from "react-dom"; import "firebase/firestore"; import { FirebaseAppProvider, useFirestoreCollectionData, useFirestore } from "reactfire"; const firebaseConfig = { apiKey: "...", authDomain: "...", databaseURL: "...", projectId: "...", storageBucket: "...", messagingSenderId: "...", appId: "..." }; const TodoList = () => { const firestore = useFirestore(); const todosRef = firestore().collection("todos"); const values = useFirestoreCollectionData(todosRef, { idField: "id" }); return ( <ul> {values.map(value => ( <li key={value.id}>{value.title}</li> ))} </ul> ); }; const NewTodo = () => { const [title, setTitle] = useState(""); const [pending, setPending] = useState(false); const firestore = useFirestore(); const add = async () => { setTitle(""); setPending(true); try { await firestore() .collection("todos") .add({ title }); } finally { setPending(false); } }; return ( <div> <input value={title} onChange={e => setTitle(e.target.value)} /> <button type="button" onClick={add}> Add </button> {pending && "Pending..."} </div> ); }; const App = () => { return ( <FirebaseAppProvider firebaseConfig={firebaseConfig}> <Suspense fallback={<div>Loading...</div>}> <div> <TodoList /> <NewTodo /> </div> </Suspense> </FirebaseAppProvider> ); }; const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);動きました。nextではだめで、canaryを使う必要がありました。
おわりに
ドキュメントがまだ追いついていなくて、ソースコードを読まないといけなかったです。
- 投稿日:2019-12-09T16:14:46+09:00
FirebaseとReactとReduxで多機能チャットを実装しよう【環境準備編】
FirebaseとReactとReduxで多機能チャットを実装しよう【環境準備編】
記事を順次書いていきます。
リンク集
- 環境準備編 ←イマココ
- チャットルーム選択編
- メッセージ機能編
- チャットルーム設定編
- デモページ
- github(公開準備中)TL;DR
FirebaseとReactを使って、メッセージを送り合う簡単なチャットアプリの解説記事はいくつか見かけますよね。
だけど、実務で使うチャットアプリならもっと複雑なはず。
そこで以下の7機能を持つチャットアプリを実装しました。
- 個人チャット
- グループチャット
- 新規チャットルーム作成
- チャットルームの削除
- チャットルームから退出
- チャットルームメンバー変更(追加/削除)
- チャットルーム名とアイコンの設定
デザインは、イケてるアプリ風にできるMaterial-uiにお任せした。
FirebaseとReactで開発するための環境構築
Firebase、React、Reduxを使って開発していくための環境を構築します。
React関連モジュールのインストール
Reactの環境をイチから構築するのは面倒です。
そこでcreate-react-appを使います。create-react-app
以下のコマンドでcreate-react-appしてください。
npx create-react-app react-chat※npmでグローバルインストールしたcreate-react-appは非推奨になりました。
過去にインストールしたことがある場合は、以下コマンドでアンインストールしておきましょう。npm uninstall -g create-react-app"react-chat"というフォルダが作成されます。
Reactの開発環境が作られています。(簡単すぎて感激)npm startしてブラウザ画面が表示されることを確認しましょう。
npm startその他モジュールのインストール
Reduxとmaterial-uiに必要なモジュールをインストールします。
npm install --save redux react-redux redux-actions immer @material-ui/core @material-ui/iconsFirebaseとの紐付け
Firebaseとの紐付けを行なっていきます。
1. Firebaseプロジェクトの作成
Firebase Consoleからプロジェクトを作成します。
今回は"react-chat"という名前にしました。
Analyticsを使うかどうか聞かれますが、どちらでも良いです。
使う場合はGoogle Analyticsのアカウントが必要になります。次へ次へと進めてプロジェクトを作成。
プロジェクトが作成されたら、</>マークをクリックしてWebアプリを追加しましょう。
アプリ名はなんでも良いです。「このアプリのFirebase Hostingも設定します。」にチェック。
次へ次へと進めてデプロイ完了させてください。
※モジュールインストールやログインなど基本的なところは割愛。
公式ドキュメントが詳しいです。2. configファイルの作成
まず、Firebaseモジュールをインストールします。
npm install firebase --saveFirebaseコンソールから[歯車ボタン] > [プロジェクトと設定] > [全般]の下部 [Firebase SDK snippet] > [構成]をチェック
で以下のような構成をコピーできます。firebaseフォルダとconfig.jsファイルを作成してペーストしてください。
src/firebase/config.jsexport const firebaseConfig = { apiKey: "YOUR-API-KEY", authDomain: "YOUR-PROJECT-ID.firebaseapp.com", databaseURL: "https://YOUR-PROJECT-ID.firebaseio.com", projectId: "YOUR-PROJECT-ID", storageBucket: "YOUR-PROJECT.appspot.com", messagingSenderId: "xxxxxxxxxx", appId: "x:xxxxxxxxxxx:web:xxxxxxxxxxxxxxxxxx", measurementId: "G-XXXXXXXXX" };index.jsファイルを作成して、設定をexportします。
src/firebase/index.jsimport firebase from 'firebase'; import { firebaseConfig } from './firebase/config.js'; export const firebaseApp = firebase.initializeApp(firebaseConfig); export const database = firebaseApp.database();3. Firebase init
initする前に、デフォルトリソースロケーションを選択しましょう。
(これ設定しておかないと、後から怒られます。)コンソールで
firebase init
を実行します。どのプロダクトを使うか選択。
今回はDatabase, Hosting, Storageの3つです。
プロジェクトの選択では、[Use an existing project] > [react-chat]を選択。
基本はEnterで進めていきます。
public directoryは"build"に変更してください。
また、single-page appとして設定しましょう。
これでFirebaseの設定は完了です。
4. Firebase Hostingにデプロイ
ビルドしてデプロイします。
npm run buildfirebase deployFirebase Hostingの公開先URLにアクセスして
npm start
の時と同じ画面が表示されることを確認しましょう。次はFirebaseとReactとReduxで多機能チャットを実装しよう【メッセージ編】です。
(現在編集中)参考記事
- 投稿日:2019-12-09T15:26:29+09:00
【React】Firebase Authで、アカウントに複数のプロバイダを紐付ける
Firebase Authentication、とても便利ですよね。
今回、Firebase Authenticationを使って、
Googleログイン
したアカウントに対して、電話番号
を紐付けます。Firebaseの準備
Fireabse Authの設定方法はこちら。
ログイン方法として、電話番号
と有効
にしておいてください。googleアカウントでの認証
まずはgoogleアカウントで認証させます。
signInWithPopup
というメソッドを使います。電話番号での認証
次に、電話番号での認証です。
reCAPTCHA
の設定がちょっとめんどくさいので注意しましょう。
signInWithPhoneNumber
というメソッドを使います。アカウントに複数の認証プロバイダをリンク
ここまで完了したら、
currentUserに対して、linkAndRetrieveDataWithCredential
というメソッドを使い、アカウントに複数の認証プロバイダをリンクさせることができます。とても簡単に実装することができました。
2要素認証
とかも簡単に実現できますね。今回作成したソースコードをgithutbで公開しています。こちらから
(めっちゃ無駄ですが、アーキテクチャとして、Reduxを採用しています。。。
ちょっと前にリリースされたhooks APIに対応した機能を使ってみたくて...)参考: 公式ドキュメント
以上です。
- 投稿日:2019-12-09T11:55:42+09:00
【必読】React開発で役に立つプラクティス8選
以下は、jsmanifestさんの記事、8 Useful Practices for React Apps You Should Knowの日本語訳です。
【必読】Reactアプリで役に立つプラクティス8選(8 Useful Practices for React Apps You Should Know)
mediumで私を見つけてくださいね。
Reactはとても多くのステージへの変化を行ってきました。そしてそれらは、必ず私達を驚かせます。
最初、私達はmixinsを使って、インターフェースを作ったり管理したりしていました。そして次にクラスコンポーネントというコンセプトが到来し、現在は、Reactでの私達の開発方法を変えるreact hooksです。
他になにか素晴らしいことを知っていますか? 例えば、Reactで使える、アプリをより良くするのに役立つ巧みなトリックです。
この記事では、全てのReactデベロッパーが知っておくべき8つの高度なトリックを紹介します。このリストの全てがあなたにとって目新しいものではないと思いまが、最低でも1つ、便利なトリックを見つけることを願っています。
これが、あなたが知っておくべきreactで使える8つのトリックです。
1. 文字列でReact要素を生成する(Create react elements with strings)
まず1つ目は、HTML DOMタグを表すシンプルな文字列でReact DOM要素を作る方法です。
例えば、変数に
div
という文字列を代入することで、Reactコンポーネントを作ることができます。import React from 'react' const MyComponent = 'div' function App() { return ( <div> <h1>Hello</h1> <hr /> <MyComponent> <h3>I am inside a {'<div />'} element</h3> </MyComponent> </div> ) }Reactは、
React.createElement
を呼び、与えられた文字列を使って、内部で要素を生成します。これってクールでしょ?Material-UIのようなコンポーネントライブラリでよく使われます。コンポーネントのルートnodeを決める
component
propsを宣言できます。function MyComponent({ component: Component = 'div', name, age, email }) { return ( <Component> <h1>Hi {name}</h1> <div> <h6>You are {age} years old</h6> <small>Your email is {email}</small> </div> </Component> ) }このように使用することができます。
function App() { return ( <div> <MyComponent component="div" name="George" age={16} email="george@gmail.com"> </div> ) }カスタムコンポーネントを渡すこともできます。
function Dashboard({ children }) { return ( <div style={{ padding: '25px 12px' }}> {children} </div> ) } function App() { return ( <div> <MyComponent component={Dashboard} name="George" age={16} email="george@gmail.com"> </div> ) }2. Error Boundariesを使う(Use Error Boundaries)
JavaScriptでは、私達はほとんどのエラーを
try/catch
ー発生したエラーを"catch"できるコードのブロックー で対応しています。エラーがcatchブロックに補足された時、私達はアプリがクラッシュするのを避けることができます。例はこのようなものです。
function getFromLocalStorage(key, value) { try { const data = window.localStorage.get(key) return JSON.parse(data) } catch (error) { console.error } }Reactは結局ただのJavaScriptなので、エラーをキャッチしてハンドルできると考えるでしょう。しかし、Reactの性質上、コンポーネント内でのJavScriptエラーは内部のstateを破壊し、将来のレンダーにおいてcryptic errors(不可解なエラー)を引き起こします。
このような理由で、ReactチームはError Boundariesを紹介しています。そして全てのReactデベロッパーがそれらを知っておくべきです。そうすれば自らのReactアプリで使用することができます。
Error Boundaries登場以前のエラー発生の問題点は、これらの不可解なエラーが発生した時、Reactはそれらに対処したり、リカバリーする方法を私達に与えていなかったことです。ですので、私達は皆Error Boundariesが必要なのです。
Error Boundariesは、Reactコンポーネントです。コンポーネントツリーのどこででもエラーをキャッチすることができ、ログ出力し、クラッシュしたコンポーネントツリーの代わりにフォールバック用UIを表示させることができます。Error Boundariesは、レンダー中、ライフサイクルメソッド内、そしてError Boundaries以下に存在する全体のツリーのコンストラクタ内でエラーをキャッチします(そしてこれが、私達のアプリのトップのどこかでError Boundariesを宣言しレンダーする理由です)。
class ErrorBoundary extends React.Component { constructor(props) { super(props) this.state = { hasError: false } } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true } } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service logErrorToMyService(error, errorInfo) } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1> } return this.props.children } }そして、普通のコンポーネントと同じように使用できます。
<ErrorBoundary> <MyWidget /> </ErrorBoundary>3. 前回の値を保持する(Retain Previous Values)
propsやstateを更新している時、あなたは
React.useRef
を使ってそれらの前回の値を保持することができます。例えば、1つの配列のアイテムの、現在と前回の変更を追跡するために、前回の値が割り当てられる
React.useRef
と、現在の値が割り振られるReact.useState
を使用することができます。function MyComponent() { const [names, setNames] = React.useState(['bob']) const prevNamesRef = React.useRef([]) React.useEffect(() => { prevNamesRef.current = names }) const prevNames = prevNamesRef.current return ( <div> <h4>Current names:</h4> <ul> {names.map((name) => ( <li key={name}>{name}</li> ))} </ul> <h4>Previous names:</h4> <ul> {prevNames.map((prevName) => ( <li key={prevName}>{prevName}</li> ))} </ul> </div> ) }これは正しく動作します。なぜなら
React.useEffect
はレンダーが完了した後に実行されるからです。
setNames
が実行される時、コンポーネントは再度レンダーを行い、prefNamesRef
は前回の値を保持します。なぜなら、前回のレンダーから見てReact.useEffect
が最後に実行されるコードだからです。
そしてuseEffect
内でprevNamesRef.current
に値が再び代入されるので、次回のレンダー時には前回のnamesの値を保持します。4. 値チェックにReact.useRefを使う(Use React.useRef for flexible non-stale value checks)
React Hooksが登場する以前、ComponentがDOMにマウントされた後に、dataのフェッチなどの処理を行いたい場合、私達は
componentDidMount
というクラスコンポーネントのStaticメソッドを使用していました。React Hooksが発表された時、それはすぐにコンポーネントを書く最も人気の手段となりました。コンポーネントがアンマウントされた後にstateがセットされるのを防ぐために、コンポーネントがマウントされたかを監視したい時、私達はこのようにするでしょう。
import React from 'react' import axios from 'axios' class MyComponent extends React.Component { mounted = false state = { frogs: [], error: null, } componentDidMount() { this.mounted = true } componentWillUnmount() { this.mounted = false } async fetchFrogs = (params) => { try { const response = await axios.get('https://some-frogs-api.com/v1/', { params }) if (this.mounted) { this.setState({ frogs: response.data.items }) } } catch (error) { if (this.mounted) { this.setState({ error }) } } } render() { return ( <div> <h4>Frogs:</h4> <ul> {this.state.frogs.map((frog) => <li key={frog.name}>{frog.name}</li> )} </ul> </div> ) } }React Hooksに統合後、Hooksは
componentDidMount
を持っていません。そして、アンマウント後に起こったstateアップデートによるメモリリークのコンセプトは未だにhooksに当てはまります。しかし、React Hooksを使ったcomponentDidMount
に似た方法はReact.useEffect
を使用することです。なぜなら、それはコンポーネントがレンダリングされた後に実行されるからです。もしあなたがReact.useRef
を使って、マウントされた値を割り当てるなら、以下のクラスコンポーネントの例で同じことができます。import React from 'react' import axios from 'axios' function MyComponent() { const [frogs, setFrogs] = React.useState([]) const [error, setError] = React.useState(null) const mounted = React.useRef(false) async function fetchFrogs(params) { try { const response = await axios.get('https://some-frogs-api.com/v1/', { params, }) if (mounted.current) { setFrogs(response.data.items) } } catch (error) { if (mounted.current) { setError(error) } } } React.useEffect(() => { mounted.current = true return function cleanup() { mounted.current = false } }, []) return ( <div> <h4>Frogs:</h4> <ul> {this.state.frogs.map((frog) => ( <li key={frog.name}>{frog.name}</li> ))} </ul> </div> ) }リレンダーをせず最新の変更をトラッキングするもう一つ別の例は、以下のように、
React.useMemo
と併せて使用することです。(source)function setRef(ref, value) { // Using function callback version if (typeof ref === 'function') { ref(value) // Using the React.useRef() version } else if (ref) { ref.current = value } } function useForkRef(refA, refB) { return React.useMemo(() => { if (refA == null && refB == null) { return null } return (refValue) => { setRef(refA, refValue) setRef(refB, refValue) } }, [refA, refB]) }もしref propsが変更され、定義されたら、新たな関数を作ります。
つまり、Reactは古いフォークされたrefをnull、新しいフォークされたrefを現在のrefで呼ぶことを意味します。そしてReact.useMemo
が使われているので、refの変数たちはrefA
またはrefB
のref propsが変更されるまで記憶されます。ナチュラルなクリーンアップが起こるのです。5. 他の要素に依存したカスタム要素にReact.useRefを使う(Use React.useRef for customizing elements that depend on other elements)
React.useRef
にはいくつか役に立つユースケースがあります。例えば、React.useRef
自体をref propsに割り当てるなどです。function MyComponent() { const [position, setPosition] = React.useState({ x: 0, y: 0 }) const nodeRef = React.useRef() React.useEffect(() => { const pos = nodeRef.current.getBoundingClientRect() setPosition({ x: pos.x, y: pos.y, }) }, []) return ( <div ref={nodeRef}> <h2>Hello</h2> </div> ) }もし
div
要素の座標位置を取得したいなら、この例が役に立ちます。しかし、アプリケーションのどこかに存在する別の要素の位置をアップデートしたかったり、それに応じて条件ロジックを変更したり適応する場合、ベストな方法はref callback function pattern
を使うことです。callback function patternを使用すると、ReactコンポーネントまたはHTML DOM要素を第一引数として受け取ります。下の例は、コールバック関数である
setRef
がどこでref
propsに適応されるかを示すシンプルなものです。直接React.useRef
をDOM要素に割り当てるのとは対象的に、setRef
の内側では必要なことは何でもできるということがわかります:const SomeComponent = function({ nodeRef }) { const ownRef = React.useRef() function setRef(e) { if (e && nodeRef.current) { const codeElementBounds = nodeRef.current.getBoundingClientRect() // Log the <pre> element's position + size console.log(`Code element's bounds: ${JSON.stringify(codeElementBounds)}`) ownRef.current = e } } return ( <div ref={setRef} style={{ width: '100%', height: 100, background: 'green' }} /> ) } function App() { const [items, setItems] = React.useState([]) const nodeRef = React.useRef() const addItems = React.useCallback(() => { const itemNum = items.length setItems((prevItems) => [ ...prevItems, { [`item${itemNum}`]: `I am item # ${itemNum}'`, }, ]) }, [items, setItems]) return ( <div style={{ border: '1px solid teal', width: 500, margin: 'auto' }}> <button type="button" onClick={addItems}> Add Item </button> <SomeComponent nodeRef={nodeRef} /> <div ref={nodeRef}> <pre> <code>{JSON.stringify(items, null, 2)}</code> </pre> </div> </div> ) }6. 高階コンポーネント(Higher Order Components)
プレインなJavaScriptで強力な再利用性を持つ関数を作るよくあるパターンは高階関数です。Reactも結局はJavaScriptなので、内部で高階関数を使用することができます。
再利用性のあるコンポーネントには、高階コンポーネントとというトリックが使えます。
高階コンポーネントとは、あなたがコンポーネントを引数にして、返り値としてコンポーネントを返す関数を持っているときのことです。高階関数がロジックを抽象化し、他の関数間で共有されるように、高階コンポーネントもコンポーネントをロジックから切り離し、他のコンポーネント間で共有することができます。つまり、たくさんの再利用可能なコンポーネントを採用し、アプリケーション内で実際に繰り返し使うことが可能だということなのです。
これは高階コンポーネントの例です。このスニペットでは、高階コンポーネントであるwithBorderはカスタムコンポーネントを引数として取り込み、隠されたミドルレイヤーコンポーネントを返します。そして、親がこの高階コンポーネントをレンダーするとき、これはコンポーネントとして呼ばれ、ミドルレイヤーコンポーネントからから渡されたpropsを受け取ります。
import React from 'react' // Higher order component const withBorder = (Component, customStyle) => { class WithBorder extends React.Component { render() { const style = { border: this.props.customStyle ? this.props.customStyle.border : '3px solid teal', } return <Component style={style} {...this.props} /> } } return WithBorder } function MyComponent({ style, ...rest }) { return ( <div style={style} {...rest}> <h2>This is my component and I am expecting some styles.</h2> </div> ) } export default withBorder(MyComponent, { border: '4px solid teal', })7. Render Props
React内で使う私のお気に入りのトリックの一つは、render prop patternです。複数のコンポーネントでコードを共有するという問題を解決する点において、これは高階コンポーネントに似ています。Render propsは、レンダーに必要なものを全て差し戻すことが目的の関数をはっきりとさせます。
Render props expose a function who's purpose is to pass back everything the outside world needs to render its children.Reactでコンポーネントをレンダーする最もベーシックな方法は、このようなものでしょう。
function MyComponent() { return <p>My component</p> } function App() { const [fetching, setFetching] = React.useState(false) const [fetched, setFetched] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) const [frogs, setFrogs] = React.useState([]) React.useEffect(() => { setFetching(true) api .fetchFrogs({ limit: 1000 }) .then((result) => { setFrogs(result.data.items) setFetched(true) setFetching(false) }) .catch((error) => { setError(error) setFetching(false) }) }, []) return ( <MyComponent fetching={fetching} fetched={fetched} fetchError={fetchError} frogs={frogs} /> ) }Render Propsを使う場合は、その子をレンダーするpropは慣例的にrenderと呼ばれます。
function MyComponent({ render }) { const [fetching, setFetching] = React.useState(false) const [fetched, setFetched] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) const [frogs, setFrogs] = React.useState([]) React.useEffect(() => { setFetching(true) api .fetchFrogs({ limit: 1000 }) .then((result) => { setFrogs(result.data.items) setFetched(true) setFetching(false) }) .catch((error) => { setError(error) setFetching(false) }) }, []) return render({ fetching, fetched, fetchError, frogs, }) }この例では、
MyComponent
はrender prop componentとして参照するコンポーネントの例です。なぜなら、MyComponent
はpropとしてのrender
を期待していますし、子をレンダーするためのそれを実行するからです。
これはReactにおいて、とてもパワフルなパターンです。renderのコールバックを通して引数として、共有されたstateとdataを渡すことができるのです。複数のコンポーネントにおいて、そのコンポーネントはリレンダーされ、再利用されることが可能になります。function App() { return ( <MyComponent render={({ fetching, fetched, fetchError, frogs }) => ( <div> {fetching ? 'Fetching frogs...' : fetched ? 'The frogs have been fetched!' : fetchError ? `An error occurred while fetching the list of frogs: ${fetchError.message}` : null} <hr /> <ul style={{ padding: 12, }} > {frogs.map((frog) => ( <li key={frog.name}> <div>Frog's name: {frog.name}</div> <div>Frog's age: {frog.age}</div> <div>Frog's gender: {frog.gender}</div> </li> ))} </ul> </div> )} /> ) }8. Memoize
Reactデベロッパーとして知っておくべき最も大事なことの一つは、
React.memo
のようなコンポーネントのパフォーマンス最適化です。それを知っておくと、無限ループのようなとてもひどいエラーを防ぐことができます。Reactアプリにおいて、Memoization(メモ化)を行う、いくつかの方法があるので是非読んでみてください。
締め
ここでこの投稿は終わりです!この記事があなたにとって価値があることを願います。
Mediumも見てくださいね。
感想
まず、今回の記事は翻訳が難しかったです。英語もさることながら、自分のReact Hooksの理解が浅いなと実感しました。
逆にReact HooksやよりReactの理解が進めば、これらのトリックはとても役に立ちそうだと思いました。間違い等見つけましたら教えていただけるとありがたいです。
- 投稿日:2019-12-09T11:55:42+09:00
【必読】React開発で役に立つTips 8選
以下は、jsmanifestさんの記事、8 Useful Practices for React Apps You Should Knowの日本語訳です。
【必読】Reactアプリで役に立つプラクティス8選(8 Useful Practices for React Apps You Should Know)
mediumで私を見つけてくださいね。
Reactはとても多くのステージへの変化を行ってきました。そしてそれらは、必ず私達を驚かせます。
最初、私達はmixinsを使って、インターフェースを作ったり管理したりしていました。そして次にクラスコンポーネントというコンセプトが到来し、現在は、Reactでの私達の開発方法を変えるreact hooksです。
他になにか素晴らしいことを知っていますか? 例えば、Reactで使える、アプリをより良くするのに役立つ巧みなトリックです。
この記事では、全てのReactデベロッパーが知っておくべき8つの高度なトリックを紹介します。このリストの全てがあなたにとって目新しいものではないと思いまが、最低でも1つ、便利なトリックを見つけることを願っています。
これが、あなたが知っておくべきreactで使える8つのトリックです。
1. 文字列でReact要素を生成する(Create react elements with strings)
まず1つ目は、HTML DOMタグを表すシンプルな文字列でReact DOM要素を作る方法です。
例えば、変数に
div
という文字列を代入することで、Reactコンポーネントを作ることができます。import React from 'react' const MyComponent = 'div' function App() { return ( <div> <h1>Hello</h1> <hr /> <MyComponent> <h3>I am inside a {'<div />'} element</h3> </MyComponent> </div> ) }Reactは、
React.createElement
を呼び、与えられた文字列を使って、内部で要素を生成します。これってクールでしょ?Material-UIのようなコンポーネントライブラリでよく使われます。コンポーネントのルートnodeを決める
component
propsを宣言できます。function MyComponent({ component: Component = 'div', name, age, email }) { return ( <Component> <h1>Hi {name}</h1> <div> <h6>You are {age} years old</h6> <small>Your email is {email}</small> </div> </Component> ) }このように使用することができます。
function App() { return ( <div> <MyComponent component="div" name="George" age={16} email="george@gmail.com"> </div> ) }カスタムコンポーネントを渡すこともできます。
function Dashboard({ children }) { return ( <div style={{ padding: '25px 12px' }}> {children} </div> ) } function App() { return ( <div> <MyComponent component={Dashboard} name="George" age={16} email="george@gmail.com"> </div> ) }2. Error Boundariesを使う(Use Error Boundaries)
JavaScriptでは、私達はほとんどのエラーを
try/catch
ー発生したエラーを"catch"できるコードのブロックー で対応しています。エラーがcatchブロックに補足された時、私達はアプリがクラッシュするのを避けることができます。例はこのようなものです。
function getFromLocalStorage(key, value) { try { const data = window.localStorage.get(key) return JSON.parse(data) } catch (error) { console.error } }Reactは結局ただのJavaScriptなので、エラーをキャッチしてハンドルできると考えるでしょう。しかし、Reactの性質上、コンポーネント内でのJavScriptエラーは内部のstateを破壊し、将来のレンダーにおいてcryptic errors(不可解なエラー)を引き起こします。
このような理由で、ReactチームはError Boundariesを紹介しています。そして全てのReactデベロッパーがそれらを知っておくべきです。そうすれば自らのReactアプリで使用することができます。
Error Boundaries登場以前のエラー発生の問題点は、これらの不可解なエラーが発生した時、Reactはそれらに対処したり、リカバリーする方法を私達に与えていなかったことです。ですので、私達は皆Error Boundariesが必要なのです。
Error Boundariesは、Reactコンポーネントです。コンポーネントツリーのどこででもエラーをキャッチすることができ、ログ出力し、クラッシュしたコンポーネントツリーの代わりにフォールバック用UIを表示させることができます。Error Boundariesは、レンダー中、ライフサイクルメソッド内、そしてError Boundaries以下に存在する全体のツリーのコンストラクタ内でエラーをキャッチします(そしてこれが、私達のアプリのトップのどこかでError Boundariesを宣言しレンダーする理由です)。
class ErrorBoundary extends React.Component { constructor(props) { super(props) this.state = { hasError: false } } static getDerivedStateFromError(error) { // Update state so the next render will show the fallback UI. return { hasError: true } } componentDidCatch(error, errorInfo) { // You can also log the error to an error reporting service logErrorToMyService(error, errorInfo) } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1> } return this.props.children } }そして、普通のコンポーネントと同じように使用できます。
<ErrorBoundary> <MyWidget /> </ErrorBoundary>3. 前回の値を保持する(Retain Previous Values)
propsやstateを更新している時、あなたは
React.useRef
を使ってそれらの前回の値を保持することができます。例えば、1つの配列のアイテムの、現在と前回の変更を追跡するために、前回の値が割り当てられる
React.useRef
と、現在の値が割り振られるReact.useState
を使用することができます。function MyComponent() { const [names, setNames] = React.useState(['bob']) const prevNamesRef = React.useRef([]) React.useEffect(() => { prevNamesRef.current = names }) const prevNames = prevNamesRef.current return ( <div> <h4>Current names:</h4> <ul> {names.map((name) => ( <li key={name}>{name}</li> ))} </ul> <h4>Previous names:</h4> <ul> {prevNames.map((prevName) => ( <li key={prevName}>{prevName}</li> ))} </ul> </div> ) }これは正しく動作します。なぜなら
React.useEffect
はレンダーが完了した後に実行されるからです。
setNames
が実行される時、コンポーネントは再度レンダーを行い、prefNamesRef
は前回の値を保持します。なぜなら、前回のレンダーから見てReact.useEffect
が最後に実行されるコードだからです。
そしてuseEffect
内でprevNamesRef.current
に値が再び代入されるので、次回のレンダー時には前回のnamesの値を保持します。4. 値チェックにReact.useRefを使う(Use React.useRef for flexible non-stale value checks)
React Hooksが登場する以前、ComponentがDOMにマウントされた後に、dataのフェッチなどの処理を行いたい場合、私達は
componentDidMount
というクラスコンポーネントのStaticメソッドを使用していました。React Hooksが発表された時、それはすぐにコンポーネントを書く最も人気の手段となりました。コンポーネントがアンマウントされた後にstateがセットされるのを防ぐために、コンポーネントがマウントされたかを監視したい時、私達はこのようにするでしょう。
import React from 'react' import axios from 'axios' class MyComponent extends React.Component { mounted = false state = { frogs: [], error: null, } componentDidMount() { this.mounted = true } componentWillUnmount() { this.mounted = false } async fetchFrogs = (params) => { try { const response = await axios.get('https://some-frogs-api.com/v1/', { params }) if (this.mounted) { this.setState({ frogs: response.data.items }) } } catch (error) { if (this.mounted) { this.setState({ error }) } } } render() { return ( <div> <h4>Frogs:</h4> <ul> {this.state.frogs.map((frog) => <li key={frog.name}>{frog.name}</li> )} </ul> </div> ) } }React Hooksに統合後、Hooksは
componentDidMount
を持っていません。そして、アンマウント後に起こったstateアップデートによるメモリリークのコンセプトは未だにhooksに当てはまります。しかし、React Hooksを使ったcomponentDidMount
に似た方法はReact.useEffect
を使用することです。なぜなら、それはコンポーネントがレンダリングされた後に実行されるからです。もしあなたがReact.useRef
を使って、マウントされた値を割り当てるなら、以下のクラスコンポーネントの例で同じことができます。import React from 'react' import axios from 'axios' function MyComponent() { const [frogs, setFrogs] = React.useState([]) const [error, setError] = React.useState(null) const mounted = React.useRef(false) async function fetchFrogs(params) { try { const response = await axios.get('https://some-frogs-api.com/v1/', { params, }) if (mounted.current) { setFrogs(response.data.items) } } catch (error) { if (mounted.current) { setError(error) } } } React.useEffect(() => { mounted.current = true return function cleanup() { mounted.current = false } }, []) return ( <div> <h4>Frogs:</h4> <ul> {this.state.frogs.map((frog) => ( <li key={frog.name}>{frog.name}</li> ))} </ul> </div> ) }リレンダーをせず最新の変更をトラッキングするもう一つ別の例は、以下のように、
React.useMemo
と併せて使用することです。(source)function setRef(ref, value) { // Using function callback version if (typeof ref === 'function') { ref(value) // Using the React.useRef() version } else if (ref) { ref.current = value } } function useForkRef(refA, refB) { return React.useMemo(() => { if (refA == null && refB == null) { return null } return (refValue) => { setRef(refA, refValue) setRef(refB, refValue) } }, [refA, refB]) }もしref propsが変更され、定義されたら、新たな関数を作ります。
つまり、Reactは古いフォークされたrefをnull、新しいフォークされたrefを現在のrefで呼ぶことを意味します。そしてReact.useMemo
が使われているので、refの変数たちはrefA
またはrefB
のref propsが変更されるまで記憶されます。ナチュラルなクリーンアップが起こるのです。5. 他の要素に依存したカスタム要素にReact.useRefを使う(Use React.useRef for customizing elements that depend on other elements)
React.useRef
にはいくつか役に立つユースケースがあります。例えば、React.useRef
自体をref propsに割り当てるなどです。function MyComponent() { const [position, setPosition] = React.useState({ x: 0, y: 0 }) const nodeRef = React.useRef() React.useEffect(() => { const pos = nodeRef.current.getBoundingClientRect() setPosition({ x: pos.x, y: pos.y, }) }, []) return ( <div ref={nodeRef}> <h2>Hello</h2> </div> ) }もし
div
要素の座標位置を取得したいなら、この例が役に立ちます。しかし、アプリケーションのどこかに存在する別の要素の位置をアップデートしたかったり、それに応じて条件ロジックを変更したり適応する場合、ベストな方法はref callback function pattern
を使うことです。callback function patternを使用すると、ReactコンポーネントまたはHTML DOM要素を第一引数として受け取ります。下の例は、コールバック関数である
setRef
がどこでref
propsに適応されるかを示すシンプルなものです。直接React.useRef
をDOM要素に割り当てるのとは対象的に、setRef
の内側では必要なことは何でもできるということがわかります:const SomeComponent = function({ nodeRef }) { const ownRef = React.useRef() function setRef(e) { if (e && nodeRef.current) { const codeElementBounds = nodeRef.current.getBoundingClientRect() // Log the <pre> element's position + size console.log(`Code element's bounds: ${JSON.stringify(codeElementBounds)}`) ownRef.current = e } } return ( <div ref={setRef} style={{ width: '100%', height: 100, background: 'green' }} /> ) } function App() { const [items, setItems] = React.useState([]) const nodeRef = React.useRef() const addItems = React.useCallback(() => { const itemNum = items.length setItems((prevItems) => [ ...prevItems, { [`item${itemNum}`]: `I am item # ${itemNum}'`, }, ]) }, [items, setItems]) return ( <div style={{ border: '1px solid teal', width: 500, margin: 'auto' }}> <button type="button" onClick={addItems}> Add Item </button> <SomeComponent nodeRef={nodeRef} /> <div ref={nodeRef}> <pre> <code>{JSON.stringify(items, null, 2)}</code> </pre> </div> </div> ) }6. 高階コンポーネント(Higher Order Components)
プレインなJavaScriptで強力な再利用性を持つ関数を作るよくあるパターンは高階関数です。Reactも結局はJavaScriptなので、内部で高階関数を使用することができます。
再利用性のあるコンポーネントには、高階コンポーネントとというトリックが使えます。
高階コンポーネントとは、あなたがコンポーネントを引数にして、返り値としてコンポーネントを返す関数を持っているときのことです。高階関数がロジックを抽象化し、他の関数間で共有されるように、高階コンポーネントもコンポーネントをロジックから切り離し、他のコンポーネント間で共有することができます。つまり、たくさんの再利用可能なコンポーネントを採用し、アプリケーション内で実際に繰り返し使うことが可能だということなのです。
これは高階コンポーネントの例です。このスニペットでは、高階コンポーネントであるwithBorderはカスタムコンポーネントを引数として取り込み、隠されたミドルレイヤーコンポーネントを返します。そして、親がこの高階コンポーネントをレンダーするとき、これはコンポーネントとして呼ばれ、ミドルレイヤーコンポーネントからから渡されたpropsを受け取ります。
import React from 'react' // Higher order component const withBorder = (Component, customStyle) => { class WithBorder extends React.Component { render() { const style = { border: this.props.customStyle ? this.props.customStyle.border : '3px solid teal', } return <Component style={style} {...this.props} /> } } return WithBorder } function MyComponent({ style, ...rest }) { return ( <div style={style} {...rest}> <h2>This is my component and I am expecting some styles.</h2> </div> ) } export default withBorder(MyComponent, { border: '4px solid teal', })7. Render Props
React内で使う私のお気に入りのトリックの一つは、render prop patternです。複数のコンポーネントでコードを共有するという問題を解決する点において、これは高階コンポーネントに似ています。Render propsは、レンダーに必要なものを全て差し戻すことが目的の関数をはっきりとさせます。
Render props expose a function who's purpose is to pass back everything the outside world needs to render its children.Reactでコンポーネントをレンダーする最もベーシックな方法は、このようなものでしょう。
function MyComponent() { return <p>My component</p> } function App() { const [fetching, setFetching] = React.useState(false) const [fetched, setFetched] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) const [frogs, setFrogs] = React.useState([]) React.useEffect(() => { setFetching(true) api .fetchFrogs({ limit: 1000 }) .then((result) => { setFrogs(result.data.items) setFetched(true) setFetching(false) }) .catch((error) => { setError(error) setFetching(false) }) }, []) return ( <MyComponent fetching={fetching} fetched={fetched} fetchError={fetchError} frogs={frogs} /> ) }Render Propsを使う場合は、その子をレンダーするpropは慣例的にrenderと呼ばれます。
function MyComponent({ render }) { const [fetching, setFetching] = React.useState(false) const [fetched, setFetched] = React.useState(false) const [fetchError, setFetchError] = React.useState(null) const [frogs, setFrogs] = React.useState([]) React.useEffect(() => { setFetching(true) api .fetchFrogs({ limit: 1000 }) .then((result) => { setFrogs(result.data.items) setFetched(true) setFetching(false) }) .catch((error) => { setError(error) setFetching(false) }) }, []) return render({ fetching, fetched, fetchError, frogs, }) }この例では、
MyComponent
はrender prop componentとして参照するコンポーネントの例です。なぜなら、MyComponent
はpropとしてのrender
を期待していますし、子をレンダーするためのそれを実行するからです。
これはReactにおいて、とてもパワフルなパターンです。renderのコールバックを通して引数として、共有されたstateとdataを渡すことができるのです。複数のコンポーネントにおいて、そのコンポーネントはリレンダーされ、再利用されることが可能になります。function App() { return ( <MyComponent render={({ fetching, fetched, fetchError, frogs }) => ( <div> {fetching ? 'Fetching frogs...' : fetched ? 'The frogs have been fetched!' : fetchError ? `An error occurred while fetching the list of frogs: ${fetchError.message}` : null} <hr /> <ul style={{ padding: 12, }} > {frogs.map((frog) => ( <li key={frog.name}> <div>Frog's name: {frog.name}</div> <div>Frog's age: {frog.age}</div> <div>Frog's gender: {frog.gender}</div> </li> ))} </ul> </div> )} /> ) }8. Memoize
Reactデベロッパーとして知っておくべき最も大事なことの一つは、
React.memo
のようなコンポーネントのパフォーマンス最適化です。それを知っておくと、無限ループのようなとてもひどいエラーを防ぐことができます。Reactアプリにおいて、Memoization(メモ化)を行う、いくつかの方法があるので是非読んでみてください。
締め
ここでこの投稿は終わりです!この記事があなたにとって価値があることを願います。
Mediumも見てくださいね。
感想
まず、今回の記事は翻訳が難しかったです。英語もさることながら、自分のReact Hooksの理解が浅いなと実感しました。
逆にReact HooksやよりReactの理解が進めば、これらのトリックはとても役に立ちそうだと思いました。間違い等見つけましたら教えていただけるとありがたいです。
- 投稿日:2019-12-09T10:59:18+09:00
とりあえずTypeScriptでReact入門するかぁ
はじめに
はじめに、筆者はWeb開発初心者です。ついでに学生です。間違った内容を提示してしまうことがあるかもしれませんが、その際は指摘してくれるとありがたいです。
Web開発は難しい
私がこの記事を書く理由でもありますが、Webページを作るというのは意外と難しいです。ひと昔前ではHTMLとCSSが書ければWebページとしての体裁は整いました。しかし、用途の多様化に伴い、静的なページからHTMLを自己編集する動的なページへと需要が移り、必要な知識と技術が爆発的に増えました。それに伴って、Ruby on RailsやPlay Frameworkが生まれたのです。これは10年以上前の話ですが最近はどうでしょうか。よく挙げられるフレームワークとしてAngular、React、Vue.jsがありますが、これらが2013年頃から出てきました。さて、例をいくつか挙げましたが初心者は何を使えばいいでしょうか?私は分かりません。諺でいう、亡羊の嘆ってやつです。
とりあえずReactするかぁ~
Reactって何?
Reactをはじめる前にどんな設計思想をもったツールなのかを考えたいと思います。
コンポーネント
Reactに限った話ではありませんが、ある機能を実装するとき、細かく分割したコンポーネントと呼ばれる部品を組み合わせて実現させており、コード再利用性の向上やメンテナンスを容易にすることができます。しかし、実装の際には親コンポーネントから子コンポーネントへのオブジェクトの引き渡し(props)やコンポーネント内での状態の管理(state)、それらを踏まえたコンポーネントのライフサイクルを考慮する必要があります。
Virtual DOM
次にVirtual DOM(Document Object Model)という概念について説明したいと思います。ちなみに、Virtual DOMはVueにも採用されています。
DOMはいわゆる、ブラウザ表示に使われるhtmlの構造を見て制御を行うAPIです。ここで考えるべきなのは、あるコンポーネント処理の結果としてDOMの変更が起こる時です。このとき毎回DOMを一から生成するにはブラウザ表示に遅延が生じることが考えられます。そこで表示するDOMを生成し直さずに一旦Virtul DOMというものを生成し、Virtul DOMを更新しつつ、表示するDOMとの差分のみを更新することで表示の遅延をなくします。React以前の環境構築
別の記事にTypeScriptやnpm関連の導入の仕方は書いたので、こちらを読んでください。
TypeScriptをはじめよう 1Reactをはじめる
Reactをはじめる前に、React自体にはテンプレートを生成する機能はありません。そのため、以下のオンラインエディタを使うかFacebookから提供されるcreate-react-appを使います。
今回は自前の環境で開発を進めたいので、オンラインエディタは使いません。では、以下のコマンドを適当なディレクトリで行なってください。
npm install -g create-react-app create-react-app <プロジェクト名> --typescript次にcreate-react-appがyarnを推奨しているのでyarnを導入します。yarnはnpmと同じくパッケージ管理ツールです。
npm install -g yarnでは、プロジェクトのディレクトリに移って以下のコマンドを実行しましょう。
yarn startこれでブラウザが立ち上がり、以下の画面が映れば問題ありません。
React TSX
ブラウザに表示されていますが、src/App.tsxを編集することでページを編集することができます。TSXはJSXのTypeScript版で、HTMLライクな記述を可能にしたTypeScriptの拡張です。とりあえず、説明しやすいように簡単な成績表示をするページを考えます。サンプルコードは以下です。
App.tsximport React from 'react'; import './App.css'; const App: React.FC = () => { const tdata = [ {name: "Aさん", japanese: 68, math: 75, social: 74, socience: 48, english: 70}, {name: "Bさん", japanese: 90, math: 43, social: 80, socience: 55, english: 76}, {name: "Cさん", japanese: 53, math: 64, social: 54, socience: 64, english: 38} ] return ( <div> <h1> 点数表 </h1> <table className= "studentAchive"> <tbody> <tr> <td>{"名前"}</td><td>{"国語"}</td><td>{"数学"}</td><td>{"社会"}</td><td>{"理科"}</td><td>{"英語"}</td> </tr> <tr> <td>{tdata[0].name}</td><td>{tdata[0].japanese}</td><td>{tdata[0].math}</td><td>{tdata[0].social}</td><td>{tdata[0].socience}</td><td>{tdata[0].english}</td> </tr> <tr> <td>{tdata[1].name}</td><td>{tdata[1].japanese}</td><td>{tdata[1].math}</td><td>{tdata[1].social}</td><td>{tdata[1].socience}</td><td>{tdata[1].english}</td> </tr> <tr> <td>{tdata[2].name}</td><td>{tdata[2].japanese}</td><td>{tdata[2].math}</td><td>{tdata[2].social}</td><td>{tdata[2].socience}</td><td>{tdata[2].english}</td> </tr> </tbody> </table> </div> ); } export default App;気づいただろうか?これはクソプログラムです。
では、これをもう少しまともなプログラムに直していきましょう。ブラウザ上では、以下のように表示されます。
コンポーネントの細分化
このプログラムの問題点を挙げると、このプログラムには汎用性がありません。例えば、現在使われている成績は3人分のデータを使っていますが、これを30~40人程度の1教室分や更に多い1学年分を表示するとした場合はどうでしょう。はっきり言えば面倒ですね。Webアプリに限らず、プログラムを機能ごとに切り出して考えることは必要不可欠です。それでは、このコードを改善していきましょう。
propsとかいうやつ
先にコードだけ載せます。
import React from 'react'; import './App.css'; interface propsType { name: String, japanese: Number, math: Number, social: Number, socience: Number, english: Number, } const Td:React.FC<propsType> = (props) => { return( <tr> <td>{props.name}</td><td>{props.japanese}</td><td>{props.math}</td><td>{props.social}</td><td>{props.socience}</td><td>{props.english}</td> </tr> ); } const App: React.FC = () => { const tdata = [ {name: "Aさん", japanese: 68, math: 75, social: 74, socience: 48, english: 70}, {name: "Bさん", japanese: 90, math: 43, social: 80, socience: 55, english: 76}, {name: "Cさん", japanese: 53, math: 64, social: 54, socience: 64, english: 38} ] return ( <div> <h1> 点数表 </h1> <table className= "studentAchive"> <tbody> <tr> <td>{"名前"}</td><td>{"国語"}</td><td>{"数学"}</td><td>{"社会"}</td><td>{"理科"}</td><td>{"英語"}</td> </tr> { Array.from(tdata).map( (data:propsType) => <Td name={data.name} japanese={data.japanese} math={data.math} social={data.social} socience={data.socience} english={data.english}/> ) } </tbody> </table> </div> ); } export default App;追加したのはこの部分と
interface propsType { name: String, japanese: Number, math: Number, social: Number, socience: Number, english: Number, } const Td:React.FC<propsType> = (props) => { return( <tr> <td>{props.name}</td><td>{props.japanese}</td><td>{props.math}</td><td>{props.social}</td><td>{props.socience}</td><td>{props.english}</td> </tr> ); }この部分
{ Array.from(tdata).map( (data:propsType) => <Td name={data.name} japanese={data.japanese} math={data.math} social={data.social} socience={data.socience} english={data.english}/> ) }propsは親コンポーネントから子コンポーネントに値や関数を渡す方法です。型はinterfaceで宣言して使います(宣言しないとエラー起こす)。また、propsの引き渡しはHTMLの属性のようにかくことができます。
おわり
最後の方がだいぶ雑になったこととState、ライフサイクルの話がなくて申し訳ないです。まだ自分の理解が至っていないこととあまり間違った内容を載せたくないという気持ちでこうなってしまいました。次はもう少し頑張ります。
- 投稿日:2019-12-09T09:35:47+09:00
create-react-appを用いて,React/JSXのコンパイル環境を構築する
はじめに
create-react-app
を用いて,コンパイル環境を構築する例を備忘録としてまとめます.環境
- Windows 10 Home
- npm 6.12.1
- Git bashでコマンド実行
Reactのツールでビルド
React/JSXを使用するときは,ソースコードをあらかじめコンパイルしておくのが一般的です.
ここでは,React/JSXのコンパイル環境を整える方法を紹介します.React/JSXのコンパイル環境を作る
create-react-app
というアプリを使ってコンパイル環境を構築します.
これはFacebookが用意しているアプリです.以下のコマンドで
create-react-app
をグローバルインストールします.$npm install -g create-react-app次に,このツールを利用したプロジェクト(sample1)を作成します.
作業ディレクトリの直下にsample1
というフォルダが生成されます.$create-react-app sample1 Creating a new React app in C:\Users\Let-create-ReactApp\src\ch2\sample1. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts with cra-template... ・・・略・・・ We suggest that you begin by typing: cd sample1 npm start Happy hacking!次に,
sample1
のディレクトリに移動して,npm start
と入力してアプリを起動します.//作業ディレクトリを表示 $pwd C:\Users\Let-create-ReactApp\src\ch2\ //sample1というフォルダが直下にあることを確認 $ls sample1/ $ch sample1 $npm start > sample1@0.1.0 start C:\Users\Let-create-ReactApp\src\ch2\sample1 > react-scripts start ・・・略・・・ Compiling... Compiled with warnings. ./src/App.js Line 2:8: 'logo' is defined but never used no-unused-vars Search for the keywords to learn more about each warning. To ignore, add // eslint-disable-next-line to the line before. Compiling... Compiled successfully!Windowsのネイティブ環境で実行している場合には,Webブラウザが起動し,Reactのひな型が以下のように表示されます.
create-react-app sample1
を実行するとき,カレントディレクトリに以下のフォルダが生成されます.
node_modules
: インストールされたNode.jsのモジュールが存在src
:コンパイル前のソースコードが存在public
:index.htmlのひな型となるファイルが存在
src
ディレクトリにあるファイルを編集して保存すると,ファイルの変更を自動的に検知して,Webブラウザに反映できます.
そのため,リアルタイムで実行結果を確認して,ファイルの編集を行うことができます.
src
の中のApp.js
を下記のように編集して保存すると,自動的にWebブラウザが更新され,以下のような画面が表示されます.App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <h1>こんにちは</h1> </div> ); } export default App;プログラムが完成し,開発したプログラムをWeb上に公開するためには,以下のコマンドを実行します.
$npm run buildこれにより,
sample1
フォルダ直下にbuild
というフォルダが生成されます.
そのフォルダ以下に各種ファイルが圧縮した状態で生成されます.
ローカル環境でうまくビルドされたかを確認するには,Webサーバが必要になります.
以下のようにコマンドを実行してserve
コマンドをインストールします.そしてserve -s build
を実行すると,Webサーバが起動するので,指示されたURLにWebブラウザでアクセスすると,実行状態を確認できます.$npm install -g serve $serve -s build INFO: Accepting connections at http://localhost:5000Webブラウザで
http://localhost:5000
でアクセスします.まとめ
create-react-app
で環境を整えると,リアルタイムで変更が反映され,エラー表示もわかりやく,リソース様にビルドするのも容易です.
- 投稿日:2019-12-09T08:52:36+09:00
Theme UI で Theme Driven な快適 React スタイリング生活
こんにちは。YAMAP でフロントエンドエンジニアをしている @SotaSuzuki と申します。
この記事は YAMAP エンジニア Advent Calendar 2019 の 9日目の記事となります。
拙い文章で恐縮ですが何卒よろしくお願いいたします。tl;dr
- Styled System の Theming は Emotion の Theming と比べて 3倍便利
- Theme UI の Theming はさらにその 2倍高機能
- Styled System の Style Prop API と TypeScript の併せ技で 3倍高速なマークアップ
- Theme UI はいいぞ
Styled System とは
本題の Theme UI に入る前に、Theme UI のベースとなるライブラリ Styled System について簡単に説明します。
Styled System は Theme Specification に基づく theming と、Style Prop API 、Variants 機構などを備えた UI 構築用ライブラリです。
これらの機能はあくまでオリジナルの UI コンポーネントを構築するための low-level な API として提供されており、たとえばButton
などの UI コンポーネント自体は Styled Stystem からは提供されていません。いうなれば Styled System はメタ UI ライブラリともいうことができそうです。
Styled System の機能で個人的に強力だと思うのが Style Prop API で、これと TypeScript を併用することでコード補完も効くようになるため、UI のスタイリングが体感で通常の3倍(当社比)になります。
参考
- System UI Theme Specification
- API - Styled System
- Variants - Styled System
- Ecosystem - Styled System
Theming
Theme は Styled System 以外のライブラリでも実装されている概念です。たとえば Emotion や Material UI でも実装されています。
Styled System との比較のために、Emotion の Theming を取り上げます。
emotion-theming を使った Theming
Emotion で Theming する場合、まず
ThemeProvider
にtheme
を設定します。theme.jsimport React from 'react'; import styled from '@emotion/styled'; import { ThemeProvider } from 'emotion-theming'; const theme = { colors: { primary: '#3cf', }, }; export default () => { return ( <ThemeProvider theme={theme}> <Main /> </ThemeProvider> ); };そして、
theme
の値を使用するコンポーネントではprops
、またはuseTheme
hook を使いtheme
の値を取り出します。Main.jsimport React from 'react'; /** @jsx jsx */ import { jsx } from '@emotion/core'; import styled from '@emotion/styled'; import { useTheme } from 'emotion-theming'; const Main = () => { const theme = useTheme(); return ( <> {/* with css prop */} <h1 css={theme => ({ color: theme.colors.primary })}>Emotion Theming</h1> {/* with Styled */} <StyledH1>Emotion Theming</StyledH1> {/* with useTheme hook */} <h1 css={{ color: theme.colors.primary }}>Emotion Theming</h1> </> ); }; const StyledH1 = styled({ color: theme => theme.colors.primary }); export default Main;Styled System の Theming
まず Styled System の theming は System UI Theme Specification に基づいて実装されています。emotion-theming との最大の違いはこれと言ってよいでしょう。
System UI Theme Specification はその名の通り Theming の仕様です。ここでは Theme key と対応する CSS プロパティが定義されており、各CSS プロパティでは対応する Theme key に設定している値のみ使用できます。
たとえば、Theme key
colors
はcolor
,background-color
,border-color
と、Theme keyspace
はmargin
,padding
などと対応しています。下記の例は、padding は
space
Theme key に対応しているためspace
の値を使えていますが、transform はspace
に対応していないためspace
の値が使えていません。// Theme const theme = { // space が Theme key にあたる space: [0, 4, 8, 12] }; // p={2} -> padding: 8px // transform={translateY(2)} -> transform: translateY(2); render(<Box p={2} transform={trnaslateY(2)}></Box>);また、特殊な Theme key として breakpoints が存在します。これは mediaQuery のショートハンドのようなもので非常に便利です。
以下は Styled System で Theming を使用する例です。
theme.tsconst theme = { colors: { text: '#333', background: '#fff', primary: '#48f', secondary: '#37e', muted: '#e4e4e4', }, space: [0, 4, 8, 12, 16, 20, 24, 32], fontSizes: [12, 14, 16, 18, 20, 24, 32, 48, 64, 80], breakpoints: ['480px', '768px', '1024px'], buttons: { primary: { color: 'white', backgroundColor: '#48f', }, }, };App.tsximport React from 'react'; import { ThemeProvider } from 'styled-components'; import { buttonStyle, ButtonStyleProps, color, ColorProps, background, backgroundProps, border, borderProps, display, DisplayProps, space, SpaceProps, } from 'styled-system'; import theme from './theme' export default () => { return ( <ThemeProvider theme={theme}> <> {/* - ~479px -> padding: 8px - 480px~ -> padding: 12px - 768px~ -> padding: 16px */} <Box p={[2, 3, 4]} bg="muted"> {/* using theme values */} <Button color="text" bg="background">Normal Button</Button> {/* using variant prop */} <Button variant="primary">Primary Button</Button> </Box> </> </ThemeProvider> ); } type BoxProps = SpaceProps & ColorProps & DisplayProps & BorderProps; const Box = styled.div<BoxProps>( space, color, display, border, ); type ButtonProps = ButtonStyleProps & ColorProps & BackgroundProps; const Button = styled.button<ButtonProps>( buttonStyle, color, background, );または @styled-system/css の
css
関数を使えば styled-component でtheme
の値を扱うこともできます。FlexColorBox.tsximport React from 'react'; import styled from 'styled-components'; import { display, DisplayProps } from 'styled-system'; import { css } from '@styled-system/css'; import { Box } from './Box'; // 先の例の Box コンポーネントを import しています export const FlexColorBox: React.FC = styled<BoxProps & DisplayProps>(Box)( display, css({ display: 'flex', alignItems: 'center', color: 'background', bg: 'primary', border: '1px solid', borderColor: 'secondary', p: [2, 3, 4], }) );このように、StyledSystem の Theming は variants や breakpoints などの便利機能を多数取り揃えています。
また、System UI Theme Specification に制約ベースの仕様のため、必然的に Theme ファイルが混沌となりづらくなるのもメリットといえるでしょう。
とはいえ、Styled System 自体ははじめに説明したとおり、UI コンポーネントを構築するためのシステムであり、UI ライブラリではありません。
実際の現場におけるアプリケーション開発で Styled System をそのまま使うのは、ちょっと悠長な気もします。たとえるなら、create-react-app や next.js を使わずに react アプリをイチから構築するのに近いかもしれません。参考
Theme UI
One of the primary motivations behind Theme UI is to make building themeable, constraint-based user interfaces in React as simple and as interoperable as possible.
ThemeUI は、UI コンポーネントカタログや Styled System Theming の拡張、MDX、Color Mode、Gatsby 向けのプラグインまで、様々な要素を携えた React 用に作られた ライブラリです。
Theme UI 自体は Theming に特化しており、
sx
prop と MDX 向けの Theming ができるのが特徴です。他にもサブディレクトリに様々なパッケージが存在し、それぞれ開発が進められているようです。
Chrome Extension の Theme UI DevTools や、アプリケーション上で Theme を編集するための UI を取り揃えている @theme-ui/editor といった面白いパッケージもあるので、Theme UI を使うなら目を通しておくとよいでしょう。https://github.com/system-ui/theme-ui/tree/master/packages
ちなみに Theme UI の作者は Styled System の作者と同じ @jxnblk 氏(Gatsby 社勤務)です。彼は Rebass という Styled System に基づいて構築された UI ライブラリも公開しています。
本人の Twitter や GitHub の Activity を見たところ、現在は ThemeUI のほうに注力しているようなので、これから導入するなら Rebass よりも Theme UI がよいでしょう。
ただし、ThemeUI は比較的新しいプロジェクト(2019年4月頃〜)で未だバージョン 0.2.49 です。
これからどんどん API の追加/削除/変更はされていくことが予想されるため、導入する際そのあたりは注意しておきたいところです。sx prop と theme.styles
本記事では Theme UI への Deep Dive はしません。筆者も Theme UI について、ドキュメントを通したくらいなので、まだ Theme UI について多くを語ることができないからです。
そのかわりといってはなんですが、Theme UI のさわり、sx prop と theme.styles について紹介し、本記事の締めとしたいと思います。
sx prop
sx prop は Emotion の css prop と似ていますが下記のような拡張がなされているようです。
theme
で設定した値が使える- Style Prop API のショートハンドが使える
つまり Style Prop と同じ感覚でスタイルを書くことができるのです。ただし、sx props はオブジェクトリテラルしか許容しません。
sx prop を使用するにあたっては、 Emotion の css prop 同様 JSX pragma を記述する必要があります。
ThreeBoxes.tsximport React from 'react'; /** @jsx jsx */ // <- これが JSX Pragma imprt { jsx } from 'theme-ui'; const ThreeBoxes: React.FC = () => { return ( <div sx={{ display: 'flex', p: 2, }}> {[1, 2, 3].map(n => ( <div key={n} sx={{ bg: 'accent', color: 'background', p: 4, ml: n !== 1 ? 2 : 0, }}>BOX{n}</div> ))} </div> ); }; export default ThreeBoxes;参考
theme.styles
theme.styles の
styles
はtheme
オブジェクトの key です。これの役割は以下の2つです。
- MDX のスタイリング
- グローバルなスタイリング
ひとつめの MDX のスタイリングについては、まだ筆者が試せていないため言及はしません。
なので、ふたつめのグローバルなスタイリングについて、簡単な例を出し説明したいと思います。まずは Theme ファイルを作成します。
theme.tsimport { Theme } from 'theme-ui'; import { invert } from '@theme-ui/color' const theme: Theme = { breakpoints: ['480px', '768px', '1024px'], colors: { text: '#fff', primary: '#48f', accent: 'f72', }, fonts: { body: 'system-ui, sans-serif', heading: '"Avenir Next", sans-serif', monospace: 'Menlo, monospace', }, fontSizes: [10, 12, 14, 16, 20, 24, 30, 48, 64], space: [0, 4, 8, 12, 16, 20, 24, 30, 60, 100], borders: { base: '1px solid #444', accent: '1px solid #f72', }, }; theme.styles = { h1: { fontSize: [5, 6, 7], fontFamily: 'heading', mt: [2, 3, 4], mb: ['2px', 1, 2], }, button: { bg: 'accent', color: invert('accent'), border: 'accent', p: 1, }, }; export default theme;次に、Theme ファイルで定義した theme.styles のスタイルをコンポーネントに適用します。
theme-ui から
Styled
モジュールを import して使います。これは Styled Components のstyled
に似た API です。App.tsximport * as React from 'react'; /** @jsx jsx */ import { Styled, jsx, ThemeProvider } from 'theme-ui'; import theme from './theme'; const App: React.FC = () => { return ( <ThemeProvider theme={theme}> <Styled.h1>Changing the HTML element or component</Styled.h1> <p>The rendered HTML element can be changed for each Styled component. This can be useful for ensuring accessible, SEO-friendly, and semantically-correct HTML is rendered while leveraging styles from the theme. This API originated in Rebass and is similar to the <Styled.button as="a" href="https://emotion.sh/docs/styled">@emotion/styled</Styled.button> API. </p> <Styled.h1 as="h2">H2 Styled as h1</Styled.h1> <p>blah blah blah</p> </ThemeProvider> ); }; export default App;参考
おわります
Theme UI はあんまり日本語の情報が落ちていませんでした。追いかけるなら、英語の情報を・・というより作者の Twitter とか GitHub を watch する必要がありそうです。
筆者は MDX も気になっているので、今度は Theme UI + MDX コンポーネントを試してみようかしらん、と画策しています。
以上です。
ちなみに Styled System ベースのライブラリを使うときは TypeScript を使うと、CSS Property と Style Prop API の補完が非常に気持ちよいです。鬼に金棒コンビでおすすめです
![]()
付録
その他の Styled System ベースのライブラリ
Chakra UI
GitHub Primer Components
https://primer.style/components/
sx prop VS built-in style prop
build-in style prop というのはスタイル属性のことです。
sx のメリット
- ランダムな class 名を当ててくれる
- media-query を書ける
- 疑似セレクタ/疑似要素を書ける
- Theme が使える
style 属性のメリット
- library を必要としない
- 強い(CSS詳細度が)
Example で使っているパッケージ
yarn add theme-ui @types/theme-ui @emotion/core @mdx-js/react styled-system styled-components @types/styled-system @/types/styled-components @styled-system/css @types/styled-system__css今回書いた Examples 全貌(ちょっと変わっているかも)
Code Sandbox で用意しています。
example1: UI built with Styled System
https://codesandbox.io/embed/styled-system-ui-j17bu?fontsize=14&hidenavigation=1&theme=dark
example2: Theme UI の sx prop と theme.styles
https://codesandbox.io/embed/sx-prop-ivin7?fontsize=14&hidenavigation=1&theme=dark
- 投稿日:2019-12-09T03:13:05+09:00
react hooks+redux toolkitを使ってtodoアプリを作ってみた
この記事はTOWN株式会社アドベントカレンダー6日目の記事です(大遅刻して申し訳ないです。。。)
https://adventar.org/calendars/4335React HooksとはReact16.8から追加された機能で、関数コンポーネントにstateなどの機能を使えるようになった記法です。
https://ja.reactjs.org/docs/hooks-intro.htmlRedux toolkitとはどうしても複雑な記述を書かなくてはいけないイメージが(個人的に)あるreduxを簡潔に書くことができるライブラリです。reduxの公式チームがリリースしているそうです。
https://github.com/reduxjs/redux-toolkit今回はreact hooksとredux toolkitを使ってtodoアプリを作成してみました。(初めて一からreduxでアプリを作ったのでアンチパターンなどあるかもしれないです。。)
アプリの作成
まず
create-react-app
を使ってアプリのテンプレートを作成します。npx create-react-app hooks-toolkit-sampleパッケージをインストールします。
yarn add react-redux @reduxjs/toolkitor
npm i react-redux @reduxjs/toolkit
yarn start
もしくはnpm start
でアプリを起動します。reactのロゴsvgなどのいらないファイルやいらない記述等は削除しておきます。actionとreducerの作成
redux toolkitのcreateSliceを使用し、actionとreducerを同時に作成しています。このおかげでactionとreducerを別々のディレクトリに分ける必要がなくなりました。
modules/listModule.jsimport { createSlice } from "@reduxjs/toolkit"; const listModule = createSlice({ name: "list", initialState: [], reducers: { addList: (state, action) => [...state, action.payload], deleteList: (state, action) => state.filter(el => el.id !== action.payload), } }); export default listModule;storeの作成
reducerとmiddlewareを登録します。先ほどのmoduleのreducerをredux toolkitの
combineReducers
を使って登録しています。また開発環境のみロガーを使えるようにここで設定しています。src/store.jsimport { combineReducers, configureStore } from "@reduxjs/toolkit"; import listModule from "./modules/listModule"; const rootReducer = combineReducers({ list: listModule.reducer, }); export const setupStore = () => { const store = configureStore({ reducer: rootReducer, }); return store }コンポーネントの作成
まず
useDispatch()
を使ってactionが使えるようにします。todoを追加するためのテキストのinputの操作ですが、inputの
onChange
にhandleChangeList
関数を設定して文字を入力するたびに関数が実行されるようにします。
そしてreact hooksのuseState
を用い、入力したテキストを一時的にnewListName
のstateに入れておきます。
addList
ボタンを押した瞬間にnewListName
を引数にしてactionのaddList
を実行し、storeのstateにtodoのテキストが入ることで画面が更新されます。components/App.jsimport React from 'react'; import { useState } from 'react'; import { useDispatch, useSelector } from "react-redux"; import listModule from "../modules/listModule"; import List from "./List.js"; const App = () => { const lists = useSelector(state => state.list); const [newListName, setListName] = useState(""); const dispatch = useDispatch(); const addList = () => { if (newListName !== "") { dispatch(listModule.actions.addList({id: lists.length > 0 ? lists.reduce((a,b) => a.id > b.id ? a : b).id + 1 : 1, title: newListName})); } setListName(""); }; const handleChangeList = (e) => { setListName(e.target.value); }; return ( <div className="App"> <p>Redux TODO sample</p> {lists.map((list) => <List key={list.id} item={list} /> )} <input type="text" onChange={handleChangeList} value={newListName}/> <button onClick={addList}>addList</button> </div> ); }; export default App;components/List.jsimport React from 'react'; import { useState } from 'react'; import { useDispatch, useSelector } from "react-redux"; import listModule from "../modules/listModule"; const List = (props) => { const { item } = props; const dispatch = useDispatch(); const deleteList = () => dispatch(listModule.actions.deleteList(item.id)); return ( <div> {item.title} <button onClick={deleteList}>deleteList</button> </div> ) } export default List;ここまでクラスコンポーネントを使わず、かつとてもコンパクトにreduxのアプリを作成することが出来ました。書いていてかなり楽しかったので今度はもう少し複雑なアプリも作ってみようかと思います。
参考:
TypescriptとReact HooksでReduxはもうしんどくない
https://qiita.com/ky7ieee/items/b3f43ecc497b9115449a
Redux Hooks によるラクラク dispatch & states
https://qiita.com/Ouvill/items/569384e5c8c7ce78f98e
Redux の記述量多すぎなので、 Redux の公式ツールでとことん楽をする。 ( Redux Toolkit)
https://qiita.com/Ouvill/items/a76e9cbce569d01f2931
- 投稿日:2019-12-09T00:38:35+09:00
【Firebase】Reactのcreate-react-appで作ったWEBアプリをデプロイする際の注意点
build後、firebaseにデプロイしたらコンソールログでエラーが出た
エラー内容はこんな感じです。
/%PUBLIC_URL%/favicon.ico:1 Failed to load resource: the server responded with a status of 400 ()
firebase.jsonを修正すれば解決!
firebase.jsonのホストティング対象はデフォルトではpublicになっています。
{ "hosting": { "public": "public", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ] } }こいつの"public": "public" を "public": "build"に変更すれば解決しました。
{ "hosting": { "public": "build", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ] } }こういう系ってハマるととことんハマりますよね。。。
- 投稿日:2019-12-09T00:01:10+09:00
ワイ「アニメーションするにはこのuseTransitionってのを使えばええんか?」
社長「やめ太郎くん」
ワイ「なんでっか社長、ワイは今Reactのアプリを半分だけVueに書き換える作業で忙しいんでっせ」
ハスケル子「(何でそんな意味不明なことを……)」
社長「せやったな、これからはVueの時代やからVueの使用実績を増やさなあかんねん」
ワイ「とはいえReactも今年公式ドキュメントの日本語版が出たり勢いづいとるから捨てがたい」
社長「せやから半々にしてどっちも取り入れるんや! 素晴らしい施策やろ!」
ワイ「さすが社長!」
ハリー先輩「(案件を半々にするんちゃうのかい!)」
ハスケル子「(私は何でこんな所でインターンしているんだろう)」
※ この記事は全面無職やめ太郎さんリスペクトのワイ記法でお送りする二次創作記事です。(6ヶ月ぶり3回目)
Reactでアニメーションを実装したい
社長「さて、今回はアプリにいい感じのアニメーションを追加してもらいたいんや。これからはUXの時代やからな」
社長「React側に実装を頼むで」
ワイ「承知しましたで」
ハスケル子「やめ太郎さんはReactでアニメーションの実装したことあるんですか?」
ワイ「無いで」※この記事における設定です。現実とは異なる可能性がありますのでご注意ください。
ワイ「でもまあCSSで
transition
ってのを使えばええことくらいは知っとるで」ワイ「アニメーションの実装くらい楽勝やな!」
〜数時間後〜
ワイ「あかん、こんがらがってきたで」
ワイ「今回はコンポーネントが増えたり減ったりするんや」
娘(4歳)「うん」
ワイ「新しいコンポーネントが増えたときにアニメーションさせなあかん」
娘「うん」
ワイ「でもアニメーションさせるにはまずアニメーション前の状態をレンダリングせなあかん」
娘「うん」
ハスケル子「(何でこの人は娘さんに相談してるんだろう)」
娘「
transition
によるアニメーションはあくまでCSSの変化をアニメーションしてくれるものだから、変化前のCSSの状態を一旦作ることが必要になるんだよね」ワイ「せや」
娘「コンポーネントが出現するとき、アニメーションの前の状態をまず描画→その後アニメーションさせるという2段階の処理が必要なんだね」
ワイ「せや」
娘「これを実装するには、コンポーネントがマウントされていない状態→コンポーネントがマウントされた状態(CSSは変化前の状態)→コンポーネントがマウントされた状態(CSSは変化後の状態)という2段階の状態変化を実装する必要があるんだね」
ワイ「せや」
ハスケル子「(この記事娘さんだけでいいんじゃないかな)」
ワイ「コンポーネントが消えるときも難しいんや」
娘「コンポーネントを消したいときはアニメーションが終わるまでコンポーネントを残しておいて、それから消す必要があるんだね」
ワイ「せや」
娘「意味上はもう消えているコンポーネントを、アニメーションというUIの都合で残しておかないといけないのがReact的じゃなくて辛いよね」
ハスケル子「そういうロジックを組めばいいじゃないですか」
ワイ「まあできないわけやないで」
ワイ「でも何かコードが汚くなってまうんや」
useTransitionを知る
ハスケル子「ふふふ」
ワイ「何や気味の悪い」
ハスケル子「その辺をいい感じにやってくれるのがuseTransitionですよ」
ワイ「ほう、ちょっと調べてみよか……おお、useTransitionのドキュメントがあったで」
ハスケル子「そうです、useTransitionのドキュメントに書いてある通りこれで簡単にアニメーションが実装できます」
ワイ「でもこれ次の画面に移るときに使うとか書いてあるで」
useTransition
allows components to avoid undesirable loading states by waiting for content to load before transitioning to the next screen. It also allows components to defer slower, data fetching updates until subsequent renders so that more crucial updates can be rendered immediately.(useTransitionのドキュメントから引用)
ハスケル子「ああ、useTransitionはルーターと組み合わせてページ遷移のアニメーションにも使えるんですよ」
(useTransitionのドキュメントから引用)
ハスケル子「というかやめ太郎さん英語のドキュメント読めるんですか」
ワイ「当たり前や」
ハスケル子「……」
ハスケル子「そのネタ好きですね」
ハスケル子「あと記事冒頭で社長をアホキャラにしてますけど実在の会社とは関係ありませんからね」
ワイ「とにかく」
ワイ「この
useTransition
を使ってアニメーションを実装してみるで」ハスケル子「まあ使い方がさっきのドキュメントに書いてあるのでそんなに難しくないですよ」
ワイ「探したらuseTransitionの日本語解説記事もあったから余裕やな!(宣伝)」
useTansitionでアニメーションを実装してみた
〜翌日〜
ワイ「半分だけ実装できたで」
ハスケル子「半分……?」
ワイ「コンポーネントが消えるときはうまく行くんやけど出てくるときがまだ実装できてへんねん」
ハスケル子「
useTransition
をどんな使い方したら消えるときだけなんて挙動になるのか逆に気になりますよ」ワイ「まあ見てくれや」
ハスケル子「……ボタンがあって押すと青い箱が出たり消えたりするんですね」
ワイ「消えるときだけアニメーションがかかるやろ」
ハスケル子「どれどれ実装は……!?」
ワイ「なんや珍しく驚いた顔なんかしよって」
ワイ「あとワイは本家と違うて絵心は無いから驚いた顔の絵とかは無いで」
index.jsconst [startTransition, isPending] = useTransition({ timeoutMs: 1000 });ハスケル子「なんですかこのuseTransitionって」
ワイ「なんや、ハスケル子ちゃんが教えてくれたんやろ」
ハスケル子「私が言ってたのは誰もが知ってるuseTransitionのことですよ」
ハスケル子「useTransitionなんて意味不明な機能知りませんよ、というかまだ実験段階じゃないですか」
ワイ「なんやuseTransitionやなくてuseTransitionのこと言っとったんかい! 紛らわしいわ!」
ワイ「ワイもよう分からんようなってきたで、こういうときのための娘ちゃんや」
娘「パパが言ってるのはReact Concurrent ModeのuseTransitionで、ハスケル子ちゃんが言っているのはreact-springのuseTransitionだよ!」
娘「この記事で扱うのはReact Concurrent ModeのuseTransitionで、react-springは特に関係ないよ!」
ワイ「よう分かったで、さすがワイの娘や!」
ハスケル子「(何なんだろうこの茶番)」
娘「Concurrent Modeの詳しい説明とかuseTransitionの解説は筆者の既存記事を見てね!(宣伝)」
ハスケル子「それで、このサンプルはどうやってアニメーションしてるんですか?」
ワイ「せやったな、まず箱が出とるかどうかは
show
というステートで管理するんや」index.jsconst [show, setShow] = useState(false);ワイ「レンダリング部分では
show
がtrue
のときだけBox
をレンダリングするんや」index.jsreturn ( <div className="App"> <h1>Animation Sample</h1> <p> <button onClick={toggle}>toggle</button> </p> {show ? <Box show={show && !isPending} /> : null} <Suspense fallback={null}> <Waiter timer={timer} /> </Suspense> </div> );ワイ「
Box
はshow={false}
のときはopacity: 0
でshow={true}
のときはopacity: 1
や」ハスケル子「ここにある
isPending
がよく分からないですけど、ここに秘密があるんですね」ワイ「せや」
ワイ「
toggle
関数ではshow
のtrue
とfalse
を切り替えるんやけど」ワイ「何も考えんでやると
show
がfalse
になった瞬間に箱がDOMから消えるからアニメーションにならんのや」娘「そこで
useTransition
の出番だね」ワイ「せや」
ハスケル子「よく見ると
useTransition
を呼んでstartTransition
とisPending
を取得していますね」ワイ「せや、そして
startTransition
はtoggle
の中で使われとるで」index.jsconst toggle = () => { if (show) { startTransition(() => { setShow(false); setTimer(new Timer(500)); }); } else { setShow(true); } };ワイ「箱が出とるときに
toggle
を呼ぶとstartTransition
が使われるで」ハスケル子「
startTransition
はコールバック関数を受け取るんですね」ワイ「せや、これはすぐ呼ばれるで」
ワイ「この中でステートの更新をすると
useTransition
の効果が発揮されるんや」ハスケル子「今回は
setShow(false)
とsetTimer(new Timer(500))
ですね」娘「
Timer
ってなに?」ワイ「ええ質問やな、でもそれは後で説明するで」
ワイ「
useTransition
の効果はな、ステート更新によって発生したレンダリングが完了するまで画面の更新が遅延されるっちゅうことや」ハスケル子「???」
ワイ「ステートを更新したら当然再レンダリングが起こるやろ」
ハスケル子「はい」
ワイ「実はこのレンダリングが完了するまで500ミリ秒かかるんや」
娘「
Timer
のおかげだね!」ワイ「せや、ワイが説明する前に理解するとはさすが娘ちゃんやで」
ハスケル子「(あとで娘ちゃんに説明してもらおう)」
ワイ「レンダリング完了まで500ミリ秒かかるってことは
setShow(false)
が画面に反映されるまで500ミリ秒かかるってことやで」ハスケル子「なるほど、その間にアニメーションさせればいいんですね」
ワイ「物分かりがええやないか」
ハスケル子「でも、肝心のアニメーションはどうやっているんですか?」
ワイ「ここが
isPending
の出番や」index.js{show ? <Box show={show && !isPending} /> : null}ワイ「
isPending
は画面の更新が遅延されている最中にtrue
になるんや。普段はfalse
やで」娘「図を用意したからこれを見ながら説明するよ」
ワイ「さすが娘ちゃんや、天才やな!」
ワイ「最初は
show
がtrue
でisPending
はfalse
やから<Box show={true} />
がレンダリングされとるで」ワイ「ボタンを押すと
toggle()
が呼ばれるんや」ハスケル子「図によると、
toggle
を呼んだ瞬間、つまりstartTransition
の中でsetShow(false)
を呼んだ瞬間はshow
はtrue
のままでisPending
もtrue
になる」ハスケル子「そうなると
<Box show={false} />
がレンダリングされるんですね」ワイ「せや」
ワイ「
Box
はCSSでtransition: opacity 500ms linear
と指定してあるさかい、ここでアニメーションが起こるで」ハスケル子「そして500ミリ秒後に
show
がfalse
になる」ワイ「せや、これは
startTransition
による遅延が終わったからやな」ワイ「
isPending
もfalse
に戻るんや」ハスケル子「500ミリ秒というのはちょうどアニメーションにかかる時間ですね」
ワイ「せや、アニメーションが終わった頃に
useTransition
の遅延が終わってBox
が消されるっちゅう寸法や」ハスケル子「なるほど……なんとなく分かったような気がします」
ワイ「コードのURLをもう一回貼っとくから自分でいろいろ試してみるんやで」
レンダリング遅延の仕組み
ハスケル子「ところで、レンダリングが500ミリ秒遅延されたのはどうしてですか?」
ハスケル子「明らかにこの
Timer
が怪しいですよね」index.jsconst [timer, setTimer] = useState(null); // ... 中略 ... setTimer(new Timer(500));ハスケル子「
timer
はここで描画に使われています」index.js<Suspense fallback={null}> <Waiter timer={timer} /> </Suspense>娘「じゃあまず
Timer
について説明するよ!」娘「実装は別ファイルに書いてあるよ」
Timer.jsexport class Timer { constructor(duration) { const timer = new Promise(resolve => setTimeout(resolve, duration)); this.done = false; this.promise = timer.then(() => { this.done = true; }); } throwIfNotDone() { if (!this.done) { throw this.promise; } } }娘「
new Timer(duration)
が実行されると、duration
ミリ秒後に解決されるPromise
が作られるよ」ハスケル子「それは
this.promise
に入っているんですね」娘「そして、この
Promise
が解決されたらthis.done
がtrue
になるよ」ハスケル子「この
throwIfNotDone
メソッドは何ですか?」娘「これはまだPromiseが解決されていなかったらPromiseをthrowするというメソッドだよ」
ワイ「
throw
ってエラーが出たときに使う奴ちゃうんか?」ハスケル子「言語仕様上は
throw
は何でも投げられるんですよね」娘「この
Promise
を投げるというのがReactのConcurrent Modeの特徴なんだよ」娘「Reactは、コンポーネントが
Promise
を投げたらそのコンポーネントの描画が先延ばしにされたと判断するよ」ハスケル子「ということは、先延ばしの実態はここにあったんですね」
ワイ「せや、ここで投げられたPromiseを
useTransition
が検知して画面更新の遅延を発生させるんや」娘「まとめると、
new Timer(500)
は解決に500ミリ秒かかるPromiseを用意したってことなんだよ!」ハスケル子「じゃあ、これを使う側はどうなっているんですか?」
index.js<Suspense fallback={null}> <Waiter timer={timer} /> </Suspense>index.jsfunction Waiter({ timer }) { if (timer) timer.throwIfNotDone(); return null; }ワイ「
Waiter
コンポーネントがPromiseをthrow
する役目を持っとるで」ワイ「渡された
timer
がまだ解決していなかったらPromiseを投げるんや」娘「つまり
Waiter
は渡されたtimer
がまだ完了していなかったら完了するまでコンポーネントの描画を先延ばしにするという役目のコンポーネントだよ」ハスケル子「
Waiter
の周りにあるSuspense
は何ですか?」ワイ「Promiseを
throw
するコンポーネントは周りをSuspense
で囲む必要があるんや」ワイ「細かいことは筆者の既存記事を読むんやで」
娘「まとめるとこうだよ」
娘「
startTransition
の中でsetShow(false)
と同時にsetTimer(new Timer(500))
を実行したよね」娘「これにより再描画が発生するけど
Waiter
がPromiseを投げることで描画が先延ばしにされる」ハスケル子「
new Timer(500)
なので500ミリ秒先延ばしになるんですね」娘「あとはさっきパパが説明した通りだよ」
ワイ「せや」
ワイ「先延ばしになっている間に
useTransition
の効果でisPending
がtrue
になるんや」ワイ「かわいい娘ちゃんが作った図をもう一度出しておくで」
ワイ「以上がワイの実装の説明やで、随分長くなってもうたな」
ハスケル子「でもまだ半分しか実装できてないんですよねこれ」
ワイ「うっ……」
ワイ「待っとけや、もう半分もすぐに実装してくるで」
娘「がんばれパパ!」
出てくるときのアニメーションの実装
〜1ヶ月後〜
ハスケル子「いやどれだけ時間かかってるんですか」
ワイ「仕方ないやろ、筆者が思いつくまで1ヶ月くらいかかったんや(実話)」
ワイ「まあ上んとこまで記事書いてから考えなおしたら一瞬でできたんやけどな」
娘「考えをまとめ直すのは大事だね!」
ワイ「完全版の実装はこれやで」
ハスケル子「どれどれ……あまり変わってませんね」
ワイ「変わったのは1箇所だけや、
toggle
の定義部分な」index.jsconst toggle = () => { if (show) { startTransition(() => { setShow(false); setTimer(new Timer(500)); }); } else { setShow(true); startTransition(() => { setTimer(new Timer(10)); }); } };ハスケル子「
false
からtrue
になるときは、setShow(true)
はstartTransition
の外で実行してsetTimer
だけ中で実行するんですね」ワイ「せや」
娘「こうすると、
toggle
を押した直後はshow
がtrue
でisPending
もtrue
の状態になるよ」ハスケル子「そのときは……
<Box show={false} />
が描画されますね」娘「そして、10ミリ秒後に
isPending
がfalse
になるよ」ワイ「図にするとこうやで」
ハスケル子「今回も、何も無い状態からまず
opacity: 0
の状態を描画してそれからopacity: 1
にしているんですね」ハスケル子「10ミリ秒というのは何の意味があるんですか?」
ワイ「今回は
opacity: 0
の状態を描画するのは一瞬だけでええんや」ワイ「すぐ
opacity: 1
にしないとアニメーションが開始せんからな」ワイ「一瞬だけ
opacity: 0
を描画したかったからなるべく短い時間にしたんやけど」ワイ「
0
とかにすると短すぎてうまくいかへんのや」娘「0ミリ秒後というのは短すぎてReactが遅延したと認識しないみたいだね」
ハスケル子「なるほど……あれ」
娘「どうしたの?」
ハスケル子「何回もボタンを押しているとたまに箱が出るときのアニメーションが無いですね」
ワイ「せや、10ミリ秒でもたまに短すぎて
useTransition
が働かないときがあるで」ワイ「Reactは気まぐれやからな」
ハスケル子「100ミリ秒とかもっと長い時間にするのは駄目なんですか?」
ワイ「そしたらアニメーションが始まるのが100ミリ秒間遅れてまうやろ」
娘「逆に言えば、アニメーション開始を遅らせたいときはそれも有効な対策だよ!」
今回の実装の欠点
ハスケル子「ところで」
ハスケル子「ボタンを連打したときの動きが変ですよね?」
ワイ「うっ」
ハスケル子「特に、箱が消えている途中でボタン押しても何も起きずにそのまま消えますよね」
ハスケル子「消えている途中でボタンを押したら
show
がfalse
からtrue
に戻って箱が復活するべきじゃないですか?」ハスケル子「宣言的UIが聞いてあきれますね」
ワイ「そうでもないで」
ワイ「まあワイの実装があかんのは確かやな、今後の課題や」
娘「でも今回
show
をtrue
からfalse
に変えるのはstartTransition
の中だよね」娘「ということは
show
をfalse
に変えるのに500ミリ秒かかっているってことなんだよ」ワイ「せや」
ワイ「まだ
show
がtrue
のままなんやから、何回ボタン押してもfalse
になるだけのほうがええんちゃうか?」娘「図にするとこうだね」
娘「
toggle
はあくまでshow
がtrue
かfalse
かで動作を変えているよ」ハスケル子「そ……それは確かに」
ワイ「せや! 次のインターンの課題は今のバグを修正することにするで!」
ハスケル子「うっ……藪蛇でした」
まとめ
ハスケル子「結局この記事って何がテーマだったんですか?」
ワイ「おう、テーマな」
ワイ「useTransitionとアニメーションをタイトルに入れてreact-spring勢を釣りたいがテーマや」
ハスケル子「ぐっ……まんまと釣りに加担してしまったわけですか」
社長「おい」
ワイ「しゃ、社長!」
社長「お前この記事が今年イチ!お勧めしたいテクニック by ゆめみ feat.やめ太郎 Advent Calendar 2019の9日目って知っとんのか? テーマは「今年イチ!お勧めしたいテクニック」だったやろがい!」
ワイ「も、もちろん分かっておりまっせ! 今年イチお勧めしたいテクニックは何と言っても
useTransition
ですわ!」娘「react-springじゃなくてReact Concurrent Modeの
useTransition
だよ!」ワイ「筆者の前記事でも言っとった通り、
useTransition
は状態を複数同時に管理できるんですわ」ハスケル子「具体的には、『ステート変更反映前で
isPending
がtrue
の状態』と『ステート変更反映後の状態』ですね」ワイ「その2つの状態をうまく扱うことでアニメーションのような時間差処理が必要な処理を綺麗に扱うことができるんでっせ!」
社長「(綺麗だったか……?)」
ワイ「一定時間レンダリングを遅延させるために
Timer
とWaiter
を使うのはなかなか有用なテクニックとちゃいますか!?」ハスケル子「(
Waiter
とかかなり無理やりでは……?)」ハリー先輩「(絶対そんな使い方想定されてへんやろ)」
ワイ「他にも
useTransition
は色々な使い道があるで! ぜひ使いこなしてや!」ワイ「まだ正式リリースされとらんけどな!」
娘「今年中にリリースされる可能性はかなり低いよ!」
ハスケル子「(今年イチとは何だったのか)」
余談:もうひとつのお勧めテクニック
ワイ「もうひとつ考えたことがあるんや」
ハスケル子「何ですか?」
ワイ「実は真にお勧めしたいテクニック、それはワイ記法や」
ハスケル子「……」
ワイ「筆者はこう見えてもこの記事でワイ記法は3本目や、全部今年やで」
ハスケル子「何回も人のネタパクって何やってるんですか一体……」
ハスケル子「それどころかこういう第四の壁越えた茶番は客観的に見ると結構痛いですよ」
ワイ「しゃあないやろ、ワイはワイ記法を使うときは最初から最後まで貫く主義なんや」
ワイ「『くぅ〜疲れましたw』よりはマシやろ」
ハスケル子「(人のネタで相撲取っておいて何を言ってるんだか)」
ワイ「まあやってみて考えたことがあるんや、第三者の目線ってやつやな」
ハスケル子「はあ」
ワイ「普段ワイは文章構成とか気を使って書くほうなんやけど」
ワイ「会話形式やと文章構成もクソも無いんや」
娘「思考をそのまま書き下すのが主流みたいだね」
ワイ「でも本家の記事は分かりやすい言うてめっちゃ評判ええやろ」
ワイ「ワイなりにその理由を考えたんや」
ハスケル子「理由……」
ワイ「まあ一言で言うとキャラ付けや」
ワイ「キャラごとの役割を明確にするのが分かりやすさの理由の一つやと思ってこの記事でも実践してみたで」
ハスケル子「私は言うまでもなく聞き手役ですね」
ワイ「せや、解説役はワイと娘ちゃんや」
ハスケル子「2人はどう違うんですか?」
ワイ「まあはっきりした違いがあるわけではないんやけどな」
ワイ「ワイはどちらかというと読者寄り、娘ちゃんは教師役って立ち位置にしてみたで」
ワイ「基本はワイが実装者の目線で説明して娘ちゃんがライブラリの動きとかを解説するんや」
ハスケル子「3人の場合は聞き手、読者役、教師役と分けるといいんですね」
ワイ「まあ一例や」
ワイ「あと解説役が2人いるとやりやすいこともあるで」
娘「特に複数の話題がある場合や解説を後回しにする場合は、話題の担当者を明確にすることで読者の記憶を助ける効果もあると思うよ!」
ワイ「ほら、娘ちゃんは教師役さかい賢そうなしゃべり方なんや」
ハスケル子「賢そうというかなんか地の文に近くて堅いですね」
ワイ「まあ感覚で書いとる部分もあるんやけど、こういう所もキャラ付けに気を遣ったんやで」
ワイ「あとワイはレンダリングのことを『レンダリング』と呼ぶけど娘ちゃんは『描画』って呼んどるで」
ハスケル子「はあ」
ワイ「まあワイ記法も奥が深いってことを言いたかったんや」
ワイ「結構楽しいからみんなもワイ記法で記事書いてみてや!」
〜完〜