- 投稿日:2019-12-14T23:13:17+09:00
[React] 「create-react-app」環境構築を忘れないようにメモ
- 投稿日:2019-12-14T21:51:34+09:00
React入門まとめ
- 投稿日:2019-12-14T20:31:48+09:00
prerender-spa-pluginでogp対応
prerender-spa-plugin
SPAをプリレンダリングして事前に各ページのhtmlを作って動的なOGPに対応する方法としてはrendertron、prerender.io、netlify などいろんな外部サービスがあるが、ページ数の少ないSPAの場合は、prerender-spa-plugin を使うのが一番お手軽だった。
使い方もwebpackに以下のような設定を入れておくだけ。
const PrerenderSPAPlugin = require("prerender-spa-plugin"); const path = require("path"); module.exports = (config, env) => { if (env === "production") { config.plugins = config.plugins.concat([ new PrerenderSPAPlugin({ routes: ["/", "/sign-in"], // 事前にレンダリングしておきたいページ staticDir: path.join(__dirname, "build") }) ]); } return config; };このwebpackの設定をしている状態でビルドを行うと、以下のような感じで各ページごとにhtmlが吐き出される。
build ├── index.html ├── sign-in │ └── index.htmlあとはreact-helmetなどを使って各ページにmetaタグを入れておけば動的にogpが変わるようになる。
動作確認
動的にOGPが変わることをローカル環境で確認したい場合は、
1) まずビルド行なう
2) web-server-for-chromeや、serveなどを使ってビルド後のファイルが格納されているディレクトリを対象にサーバーを立てる。
3) ngrokを使って 2 で立てたサーバーを外部公開する
4) https://metatags.io/ などのogpが確認できるサイトに 3 で生成されたurlを貼り付ける。をすれば確認できる。
ci
prerender-spa-plugin
はpuppeteer
を使っているのでcircle ci上などでビルドしている場合はpuppeteer
のインストールも必要。
https://github.com/puppeteer/puppeteer/blob/master/docs/troubleshooting.md#running-puppeteer-on-circleciインストールしていない状態でビルドすると以下のようなエラーが出る。
Creating an optimized production build... Error: Failed to launch chrome! /home/circleci/repo/node_modules/puppeteer/.local-chromium/linux-686378/chrome-linux/chrome: error while loading shared libraries: libXtst.so.6: cannot open shared object file: No such file or directory TROUBLESHOOTING: https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md
トラブルシューティングの説明にもあるように、以下のようなpuppeteerをインストールするコマンドを定義し、実行しておけば上記エラーは解消された。
.circleci/config.ymlcommands: puppeteer_install: description: Install puppeteer and headless chrome steps: - run: name: Install Headless Chrome dependencies command: | sudo apt-get install -yq \ gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \ libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \ libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \ libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates \ fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget - run: name: Install puppeteer with chronium command: | yarn add puppeteerprerender-spa-pluginはページが多いようなSPAだと不向きだが、個人で作ってるようなページ数が少ないSPAで使うなら必要十分なんじゃないかと感じた。
- 投稿日:2019-12-14T19:00:43+09:00
React入門 ~基礎編~
現在業務で使用しているReactについて、より理解を深めていくために勉強したことを記事に書くことにしました。
ということで、まずは基礎編です。記事を書くにあたって
2019年秋頃からQiitaの記事を書いてきましたが、環境構築系ばかりで、いまだ技術の中身についての記事を書いたことがなかったので、そろそろ書いてみたいなーと思っていました。
技術記事となると、すでに同じテーマで記事を書かれている方がいてクオリティも高くてと、自分なんかが書いてもなと尻込みをしていたのですが、自分の理解を深めるという目的で思い切って書くことにしました。
記事を書く上で自然とまとめるようになるので、頭の中が整理できるので。もし自分がその技術から離れてしまって、また戻ってきたときに記事を見返して助けになればとも思います。
まだまだ勉強中の身であるため内容に間違い等ありましたら、コメントいただけると幸いです。なお、環境構築に関してはここでは書きません。
ただ、過去にDockerで環境構築した記事を書いているので、もしよろしければそちらをご覧ください。
→ DockerでReact + Swagger 環境を作ろうReactとは
React はユーザーインターフェイスを構築するための、宣言型で効率的で柔軟な JavaScript ライブラリです。複雑な UI を、「コンポーネント」と呼ばれる小さく独立した部品から組み立てることができます。
※React - チュートリアル:React の導入 - React とは? より
React (リアクト) は、Facebookとコミュニティによって開発されているユーザインタフェース構築のためのJavaScriptライブラリである。React.jsまたはReactJSの名称でも知られている。
Reactはシングルページアプリケーションやモバイルアプリケーションの開発におけるベースとして使用することができる。
Reactの基本
以下、記述しているコードはReact公式ドキュメントや公式チュートリアルを引用、もしくはベースにしています。
基本文法
index.jsで、
ReactDOM.render
でレンダリングするコンポーネントと、レンダリングする箇所を定義。
この例では、Appというコンポーネントに定義されたReact要素を、ルートDOMノード(index.htmlのid="root"の要素)にレンダリングするよう定義しています。public/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <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="apple-touch-icon" href="logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> </body> </html>src/index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; ReactDOM.render(<App />, document.getElementById('root')); // ルートDOMノードにAppコンポーネントをレンダリングするよう定義コンポーネント定義の中のreturnで返されるReact要素(JSX)がレンダリングする内容。
(※関数コンポーネントのため省略されていますが、正確にはrender()
メソッドが返すReact要素がレンダリングする内容)
エクスポートすることで、他ファイルからこのコンポーネントを呼び出すことができるようになります。src/App.jsimport React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( // レンダリングする内容 <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App; // コンポーネントをエクスポートコンポーネント
クラスコンポーネント
React.Component
を継承したクラスで定義されたコンポーネント。
後述する状態(state)やライフサイクルを持つことができます。import React from 'react'; class App extends React.Component { // React.componentを継承するクラスの定義 render() { // JSXを戻り値とするrenderメソッドを定義 return ( <h1>Hello World</h1> ); } } export default App;関数コンポーネント
クラスコンポーネントと違い、
render()
メソッドのみを持つコンポーネントをよりシンプルに記述したもの。
render()
メソッドのみであるため、render()
の記述が省略できます。最近の傾向では、なるべく関数コンポーネントを使用する方向であるようですが、状態(state)やライフサイクルを持つことができないため、関数コンポーネントもクラスコンポーネントと同等の機能を有せるようにと
recompose
やReact Hooks
が生まれたそうです。import React from 'react'; function App() { return ( <h1>Hello World</h1> ); } export default App;アロー関数で以下のようにも書くことができます。
import React from 'react'; const App = () => { return ( <h1>Hello World</h1> ); } export default App;JSX
基本
コンポーネントのrender()メソッド内に記述するHTMLに似た構文で、React要素を生成するもの。
Babelでのコンパイル時にReact.createElement()
形式に変換されます。
そのため、以下のコードは等価のものになります。const element = ( <h1 className="title"> Hello, world! </h1> );const element = React.createElement( 'h1', {className: 'title'}, 'Hello, world!' );また、JSXでは一つのReact要素を返す必要があります。
複数の要素から構成される場合は、divで囲うなどして、一つのReact要素を返すようにしましょう。
(※divで囲うなどで支障が出る場合は、React.Fragmentの欄を参照)NGな例
render() { return ( <h1>h1です</h1> <h2>h2です</h2> <p>pです</p> ) }OKな例
render() { return ( <div> <h1>h1です</h1> <h2>h2です</h2> <p>pです</p> </div> ); }JSXはHTMLと似ていますが閉じタグに関しては注意が必要。
imgタグのようにHTMLでは閉じタグが必要なかったものについては、末尾に/
とつけて閉じる必要があります。<img src='画像URL' />React.Fragment(※2019/12/17追記)
場合によっては、複数要素をまとめるためにdivで囲うことで支障が出ることもあります。
そういったときはReact.Fragment
というものを使うことができ、DOMに余分なノードを追加することなく子要素をまとめられます。
また、React.Fragment
では後述するkey
プロパティを持つことができます。詳しくはこちら
- React - Docs - フラグメントrender() { return ( <React.Fragment> <h1>h1です</h1> <h2>h2です</h2> <p>pです</p> </React.Fragment> ); }短縮記法で以下のようにも書くことができますが、こちらでは
key
プロパティを持つことができないので注意が必要です。render() { return ( <> <h1>h1です</h1> <h2>h2です</h2> <p>pです</p> </> ); }変数の埋め込み
JSX内に変数を埋め込む際は
{}
で囲って記述します。const name = 'Yamada Taro'; const element = <h1>Hello, {name}</h1>;なお、デフォルトでは、React DOMはJSXに埋め込まれた値をレンダリングされる前にエスケープするため、以下のようにユーザーの入力を受け付ける場合でも、XSSなどインジェクション攻撃を防ぐことができます。
const title = response.potentiallyMaliciousInput; const element = <h1>{title}</h1>;命名規則
要素のプロパティに関してはキャメルケース。
例として、class はclassName
となり、tabindex はtabIndex
となります。props
コンポーネントに渡せるプロパティのこと。
propsを使うことで親コンポーネントが子コンポーネントに情報を渡すことができ、同じコンポーネントでも、渡すpropsによって変化をつけることができます。呼び出し側
<Message name="Taro" />コンポーネント側(関数コンポーネントの例)
function Message(props) { return <h1>Hello, {props.name}</h1>; }コンポーネント側(クラスコンポーネントの例)
class Message extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }state
基本
クラスコンポーネントではstate(状態)を持つことができます。
設定するには、まずクラスコンポーネントにコンストラクタを追加してstateを初期化。
(クラスコンポーネントのコンストラクタは常にpropsを引数として、親クラスのコンストラクタを呼び出す必要があります)class Square extends React.Component { constructor(props) { super(props); this.state = { value: null, }; } ※以下省略 }stateの更新
直接stateに代入するのでなく、
setState
を使用して更新するようにします。
なお、一度のsetState
で複数のstateの値を更新することもできます。stateが更新されると、そのコンポーネントは再度レンダリングされます。
(setState
を使用せずにstateを直接変更した場合は再レンダリングされないのでやらないこと)class Square extends React.Component { ※一部省略 render() { return ( // ボタンクリックでstateに値をセットする <button className="square" onClick={() => this.setState({value: 'X'})}> {this.state.value} </button> ); } }また、this.propsとthis.stateは非同期に更新されるため、
setState
時にそれらの値に依存してはいけません。ダメな例
this.setState({ counter: this.state.counter + this.props.increment, });子コンポーネントから親コンポーネントのstateを変更したい場合は、
this.state
はそのコンポーネント内でプライベートになるため、直接変更することはできません。
この場合は、親コンポーネントから子コンポーネントにstateを更新する関数を渡すようにして、この関数を子コンポーネントで呼び出すことで対処できます。class Square extends React.Component { ※一部省略 render() { return ( <button className="square" onClick={() => this.props.onClick()}> {this.props.value} </button> ); } } class Board extends React.Component { ※一部省略 handleClick(i) { // 配列のコピーを作成 const squares = this.state.squares.slice(); squares[i] = 'X'; this.setState({squares: squares}); } renderSquare(i) { return <Square value={this.state.squares[i]} onClick={() => this.handleClick(i)}/>; }イベント
基本
基本的にはタグ内に
イベント名={() => {処理}}
で記述することができます。<button onClick={() => {console.log('Hello World')}}>こんにちは</button>イベントの例
- onClick クリックされた時、buttonタグなど
- onSubmit 送信された時、formタグなど
- onChange 入力や削除が行われた時、inputタグなど
- onMouseOver マウスが上に置かれた時
- onMouseOut マウスが外れた時onChangeの例
event
は合成 (synthetic) イベントです。event.target.value
で入力された値を取得できます。<input onChange={(event) => {console.log(event.target.value)}} />preventDefault
aタグのリンクやチェックボックスなど、元々のイベントをキャンセルするものです。
function ActionLink() { function handleClick(e) { e.preventDefault(); console.log('The link was clicked.'); } return ( <a href="#" onClick={handleClick}> Click me </a> ); }key
要素のリストをレンダリングする際、リストの項目それぞれについて、Reactは情報を保持します。
リストに変更、追加、削除などがあった時に、Reactがどのアイテムが変更になったのかを知るために、keyプロパティを与えるようにしましょう。リストが再レンダリングされる際、Reactはそれぞれのリスト項目のkeyについて、前回のリスト項目内に同じkeyを持つものがないか探し、その結果によってリスト項目を追加したり、削除したりします。
keyプロパティの値は兄弟要素の中で一意であれば問題ないようです。なお、リスト項目にkeyプロパティを設定していないと、コンソールで警告が表示されます。
また、keyプロパティはpropsの一部のようにも見えるが、this.props.key
で参照することはできません。const numbers = [1, 2, 3, 4, 5]; const listItems = numbers.map((number) => <li key={number.toString()}> {number} </li> );リスト項目をコンポーネント化した際は、呼び出し時にkeyプロパティを設定するようにしましょう。
function ListItem(props) { // ここではkeyプロパティを指定しない return <li>{props.value}</li>; } function NumberList(props) { const numbers = props.numbers; const listItems = numbers.map((number) => // ここでkeyプロパティを指定 <ListItem key={number.toString()} value={number} /> ); return ( <ul> {listItems} </ul> ); }ライフサイクル
※React Lifecycle Methods diagram より(GitHub - react-lifecycle-methods-diagram)
マウント
コンポーネントのインスタンスが作成されてDOMに挿入される時に、以下のメソッドが次の順序で呼び出されます。
- constructor()
- static getDerivedStateFromProps()
- render()
- componentDidMount()
更新
propsやstateの変更によって発生します。
コンポーネントが再レンダリングされるときに、以下のメソッドが次の順序で呼び出されます。
- static getDerivedStateFromProps()
- shouldComponentUpdate()
- render()
- getSnapshotBeforeUpdate()
- componentDidUpdate()
アンマウント
コンポーネントがDOMから削除されるときに呼び出されます。
- componentWillUnmount()
エラーハンドリング
任意の子コンポーネントのレンダリング中、ライフサイクルメソッド内、またはコンストラクタ内でエラーが発生したときに呼び出されます。
- static getDerivedStateFromError()
- componentDidCatch()
それぞれのメソッドの解説
長くなりそうなので割愛します。
こちらで詳しく解説されていますのでご参照ください。
- React - Docs - React.Component
- React(v16.4) コンポーネントライフサイクルメソッドまとめなお、自分は
render()
後に一度だけ呼ばれるcomponentDidMount()
を使用することが多いです。(といいつつ、最近まで呼ばれるタイミングを少し勘違いしていたのですが...)
Reactの基礎としてどこまで書くか迷いましたが、公式チュートリアルで扱われたものを中心に今回書きました。
続編として、フォームやrouter、recomposeやRedux、React Hooksなどについて書いていけたらと思います。
React Hooksについては、現状全く使ったことがない未知の領域ですが、モダンな書き方を追求するためにも今後ぜひ勉強したいです。参考リンクまとめ
- 投稿日:2019-12-14T17:54:49+09:00
reducerを遅延させてみる
ファーストビューの効率化のために、ファーストビューのタイミングで不要なものを遅延ロードにしていたのですが、reducerを遅延させるのもいいかなと思えてきました。
reducerが必要となるタイミング
コンポーネントを描画した直後に
dispatch
を行う必要性というのは、そうそう生じないものです。ユーザーインタラクションでしかdispatch
が行われない、ということでしたら、コンポーネントを作成したタイミングで遅延ロードを開始して、そこから1秒もしないうちにロードされる、というようなタイミングでじゅうぶん間に合います。そして、巨大なオブジェクトを取り扱うreducerは、往々にして多くのコードを必要とします。
とはいえ、ふつうの
React.useReducer
では、reducer関数は同期的に必要となってしまいます。
useLazyReducer
を作ってみたということで、遅延ロードでreducerをロードするようなHooksを立ててみました。
// ロード前は何もしないreducerを入れておく const temporaryReducer = (store, _action) => store; export default function useLazyReducer(lazyReducer, initialArg, initializer) { // 遅延データを入れる const reducerRef = React.useRef(temporaryReducer); React.useEffect(() => { lazyReducer().then((obj) => { reducerRef.current = obj.default; }); }, [lazyReducer]); const reducer = React.useCallback( (store, action) => reducerRef.current(store, action), [] ); return React.useReducer(reducer, initialArg, initializer); }仕掛けとしてはごく単純で、reducerとしてセットする関数はrefの中身につなぐだけ、としておいて、遅延ロードでrefを書き換える、というものです。
lazyReducer
として渡すものは、React.lazy
に倣って、「{default: reducer関数}
をresolve
するPromise
を返す関数」を受け取るようにしてあります。使用感
遅延ロードが間に合わないぐらいのタイミングでユーザー操作に入るのは現実的に困難ですので、これで十分に間に合っている印象です。
- 投稿日:2019-12-14T16:36:10+09:00
DECK.GLを使ってGoogleMapタイムラインをビジュアライズしてみよう!#2
前回に引き続き、DECK.GLを使ってGoogleMapのタイムラインをビジュアル化してみよう!の第2弾の記事です。
前回はPCにReactとDECK.GLの環境構築まで終わりました。
今回は実際にコーディングに入っていきます!不要なファイルを削除しよう!
前回、
create-react-app
コマンドを使ってReactの環境を作成しましたが、まずはシンプルに作りたいので不要なファイルを削除したいと思います。これがデフォルトの状態。
なんかいっぱいファイルがありますね。
src
ディレクトリの中からindex.js
を残してそれ以外は削除しちゃいましょう。
こんな感じ
index.jsファイル
初期の
index.js
はこんな感じだと思います。index.js (デフォルト)import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render(<App />, document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();ここにも不要な記述がいっぱいあるので削除しちゃいましょう。
削除後のindex.js
は下記のような感じ。index.js (修正後)import React from "react"; import ReactDOM from "react-dom"; import App from "./App.jsx"; ReactDOM.render(<App />, document.getElementById("root"));これから
App.jsx
にコードを書いていきます。
※拡張子は.js
でもいいのですが、.jsx
にするとvscodeにReactのアイコンが表示されて気分が上がるので、私はいつもこっちで書いてますw
こんな感じ
App.jsxに基本的なコードを書いていこう!
それでは、ようやくコーディングに入ります!
まずは、index.js
と同じ階層にApp.jsx
を作成しましょう。
できたら、下記のコードを
App.jsx
に書きます。(まだDECK.GLは全く関係ないです)
まずは基本的なReactのコードを書いて動くのを確認してみましょう!App.jsximport React from "react"; export default class App extends React.Component { render() { return <div>Hello, DECK.GL!!</div>; } }動作確認は
yarn start
のコマンドを実行するだけでOK!
create-react-app
で作成したプロジェクトは、babelやwebpack、ESLintの設定が済んでおり、このコマンドを実行だけでサーバを起動しhttp://localhost:3000
でアクセスできるようになります。動いたー!
まとめ
今回もまだDECK.GLに触れることができませんでしたが、次回こそは本題に入れるといいな・・・!
- 投稿日:2019-12-14T15:59:52+09:00
React+hookで映画検索アプリを作る
はじめに
この記事はFreeCodeCampで公開されているサンプルアプリをチュートリアルした時の記録です。1
学んでみた所感であったり、英語の解釈など残していきます。
!!!もし見当違いな解釈などあれば指摘していただけると嬉しいです!!!準備するもの
- Node >= 6
- API key (ここで取得してください)
プロジェクトの作成
Reactアプリの雛形を作成
$ npm install -g create-react-app $ create-react-app hooked # "hooked"というプロジェクトが作成されるこんな感じのディレクトリ構成になる
hooked ├── README.md ├── node_modules # 省略 ├── package-lock.json ├── package.json ├── public # 省略 └── src # ここにプログラムを追記していきます ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── serviceWorker.jsComponentの作成
src
内にcomponents
ディレクトリを作成します。
作成した後、src/App.js
をsrc/components
内へ移動します。
続いて、
-src/components/Header.js
-src/components/Movie.js
-src/components/Search.js
を作成します。コードは後ほど。Componentの役割
ここで先に
Component
の役割について確認します。
その後、先ほど作成したファイルの役割について見ていきます。"コンポーネント"="GUIのパッケージをモジュール化したもの"
GUIで使われる様々な粒度のパーツを”コンポーネント”という単位に区切って再利用できるようにすること、共同作業しやすくすることを目的としています。2各ファイルの役割
次に先ほど作成したファイルの役割を見ていきます。
- App.js : Header.js, Movie.js, Search.jsの親コンポーネントになります。また、ここではAPIリクエストの処理関数や、コンポーネントの初期レンダリング中にAPIを呼び出す関数を含みます。
- Header.js : アプリケーションのヘッダーをレンダリングし、タイトルのprop(プロパティ)を受け取るシンプルなコンポーネントです。
- Movie.js : 各映画情報をレンダリングします。映画のオブジェクトはpropとして渡されます。
- Search.js : 入力要素と検索ボタンを含むフォーム、入力要素の処理と、フィールドのリセットを行う関数、およびpropとして渡される検索関数を呼び出す関数を含みます。
作っていく
1.
src/components/Header.js
にpropのテキストをレンダリングする機能を実装src/components/Header.jsimport React from "react"; const Header = (props) => { return ( <header className="App-header"> {/* propsのテキストをレンダリング */} <h2>{props.text}</h2> </header> ); }; export default Header;2.'src/index.js`の4行目を変更
src/index.js〜省略〜 import App from './components/App'; // App.jsのパスを変更する 〜省略〜3.
src/App.css
を書き換えるsrc/App.css.App { text-align: center; } .App-header { background-color: #282c34; height: 70px; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; padding: 20px; cursor: pointer; } .spinner { height: 80px; margin: auto; } .App-intro { font-size: large; } /* new css for movie component */ * { box-sizing: border-box; } .movies { display: flex; flex-wrap: wrap; flex-direction: row; } .App-header h2 { margin: 0; } .add-movies { text-align: center; } .add-movies button { font-size: 16px; padding: 8px; margin: 0 10px 30px 10px; } .movie { padding: 5px 25px 10px 25px; max-width: 25%; } .errorMessage { margin: auto; font-weight: bold; color: rgb(161, 15, 15); } .search { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; margin-top: 10px; } input[type="submit"] { padding: 5px; background-color: transparent; color: black; border: 1px solid black; width: 80px; margin-left: 5px; cursor: pointer; } input[type="submit"]:hover { background-color: #282c34; color: antiquewhite; } .search > input[type="text"]{ width: 40%; min-width: 170px; } @media screen and (min-width: 694px) and (max-width: 915px) { .movie { max-width: 33%; } } @media screen and (min-width: 652px) and (max-width: 693px) { .movie { max-width: 50%; } } @media screen and (max-width: 651px) { .movie { max-width: 100%; margin: auto; } }4.続いて、
src/components/Movie.js
を書き換える。src/components/Movie.jsimport React from "react"; const DEFAULT_PLACEHOLDER_IMAGE = "https://m.media-amazon.com/images/M/MV5BMTczNTI2ODUwOF5BMl5BanBnXkFtZTcwMTU0NTIzMw@@._V1_SX300.jpg"; const Movie = ({movie}) => { const poster = movie.Poster === "N/A" ? DEFAULT_PLACEHOLDER_IMAGE : movie.Poster; return ( <div className="movie"> <h2>{movie.Title}</h2> <div> <img width="200" alt={`The movie titled: ${movie.Title}`} src={poster} /> </div> <p>({movie.Year})</p> </div> ); }; export default Movie;この時、
movie
を引数に取っているのですが、引用元はまだ作成していないため、movie.Poster
などの要素はまだ参照できません。
また、DEFAULT_PLACEHOLDER_IMAGE
はAPIから取得した一部の映画に画像がないため、リンク切れの代わりにプレースホルダー画像をレンダリングするためのurlです。5.次に
src/components/Search.js
を書き換える。
ここでは、onChange
イベントが呼ばれると、新たな値で状態更新関数を呼び出すhandleSearchInputChanges
が呼び出されます。src/components/Search.jsimport React, {useState} from "react"; const Search = (props) => { const [searchValue, setSearchValue] = useState(""); const handleSearchInputChanges = (e) => { setSearchValue(""); }; const resetInputField = () => { setSearchValue(""); }; const callSearchFunction = (e) => { e.preventDefault(); props.search(searchValue); resetInputField(); }; return ( <form className="search"> <input value={searchValue} onChange={handleSearchInputChanges} type="text" /> <input onClick={callSearchFunction} type="submit" value="SEARCH"/> </form> ); }; export default Search;ここで、
useState
というReactのhook*機能を使用することで、クラスを記述することではなく、状態やその他のReact機能を使用することができます。
* hookとは、状態を関数コンポーネントに追加できる機能って認識でいいのかな
useState
は初期状態を引数とし、現在の状態(≒this.state)と更新関数(≒this.setState)を含む配列を返します。6.以上を記述したら、
src/components/App.js
を書き換える。src/components/App.jsimport React, {useState, useEffect} from 'react'; import '../App.css'; import Header from "./Header"; import Movie from "./Movie"; import Search from "./Search"; const MOVIE_API_URL = "http://www.omdbapi.com/?i=tt3896198&apikey=bc2a5cf"; // you should replace this with yours const App = () => { const [loading, setLoading] = useState(true); // 1 const [movies, setMovies] = useState([]); // 2 const [errorMessage, setErrorMessage] = useState(null);// 3 useEffect(() => { fetch(MOVIE_API_URL) .then(response => response.json()) .then(jsonResponse => { setMovies(jsonResponse.Search); setLoading(false); }); }, []); const search = searchValue => { setLoading(true); setErrorMessage(null); fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=bc2a5cf`) .then(response => response.json()) .then(jsonResponse => { if (jsonResponse.Response === "True") { setMovies(jsonResponse.Search); setLoading(false); } else { setErrorMessage(jsonResponse.Error); setLoading(false); } }); }; return ( <div className="App"> <Header text="HOOKED"/> <Search search={search}/> <p className="App-intro">Sharing a few of our favourite movies</p> <div className="movies"> {loading && !errorMessage ? ( <span>loading...</span> ) : errorMessage ? ( <div className="errorMessage">{errorMessage}</div> ) : ( movies.map((movie, index) => ( <Movie key={`${index}-${movie.Title}`} movie={movie}/> )) )} </div> </div> ); }; export default App;ここで、
useState
関数を3回使用しています。そのため、1つのコンポーネントに複数のuseState
関数を所持できます。useState1では、ロード状態を処理するために使用されます。
useState2では、サーバから取得した映画の配列を処理するために使用されます。
useState3では、APIリクエストを行う時に発生する可能性のあるエラーを処理するために使用されます。また、
useEffect
フックもここで使用します。useEffectでは、関数コンポーネントにライフサイクルメソッドを追加することができます。ただ、ここは自分自身理解できていないので説明は省略します。最初のレンダリングの後と、更新の後に
useEffect
が呼び出されます。
更新のたびに呼び出されるときは、useEffect
関数が実行される関数と第二引数をみて、渡された変数が変更されていない場合、エフェクトの適用をスキップするようReactに指示する値を渡します。ここで、
App.js
を変更する。src/components/App.jsimport React, { useReducer, useEffect } from "react"; import "../App.css"; import Header from "./Header"; import Movie from "./Movie"; import Search from "./Search"; const MOVIE_API_URL = "https://www.omdbapi.com/?s=man&apikey=4a3b711b"; const initialState = { loading: true, movies: [], errorMessage: null }; const reducer = (state, action) => { switch (action.type) { case "SEARCH_MOVIES_REQUEST": return { ...state, loading: true, errorMessage: null }; case "SEARCH_MOVIES_SUCCESS": return { ...state, loading: false, movies: action.payload }; case "SEARCH_MOVIES_FAILURE": return { ...state, loading: false, errorMessage: action.error }; default: return state; } }; const App = () => { const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { fetch(MOVIE_API_URL) .then(response => response.json()) .then(jsonResponse => { dispatch({ type: "SEARCH_MOVIES_SUCCESS", payload: jsonResponse.Search }); }); }, []); const search = searchValue => { dispatch({ type: "SEARCH_MOVIES_REQUEST" }); fetch(`https://www.omdbapi.com/?s=${searchValue}&apikey=4a3b711b`) .then(response => response.json()) .then(jsonResponse => { if (jsonResponse.Response === "True") { dispatch({ type: "SEARCH_MOVIES_SUCCESS", payload: jsonResponse.Search }); } else { dispatch({ type: "SEARCH_MOVIES_FAILURE", error: jsonResponse.Error }); } }); }; const { movies, errorMessage, loading } = state; return ( <div className="App"> <Header text="HOOKED" /> <Search search={search} /> <p className="App-intro">Sharing a few of our favourite movies</p> <div className="movies"> {loading && !errorMessage ? ( <span>loading... </span> ) : errorMessage ? ( <div className="errorMessage">{errorMessage}</div> ) : ( movies.map((movie, index) => ( <Movie key={`${index}-${movie.Title}`} movie={movie} /> )) )} </div> </div> ); }; export default App;
useReducer
の基本的な使い方は以下の通りです。useReducerconst [state, dispatch] = useReducer( reducer, initialState );Reducerは
initialState
とaction
を受け取るため、action
タイプに基づいて新しい状態オブジェクトを返します。また、useEffectで、サーバーから取得しているmovies
配列としてpayload
を使用してアクションをディスパッチします。 また、検索機能では、3つの異なるアクションを実際にディスパッチしています。以上で、ことチュートリアルで必要なプログラムの書き換えおよび知識は揃いました。ここから実行してみたところこのようになりました。
どうしてだろう。。。
どこが間違えていたか見返してみます。また、この結果は追記したいと思いますが、わかる方がいらっしゃれば、コメントをいただけたら嬉しいです。参考にさせていただいたサイト
- 投稿日:2019-12-14T13:32:04+09:00
React まとめ②
Reactアプリケーションはelement(要素)という構成ブロックにより構成されています。
コンポーネントは要素によって構成されたものです。
コンポーネントを使うことにより、UIを部品に分割し分離させることができます。コンポーネントはpropsと呼ばれる任意の値を受け取り、画面上に表示すべきものを返すReaxt要素を返します。
function Welcome(props) { return <h1>Hello, {props.name}</h1>; }これはpropsで受け取ったnameを表示させる関数コンポーネントです。
class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }これは等価なコードです、classコンポーネントを使う際はextends React.Componentを記載して下さい。
またファイル冒頭にimport React, { Component } from 'react';を書くことも必要です。function Welcome(props) { return <h1>Hello, {props.name}</h1>; } const element = <Welcome name="Sara" />; ReactDOM.render( element, document.getElementById('root') );このコードではelementと定義された要素を引数としてReactDOM.renderが呼び出されます。
そのあとelementに代入されたwelcomeコンポーネントを呼び出し、その時propsとしてname=’Sara’が渡されます。
その後ReactDOMがHello, Sara
になるようDOMを更新します。Reactでは
<welcome />はDOMタグ <Welcome />はコンポーネント と認識されることを覚えておきましょう。class App extends React.Component { render() { return ( <div> <h1>Hello World</h1> </div> ); } }これはclassコンポーネントですが、特徴として内部にプライベート状態(state)をもたせたり、
生成状況によって呼び出されるライフサイクルメソッドを持っています。
主に親コンポーネントとして使用し関数コンポーネントで作った子コンポーネントに、保持している値を伝える親玉のような存在と言えるでしょう。const App = (props) => { return ( <div> <h1>Hello World</h1> </div> <h2>Hello React</h2> ); };これは同レベルのReact Elementがあるためエラーになります。
const App = (props) => { return ( <div> <h1>Hello World</h1> <h2>Hello React</h2> </div> ); };とdivタグで囲ってあげましょう。
// 親コンポーネント class Parent extends React.Component { //superは必須です constructor(props) { super(props); this.state = { value1: 'foo', value2: [ 'bar', 'baz' ], }; } render() { return ( <div> <Child1 data={this.state.value1} /> <Child2 data={this.state.value2} /> </div> ); } } // Functionalコンポーネントで受け取る場合 const Child1 = (props) => ( <div> {props.data} </div> ); // Classコンポーネントで受け取る場合 class Child2 extends React.Component { render() { return ( <div> {this.props.data} </div> ); } }親で独自の名前を付けて子に流していく流れですね。
クラスコンポーネント間でも受け取ることができます。viewで発生したイベントハンドラはクラスコンポーネントのメソッドで設定して下さい。
その際はキャメルケースで書く必要があります。
またコールバック関数として実行する際にthisが機能するようconstructor()内でthisをbindしておく必要があります。
これはjavascriptにまつわる話題です。ReactDom.render( <App />, document.querySelector('.content') );これは冒頭紹介したコードです、深掘りしていきましょう。
親コンポーネント(App.js)のrender()メソッドで返されたレンダリングの実態は仮想DOMと呼ばれるものです。仮想DOMではブラウザで表示できないので生のDOMに変換する必要があるのですが、そこでこのコードが活躍します。
第一引数に親コンポーネント、第二引数にhtmlで表示・挿入する部分を渡しています。
- 投稿日:2019-12-14T13:28:33+09:00
【Redux】dispatchして,dispatchして,dispatchする
VALU Advent Calendar 2019 13 日目の投稿になります??
こんにちは !! VALU サービスの Web アプリケーションにおいて,クライアントサイド開発を担当している藤本です. 6 日目の投稿 でも登場しました.今回も前回同様、頑張って書いていこうと思います!はじめに
前回の記事で取り上げた Redux について今回も引き続き書いていこうと思っています.前回は, Reducers と Store に着目したのですが,今回は主に Actions について触れていこうと思います.
また, VALU サービスにおける Redux の扱い方は前回の記事にて少し触れたので今回は割愛しています.題目について
題目だけで察して頂ける方も多いと思うのですが,Redux の
dispatch()
( Dispatcher ) って最初わかりにくくありませんでしたか?ぼくは、 Redux を理解しようとする上で,このdispatch
にかなり苦しめられることになりました.題目をもっと Redux の文脈に当てはめて書くと,
Container から
dispatch
して,Actions からdispatch
して,Store からdispatch
する.になります.
...?
もっとマシな感じで言うと,
Container から Actions に
dispatch
して,場合によってはActions からもdispatch
して,Store から Container にdispatch
する.になります.
ん...??
何がわかりにくいのか
ぼくは先輩に言われていた「 Redux は非同期が難しい」の意図がわかっていませんでした.いわゆる Redux ミドルウェア問題です. VALU サービスのフロントエンド開発における非同期処理は
redux-thunk
を使ってますが,わかりにくいのは Actions でもdispatch
している点だと思っています.今回はその部分について重点的に話していきたいと思います.一旦ミドルウェアに関しては, またあとで触れるとして,「わかりにくい」
dispatch
のことを少しずつ解明していきましょう ?そもそも
dispatch
ってどういう意味?ggると,発送するとか派遣するとか出てきます.
一旦簡潔に「運ぶ」にしておきましょう。
え?何を運ぶの?
それは,Actionオブジェクトです.
action: { type: 'FETCH_HOGE', payload: { hoge: hogehoge } }こんなやつですね.
なぜ Actions で
dispatch
するのかRedux ミドルウェアで,
redux-thunk
を使っている場合,Actions でも, Action オブジェクトを運ぶ必要があるからです.VALU のフロントエンド開発ではどうしているのか?
例のごとく,以下 ? のような API からのレスポンスがあるとします.
?EventType.js
EventType.jsexport type Event = { id: number, // イベントID title: string, // イベントタイトル image_url?: string, // イベントのTop画像 address?: string, // イベントの開催地 geotag?: string, // イベントの位置情報 min_age: number, // 年齢制限 start_at: string, // イベントの開始時刻 end_at: string, // イベントの終了時刻 detail?: string // イベントの内容詳細 };このレスポンスを受けるための API 通信を Actions で行います.
actions/event.jsimport { fetchEvent } from '../api/Event'; import createAsyncActions from '../utils/createAsyncActions'; export const FETCH_EVENT = 'FETCH_EVENT'; export const fetchEventCreators = createAsyncActions(FETCH_EVENT); export const fetchEventCreator = () => dispatch => { dispatch(fetchEventCreators.pending()); fetchEvent().then(response => { dispatch(fetchEventCreators.fulfilled({ ...response})); }); .catch(error => { dispatch(fetchEventCreators.rejected(error)); }); };このように
actions/event.js
の中でdispatch
を行なっています.なぜここでも行なっているのかというと, 以下 ? のように API 通信をおこなう際に,(当然ですが....)レスポンスの状態が Action オブジェクトとして返ってくるわけではないからです.VALU ではaxios
を使っているのでPromise
オブジェクトになってるわけですね.api/Event.js// @flow import { apiClient } from './Api'; // ここの`Event`は上述した`EventType`を使用しているものとします type EventResponse = {| event: Event |}; export const fetchEvent: () => Promise<EventResponse> = () => apiClient .get<EventResponse, void>(`Endpointが入ります`) .then(res => (res.type === 'Success' && res.data : null)); // 注意: apiClientの処理は割愛します.では,API 通信を行なった後に再度 Action オブジェクトに変換する処理が必要になります.
上述したactions/event.js
のコードで謎の記述があったと思うのですが(createAsyncActions
です),これは,VALU の自作の関数になるのですが,それを使って Action オブジェクトを以下 ? のように作り直します.utils/createAsyncActions.jsimport { createAction } from 'redux-actions'; export default function createAsyncActions( type, payload = null, meta = null ) { return { pending: createAction(`${type}_PENDING`, payload, meta), fulfilled: createAction(`${type}_FULFILLED`, payload, meta), rejected: createAction(`${type}_REJECTED`, payload, meta) }; }こうすることで晴れて,Action が作成されたので,それを
dispatch
するわけですね..!
つまり, Actions で Redux ミドルウェアで非同期処理するために Action を作り直す必要があるため再度dispatch
しているのです.Actionを分割すればいいのでは? ?
Actions を配置するディレクトリに非同期処理が混じっていることが Redux をよりわかりにくいものにしているとぼくは考えます.
なので,非同期処理を行なっている部分とそうでない部分を単純に分けたディレクトリ構造にすればいいのではないかと考えています.
そうすれば,僕のように Actions でもdispatch
しているけど,どういうことだろう?? ということになる人が減るのではないでしょうか?
この提案は,redux-thunk
を使っている場合に限るかもしれませんが,以下のようなディレクト構造に変えるとよりいいのでは?と思っております.├── actions/ │ ├── user.js │ └── event.js ├── middlewares/ │ ├── user.js │ └── event.js終わりに
Redux でも個人的には未だに難解と考えるミドルウェアについて話してみました..かなりハイカロリーだった..
これからも Redux とはうまく付き合いつつもそろそろ Hooks を使った状態管理もがっつりやっていかないとなーと考えています.
最後までありがとうございました!
- 投稿日:2019-12-14T10:44:11+09:00
use-reducer-asyncの紹介
はじめに
先日の記事でreact-trackedの紹介をしました。react-trackedはreactのcontextとhooksを使ったglobal stateのライブラリです。プリミティブな機能を提供しており、必要に応じて拡張(custom hooks化など)して使えます。非同期処理も可能なのですが、ドキュメントサイトでチュートリアルを作成する際には、オススメの非同期処理なども記述する必要があると思いました。react-trackedはreduxなどと違って外部にstoreを持たないため、非同期処理もReactの範囲でやることになります。custom hooksで非同期処理用のcallbackを作る方法もありますが、今回は、useReducerを拡張して非同期処理を記述するライブラリを紹介します。
react-trackedと合わせて使うことを想定して説明しましたが、特にreact-tracked専用のライブラリではなく、普通のReact stateでも使うことができます。
use-reducer-async
リポジトリはこちらです。
https://github.com/dai-shi/use-reducer-async
コードはとても小さいです。ライブラリにしなくても自分でcustom hooksを書いても同じことができます。このライブラリは、その機能より、コーディングパターンを提案することに意味があります。
使い方
一例として、データ取得を行うケースを実装してみたいと思います。
ライブラリをインポートします。一つのhookだけです。
import { useReducerAsync } from 'use-reducer-async';初期ステートを定義します。personというデータを取得するケースを想定しています。
const initialState = { loading: false, person: null, };reducerを定義します。
const reducer = (state, action) => { switch (action.type) { case 'FETCH_STARTED': return { ...state, loading: true }; case 'FETCH_FINISHED': return { ...state, loading: false, person: action.person }; default: throw new Error('no such action type'); } };非同期処理を行うハンドラーを定義します。今回は一つだけです。
const asyncActionHandlers = { START_FETCH: (dispatch, getState) => async (action) => { dispatch({ type: 'FETCH_STARTED' }); const data = await fetchData(); dispatch({ type: 'FETCH_FINISEHD', person: data }); }, };最後にコンポーネントでhookを使います。
const Component = () => { const [state, dispatch] = useReducerAsync(reducer, initialState, asyncActionHandlers); return ( <div> <button type="button" onClick={() => dispatch({ type: 'START_FETCH' })}>Fetch Person</button> {state.loading && 'Loading...'} {state.person && <Person person={state.person} />} </div> ); };これにより非同期処理が実行されて、ステートが更新されます。
ポイント
今回の例では使いませんでしたが、本ライブラリはTypeScriptの型定義があるため、それを使うとより便利です。
ところで、Reduxでは公式ドキュメントでredux-thunkを推奨していますが、型付けが難しいことが課題です。use-reducer-asyncやcustom hooksで非同期処理をする場合は、その課題が軽減されます。ちなみに、本ライブラリのハンドラー定義はredux-thunkのAPIとそっくりです。
また、Reduxの場合はexternal storeを使うため、Concurrent Modeの対応が限定的になる部分があります。use-reducer-asyncはReactのstateを使うだけなので、Concurrent Modeに素直に対応できます。
おわりに
このライブラリを作ろうと思ったきっかけは、use-saga-reducerを見つけたからです。redux-sagaにExternal APIというものがあることを知らず、Redux以外で使えることは驚きでした。redux-sagaはとても強力でいいとは思うのですが1、ドキュメントサイトのチュートリアルで紹介するには向かず、シンプルな非同期処理のみを対象にした本ライブラリを開発しました。
Reduxの公式ドキュメントでも、redux-sagaは標準採用ではなく、redux-thunkが標準になっています。 ↩
- 投稿日:2019-12-14T08:32:19+09:00
@types/react の中を少し読んでみる
はじめに
ここ最近はTypeScriptを利用することがデファクトスタンダードになりつつありますね。
そこでその際に必要になるReactの型定義(@types/react
)について色々おさらいしてみます。対象のパッケージはこちら
npm: https://www.npmjs.com/package/@types/react
GitHub: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react解説
@types/react
ではglobal.d.ts
とindex.d.ts
の2つのファイルが提供されていますので
それぞれを見ていきましょう。
global.d.ts
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/global.d.ts
見てみるとわかるのですが空のinterface定義が並んでいます。
そしてファイルの先頭に下記のコメントが。/* React projects that don't include the DOM library need these interfaces to compile. React Native applications use React, but there is no DOM available. The JavaScript runtime is ES6/ES2015 only. These definitions allow such projects to compile with only `--lib ES6`. Warning: all of these interfaces are empty. If you want type definitions for various properties (such as HTMLInputElement.prototype.value), you need to add `--lib DOM` (via command line or tsconfig.json). */
適当に訳すと
「これらの定義を用意することでlib.dom
を読み込まなくてもコンパイルできるようになります」
「ただし、空の定義なのでプロパティなどにアクセスしたいのならちゃんと読み込んでね」
ってことみたいです。大体は
lib.dom
を読み込むと思うのでこのファイルはあまり気にしなくていいかもしれません。
index.d.ts
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts
それではReact本体の型定義を見ていきます。
今回紹介する定義は以下になります。
- React.Component
- React.ComponentClass
- React.FunctionComponent
- React.ComponentType
- React.ComponentProps
- React.ReactNode
- React.ComponentProps
- React.ReactDOM
React.Component
言わずとしれたクラスコンポーネントの型です。
正確にはクラスなのですが重要なのでとりあげます。型定義interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { }実際はクラスなので継承して使うことになります。
type Props = {}; type State = {}; class Sample extends React.Component<Props, State> { }これで
this.props
やthis.state
,this.setState
などが型安全になりました。contextType
クラスの定義をよく読むとこんな記述が。
static contextType?: Context<any>; context: any;これはContextAPIをクラスコンポーネントで利用する際の型定義になるのですが
any
と書かれているので実際に使う際は以下のようにしてあげましょう。const hogeCtx = React.createContext({ count: 0 }); class Child extends React.Component<Props, State> { static contextType = hogeCtx; context!: React.ContextType<typeof hogeCtx>; render() { return <h1>{this.context.count}</h1>; } }TypeScript3.7以降であれば以下の書き方になります
class Child extends React.Component<Props, State> { static contextType = hogeCtx; declare context: React.ContextType<typeof hogeCtx> // ↑ここが変数の再定義ではなく型の定義のみでよい ...こうすることで
this.context
が型安全に利用することができます。注記
React 16.3より前から存在する
Legacy Context
についても同様にthis.context
を利用していました。
その場合はcontextType
の定義はせずany
のまま利用するほうが良いかと思います。React.ComponentClass
これは
React.Component
などのインターフェースになります。型定義interface ComponentClass<P = {}, S = ComponentState> extends StaticLifecycle<P, S> { new (props: P, context?: any): Component<P, S>; propTypes?: WeakValidationMap<P>; contextType?: Context<any>; contextTypes?: ValidationMap<any>; childContextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; }クラスとインターフェースと何が違うのかというと言語化が難しいのですが以下のような挙動になります。
type Props = {}; class Hoge extends React.Component<Props> { } const hoge: Hoge = Hoge; // これはエラー const hogeType: React.ComponentClass<Props> = Hoge;クラスにするということはインスタンスの型となるため、
純粋なコンポーネントのインターフェースが欲しい場合はこちらの型を使うことになります。
具体的な利用例は後述するReact.ComponentType
でご紹介します。React.FunctionComponent
関数型コンポーネントの型となる
FunctionComponent
です。
最近はクラスコンポーネントよりもこちらのほうが主流ですね。エイリアスとして
React.FC
というものもあります。(私は短いほうが好きなのでこちらを多用しています型定義interface FunctionComponent<P = {}> { (props: PropsWithChildren<P>, context?: any): ReactElement | null; propTypes?: WeakValidationMap<P>; contextTypes?: ValidationMap<any>; defaultProps?: Partial<P>; displayName?: string; } type FC<P = {}> = FunctionComponent<P>;ここではそれぞれのプロパティについて説明します。
関数本体
関数の第2引数は
Legacy Context
の値になるので最近だと利用することはないです。
(非推奨なのでanyのまま放置されているのだと思います)propsTypes
propsTypes
もありますがTypeScriptの場合はコンパイルエラーでカバーできるので
わざわざ記述することはなくなりました。contextTypes
contextTypes
についてもLegacy Context
関連なので無視!defaultProps
こちらは
props
に対してデフォルト値を与えるものになります(そのまま
個人的にはdefaultProps
で定義した項目はoptionalじゃなくできるようになってほしいです...displayName
これを設定しておくことでデバッグ時などにコンポーネント名として表示してくれます。
minify時に関数名などは消えてしまいデバッグが辛いので極力設定しましょう!ちなみに
Hooksが出てくるまではStatelessFunctionComponent
(SFC
)というものがありましたが
現在は非推奨になっているためこちらに切り替えましょう。React.ComponentType
まずは型定義
型定義type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P>;上記で説明した「クラスコンポーネントor関数コンポーネント」という型で、
「とにかくコンポーネントがほしい!!」って場合に利用します。
具体的な例はこちらinterface WithHoge { hoge: string } function withHoge<P extends WithHoge>( Component: React.ComponentType<P> ): React.FC<Omit<P, keyof WithHoge>> { return (props) => { const inProps = {...props, hoge: 'value' } as P; return <Component {...inProps} />; } }このようなHOCを作ったりする場合は、
関数コンポーネントでもクラスコンポーネントでもよいのでComponentTypeを利用すると良いです。
その他にもコンポーネントそのものを受け渡す場合はこちら利用していきましょう。React.ReactNode
React.Component
のrender()
の戻り値など色んな所で登場してくるReact.ReactNode
。
簡単に言うと「JSXの中で存在できる要素」を指します。(この表現が正しいかはあやしい)型定義type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined; // 関連↓ type ReactText = string | number; type ReactChild = ReactElement | ReactText;コンポーネントでも文字列でも数字でもなんでも来い!っていう定義で
「とにかくJSXの中に突っ込みたいけど型がバラバラ」といった場合に活躍します。ちなみに
関数コンポーネント(FunctionComponent
)の戻り値はReactElement
です。
なのでundefined
や数値など適当なものは返却できないようになっています。
(Component.renderはできるのに)React.ComponentProps
これは便利な型として紹介いたします。
コンポーネントからPropsのを抜き出す際に利用します。型定義type ComponentProps<T extends keyof JSX.IntrinsicElements | JSXElementConstructor<any>> = T extends JSXElementConstructor<infer P> ? P : T extends keyof JSX.IntrinsicElements ? JSX.IntrinsicElements[T] : {};
infer
で目的の型を抜き出しているのですが難しい定義ですね。使用例)
const Sample = (props: { name: string; age: number }) => { return <>hello</>; }; type SampleProps = React.ComponentProps<typeof Sample>; /* type SampleProps = { name: string; age: number; } */このようにコンポーネントからPropsの型を抜き出すことができました。
利用しているライブラリでProps
が公開されていない場合でも型を抜き出すことができるので重宝します。React.ReactDOM
こちらは自作のコンポーネントではなく
div
やinput
などのHTML,SVGタグの定義となります。型定義interface ReactDOM extends ReactHTML, ReactSVG { } interface ReactHTML { a: DetailedHTMLFactory<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>; abbr: DetailedHTMLFactory<HTMLAttributes<HTMLElement>, HTMLElement>; address: DetailedHTMLFactory<HTMLAttributes<HTMLElement>, HTMLElement>; .... } interface ReactSVG { animate: SVGFactory; circle: SVGFactory; ... }これを単体で使うことはあまりないのですが
ComponentProps
と組み合わせると以下のようなことが可能です。type Div = React.ReactDOM['div']; type DivProps = React.ComponentProps<Div>; const props: DivProps = { className: 'class-name' }; return <div {...props} />自作コンポーネントじゃないタグに動的な
props
を設定したいとき、
型安全な変数として定義することができます。このようなショートハンドの型を定義するともっと使いやすくなりそうです。
type DOMProps<E extends keyof React.ReactDOM> = React.ComponentProps<React.ReactDOM[E]>; // 使う type DivProps = DOMProps<'div'>;まとめ
実際にはコンポーネントの型定義が他にもいくつかあって複雑に絡み合っているのですが
今回はこのあたりのよく目にする部分のみをまとめてみました。(時間がなかった)Reactなど関数的な考え方を持ち込んでいるライブラリは型定義が面白かったりと勉強になるので
皆さんもぜひ読んでみてはいかがでしょうか。
おしまい
- 投稿日:2019-12-14T02:52:03+09:00
Redux(React)のパフォーマンス改善
Ateam Lifestyle Advent Calendar 2019の15日目は、株式会社エイチームライフスタイルでWebエンジニアをしている@turkeyzawaが担当します。
はじめに
昨年のアドベントカレンダーの時期からReactの布教も弊社内で進み、Reactに触れられる機会が増えてきました。嬉しいです。
今年はそんなReactで書かれたWebページのパフォーマンス改善、特にRedux周辺の改善について共有できればと思います。当記事内のコードは全てSampleのため、そのままコピーしても動作しません。
また、実際のプロダクトに使われているコードからも大部分変更しています。前提
Redux不要論を唱える意図はありませんが、「とりあえずReduxでState管理しよう」の精神で導入することは悪だと思っていて、本当に使う必要があるのか?の疑問は常に持ちながら使うべきだと思っています。
背景
Google Chromeでは早いのにIEで触ると遅い!そんな経験をお持ちの方は多いのではないでしょうか。
弊社でもつい最近、ReactとReduxで作られたとあるサービスのパフォーマンスが悪くなっていました。
ユーザが1文字入力するたびに画面がカクついてしまい、まともに入力ができない状態でしたので、取り急ぎ改善に取り組みました。TL;DR
特に効果の大きかった改修ポイント
- Reduxのstoreに保持していたformの値をよりformへ近いstateへ移動
- 発行するactionの数の抑制
- reselectの導入と、propsとして流し込むstateの数の抑制
具体的に何をしたのかは下の方で記載しています。
beforeに比べてafterは負荷の高い処理(赤いライン)が少なくなっています。
事前準備
- eslint,prettierの導入
- ディレクトリ構造の整理
- re-ducksパターンの導入
上記はパフォーマンスに直接影響はしません。
ですが、この先書くコードの品質に良い影響を少なからず与えることができ、以下のようなことが期待できます。
- actionとthunkが散らばってしまい、ビジネスロジックが迷子になることを防ぐ
- 責務の分離を促進しやすくなる
もちろんディレクトリ構造を無視したコードは書けてしまうので、この事前準備が全ての負債を防げるわけではないです。
ルールが陳腐化しないように常に気を配る必要があります。改修対象箇所の特定
読みやすく、詳細かつ丁寧に書かれている記事がありますので、是非こちらをご覧ください。
中でも特に、
Google Developer Tools の機能で,CPUの性能を4倍低速にします
こちらは常に有効にしています。
そうすることで、productionに出してからパフォーマンスの課題に気がつく、といったようなことも大幅に減らすことができます。特に効果の大きかった改修ポイント
ここからは改修したポイントの中で、特にパフォーマンス改善に大きく影響した内容を解説していきます。
- Reduxのstoreに保持していたformの値をよりformへ近いstateへ移動
- ユーザのインタラクションに応じて動く処理の数の抑制
- selectorの導入と、propsとして流し込むstateの数の抑制
Reduxのstoreに保持していたformの値をよりformへ近いstateへ移動
StoreにFormの値を保持することは今ではアンチパターンです。
少し前はredux-formなどが使われていましたが、今から作成するアプリケーションではなるべく使わないようREADMEにも記載されています。改修しているサービスでもformの入力値が全てStoreに保持されていました。reducer.jsconst initialState = { input: { // formの入力値 name: '', email: '', prefecture: '', // and more }, // エラー error: {}, errorList: {}, // 県や市などの固定値 prefectures: [], cities: [], towns: [], streets: [], cities: [], // formの表示制御 showComponents: ['Form1'], isForm1Visible: false, isForm2Visible: false, isForm3Visible: false, isForm4Visible: false, // ...others suggestEmails: [], isSubmitButtonDisabled: true, scrollToBottomFlag: false, scrollMoreFlag: true, // and more }また、入力値が変更されるたびにform全体のvalidationをしなければならない仕様になっており、ユーザが1文字入力する毎に大量のactionが発行されてしまっていました。
form3.jsxclass Form3 extends Component { render() { // それぞれconnectで流し込まれたstateとdispatcherです const { store, suggestEmail } = this.props return ( <> <label for='email'>メールアドレス</label> <input type='email' name='email' placeholder='例)mail@example.com' onChange={suggestEmail} onBlur={suggestEmail} value={store.input.email || ''} /> <EmailSuggest /> <Error error={store.error.email} /> </> ) } }action.jsexport const suggestEmail = el => { return (dispatch, getState) => { const suggestEmails = [] // 略 dispatch(actionTypes.setSuggestEmails(suggestEmails)) if (suggestEmails.length === 1 && el.type === 'blur') { dispatch( actionTypes.changeRequestInput({ [el.target.name]: suggestEmails[0] }) ) dispatch(actionTypes.setSuggestEmails([])) } else { dispatch( actionTypes.changeRequestInput({ [el.target.name]: el.target.value }) ) } // validate処理.内部でさらにactionを複数dispatchしている. common.validate({ el: el, dispatch: dispatch, model: getState(), validator: 'form3Validator', additionalComponent: 'Form4', }) common.validate({ el: el, dispatch: dispatch, model: getState().Request, validator: 'form1Validator', }) common.validate({ el: el, dispatch: dispatch, model: getState().Request, validator: 'form2Validator', }) common.validate({ el: el, dispatch: dispatch, model: getState().Request, validator: 'form4Validator', }) // dispatchされたthunkの中で更に別のthunkが呼ばれており、更に(ry dispatch(focusElement({ el: el })) // and more... } }更に、reducerが呼び出されるたびにstateが新しいobjectとして生成されてしまっていたため、connectしているComponentが全てrerenderされていました。
reducer.jsimport { handleActions } from 'redux-actions' export default handleActions( { INITIALIZE_REQUEST: () => initialState, CHANGE_STATE: (state, action) => ({ ...state, ...action.payload, }), CHANGE_INPUT: (state, action) => ({ ...state, input: { ...state.input, ...action.payload, }, }), SET_ERROR: (state, action) => ({ ...state, error: action.payload, }), SET_ERROR_LIST: (state, action) => ({ ...state, errorList: action.payload, }), SET_COMPONENT: (state, action) => ({ ...state, showComponents: action.payload, }), ACTIVATE_SUBMIT_BUTTON: state => ({ ...state, isSubmitButtonDisabled: false, }), FINISH_CREATE_REQUEST: (state, action) => ({ ...initialState, ...action.payload, }), SHOW_FORM1: state => ({ ...state, isForm1Visible: true, }), SHOW_FORM2: state => ({ ...state, isForm2Visible: true, }), SHOW_FORM3: state => ({ ...state, isForm3Visible: true, }), SHOW_FORM4: state => ({ ...state, isForm4Visible: true, }), // and more... }, initialState )改修後はStoreにformの値を保持することをやめています。
また、validationや入力値の制御にはreact-hook-formを使用するように変更しました。
react-hook-formのapiの解説は主旨から外れてしまうので割愛します。form3.jsconst Form3 = () => { const prefectures = useSelector(selectors.selectPrefectures) const cities = useSelector(selectors.selectCities) const towns = useSelector(selectors.selectTowns) const { register, errors, setValue, watch, triggerValidation, } = useFormContext() const setEmail = useCallback( email => { setValue('email', email) triggerValidation({ name: 'email' }) }, [setValue, triggerValidation] ) const watchEmail = watch('email') // registerされている入力項目全てをvalidateする関数 // triggerValidation() 相当の処理 const [validate700] = useDebouncedValidator(/* delay */ 700) const handleOnChange = useCallback(() => validate700(), [ validate700, ]) // Componentを動的に表示するための処理を内部で行なっています // valuesとerrorsから次のComponentを表示して良いかどうかを判定しており、こちらもdebounceでcancel可能にしています useSubscriptionToNextStep({ nextComponent: 'Form4', formValues, errors, delay: 500 }) return ( <> <label for='email'>メールアドレス</label> <input type='email' name='email' placeholder='例)mail@example.com' onChange={handleOnChange} ref={register({ required: constants.ERROR_EMAIL, pattern: { value: MAIL_REG_EXP, message: constants.ERROR_EMAIL_INVALID, }, maxLength: { value: 50, message: constants.ERROR_MAXLENGTH_50, }, })} /> <EmailSuggest email={watchEmail} setEmail={setEmail} /> <ErrorMessage errors={errors} keyName='email' /> </> ) }validationロジックもここに統一しています。triggerValidationで動的にvalidation処理を実行でき、かつ、マウントされているComponentの入力値でのみ処理が走るため、改修前よりも処理のコストを下げられます。
ユーザのインタラクションに応じて動く処理の数の抑制
入力の完了をトリガーに、動的に入力項目を表示させる必要があったため、改修前と同様にユーザの入力に応じてvalidationを走らせる必要がありました。
そのため、watchしたformの入力値をdependenciesに追加したuseEffectを使ってtriggerValidationをキックしていますが、debounceを活用してvalidationの実行回数を間引きました。form3.js// registerされている入力項目全てをvalidateする関数 // triggerValidation() 相当の処理 const [validate700] = useDebouncedValidator(/* delay */ 700) const handleOnChange = useCallback(() => validate700(), [ validate700, ])どれぐらいdelayを挟むのかはそれぞれのformの特性に依存しますが、今回はユーザの行動分析に明るいメンバーに協力を仰ぎ、このサービスを利用しているユーザがよりストレスを感じづらい時間を設定しています。
結果、適切に実行回数を間引くことができ、インタラクションを阻害することなくvalidationも動かせるようになりました。
selectorの導入と、propsとして流し込むstateの数の抑制
ここまでの改修により、formの入力値はStoreから引きはがせましたが、Storeが抱えている全てのstateをReduxから剥がすことまではしていません。
connectされているComponentを確認すると、Componentが関心を持っているstateに限らず全てのstateがStoreから流し込まれていました。containers/form3.jsximport { bindActionCreators } from 'redux' import { connect } from 'react-redux' import form3 from 'components/form3' import * as actionTypes from 'actions' import * as requestActions from 'actions/form' const mapStateToProps = ({ Store, Prefectures }) => ({ state: Store, prefectures: Prefectures, }) const mapDispatchToProps = dispatch => ({ ...bindActionCreators(actionTypes, dispatch), ...bindActionCreators(requestActions, dispatch), }) export default connect( mapStateToProps, mapDispatchToProps )(form3)このままでは、stateが更新されるたびに全てのComponentがrerenderされてしまいます。これは、嬉しくありませんよね。
そこで、必要のあるstateだけをStoreから引っ張ってくるようにするとともに、フィルタリングのコストも抑えるためにreselectを導入しました。form3.js// Storeからformの情報を取ってくる必要がなくなったため処理も減っている const prefectures = useSelector(selectors.selectPrefectures) const cities = useSelector(selectors.selectCities) const towns = useSelector(selectors.selectTowns)不必要なrerenderを減らせたことに加え、Componentのレンダリングに必要なstateが更新がされていない場合は、フィルタリングコストも削減することができています。
まとめ
この画像にはサイズの関係上、一部のメトリクスしか載せていませんが、実際のformはもっと長いため、全体で見ると入力開始から終了までの時間も大幅に削減できています。
効果の計測はこれからですが、より多くのユーザ様に使っていただける状態にできたかなと思っています。やった方が良いけどできなかったこと
まだまだ改修することによってパフォーマンスを改善できるポイントはいくつも存在しますが、以下の理由により今回は手を入れていません。
- かけられる工数が限られている
- 達成したかった水準は満たせた
興味のある方は、Redux Style Guideをぜひご覧ください。
終わりに
これからReduxを含めたアプリケーション開発を始める場合は、本当にReduxの導入が必要なのか?ローカルなstateで十分ではないか?ContextAPIで十分ではないか?などを検討した上で進めることをオススメします。
Reduxを導入することが決まった場合は、ぜひRedux Style Guideを参考に実装を進めてみてください。
Ateam Lifestyle Advent Calendar 2019 の16日目は、 @dabitsがk8sやECSなどの、コンテナサービスの選定方法について書きます。ご期待ください!
参考リンク
- 投稿日:2019-12-14T02:25:04+09:00
Reactアプリ開発でReduxではなくreact-trackedを採用してみた話
はじめに
みなさん、Reactは好きですか?アプリ開発でReact使ってますか?
私はここ数年、ViewライブラリはReact一択でアプリ開発をしています。
さらにはReact Hooksが登場してからというもの、local stateやlifecycle methodを扱うために仕方なく書いていたclassコンポーネントを今ではほぼ書いていません。(classでないと不可能なAPIを扱う場合はしぶしぶ書いていますが)
Functionコンポーネント最高ですね!さて、超小規模なSPAだとglobalなstateは不要だったりしますが、いざ導入しようかという時、途端にReduxが微笑みかけてきます。(私の場合)
最近はもっぱらReact開発といっても、React Native(Expo)でのモバイルアプリ開発をしており、Reduxを使う機会がなくなっていました。
本記事では、とある案件でglobal stateを扱わなければならず、Reduxではなくreact-trackedというライブラリを採用したので、その経緯や使い心地などを技術的知見すくなめ&個人的主張多めに述べていきます。
Redux採用が辛い場面
Reduxはstate管理ライブラリとしてすごく人気で素晴らしいライブラリです。しかし、導入する開発アプリとReduxの規模がアンバランスなことが多くあるのではないかと感じています。
個人プロジェクトから小規模プロダクションレベルのアプリ、開発スピードを求められるPoCなアプリに対してReduxを採用することは、導入メリットより導入・運用の労力の方が勝ってしまうことが多いのではないでしょうか。
また、いかに優秀なライブラリとはいえ、使い手が十分に設計思想を理解していないとメンテナンスのしにくいアプリへと簡単に変貌してしまいます。
Reduxの公式ドキュメントは手厚くサポートされていますが、初心者の学習ハードルは高く、身近にReduxマスターが居ないことには簡単に心が折れること必至でしょう。(最近は世の中の知見が溜まってきてそこまで苦しまないのかもしれませんが・・・)
中には頑張ったけど辛い経験しか無く、Reduxという単語を聞くだけで拒絶反応が出る人もいるのではないでしょうか。ちなみに自分はこの成分多めな人間です。憎しみがあるわけではありません。よく見かけるContextの利用について
素のContext APIをglobal stateとして利用する話をよく見かけます。
確かにReduxの代替案としては記述量の少なさや理解のしやすさから魅力的に見えるのは理解できます。しかし、変更があった際はContextが適用されているすべてのコンポーネントツリーが再描画されるAPIなので、頻繁にstateが書き換わるとなると、一概には良い代替案とは言えないかなと思ってます。
もちろん、上記にあげたような特徴を理解しつつ、個人プロジェクトやPoC用など開発スピード重視で気持ちよく書きたいとなると問題はないと思います。世の中のglobal state管理どうしようか話
「react global state library」 という検索キーワードでググると、こんな記事(Global state with React)が現れます。
素のContextの利用と注意点、使う場合の提案について詳しく解説がされてます。ページ内で動作が確認でき、Contextの挙動を体験できます。
この記事では、Contextをglobal stateに活用するには、がメイントピックですが唯一ライブラリが紹介されています。
それが本記事の本題であるreact-trackedです。react-trackedとは
Githubのrepositoryはこちら。
アドベントカレンダーの一環で、開発者の@daishiさん自らQiitaでreact-trackedの紹介記事を書いてたりもします。react-trackedは、雑に言うとHooksとContext APIをベースに作られた何やらすごいチューニングされてるglobal state管理用ライブラリです。
私の言葉で解説するよりご本人の解説を読んで頂くほうが正確なのと、そもそも解説しろと言われても出来ませんので書けませんが、今後Reactの新たなAPIとして正式リリースされるであろうconcurrent modeにも対応してるみたいです。https://github.com/dai-shi/will-this-react-global-state-work-in-concurrent-mode#result
使用例
せっかくなのでシンプルな使い方を載せておきます。
store.js
にglobal stateを準備します。exportしているProvider
はアプリ全体を包むためのコンポーネントです。Provider内のコンポーネント(ここでは、CounterとTextBox)でuseTracked()
を使うと、global stateと更新用の関数を使うことができます。(const [state, setState] = useTracked();
)これだけです。まずは、用意した4ファイルを見てみましょう。
実際のコードと動作はこちらで確認できます。// store.js import { useState } from 'react'; import { createContainer } from 'react-tracked'; const initialState = { count: 0, text: 'hello', }; const useMyState = () => useState(initialState); export const { Provider, useTracked } = createContainer(useMyState);// Counter.jsx import React from 'react'; import { useTracked } from './store'; const Counter = () => { const [state, setState] = useTracked(); const increment = () => { setState(prev => ({ ...prev, count: prev.count + 1 })); }; return ( <div> <div>count: {state.count}</div> <button onClick={increment}>+1</button> <div>random: {Math.random()}</div> </div> ); }; export default Counter;// TextBox.jsx import React from 'react'; import { useTracked } from './store'; const TextBox = () => { const [state, setState] = useTracked(); const setText = text => { setState(prev => ({ ...prev, text })); }; return ( <div> <div>text: {state.text}</div> <input value={state.text} onChange={e => setText(e.target.value)} /> {/* 以下のコメントアウト状態だとstate.counteが変化しようがレンダリングは走りません。 コメントアウトを消すとstate.counteが変化するとレンダリングが走ります。 */} {/* <div>count: {state.count}</div> */} <div>random: {Math.random()}</div> </div> ); }; export default TextBox;// App.jsx import React from 'react'; import { Provider } from './store'; import Counter from './Counter'; import TextBox from './TextBox'; const App = () => ( <Provider> <Counter />{/* Counter A */} <hr /> <Counter />{/* Counter B */} <hr /> <TextBox /> </Provider> ); export default App;ポイント
この例で見ていただきたいポイントは、レンダリングの走る様子です。
App.jsxでCounterを2つ(A,Bと呼称)用意しています。AかBのどちらかのCounterでインクリメント用のボタンを押すと、state.countが+1されるので更新され、レンダリングが両方で走ります。このとき、TextBoxでは全体のstateをuseTracked()から得ているものの、state.countにはアクセスしていません。
なので、state.countが更新されてもTextBoxではレンダリングがはしりません。
(試しにstate.countを書くとレンダリングが走るようになるか確認するためにコメントアウトで用意しています)
もちろん、TextBoxで扱っているstate.textはCounterで参照していないので、state.textが変更されてもCounterではレンダリングが走りません。素晴らしいですね。ReactのAPIだけ(Contextなど)でこの様に実装すると、先に述べたように1つstateプロパティを変更するとすべてのコンポーネントでレンダリングが走ります。
使ってみた感想
冒頭で述べたように、自身が関わるプロジェクトにreact-trackedを採用してみましたが、非常にストレス少なく導入・利用できました。
ちょっとだけネガティブな表現をすると、少しマジック感が強めな印象を受けるかもしれません。1つのコンポーネントでstateプロパティを書き換えると、そのstateプロパティが使われているコンポーネントでレンダリングが走ります。簡単に全stateにアクセスできるので、扱いを丁寧にしないと、どのstateプロパティがどこで使われて再レンダリングが走っているかが分かりにくくなると思っています。
ただ、Reduxなどよりも学習コストは少なく、React+Reduxで書いていた記述量より圧倒的に少ない記述量で済みます。reducerスタイルでも書けますし、カスタムフックを書きやすくするためのAPIも用意されています。
また、作者が今後のReactを考えてライブラリ設計してくれており、推してくれているので息が長いだろうという安心感もあります。おわりに
React Hooksの登場から、concurrent modeの登場など目まぐるしく世界が変わっていってます。
そんな中、安定してglobal state管理ライブラリの王座に君臨し続けているReduxは素晴らしく、簡単には支持率を失うことはないでしょう。
ただ、Reduxの設計思想にもあるように、大規模開発向けであるがゆえの仕様の重さがあり、採用するにはストレスを抱えている人が居ることも事実かと思います。(私を含めサンプル1以上)
ちょっと手軽にglobal stateを扱いたい、でももしRedux以外のライブラリを使うとしても安心して使いたい、など、別ライブラリの採用に前傾姿勢だけど不安もある方、react-trackedを使ってみてはどうでしょう。では、よいReactライフを!
- 投稿日:2019-12-14T01:48:30+09:00
ClojureScriptでReact 2019
こんにちは。
普段はClojureとClojureScriptだけ書いていてJavaScriptは書かないのですが、いつもより広い範囲のオーディエンスにClojureの良さを語りたくなったので参加させていただきました。
この記事ではshadow-cljsというClojureScript用のツールによって
- node&jvmさえあればClojureScriptでの開発を始められる
- 既存のnodeモジュールを簡単に利用できる
- => ClojureScriptでReactプロジェクト作るのめっちゃ楽しい!
という点を今回作った小さなプロジェクトを添えて主張しようと思います。
ClojureScriptとは
狭義のClojureはRich Hickeyが作ったJVM上で動く言語です。概要は @223kazuki さんの書いたキメるClojure高速開発がオススメです。
そのヤバめな言語であるClojureをソースとして読み、JavaScriptを吐き出すコンパイラがClojureScriptです。言語の特徴以外ではGoogle Closure Compilerに依存しており、プロダクションビルドの際にその圧縮力の恩恵を受けることが期待できる点がアピールポイントです。
(ClojureとClosureで名前が衝突する点もよく話題に上がります。)
開発環境事情
ClojureScriptは良い物だとして、素のClojureScriptコンパイラにはいくつか課題があります。
- ClojureScriptコンパイラ自体はClojureライブラリ => Clojureを動かす環境のインストールが必要
- ホットリローディングが無い
- nodeモジュールを使うのが煩雑
- + Google Closure Compilerによって行なわれる変数リネームによってプロダクションビルドで壊れがち
この辺りをナイスにケアしてくれるshadow-cljsというツールが数年前に登場し、nodeとJavaランタイムさえあればClojureScriptを始められる時代が来ています。
shadow-cljsを使う
shadow-cljs自体はnpmでインストールできるcliなので使い始める敷居が低いのが特徴です。
インストール
前提となるnodeとjvmがインストール済みの前提でそこら辺のライブラリと同じようにpackage.jsonに足すなり
npm install
するなりするだけ!shadow-cljs.edn
各ビルドの設定やClojureScriptの依存ライブラリを書くファイルです。
開発ビルド
npx shadow-cljs watch <build-name>
とshellに打つとコンパイルされ、開発用のwebサーバーが起動します。この状態ではソースの変更が監視されており、cljsのソースを触る度に最新のコードがブラウザにwebsocketで配信されます。最新の動作を確認するためにブラウザの更新ボタンを押す必要がない幸せを得る機能がデフォルトで付属しているのは嬉しいポイント。React/node moduleでデモ
今回はボタンを押すとnodeが追加、node間をドラッグするとedgeが追加される物をvis-networkとReagentというReactラッパーで作りました。
コードはこちら
(ns my.dev (:require [reagent.core :as reagent] ["vis-network" :as vis])) (defn graph [{:keys [nodes edges on-edge-add add-edge-mode]}] [:div {:ref (fn [dom] (when dom (let [parent-dom (.-parentElement dom) nw (vis/Network. dom #js {:nodes (vis/DataSet. (clj->js nodes)) :edges (vis/DataSet. (clj->js edges))} (clj->js {:layout {:randomSeed 111} ;;Make deterministic :edges {:arrows "to" :color "red" :physics false} :manipulation {:addEdge (fn [data callback] (on-edge-add {:from (.-from data) :to (.-to data)}) (callback data))}}))] (when add-edge-mode (.addEdgeMode nw)) (doto nw (.setSize (.-clientWidth parent-dom) (.-clientHeight parent-dom)) (.fit (clj->js (map :id nodes)))))))}]) (defn view [] (reagent/with-let [state (reagent/atom {})] [:div [:button {:on-click #(swap! state update :nodes conj {:id (count (:nodes @state)) :label (str (count (:nodes @state)))})} "Add Node"] [graph {:nodes (:nodes @state) :edges (:edges @state) :add-edge-mode true :on-edge-add (fn [edge] (swap! state update :edges conj edge))}]])) (defn render-view [] (reagent/render [view] (js/document.getElementById "root"))) (defn ^:dev/after-load start [] (js/console.log "start") (render-view)) (defn ^:export init [] ;; init is called ONCE when the page loads ;; this is called in the index.html and must be exported ;; so it is available even in :advanced release builds (js/console.log "init") (render-view)) ;; this is called before any code is reloaded (defn ^:dev/before-load stop [] (js/console.log "stop"))細かい解説は需要があれば足しますが、ポイントは
- nodeモジュールが特別なことをしなくても使える
- reagentというReactラッパーが広く使われている
- ホットリローディングがデフォルト
という点です。
もしClojure/ClojureScriptに興味を持っていただいた方は、twitterでつぶやくと捕捉されやすいです。あとはdosync radioというClojureの日本語ポッドキャストを今年から始めているので聴いてみてください。では良いお年を!
- 投稿日:2019-12-14T00:45:08+09:00
Gatsbyを最速で理解するためのチュートリアル
Gatsbyを触ってみたのですがチュートリアルだけだと??となる部分がいくつもあり結局ドキュメントをたくさん読むハメになりました。
ドキュメントのリンクを整理してたらいい感じのチュートリアルができたので投稿しました(所々中途半端ですが・・・)Install
npm install -g gatsby-cligatsby new [rootPath] [starter] Create new Gatsby project.starterを指定する。デフォルトではdefault starterが選択されます。
チュートリアルなのでhello-world一択。
gatsby new my-blog https://github.com/gatsbyjs/gatsby-starter-hello-worldgatusby deplop一応npm scriptにも書かれています。
npm run deplop.json"scripts": { "develop": "gatsby develop" }
-p
オプションで別のポートを指定することができるので複数のプロジェクトを立ち上げたいときに。gatusby deplop -p 3000CLIのドキュメントまで丁寧にあります。
デフォルトだとhttp://localhost:8000/ でページにhttp://localhost:8000/___graphql でGrqphiQLにアクセスできます。
入門
Page
ディレクトリの構成にルールがあって
pages/
が自動でページなります。
例えばpages/about.js
を作成すると/about
で表示されます(ページが生成されてルートが設定される)pages/index.jsimport React from 'react' export default <div> index page </div>pages/about.jsimport React from 'react' export default <div> hello page </div>その他は
components/
に共通コンポネントを作成するなど普通のReactのアプリケーションのようにを開発できます。Any React component defined in src/pages/*.js will automatically become a page.
またテンプレートファイルを作成して動的にページを生成することも可能です。creating-pages-automatically
GraphQL
gatsbyの便利な点はプロジェクト内のデータをGraphqlで取得できるところです。
例えばProjectの設定情報は以下のqueryで取得できます。
configファイルを以下のように変更します。
module.exports = { siteMetadata: { title: `Title from siteMetadata`, } }query { site { siteMetadata { title } } }このようにプロジェクトの情報をqueryで取得して、それを表示するReactのコンポネントを作成し、それを元にgatsbyがページを生成するという流れになります。
このGraphQLですがデータを追加する作業とデータを取得する作業の二つに分けて理解するのがいいと思います。
まずデータを追加する作業から見ていきます。
後述するプラグインを入れる事でプロジェクト内の情報を自由に取得できるようになります。プラグインからデータを追加する
データを追加する作業はドキュメントに
plugin driven
と書かれていて、実際プラグインを入れるだけです。gatsby-source-filesystem
例えば
gatsby-source-filesystem
を使うことでソースからデータを取得できるようになります。npm install --save gatsby-source-filesystemgatsby-config.js.jsmodule.exports = { siteMetadata: { title: `Pandas Eating Lots`, }, plugins: [ { resolve: `gatsby-source-filesystem`, options: { name: `src`, path: `${__dirname}/src/`, }, } ] }graphiqlを開くと
allFile
とfile
が新たに追加され、以下のqueryでファイル情報を取得できるようになります。query { allFile { edges { node { relativePath prettySize extension birthTime(fromNow: true) } } } }https://www.gatsbyjs.org/tutorial/part-five/
チュートリアルではプロジェクトのJSファイルを取得して表示しています。
gatsby-transformer-remark
これはデータを追加するだけのプラグインではないので、この流れで書くのも微妙なのですが
このプラグインがやってくれる一番わかりやすいのはmarkdownを変換してくれるところです。mdファイルの情報をhtmlに変換して取得できたり、frontmatterなどのメタ情報を取得できます。
allMarkdownRemark
というのが追加されます。自分でデータを追加する
createNodeAPIを使って自分でデータを流すことができます。後述しますがプラグインはこのAPIを使ってデータを流しています。
手動でデータを入れてみる
ポケモンのデータを入れてみる
exports.sourceNodes = ({ actions, createNodeId, createContentDigest }) => { const pokemons = [ { name: "Pikachu", type: "electric" }, { name: "Squirtle", type: "water" }, ] pokemons.forEach(pokemon => { const node = { name: pokemon.name, type: pokemon.type, id: createNodeId(`Pokemon-${pokemon.name}`), internal: { type: "Pokemon", contentDigest: createContentDigest(pokemon), }, } actions.createNode(node) }) }以下のqueryでポケモンを取得できるようになります。
query MyPokemonQuery { allPokemon { nodes { name type id } } }外部から取得したデータを流すこともできる。
APIから取得したデータを流してみる
QiitaのAPIからデータを流してみる
この辺のAPIは詳しく確認してないので、少し修正しただけですが、
const fetch = require("node-fetch") const fetchItems = () => fetch("https://qiita.com/api/v2/items").then(res => res.json()) exports.sourceNodes = async ({ actions, createNodeId, createContentDigest, }) => { const items = await fetchItems() items.forEach(item => { const node = { id: item.id, title: item.title, user: { name: item.user.name, }, internal: { type: "Item", contentDigest: createContentDigest(item), }, } actions.createNode(node) }) }query ItemQuery { allItem { nodes { title user { name } } } }{ "data": { "allItem": { "nodes": [ { "title": "blob & createObjectURL について ", "user": { "name": "Yu Watanabe" } }, ] } } }QiitaのAPIからとってきた情報を流す事で、queryで取得できるようになりました。
先ほどのgatsby-source-filesystemもこのcreateNodeを使ってローカルのファイル一覧を取得して流し込んでいるのがわかると思います。
ファイルの情報を流すとかwordpressから記事を取得して流すといったことをやってくれるプラグインがあるので、我々は設定ファイルにパスだったりurlを入れるだけでqueryでデータを取得できるようになります。
pixabay-source-plugin-tutorial
そしてqueryで取得した結果をページに表示するだけでいいのです。
nodeを追加する
nodeにfieldを追加することができます。
const { createFilePath } = require(`gatsby-source-filesystem`) exports.onCreateNode = ({ node, getNode, actions }) => { const { createNodeField } = actions if (node.internal.type === `MarkdownRemark`) { const slug = createFilePath({ node, getNode, basePath: `pages` }) createNodeField({ node, name: `slug`, value: slug, }) } }が追加されてslugが取得できるようになった。
query { allMarkdownRemark { edges { node { id fields { slug } } } } }Query
GraphQLを初めて触る人もいるかもしれませんがサイトを作るだけであればレファレンスの内容で十分だと思います。またGrqphiqlで左側をぽちぽち動かせば勝手にquery作ってくれるので特に困ることもないかと思います。
alias
allMarkdownRemark
の結果はそのままだとdata.allMarkdownRemark
ですが、先頭に任意のaliasをつけることができます。
data.latestPost
,data.relatedPost
のように帰ってきます。query { latestPost: allMarkdownRemark(limit: 5) { } relatedPost: allMarkdownRemark(limit: 5) { } }frontmatter
マークダウンファイルの先頭に以下のように情報を定義します。
--- title: my first post date: 2019-12-7 ---先ほどの
gatsby-transformer-remark
を使うことで以下のqueryで取得することができます。frontmatter { title: date: }params
どちらかというと
markdown-remark
の話ですが、
- limit
allMarkdownRemark(limit: 5) { }
- format
date(formatString: "YYYY月MM月DD日")
- filter
filter: { tags: { in: ["post", "page"] }, draft: { eq: false } }直近の投稿5件を取得するQuery
query { allMarkdownRemark( limit: 5, sort: {order: DESC, fields: frontmatter___create_at} ) { edges { node { id frontmatter { title } } } } }動的にページを生成する。
ページから必要なデータを取得する
実際にGraphQLからデータを取得してページに表示していきます。
import React from "react" import { graphql } from "gatsby" export const query = graphql` query TitleQuery { site { siteMetadata { title } } } ` export default ({ data }) => ( <div> <h1>{data.site.siteMetadata.title}</h1> </div> )
- 1つのファイルにつき1つのqueryを宣言できる。この時変数は関係ない。
- queryの結果がpropsのdataオブジェクトに入ってくる。
チュートリアルには書かれていないのですが、上記のルールがあります。
試しに一つのファイルに二つのqueryを宣言してみたら怒られました。
export const query1 = graphql` query HomePageQuery { site { siteMetadata { description } } } ` export const query2 = graphql` query HomePageQuery { site { siteMetadata { description } } } `Multiple "root" queries found in file初めてこれを見たときは、どうしてpropsのdataにqueryの結果が渡ってくるんだ?いつqueryは実行されるという気持ちになったのですが、今使っているのは静的サイトジェネレーターでなので、お作法に従ってファイルにqueryを定義すればそれをもとにページ生成してくれるということを忘れていました。
https://www.gatsbyjs.org/docs/recipes/#querying-data-with-a-page-query
https://www.gatsbyjs.org/docs/page-query/#add-the-graphql-query動的にページを生成する
createPage
APIを使ってテンプレートから動的にPageを生成します。チュートリアルのpart7でこんな感じになると思います。
gatsby-node.jsconst path = require(`path`) exports.createPages = async ({ actions, graphql }) => { const result = await graphql(` { allMarkdownRemark { edges { node { frontmatter { path } } } } } `) if (result.errors) { console.error(result.errors) } result.data.allMarkdownRemark.edges.forEach(({ node }) => { actions.createPage({ path: node.frontmatter.path, component: path.resolve(`src/templates/post.js`), }) }) }src/templates/post-template.jsimport React from "react" import { graphql } from "gatsby" export const pageQuery = graphql` query($path: String!) { markdownRemark(frontmatter: { path: { eq: $path } }) { html frontmatter { date(formatString: "MMMM DD, YYYY") path title } } } ` const Template = ({ data }) => { const { markdownRemark } = dat const { frontmatter, html } = markdownRemark return ( <div className="blog-post"> <h1>{frontmatter.title}</h1> <h2>{frontmatter.date}</h2> <div className="blog-post-content" dangerouslySetInnerHTML={{ __html: html }} /> </div> ) } export default Template;まず、テンプレートファイルを見てみます。テンプレートファイルはpathパラメータを使ったqueryの結果を埋め込むようになっています。
gatsby-node.js
では全てのmdファイルのquery結果を元にcreatePage
メソッドを読んでいます。
このメソッドが上のテンプレートファイルを指定していて、動的にページが生成されます。
想像はつくもののチュートリアルに書かれていない大事な点は引数のcontextがtemplateのpropsとtemplateファイルのqueryの引数として渡せる点です。https://www.gatsbyjs.org/docs/recipes/#sourcing-markdown-data-for-blog-posts-and-pages-with-graphql
https://www.gatsbyjs.org/docs/using-gatsby-without-graphql/All context values are made available to a template’s GraphQL queries as arguments prefaced with $
https://www.gatsbyjs.org/docs/programmatically-create-pages-from-data/#creating-pages
pased as props to the component this.props.pageContext as well as to the graphql query as graphql arguments.
ソースを探すの少し時間がかかりました。
概念的な
Starter
- gatsby-starter-default
- gatsby-starter-blog
- gatsby-starter-hello-world
公式のstarterは実は上の3つだけ。
https://www.gatsbyjs.org/docs/starters/
スターター一覧
https://www.gatsbyjs.org/starters/?v=2gatsby new gatsby-starter-hero-blog https://github.com/greglobinski/gatsby-starter-hero-bloghttps://www.gatsbyjs.org/starters/greglobinski/gatsby-starter-hero-blog/
これはちょっと重かった。
Plugin
https://www.gatsbyjs.org/docs/plugins/
npm install --save gatsby-transformer-jsonmodule.exports = { plugins: [`gatsby-transformer-json`], }Pluginは任意のOptionを指定できる。
plugins: [ // Shortcut for adding plugins without options. "gatsby-plugin-react-helmet", { // Standard plugin with options example resolve: `gatsby-source-filesystem`, options: { path: `${__dirname}/src/data/`, name: "data", }, } ]https://www.gatsbyjs.org/docs/using-a-plugin-in-your-site/
StyledComponentsやaxiosなどのnpx packageを使うには
https://www.gatsbyjs.org/docs/what-you-dont-need-plugins-for/
ローカルのパスを指定してプラグインを入れることもできる。
https://www.gatsbyjs.org/docs/loading-plugins-from-your-local-plugins-folder/
Config
https://www.gatsbyjs.org/docs/api-files-gatsby-node/
gatsby-config.js
gatsyby-node.js
build時に一度だけ呼ばれ、動的にページを生成したり、GraphQLのnodeを追加したりする。
https://www.gatsbyjs.org/docs/api-files-gatsby-node/
実践編
gatsby new blog https://github.com/gatsbyjs/gatsby-starter-hello-worldstyled-componentsを使う
https://www.gatsbyjs.org/docs/styled-components/
モジュールとプラグインを入れます。
npm install --save gatsby-plugin-styled-components styled-components babel-plugin-styled-componentsmodule.exports = { plugins: [`gatsby-plugin-styled-components`], }typescriptを使う
あまり参考になるかはわかりませんが、Pluginを入れる必要があります。
https://github.com/gatsbyjs/gatsby/tree/master/examples/using-typescript
https://www.gatsbyjs.org/packages/gatsby-plugin-typescript/?=type
npm install gatsby-plugin-typescript{ "include": ["./src/**/*"], "compilerOptions": { "target": "esnext", "module": "commonjs", "lib": ["dom", "es2017"], "jsx": "react", "strict": true, "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "noEmit": true, "skipLibCheck": true } }Typescriptを使うメリットがどれくらいあるかだが、公式がPropsTypesで頑張ってるのをみると入れてもいいと思う。
Syntax Hilight
GatsbyのPluginにのっかって楽をするだけであればプラグイン入れるだけです。
npm i -S gatsby-remark-prismjs
gatsby-transformer-remark
のpluginに追加してあげます。plugins: [ { resolve: `gatsby-transformer-remark`, options: { plugins: [ `gatsby-remark-prismjs`, ] } } ]先ほどのqueryがclassつきのhtmlを取得できます。
あとは自分でstyleを指定してもいいですが、標準で用意されているテーマを読み込んであげるだけです。
gatsby-browser.js
require("prismjs/themes/prism-solarizedlight.css")https://www.gatsbyjs.org/packages/gatsby-remark-prismjs/
Tag振り分け
https://www.gatsbyjs.org/docs/adding-tags-and-categories-to-blog-posts/
SEO
SEOコンポネントがドキュメントに掲載されています。
https://www.gatsbyjs.org/docs/add-seo-component/
Deploy(GitHub-Pages)
プラグイン入れてpushするだけです。
https://www.gatsbyjs.org/docs/how-gatsby-works-with-github-pages/
Plugin
エディターのpluginを使えばgraphqlの入力に補間が効くかも・・
感想
簡単簡単と言われているのでQiitaのチュートリアル見ながら30分くらいでいけるのかと思ったのですが、結局ドキュメントをたくさん読むハメになり結構時間がかかってしまいました。
チュートリアルだけではなんで?っていいうお気持ちになるので、余力があればrecipesとcontent-and-dataをみると理解が早まるのでお勧めです。お作法に従って書くのが苦手でなければかなり簡単だと思います。
というか長々とかきましたがお気に入りのstarterから初めて少し変更するぐらいが一番コスパが良いと思います。
- 投稿日:2019-12-14T00:17:24+09:00
React向けチャート描画ライブラリ Top 6
React向けチャート描画ライブラリ Top 6
React向けのチャート描画ライブラリをgithubスター数でピックアップしました。
以下、スター数の多い順です。⏲2019/12/13時点のスター数です。
Recharts
hp / github / demo / ⭐️12,888
Rechartsは、ReactおよびData Driven Documentで構築された、再定義された構成可能なチャートライブラリです。
プログラマが頭痛のないReactアプリケーションベースのチャートを作成するのを支援します。Victory
hp / github / demo / ⭐️7,629
インタラクティブなデータ視覚化とモジュラーチャートを作成するためのReactコンポーネントコレクション。
基本的なチャートとアニメーションもサポートしています。 Vistoryの最大の利点は、カスタムチャートの作成に役立ち、必要に応じて複数の独立した軸を使用できることです。React VIS
hp / github / demo / ⭐️6,614
React VISは、使いやすいReactチャートライブラリであり、棒/折れ線/面チャート、散布図、ヒートマップ、ドーナツと円グラフの等高線プロット、平行座標、サンバースト、ツリーマップ、レーダーチャートなどのチャートの視覚化に役立ちます。VX
hp / github / demo / ⭐️6,584
VXは、React環境でのD3の最適な実装です。
データからチャートを非常に簡単に構築するだけでなく、D3のようなさまざまな形状の生成にも役立ちます。
しかし、D3は低レベルのライブラリと見なされているため、D3を使用するだけでは満足できるとは限りません。VXはD3の構築力を2倍にしました。これらの両方を組み合わせることで、より高速で優れた結果が得られました。Nivo
hp / github / demo / ⭐️6,126
Nivoには、豊富なdatavizコンポーネントのセットが付属しています。
素晴らしいD3およびReactプラグイン上に構築されています。
レスポンシブチャートの作成に役立ち、高度なカスタマイズが可能です。React ChartJS 2
hp / github / demo ⭐️2,393
Chart.jsのシンプルなReactラッパー。必要な基本的なReactグラフチャートオプションがすべて備わっています。others
@data-ui/sparkline
github / ⭐️373
Rumble Charts
github / ⭐️286
React JSX Highcharts
github / ⭐️256
React Easy Chart
github / ⭐️148
React Fusion Charts
github / ⭐️63
googleトレンド
googleトレンド
※VXは少ないので省きました。
- 投稿日:2019-12-14T00:05:43+09:00
React+Reduxで音楽検索アプリを作ってみた
はじめに
この記事は,SLP KBIT Advent Calendar 2019 の14日目の記事です.
ReactとReduxでSpotifyのAPIを利用した音楽検索アプリを開発していきます.
記事を書くのは初めてで,手が震えていたりしてます?
https://adventar.org/calendars/4214Reactとは
Reactとは,Facebook製のJavaScriptライブラリです.Webアプリケーションのユーザーインターフェイスを効率的に構築することを目的としており,主にView部分を設計するだけで,データの変更を検知し,関連するコンポーネントだけを更新,描画します.React公式
なお,こちらの記事に非常に詳しく書かれておりますので,ぜひこちらも読んでください.今から始めるReact入門 ~ Reactの基本Redux
Reduxは,アプリケーションのstateを管理するためのオープンソースのJavascriptライブラリです.React以外にもAngularやVueなどと併せて使用することもできます.
Reduxの要素
Reduxには主に,3つの要素からなります.
- Action
- Reducer
- StoreActionは,何かが起こった時,Storeにどんなデータを利用するかを定義します.基本的な書き方は以下のようになります.typeにactionのタイプを指定します.
return { type: types.RECEIVE_ITEM, item }Reducerは現在のstateとActionから新しいstateを生成します.
Reducerでしてはいけないことがいくつかあります.
・ 引数が不自然に変更されるような処理はしてはいけない
・ API問い合わせやルーティングのような副作用を生み出す実装をしてはいけない
・ 純粋関数ではない関数を呼んではいけない
Reducerでは,同じ引数を渡したら常に同じ結果が返ってくるように設計をします.switch(action.type) { case types.REQUEST_ITEM: case types.RECEIVE_ITEM: return { ...state, items: action.itemData } default: return state }Storeはアプリケーションのstateを保持します.また,stateへアクセスするためのgetState()や更新をするためのdispatch(action)を提供します.storeを作るには,reducerをcreateStore()へ渡す必要があります.
import rootReducer from '../reducers'; import {createStore} from 'redux'; const store = createStore(rootReducer);Reduxの3原則
Reduxを設計する上で,以下の3つの原則に基づいて設計することが重要になります.
1. Single source of truth
アプリケーション内でStoreは1つのみとし,Stateは単独のオブジェクトとしてStoreに保持される
2. State is read-only
状態を変更する手段は、変更内容をもったactionをStoreへdispatchすることでしかstateは変更できない
3. Changes are made with pure functions
actionがどのように状態を変化するのをpureな関数(reducer)でかく
redux-saga
redux-sagaはReact/Reduxアプリケーションにおける非同期処理などをより簡単に管理するためのライブラリです.以下の状況などで利用します.
1. 特定のActionを待って,別のActionをdispatchしたい
2. 通信処理の完了を待って,別の通信処理を開始したい
3. 初期化時にデータを読み込みたい
4. 頻繁に発生するActionをバッファしてまとめてdispatchしたい
5. 他のフレームワーク,ライブラリとうまく連携したい今回は特に,Actionの発生,終了から別のActionをdispatchしたかったため,redux-sagaを利用することにしました.
redux-sagaの詳細な説明や使い方などはこちらの記事が非常に参考になります.
redux-sagaで非同期処理と戦うSpotify APIを利用した音楽検索アプリ
Spotify APIとは,アーティストやアルバム,曲情報をSpotifyが提供しているAPIです.これらの情報をJSON形式で取得することができます.今回は,これを利用して,音楽を検索できるSPAを開発していきます.
音楽検索アプリの流れ
全体の処理の流れは以下の図にようになっています.まずSpotify APIからtokenを取得します.取得したtokenはのちに使用するのでstoreに保存しておきます.入力ごとにactionをdispatchし,redux-sagaで受け取ります.sagaでは,actionが発生したことを検知したら受け取った入力値とtokenを利用してAPIをたたきに行きます.APIには,アーティスト,アルバム,トラックの3つの検索を行うため,3回リクエストが飛びます.各検索処理が完了するのをredux-sagaのcallを利用し,待ちます.完了したら3つのデータを一つにmergeして再度actionをdispatchし,storeに更新をかけます.
ディレクトリ構成
src/ ├ actions/ │ └ index.js ├ components/ │ ├ audio/ │ │ └ AudioController.js │ ├ tabs/ │ │ ├ Album.js │ │ ├ Artist.js │ │ ├ SearchBar.js │ │ ├ Track.js │ │ ├ tab-content.js │ │ ├ tab-list-item.js │ │ └ tab-list.js │ └ SearchMusic.js ├ constants/ │ ├ action.js │ ├ component.js │ └ server-constants.js ├ containers/ │ └ App.js ├ images/ ├ reducers/ │ └ index.js ├ sagas/ │ └ index.js ├ store/ │ └ index.js ├ styles/ ├ authentication.js └ index.jsSpotify APIからtokenを取得する
Spotify APIを利用して楽曲情報を取得するには,Spotifyからtokenを取得する必要があります.Spotify APIは以下の3つの認証フローを提供しています.
Flow Access User Resources Require Secret key Access Token Refresh Authorization Code Yes Yes Yes Client Crendentials No Yes Yes Implicit Grant Yes No No 今回は,手軽に利用できるClient Crendentialsの認証フローでtokenを取得していきたいと思います.指定したURLハッシュとしてtokenをつけた状態でリダイレクトされる形で取得できます.client_idとredirect_uriに関しては,spotifyAPIのサイトから発行したものを利用します.
authentication.jsfunction authentication() { if(/localhost:3000\/$/.test(window.location.href)) { // implicit grant flow (change it to authorization flows later) window.location.replace( "https://accounts.spotify.com/authorize?client_id="+serverConstants.clientId+"&redirect_uri="+serverConstants.redirectUri+"&response_type=token&state=123" ) } let url = window.location.href return url.match(/#(?:access_token)=([\S\s]*?)&/)[1] }他の認証フローや詳細などはAuthorization Guideに記載されていますのでそちらを参考にお願いします.
ユーザー入力部分の実装
ユーザーが入力した文字を,親コンポーネントのsearch-music.js内のhandleSearchBarChange関数を呼び出します.関数内では,bindActionCreatorsでactionを生成し,随時dispatchし,store内のstateを更新しています.
component/tabs/SearchBar.jsclass SearchBar extends Component { render() let input = this.props.item return ( <div className='searchBar-wrapper'> <p>Search for an Artists, Song or Album</p> <form onChange = {e => { e.preventDefault() this.props.onChange(input.value) }}> <input autoFocus defaultValue={this.props.item} placeholder="start typing..." ref={node => { input = node }} /> </form> </div> ) } } SearchBar = connect()(SearchBar) export default SearchBarcomponent/SearchMusic.jshandleSearchBarChange(nextItem) { this.props.boundActionsCreators.searchItem(nextItem) }Spotify APIから情報を取得し,更新する部分
tokenと入力データは取得できたと思うので,ようやくSpotify APIから曲情報を取得していきたいと思います.キーの入力毎にstateが更新されるので,ReactのライフサイクルのcomponentDidUpdate()を利用して,stateを更新直後にstateを利用してSpotify APIから検索をしていきたいと思います.componentDidUpdate()は更新が行われた直後に呼び出され,引数に前回のpropsを受け取ります.
前回と今回の入力値を比較し,違う場合にactionをdispatchします.また,入力した文字を消した場合も呼び出されるため,何も入力されていない場合にはactionを呼び出さないように設定します.component/SearchMusic.jscomponentDidUpdate(prevProps) { if ((this.props.item !== prevProps.item) && (this.props.item !== '')) { this.props.fetchData(this.props.item, this.props.token) } }fetchDataのactionは以下のようになっています.
actions/index.jsexport function fetchData(searchedItem, token) { return { type: types.FETCH_DATA, searchedItem, token } }通常ならば,dispatchしたactionをそのままreducerが受け取り,stateを更新していきます.今回はredux-sagaを利用しているため,saga側でactionを受け取ります.apiFlow()関数は,裏で走っており,
actionが発生するのをまだかまだかと待っている状況です.actionが発生すると,下記のfetchData関数のタスクを開始させ,Spotify APIからそれぞれアーティスト,アルバム,トラック情報を取得してきます.sagas/index.jsfunction* apiFlow() { while(typeof x === 'undefined') const {searchedItem, token} = yield take(types.FETCH_DATA); yield fork(fetchData, searchedItem, token); } } export default function* rootSaga(cable) { yield fork(apiFlow); }しかし,このままだとstateの更新ごとにcomponentDidUpdate()は呼ばれ,APIをたたきにいってしまいます.ある程度まとめて処理をできるように,リクエストの回数を減らすような工夫が必要になってきます.今回はredux-sagaのdelayを利用します.Actionを受け取ったらAPIをたたきに行く前に,1秒間処理の遅延を行います.その間に新たなActionを受け取った場合,実行中のタスクを終了させ,新たなタスクを開始させます.これにより,入力直後の1秒間はActionは何度生成されても,最終的にAPIをたたきに行くのは一度だけで済みます.
sagas/index.jsfunction forkLater(task, searchedItem, token) { return fork(function* () { yield delay(1000); yield fork(task, searchedItem, token) }) } function* apiFlow() { let task; while(typeof x === 'undefined') { const {searchedItem, token} = yield take(types.FETCH_DATA); if (task && task.isRunning()) { task.cancel(); } task = yield forkLater(fetchData, searchedItem, token); } }sagas/index.jsfunction* fetchData(searchedItem, token) { let setArtists = yield call(fetchArtists, searchedItem, token); let setAlbums = yield call(fetchAlbums, searchedItem, token); let setTracks = yield call(fetchTracks, searchedItem, token); let parsedItems = mergeFetchedData(setArtists, setAlbums, setTracks) yield put({type: types.RECEIVE_ITEM, item: searchedItem, itemData: parsedItems}) }以下が実際にAPIをたたきに行く関数の一部です.
sagas/index.jsfunction* fetchArtists(item, token) { yield put({type: types.REQUEST_ITEM, item}) return yield axios.get( `https://api.spotify.com/v1/search?q=${item}&type=artist`, { headers: { 'Authorization': 'Bearer ' + token, } }) .then(response => response.data) .then(res => res.artists.items.map(item => ({ id: item.id, name: item.name, image: item.images[1] }))) .catch(err => { console.log(err) }) }取得した情報の表示
spotifyからアルバム,アーティストとトラックデータが取得できていると思います.これらのデータをいい感じに表示していきます.それぞれにパネルを用意し,クリックで遷移するような形にしたいと思います.
まず,パネル遷移部分の実装を書いていきます.this.stateに保存している現在のpanelに合わせて,表示するpanelを返却します.SearchMusic.jsgetPanel() { if (this.props.spotify.items === undefined || this.props.spotify.items.length === 0) { return ; } else { switch(this.state.showPanel) { case constants.ARTIST_PANEL: return ( <Artist artists={this.props.spotify.items.artists} /> ); case constants.ALBUM_PANEL: return ( <Album albums={this.props.spotify.items.albums}/> ); case constants.TRACK_PANEL: return ( <Track tracks={this.props.spotify.items.tracks}/> ); } } }TabListにそれぞれのパネルのタブを入れます.クリックすると対応するパネル名をsetStateで更新し,getPanel()で表示します.
SearchMusic.jsrender() { const panel = this.getPanel() return ( <div> ... <TabList> <TabListItem dist={constants.ARTIST_PANEL} label="Artists" onClick={this.onClick.bind(this)} /> <TabListItem dist={constants.ALBUM_PANEL} label="Album" onClick={this.onClick.bind(this)} /> <TabListItem dist={constants.TRACK_PANEL} label="Track" onClick={this.onClick.bind(this)} /> </TabList> <TabContent> {panel} </TabContent> </div> ) }まとめ
JavaScriptを全く書いたことない人がReact,Reduxを利用して音楽検索アプリを作ってみました.ネットで書き方などを検索した際に,最初に混乱したのがES5とES6で書き方が変わっていたことです.参考にするサイトによってはES5で書かれていたりしたので,エラーがよく生じていました.また,reduxのactionとreducerの仕組みを理解するのにかなり時間を要しました.
色々と詰め込んだのでかなり量が多くなってしまいました.「ここおかしいぞ」とか「ここ直した方がいいぞ」等々ありましたら、指摘していただけると嬉しいです。今回のコードをこちらのGitHubのリポジトリにおいてあります。もしよかったら参考にしてみてください。GitHub: SpotifyPlayer
最後まで読んでくださり、ありがとうございました。