- 投稿日:2019-12-02T23:48:29+09:00
ReactでChrome拡張を開発して公開したお話
はじめに
この記事は、KIT Developer Advent Calendar 2019 2日目になります。
Advent Calendarの時期が来ると、今年ももう少しで終わるな〜っていう実感が湧いてきますね。それでは、ReactでChrome拡張を開発して公開したお話について書いていきます!
今回開発したもの
タブを管理するActive Tab ListerというChrome拡張になります。
開発背景
Chromeを使っていて、以下のことに困っていました。
- 前に見ていたページを探したいと思った時に、開いているタブが多いと、ページタイトルが見切れてしまい切り替えることが困難
- 見ているページをスマートフォンやiPadですぐに見たいときに、Slackなどで送るのは手間
- Chromeのタブをたくさん開いていると、メモリを沢山消費しているので開いているが不要なタブをサクッと削除したい
- しかし、タブを沢山開いているとFaviconしか表示されないような状態になってしまい、不要なタブの判断がしづらい
これらの問題を解決するため、
- 開いているタブの一覧を表示して、タップするとそのページに遷移できるようにする
- 開いているタブのQRコードを読み取ることで、スマートフォン・iPadでも別のサービスを経由することなく瞬時に開くことができるようにする
- 複数ページを開いている際に「いくつタブを開いているのか」すぐに見ることができ、タイトルを見て不要だと思ったタブはDELETEボタンで削除できるようにする
という仕様を満たすように開発を進めていきました。
manifestファイルの書き方
Chrome拡張の開発において記入が必須のマニフェストファイル(manifest.json)の書き方については、以下の記事が大変分かりやすかったです。
Chrome拡張の実行方法
npm run build
をすると、ルートディレクトリにbuild
ディレクトリを作成chrome://extensions/
へアクセスし、パッケージ化されていない拡張機能を読み込む
をクリックします。- 拡張機能のディレクトリを選択できるので、先ほど作成された
build
ディレクトリを追加します。 (manifest.jsonはpublic
ディレクトリにもありますが、build
ディレクトリを選びます)※ 普段のフロントエンド開発の要領で
npm start
,yarn start
をしてもChrome拡張の動作確認は出来ないので注意です。デバッグについて
Webアプリ開発と同じ点
- Chrome拡張のアプリでもChrome DevToolsを使って検証することが出来ます
異なる点
- React Developer Toolsを使用することが出来ない
- そのため、Stateの確認などは少々手間になってしまいます
公開手順
公開手順は以下のようになっています。
Publish in the Chrome Web Storeより
- 作成された
build
ディレクトリをzip形式に圧縮する- ChromeDeveloperDashboardにアクセスし、5ドルの登録料を払います(年会費制ではないので一度支払えばOKな点も良いですね)
- 「新しいアイテムを追加する」をクリックして、zipファイルをアップロードします
- アプリケーションの詳細な説明を追加します。
また、少なくとも以下の画像が必要です。
- ストアに表示する128x128のアイコン(ファビコンを再利用できます)
- アプリの動作を示すために、1280x800または640x400のスクリーンショット,YouTubeビデオのいずれか
- Chromeウェブストアの壁に表示される440x280のタイルアイコン
(ここがちょっと大変ですが公開まであと少しなので、もうひと頑張りです!!)
その後、1~2日ほど待って審査が通れば、公開されます。終わりに
Chrome拡張は公開する際に厳しい審査もなく、自分のアイデアを簡単に形にして届けることが出来ます。
普段Chromeを使っていて、こんなものがあれば便利だなって思うことがあれば作成してみるのも良いですね!開発リポジトリはこちらになります。
IssuesやPR大歓迎です?
- 投稿日:2019-12-02T19:28:38+09:00
【React+Firebase Auth】ログイン周りのクラス設計を考える
ReactとFirebase Authenticationのアプリでログイン周りの最適なクラス設計を考えたい。
Global Stateの管理は
unstated
を使っていましたが、同じ作者がReact Hooksを用いたunstated-next
を開発されていたのでそちらに乗り換えます。前提
Windows10で作成します。
アプリの土台はcreate-react-app
で作成しています。Twitterログインだけを実装します。Twitterアプリの登録は済ませてあります。
とりあえず動くものを
npx create-react-app
を実行した後、必要なパッケージを導入します。npm install unstated-next npm install firebaseまずは馬鹿正直に一つのtsxファイルにぶち込む?
src/App.tsximport React from 'react'; import './App.css'; import { createContainer } from "unstated-next"; import firebase from "firebase/app"; import "firebase/auth"; firebase.initializeApp({ apiKey: "this-is-your-api-key", authDomain: "your-app-name.firebaseapp.com", databaseURL: "https://your-app-name.firebaseio.com", projectId: "your-app-name", storageBucket: "your-app-name.appspot.com", messagingSenderId: "999999999999", appId: "9:999999999999:web:999xxx999xxx999xxx999x", measurementId: "Z-ZZZZZZZZ999" }); function useUserState() { const [user, setUser] = React.useState<firebase.User | null>(null); firebase.auth().onAuthStateChanged(user => setUser(user)); const signIn = async () => { try { const provider = new firebase.auth.TwitterAuthProvider(); await firebase.auth().signInWithPopup(provider); } catch (error) { setUser(null); } } const signOut = async () => { await firebase.auth().signOut(); } return { user, signIn, signOut } } const userContainer = createContainer(useUserState); const LoginPage = () => { const userState = userContainer.useContainer(); return ( <div className="App"> {userState.user ? <div className="isSignedIn"> <div className="rowContent"> <button className="signInButton" onClick={userState.signOut}> サインアウト </button> </div> <div className="rowContent"> <img src={userState.user.photoURL ? userState.user.photoURL.replace("normal", "200x200") : ""} /> </div> <div className="rowContent"> <p>{userState.user.displayName}</p> </div> <div className="rowContent"> <p>{userState.user.uid}</p> </div> </div> : <div className="rowContent"> <button className="signInButton" onClick={userState.signIn}> サインイン </button> </div> } </div> ); } const App: React.FC = () => { return ( <userContainer.Provider> <LoginPage /> </userContainer.Provider> ); } export default App;動作確認
Twitterのアカウントでログインして画像と名前を表示するだけのアプリケーションができました。
このUIはそのままに、読みやすいソースコードに分割できないか考えます。クラス設計のベストエフォートがわからないので本当に自己流です...。ご容赦!
Firebaseの初期化処理
firebase.initializeApp()
は最初に必ず通過してほしく、かつプロジェクトが変わってもAPIKeyを書き換えるだけで済むようにファイルひとつにしておきます。src/firebase.tsimport firebase from "firebase/app"; import "firebase/auth"; import "firebase/firestore"; firebase.initializeApp({ apiKey: "this-is-your-api-key", authDomain: "your-app-name.firebaseapp.com", databaseURL: "https://your-app-name.firebaseio.com", projectId: "your-app-name", storageBucket: "your-app-name.appspot.com", messagingSenderId: "999999999999", appId: "9:999999999999:web:999xxx999xxx999xxx999x", measurementId: "Z-ZZZZZZZZ999" }); export default firebase;このソースが
export
するfirebase
を参照すればinitializeApp()
済みであることが保証されます。
import
は利用するFirebaseのサービスに合わせて追加削除します。ちなみに
initializeApp()
に渡しているAPIKeyは第三者に知られても問題ないようです。GitHubのパブリックなリポジトリにこのままプッシュできて楽ですね。ログイン処理
Firebase Authenticationでログインする処理を記述するクラスを作成します。
src/service/AuthService.tsimport firebase from "../firebase"; export class AuthService { onAuthStateChanged = (observer: (user: firebase.User | null) => void) => { firebase.auth().onAuthStateChanged(observer); } signInWithTwitter = async () => { const provider = new firebase.auth.TwitterAuthProvider(); await firebase.auth().signInWithPopup(provider); } signOut = async () => { await firebase.auth().signOut(); } }必要に応じて
signInWithGoogle
などを追加します。エラー処理もしてあるとベストです(手抜き)。Container
unstated-next
を用いてstate管理するContainerを作成します。src/container/UserContainer.tsimport React from 'react'; import { createContainer } from "unstated-next"; import firebase from "../firebase"; import { AuthService } from '../service/AuthService'; const useUserState = () => { const authService = new AuthService(); const [user, setUser] = React.useState<firebase.User | null>(null); authService.onAuthStateChanged(user => setUser(user)); const signIn = async () => { await authService.signInWithTwitter(); } const signOut = async () => { await authService.signOut(); } return { user, signIn, signOut } } const userContainer = createContainer(useUserState); export default userContainer;内部でログイン処理を委譲する
AuthService
のインスタンスを生成しています。
本当は中でnew
したくないのですがいい方法が思いつきませんでした。どなたか知見があれば教えていただけると助かります。ログインページ
Containerを参照して管理している状態を画面に表示するコンポーネントを作成します。
src/components/LoginPage.tsximport React from 'react'; import './App.css'; import userContainer from "../container/UserContainer"; const LoginPage = () => { const userState = userContainer.useContainer(); return ( <div className="App"> {userState.user ? <div className="isSignedIn"> <div className="rowContent"> <button className="signInButton" onClick={userState.signOut}> サインアウト </button> </div> <div className="rowContent"> <img alt="icon" src={userState.user.photoURL ? userState.user.photoURL.replace("normal", "200x200") : ""} /> </div> <div className="rowContent"> <p>{userState.user.displayName}</p> </div> <div className="rowContent"> <p>{userState.user.uid}</p> </div> </div> : <div className="rowContent"> <button className="signInButton" onClick={userState.signIn}> サインイン </button> </div> } </div> ); } export default LoginPage;UIとロジックが分離されていて読みやすくなっているかと思います。
ルート
LoginPage.tsx
をProvider
で囲うルートのコンポーネントです。src/App.tsximport React from 'react'; import LoginPage from "./components/LoginPage"; import userContainer from "./container/UserContainer"; const App: React.FC = () => { return ( <userContainer.Provider> <LoginPage /> </userContainer.Provider> ); } export default App;
App
をレンダリングすれば上の方に載せたgifと同じものがブラウザに表示されます。ディレクトリ構成
src
配下のディレクトリ構成は下記の通りになっています。│ App.tsx │ firebase.ts │ ├─components │ LoginPage.css │ LoginPage.tsx │ ├─container │ UserContainer.ts │ └─service AuthService.tsログイン周りに限らず、ロジックを記述するソースは
service
に、状態管理に関するソースはcontainer
に保存します。
さらにFirestoreにアクセスするアプリの場合はrepository
フォルダを作成してUserRepository
クラスなんかを作ります。まとめ
ソースの再利用がしやすいコードを意識してみましたがいかがでしょうか。
優しくマサカリぶん投げてくれる方がいらっしゃればご指摘お願いいたします。
- 投稿日:2019-12-02T18:30:29+09:00
Node.js & Express & MySQL & React でTODOリスト Herokuにデプロイ編
はじめに
前回の続きです。
https://qiita.com/hcr1s/items/0e5970c5af496c221a24今回は、前回作成したAPIをHerokuへデプロイしていきます。
前提
今回は、Herokuのアカウントを所持しており、クレジット登録していることを前提とします。
Heroku app作成
まずは、git initやheroku createを実行していきます。
# 既にgit initしている場合は省略 $ git init $ git add . $ git commit -m 'commit名' # herokuでアプリ作成 $ heroku create アプリ名 $ heroku git:remote -a アプリ名 $ git push heroku masterとりあえず本番環境へpushは完了です。
MySQL
本番環境でMySQLへ接続するための設定を行なっていきます。
$ heroku addons:add cleardbHerokuでは、cleardbというクラウドサービスのMySQLを使用できます。
$ heroku config | grep CLEARDB_DATABASE_URL ↓ CLEARDB_DATABASE_URL: mysql://user名:password@host名/database名?reconnect=trueこのコマンドで、必要なデータベースの情報を取得できます。
この情報をconfigに設定していきます。$ heroku config:set DATABASE_URL='mysql://*********?reconnect=true'mysql://のあとは、grepの結果をそのままペースとしてください。
以上でcleardbの設定は終了です。
index.jsの編集
実際にプログラムに必要なコードを記述していきます。
まずは、前回も書いたcreateConnectionの編集をしていきます。index.js--- 省略 --- const databaseName = 'database名' const connection = mysql.createConnection({ host: host名, user: user名, password: 'password', database: 'database名' }) --- 省略 ---中身に関しては、MySQLの設定時のgrepを参考に記述していきます。
そして、必要なデータベースやテーブルが存在しない場合、つまり初回に限り実行されるSQLを記述します。index.jsconnection.query('create database if not exists ??;', databaseName) connection.query('use ??', databaseName) connection.query('create table if not exists todo_list(id int auto_increment, name varchar(255), isDone boolean, index(id))')前回も書きましたが、SQL文にフィールドを使用する際は??と記述します。
以上で、コードの編集も終了です。
package.jsonの編集
Herokuにデプロイした際に、package.jsonに記述がないとnpm startが実行されてしまうので編集します。
package.json{ "name": "todo-api", "version": "1.0.0", "description": "", "engines":{ "node": "12.13.0", "npm": "6.12.0" }, "main": "index.js", "scripts": { "start" : "node index.js" }, --- 省略 ---デプロイ
最後にデプロイをして終了です。
実際に使ってみましょう。
実際にPOSTを送ってみた際のスクショです。
いい感じですね。終わり
次回は、このAPIを使用してTODOアプリを開発していきます。
何か間違いがある際はおしらせください!
- 投稿日:2019-12-02T14:57:34+09:00
[React] 年末だしwindow.matchMedia()で爆速レスポンシブ対応していこうな
はじめに
この記事は [React] 年末だしstyled-componentsで爆速レスポンシブ対応していこうな の続きです。
前回は styled-components 内でレスポンシブを爆速にする方法を書きましたが、この方法では「端末がモバイルのときだけ
render
」といったことはできません。例えば以下のようにすれば
const SPOnlyBox = styled.div` background: black; /* PC */ ${media.pc` display: none; `} `;一応PC上でDOMを見えなくすることはできますが、これだとDOMそのものは残ってしまいますね。
ということで、便利な解決策を考えていきます。
目標
最近のReactはまさに大 hooks 時代です。仮に
useMedia
っていうhookがあって、端末がモバイルかPCか、こんなふうに書けたら嬉しいですよね。const media = useMedia(); const isSP = media.sp; return ( <div> { isSP && <SPOnlyBox /> } </div> )これを目指します!
Solution:
window.matchMedia()
つかうもの
世の中には便利なものがあるんですね。ブラウザ標準で、CSS media query形式から、それが現在の環境に合致するかどうか判定してくれる関数があります。
Window.matchMedia() - Web APIs | MDNこの関数を使えば次のようになります。
const isSP = window.matchMedia('screen and (max-width: 767px)').matches;hook化する
上記の状態で事は済んでいますが、hooksとして共通化しましょう。
export const useMedia = () => { const queryStrings = { pc: 'screen and (min-width: 768px)', sp: 'screen and (max-width: 767px)', short: 'screen and (max-height: 480px)', }; return Object.fromEntries( Object.entries(queryStrings).map(([k, v]: [string, string]) => [ k, window.matchMedia(v), ]) ); };できあがり
これで問題ありません。以下のように書けます。
const media = useMedia(); const isSP = media.sp.matches;実に簡潔で最高ですね。
補足
event listener
このとき
media.sp
はMediaQueryList
になっており、media.sp.addListener((e) => { ... })することでメディアクエリの変更を動的に検知することもできるみたいです。
react-media-hook
lessmess-dev/react-media-hook が近い思想で存在してます。これをつかっても良いですし、どちらにせよwrapしたくなるので、上記のように自分で書いてもいいと思います。
以上です! ありがとうございました!
- 投稿日:2019-12-02T13:41:43+09:00
React で VRM モデルを表示する方法
本エントリは金沢工業大学の学生が書く KIT Developer Advent Calendar の3日目です。
English Version: How to display 3D humanoid avatar with React - dev.toはじめに
3DCG や VR の技術は様々な場所で用いられ、私たちにとって身近なものになりました。そして Web ブラウザ上でも同じような現象が起きています。今回は VRM と React や @pixiv/three-vrm を用いてどのように VRM を表示するのか紹介します。
VRM とは?
VRM は VR アプリケーション向けの人型 3D アバター (3D モデル) データを扱うためのファイルフォーマットです。VRM に準拠したアバターを持っていれば、3D アバターが必要な様々なアプリケーションを楽しむことが出来ます。
@pixiv/three-vrm とは?
@pixiv/three-vrm
は Three.js で VRM を使うための JavaScript ライブラリです。これを使えば VRoid Hub のように Web アプリケーションでも VRM を表示することが出来ます。※ 本エントリで利用している VRM モデルは製作者から許可を得ています
VRM の準備
まずはじめに、VRoid Hub から VRM をダウンロードする必要があります。
- タグで VRM モデルを検索
- お気に入りのモデルを選択
- モデルのページに移動して「このモデルを利用する」をクリックしてダウンロード
プロジェクトのセットアップ
$ npx create-react-app three-vrm-sample $ cd three-vrm-sample/ $ yarn add @pixiv/three-vrm three react-three-fiberindex.html<!DOCTYPE html> <html> <head> <title>@pixiv/three-vrm sample</title> <style> html, body { background-color: #000; color: #fff; margin: 0; width: 100vw; height: 100vh; } #root { width: 100%; height: 100%; } </style> </head> <body> <div id="root"></div> </body> </html>index.jsimport React from 'react' import ReactDOM from 'react-dom' const App = () => null ReactDOM.render(<App />, document.getElementById('root'))VRM のローダーを追加する
VRM は GLTF と似たフォーマットなので、Three.js に組み込まれている
GLTFLoader
で読み込むことが出来ます。
この処理は<App />
コンポーネント内に直接記述しても良いのですが、関心を分離するために Custom Hook にしました。余談ですが、個人的には「use○○○」と命名できそうなものは積極的に Custom Hook に切り分けるようにしています。コードがスッキリしたり、テストしやすくなったりするだけではなくて、何をしているのか明示的に表現できるのが好きです。
import { VRM } from '@pixiv/three-vrm' import { useRef, useState } from 'react' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' const useVrm = () => { const { current: loader } = useRef(new GLTFLoader()) const [vrm, setVrm] = useState(null) const loadVrm = url => { loader.load(url, async gltf => { const vrm = await VRM.from(gltf) setVrm(vrm) }) } return { vrm, loadVrm } }react-three-fiber で VRM を表示する
react-three-fiber は react-spring がメンテナンスしている React で Three.js をより簡単に扱うためのライブラリです。普通に Three.js を書いても良いのですが、これを使うことで Three.js を宣言的に扱うことが出来るというメリットがあります。今回は、以下の3つのエレメントを使用します。
<Canvas>
: react-three-fiber のエレメントのラッパー<spotLight>
: オブジェクトを照らすライトのエレメント<primitive>
: 3D オブジェクトのエレメントまた、VRM ファイルを追加すると
handleFileChange()
がファイルの URL を生成して VRM を読み込むようになっています。import React from 'react' import { Canvas } from 'react-three-fiber' import * as THREE from 'three' const App = () => { const { vrm, loadVrm } = useVrm() const handleFileChange = event => { const url = URL.createObjectURL(event.target.files[0]) loadVrm(url) } return ( <> <input type="file" accept=".vrm" onChange={handleFileChange} /> <Canvas> <spotLight position={[0, 0, 50]} /> {vrm && <primitive object={vrm.scene} />} </Canvas> </> ) }結果:
見た目をいい感じにする
表示されている VRM モデルは小さくて向こう側を向いています。もっと拡大して、こっちを向いてもらいましょう。
1. 新しいカメラを追加する
Note:
useThree()
を使うとgl
やscene
、camera
、clock
のような Three.js の基本的なオブジェクト全てを利用できます。import React, { useEffect, useRef } from 'react' import { useThree, Canvas } from 'react-three-fiber' import * as THREE from 'three' const App = () => { const { aspect } = useThree() const { current: camera } = useRef(new THREE.PerspectiveCamera(30, aspect, 0.01, 20)) const { vrm, loadVrm } = useVrm() const handleFileChange = event => { const url = URL.createObjectURL(event.target.files[0]) loadVrm(url) } // Set camera position useEffect(() => { camera.position.set(0, 0.6, 4) }, [camera]) return ( <> <input type="file" accept=".vrm" onChange={handleFileChange} /> <Canvas camera={camera}> <spotLight position={[0, 0, 50]} /> {vrm && <primitive object={vrm.scene} />} </Canvas> </> ) }2. モデルを回転させてカメラを見てもらう
camera
をvrm.lookAt
に代入して、さらにvrm
を180°回転させます。
ここで利用しているcamera
は 1 で追加されたカメラと同じものです。import { VRM } from '@pixiv/three-vrm' import { useEffect, useRef, useState } from 'react' import { useThree } from 'react-three-fiber' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' const useVrm = () => { const { camera } = useThree() const { current: loader } = useRef(new GLTFLoader()) const [vrm, setVrm] = useState(null) const loadVrm = url => { loader.load(url, async gltf => { const vrm = await VRM.from(gltf) vrm.scene.rotation.y = Math.PI setVrm(vrm) }) } // Look at camera useEffect(() => { if (!vrm || !vrm.lookAt) return vrm.lookAt.target = camera }, [camera, vrm]) return { vrm, loadVrm } }最終的なコード:
index.jsimport { VRM } from '@pixiv/three-vrm' import ReactDOM from 'react-dom' import React, { useEffect, useRef, useState } from 'react' import { useThree, Canvas } from 'react-three-fiber' import * as THREE from 'three' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader' const useVrm = () => { const { camera } = useThree() const { current: loader } = useRef(new GLTFLoader()) const [vrm, setVrm] = useState(null) const loadVrm = url => { loader.load(url, async gltf => { const vrm = await VRM.from(gltf) vrm.scene.rotation.y = Math.PI setVrm(vrm) }) } // Look at camera useEffect(() => { if (!vrm || !vrm.lookAt) return vrm.lookAt.target = camera }, [camera, vrm]) return { vrm, loadVrm } } const App = () => { const { aspect } = useThree() const { current: camera } = useRef(new THREE.PerspectiveCamera(30, aspect, 0.01, 20)) const { vrm, loadVrm } = useVrm() const handleFileChange = event => { const url = URL.createObjectURL(event.target.files[0]) loadVrm(url) } // Set camera position useEffect(() => { camera.position.set(0, 0.6, 4) }, [camera]) return ( <> <input type="file" accept=".vrm" onChange={handleFileChange} /> <Canvas camera={camera}> <spotLight position={[0, 0, 50]} /> {vrm && <primitive object={vrm.scene} />} </Canvas> </> ) } ReactDOM.render(<App />, document.getElementById('root'))結果:
いい感じですね。
おわりに
VRM は今後より広い場面で利用されることが予測されます。読者が React で VRM を扱う場面でこの記事が助けになれば嬉しいです。また @pixiv/three-vrm はもっと多くの機能を持っているので、もし興味があればドキュメントを読んで試してみてください。
最後になりますが、もし問題点や質問があればコメントや僕の Twitter アカウントで伝えていただけると幸いです。Sample Repository: saitoeku3/three-vrm-sample
5日目は @arakappa さんが Expo の Camera と Firebase Storage について書かれるのでお楽しみに! (4日目は埋まらなかった…)
- 投稿日:2019-12-02T13:14:16+09:00
ReactでDatePicker(react-datepicker)+ reactstrap + formikを使う
やりたいこと
datepickerを使うと、そこだけCSSが適用されなかったり、独自バリデーションとなるのを、ReactStrapで見た目を整え、Formikでバリデーションしたい。
完成
以下のような感じを目指す。
準備
作業場所と必要なモジュールをインストール。
create-react-app datepicker cd datepicker yarn add react-datepicker yarn add bootstrap reactstrap moment formik yup
npm install --save bootstrap reactstrap moment formik yup
実装
ポイントはreact-datepickerのcustomInputでReactstrapのInputを指定しているところ。
dateのバリデーションはとりあえず当月以外が指定されたらエラーが出るようにしている。App.jsimport React from 'react'; import './App.css'; import { Form, FormGroup, Label, Input, FormFeedback, Button } from 'reactstrap'; import { Formik } from 'formik'; import * as Yup from 'yup'; import moment from 'moment'; //react-datepicker import DatePicker, { registerLocale } from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; //for locale ja import ja from 'date-fns/locale/ja'; registerLocale('ja', ja); class App extends React.Component { handleOnSubmit = (values) => { alert(JSON.stringify(values)); } render() { return ( <React.Fragment> <div className="container"> <Formik initialValues={{ email: '', startDate: moment(new Date()).format('YYYY/MM/DD') }} onSubmit={this.handleOnSubmit} validationSchema={Yup.object().shape({ email: Yup.string().email().required(), startDate: Yup.string().required().test('checkDate', '当月を指定して下さい。', (picked) => { const pickedMonth = moment(new Date(picked)).month() + 1; const thisMonth = moment(new Date()).month() + 1; if (pickedMonth === thisMonth) { return true; } else { return false; } }), })} > { ({ handleSubmit, handleChange, handleBlur, values, errors, touched, setFieldValue }) => ( <Form onSubmit={handleSubmit} className="col-8 my-5"> <FormGroup > <Label for="email">Email</Label> <Input type="email" name="email" id="email" onChange={handleChange} onBlur={handleBlur} invalid={Boolean(touched.email && errors.email)} /> <FormFeedback> {errors.email} </FormFeedback> </FormGroup> <FormGroup> <legend className="col-form-label">開始日時</legend> <DatePicker locale="ja" name="startDate" id="startDate" value={values.startDate} dateFormat="YYYY/MM/DD" customInput={<Input invalid={Boolean(errors.startDate)} />} onChange={date => setFieldValue("startDate", moment(date).format('YYYY/MM/DD'))} /> <p className="text-danger small">{errors.startDate}</p> </FormGroup> <div className="my-3"> <Button type="submit">送信</Button> </div> </Form> ) } </Formik> </div> </React.Fragment> ); } } export default App;
- 投稿日:2019-12-02T12:43:11+09:00
React App #1 方針決め
TL;DR
ReactでWebアプリを作ります。
構造とか意識しつつ気になるツールを組み込む予定です。(進捗次第)私について
- Reactは4年前くらいに触った気がする。記憶のかなた。
- webも数年前から触っていなかったが、最近また調べ始めた。
- 最近アーキテクチャを意識し出した。依存性逆転の原則おもしろい。
開発環境
- Mac OS Catalina
- Node.js
- Yarn
方針(アプリの内容)
名前: Rooms(仮)
概要: 色々な部屋?を見るアプリ
操作: 上下左右にフリックorボタンで移動部屋は作成もできる(作成だけで別のアプリができる分量になりそう...?最初は簡単な部屋)
他の人の作った部屋を閲覧したい...?方針(開発)
- SPA
- スマートフォン向け
アーキテクチャ
データは一方通行で流れるべきらしい。
Reactの設計思想がそうなので、その流れを組んでfluxを意識する。
後述のReduxがフレームワークとして勝手に意識してくれそう?コンポーネントの作り方はAtomic Designを意識する。
- flux
- Atomic Design
サーバーサイド
サービス
Advent Calendarの制約?により、Firebaseだが、最初はローカルの予定なので出番がなさそう。
とりあえずFirebase Hostingで、静的なウェブサイトの構築。
部屋の共有機能などを実装したくなったら、他のサービスも追加していく。クライアントサイド
CSS
ReactではCSS-ModulesかCSS-in-JSが良さそう。
後述のAnt Designは競合するのか否か。
- CSS-Modules(仮)
- SCSS
lint系
Prettierを使ったことがないので、使ってみたい。
- Prettier
- ESLint
その他
- Redux(定番そう?)
- Jest + enzyme(テスト書きたい)
- TypeScript(考え中)
- React Hook(使いたい)
- Ant Design(できれば)
- Containerize(できれば)
- GitHub Actions に載せる(できれば)
- PWA(できれば)
- Clean Architecture(必要そうなら)
- React Native でアプリに(できれば)
- 投稿日:2019-12-02T08:42:18+09:00
react-trackedの紹介
はじめに
React ContextとReact Hooksでglobal stateを実現できるとか、いや、それはReduxと違ってパフォーマンスが出ないとか、そのような議論があります。これは、不要なrender(描画)を排除できるかどうかという論点で、規模が大きくなると影響が出る場合があるものです。本稿では、React ContextとReact Hooksで不要なrenderを排除する仕組みを備えるreact-trackedというライブラリを紹介します。
不要なrenderとは何か
例えば、global stateに二つの文字列を入れている場合、つまり、
const initialState = { lastName: 'React', firstName: 'Hooks', };このような形をしている場合があるとします。このstateがcontextを通してgloal stateとして提供するには次のようにProviderコンポーネントを作ります。
const Ctx = createContext(); const Provider = ({ children }) => { const [state, setState] = useState(initialState); return ( <Ctx.Provider value={[state, setState]}> {children} </Ctx.Provider> ); };一方、このglobal stateを利用して、firstNameを表示するコンポーネントは次のようになります。
const Component = () => { const [{ firstName }] = useContext(Ctx); return <div>{firstName}</div>; };これで機能的には問題ありません。しかし、不要なrenderが起こる場合があります。具体的には、firstNameが変更された場合だけでなく、lastNameが変更された場合にもこのコンポーネントはrenderされます。常に両方同時に変更される場合は問題ありませんが、lastNameだけが頻繁に変更される場合は、不要なrenderがパフォーマンス低下を引き起こす可能性があります。
この挙動はReact Contextの仕様であり、基本的な方針としては、更新タイミングが合わないデータを一つのcontextに入れるのではなくcontextを分割したり、コンポーネントを階層構造にしてReact.memoやuseMemoを使うことで改善したりすることが推奨されます。
React Trackedとは
一方で、どうしてもcontext分割しにくかったり、global stateとして管理する方が開発効率がいい場合もあります。そのようなケースに対応するのがReact Trackedというライブラリです。
ライブラリのドキュメントサイトはこちらです。
GitHubリポジトリはこちらです。
https://github.com/dai-shi/react-tracked
使い方
上記のfirstName/lastNameのstateの例をReact Trackedを使う場合、まずはじめにcontainerを作ります。
import { createContainer } from 'react-tracked'; const { Provider, useTracked } = createContainer(() => useState(initialState));Providerは前回のものと同じように使い、useTrackedはuseContextの代わりに使います。つまり、先ほどのコンポーネントは次のようになります。
const Component = () => { const [{ firstName }] = useTracked(); return <div>{firstName}</div>; };これだけの変更で、不要なrenderが排除できます。本稿では実装方法の詳細は省きますが、Proxyを使って実現しています。
ReduxのuseSelector
また、React TrackedのcontainerはuseSelectorも提供しており、Reduxのものとほぼ同様に使えます。これを使うと、次のように書けます。
const Component = () => { const firstName = useSelector(state => state.firstName); return <div>{firstName}</div>; };useSelectorはselector関数がシンプルな場合は良いですが、オブジェクトを生成したりするものの場合は、reselectなどを使ってmemoized selectorを作る必要があったりと、あまり初心者向きではないことが難点ではあります。
React Trackedの拡張性
containerを作成する際にuseStateの代わりにuseReducerを使うことができます。実は、stateを返すものなら何でも良いので、custom hooksを使うこともできます。
また、containerのuseTrackedやuseSelectorを拡張することもできます。
ドキュメントサイトにRecipesがあり、様々なパターンが載っています。
https://react-tracked.js.org/docs/recipes
Concurrent Mode対応
現在公開されているReactの実験的なバージョンでは、Concurrent Modeというのものが提供されています。詳しくは公式ドキュメントを参照してください。
Concurrent Modeを最大限に利用するには、React stateをベースにしていることが望ましいのですが、React TrackedはReact stateのラッパーなのでこれを満たしています。
一方、external storeを使っているライブラリ(ReduxやMobXなど)では、Concurrent Modeのある機能(state branchingと呼んでいます)が使えないという難点があります。
より詳しくはこちらのリポジトリをご参照ください。Concurrent Modeで課題になり得るポイントをテストするツールになっています。
おわりに
不要なrenderからConcurrent Modeまで話を詰め込みすぎた感があります。また、機会がありましたら個別の記事にしようかと思います。それまでは、ドキュメントサイトのQuick Startやブログ記事なども合わせてご参照ください。
- 投稿日:2019-12-02T07:13:48+09:00
React×Firebaseでタスク管理アプリのドラッグ&ドロップ機能を実装する
はじめに
都内の企業でWebエンジニアとして働いているKei(@kei_ffff)と申します。
最近、Firebaseの練習がてらタスク管理アプリを作ってみました。その中でも↓のような「ドラッグ&ドロップ時にタスクの状態(ステータス)を更新し、リアルタイムで画面に反映する」という機能をReactとFirebaseを使って実装できたのでサンプルコードを交えて説明したいと思います。
ざっくりとしたコンポーネントの構成 & 実装案
ページは主にそれぞれのステータスが付与されている
TaskLane
コンポーネントによって構成されています。
TaskLane
は先程のgifの中の、縦に並んでるタスクの一覧です。tasks
というタスクの一覧のデータを各TaskLane
に流し込んでます。↓こんな感じ (実際のコードはもっと
props
が生えてたり、status
が変数化されたりしてますが、説明のため省略)<TaskLane status="TODO" tasks={tasks.filter(t => t.status === "TODO")} /> <TaskLane status="IN_PROGRESS" tasks={tasks.filter(t => t.status === "IN_PROGRESS")} /> <TaskLane status="DONE" tasks={tasks.filter(t => t.status === "DONE")} />そして、それぞれの
TaskLane
コンポーネントの中身は、同じstatus
を持つTaskCard
コンポーネントによって構成されています。<TaskCard task={task} />なので、「ドラッグ&ドロップ時にタスクの状態(ステータス)を更新し、リアルタイムで画面に反映する」ためには以下の手順が必要です。
- ドラッグされた
TaskCard
を、別のステータスを持つTaskLane
にドロップできるようにする。- ドロップされたら、その
TaskCard
のstatus
という値はドロップ先のTaskLane
が持つstatus
の値で更新する。status
の更新はFirebase
のCloud Firestore
というDBにセットされた値を書き換えるかたちで更新する。Cloud Firestore
上で、DBの値が書き換わったことを検知して、画面を再レンダリングする。順に説明します。
ドラッグされた
TaskCard
を、別のステータスを持つTaskLane
にドロップできるようにする。まず、画面上の要素をドラッグできるようにするためには、要素に
draggable
という属性を指定します。ドラッグ開始のイベントハンドラは、JSX要素のonDrag
に渡すことができます。また、現在どの
TaskCard
がドラッグされているのかという状態を考える必要があります。TaskLane
コンポーネントの親コンポーネントで、以下の状態を定義します。ここでは、各task
に割り振られたid(number)
を使って、どのid
のTaskCard
がドラッグされているのかを考えます。そして、draggedId
を更新するためのコールバック関数も定義します。const [draggedId, setDraggedId] = useState(-1); const handleChangeDraggedId = useCallback((id: number) => setDraggedId(id), []);そして、
TaskLane
コンポーネントはprops
経由で先程のコールバック関数を受け取れるようにします。
そして、ドラッグ開始時に受け取った関数を実行します。TaskCard
にカスタムデータ属性(data-id
)としてつけておいたid
をセットすることで、handleDrag
内でdraggedId
を更新できます。
TaskLane
コンポーネントは以下のようになります。export const TaskLane = ({ status, tasks, onChangeDraggedId }: Props) => { const handleDrag = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); //要素がドラッグされたときのブラウザのデフォルト動作を止める onChangeDraggedId(Number(e.currentTarget.dataset.id)); }; return ( <ul> {tasks.map(task => ( <li key={task.id} draggable data-id={task.id} onDrag={handleDrag}> <TaskCard task={task} /> </li> ))} </ul> ); }次に、ドラッグされた要素が、
TaskLane
に被さっている時と、離れた時の見た目を調整します。(↑のgif参照)
(ここではstyle
はemotion
で定義しています。)const dragOverStyle = css({ border: '2px dashed #222222', backgroundColor: '#E0E0E0', }); export const TaskLane = ({ status, tasks, onChangeDraggedId }: Props) => { const ref = useRef<HTMLElement>(null); const handleDrag = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); onChangeDraggedId(Number(e.currentTarget.dataset.id)); }; const handleDragOver = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.add(dragOverStyle); }; const handleDragLeave = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.remove(dragOverStyle); }; return ( <ul ref={ref} onDragOver={handleDragOver} onDragLeave={handleDragLeave}> {tasks.map(task => ( <li key={task.id} draggable data-id={task.id} onDrag={handleDrag}> <TaskCard task={task} /> </li> ))} </ul> ); }ドラッグされた要素が被さっているときのイベントハンドラは
onDragOver
、離れたときのイベントハンドラはonDragLeave
でJSX要素に渡すことができます。
ここでは、被さったときには被さったときのスタイルを要素に当てて、離れたときにはそのスタイルを解除する、ということを実行しています。次に、ドロップされた場合のイベントハンドラを追加します。同様に
onDrop
でJSX要素に渡すことができます。const dragOverStyle = css({ border: '2px dashed #222222', backgroundColor: '#E0E0E0', }); export const TaskLane = ({ status, tasks, onChangeDraggedId }: Props) => { const ref = useRef<HTMLElement>(null); const handleDrag = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); onChangeDraggedId(Number(e.currentTarget.dataset.id)); }; const handleDragOver = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.add(dragOverStyle); }; const handleDragLeave = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.remove(dragOverStyle); }; const handleDrop = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.remove(baseDragOverStyle); onChangeDraggedId(-1); }; return ( <ul ref={ref} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}> {tasks.map(task => ( <li key={task.id} draggable data-id={task.id} onDrag={handleDrag}> <TaskCard task={task} /> </li> ))} </ul> ); }ドロップが完了したら、スタイルを解除し、ドラッグ中の
id
を初期値に戻します。ここまでで、ドラッグ&ドロップの動きを実現できました。
ドロップされたら、その
TaskCard
のstatus
という値はドロップ先のTaskLane
が持つstatus
の値で更新する。
TaskLane
の親コンポーネントでstatus
を更新するコールバック関数を定義します。そして、props
経由でその関数をTaskLane
に渡します。const [tasks, setTasks] = useState<Task[]>([]); const [draggedId, setDraggedId] = useState(-1); const handleChangeDraggedId = useCallback((id: number) => setDraggedId(id), []); const handleUpdateTaskStatus = useCallback( (status: TaskStatus) => { const draggedTask = tasks.find(task => task.id === draggedId); if (!draggedTask || (!!draggedTask && draggedTask.status === status)) return; //ここでCloud Firestore上で値を書き換えるための関数を実行する }, [draggedId, tasks], ); return ( <> //普通はmapさせますが説明のため簡略化してます。 <TaskLane status="TODO" tasks={tasks.filter(t => t.status === "TODO")} onUpdateTaskStatus={handleUpdateTaskStatus} /> <TaskLane status="IN_PROGRESS" tasks={tasks.filter(t => t.status === "IN_PROGRESS")} onUpdateTaskStatus={handleUpdateTaskStatus} /> <TaskLane status="DONE" tasks={tasks.filter(t => t.status === "DONE")} onUpdateTaskStatus={handleUpdateTaskStatus} /> </> )そして、
TaskLane
ではドロップ時にstatus
の更新を実行したいので、handleDrop
内でprops
で受け取った関数を実行します。const dragOverStyle = css({ border: '2px dashed #222222', backgroundColor: '#E0E0E0', }); export const TaskLane = ({ status, tasks, onChangeDraggedId, onUpdateTaskStatus }: Props) => { const ref = useRef<HTMLElement>(null); const handleDrag = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); onChangeDraggedId(Number(e.currentTarget.dataset.id)); }; const handleDragOver = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.add(dragOverStyle); }; const handleDragLeave = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.remove(dragOverStyle); }; const handleDrop = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); if (!ref.current) return; ref.current.classList.remove(baseDragOverStyle); onUpdateTaskStatus(status); onChangeDraggedId(-1); }; return ( <ul ref={ref} onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop}> {tasks.map(task => ( <li key={task.id} draggable data-id={task.id} onDrag={handleDrag}> <TaskCard task={task} /> </li> ))} </ul> ); }ここまでで、ドロップ時に
status
を更新する部分までできました。
status
の更新はFirebase
のCloud Firestore
というDBにセットされた値を書き換えるかたちで更新する。つづいて、
Cloud Function
上のtasks
というコレクションの中のtask
というドキュメントの中のデータの、status
という値を更新するための関数を定義します。type Task = { id: number; title: string; content: string; labels: string[]; status: TaskStatus; createdAt: firestore.Timestamp | null; updatedAt: firestore.Timestamp | null; }; export const updateTask = async ({ updateTaskAttribute, targetId, }: { updateTaskAttribute: Partial<Pick<Task, 'title' | 'content' | 'labels' | 'status'>>; targetId: number; }) => { const doc = firestore() .collection("tasks") .doc(targetId.toString()); await doc.update({ ...updateTaskAttribute, updatedAt: firestore.FieldValue.serverTimestamp(), }); };
updateTaskAttribute
には、Task
の中で更新したい値だけオブジェクトとして渡せるように定義しています。
targetId
は更新対象のtask
のid
であり、ドキュメントidと同じ値を指定しています。
doc
の部分は更新対象のtask
のドキュメントであり、doc.update
の中で、フィールドの値を更新することができます。
ここでは、updateTaskAttribute
とupdateAt
というフィールドの値を更新します。updateAt
の値は、firestore.FieldValue.serverTimestamp()
を指定することで、更新時のタイムスタンプを設定できます。そして、この
updateTask
を`TaskLane
の親コンポーネント`に追加します。const [tasks, setTasks] = useState<Task[]>([]); const [draggedId, setDraggedId] = useState(-1); const handleChangeDraggedId = useCallback((id: number) => setDraggedId(id), []); const handleUpdateTaskStatus = useCallback( (status: TaskStatus) => { const draggedTask = tasks.find(task => task.id === draggedId); if (!draggedTask || (!!draggedTask && draggedTask.status === status)) return; updateTask({ updateTaskAttribute: { status }, targetId: draggedId, }); }, [draggedId, tasks], ); return ( <> <TaskLane status="TODO" tasks={tasks.filter(t => t.status === "TODO")} onUpdateTaskStatus={handleUpdateTaskStatus} /> <TaskLane status="IN_PROGRESS" tasks={tasks.filter(t => t.status === "IN_PROGRESS")} onUpdateTaskStatus={handleUpdateTaskStatus} /> <TaskLane status="DONE" tasks={tasks.filter(t => t.status === "DONE")} onUpdateTaskStatus={handleUpdateTaskStatus} /> </> )ここまでで、ドロップ時にドラッグされた
task
のCloud Firestore
上のstatus
の値を更新することができました。
しかし、このままだとブラウザで動きを確認しても、ドロップしたタスクはリロードしない限り、画面上で別のレーンに移動してくれません。
Cloud Firestore
上で、DBの値が書き換わったことを検知して、画面を再レンダリングする。
Cloud Firestore
上のtask
の一覧を取得するための関数を追加します。doc.data()
をmap
で返すことで、すべてのドキュメントの中身のデータ一覧を配列として返すことができます。type Task = { id: number; title: string; content: string; labels: string[]; status: TaskStatus; createdAt: firestore.Timestamp | null; updatedAt: firestore.Timestamp | null; }; export const fetchTasks = async () => { const collection = firestore().collection("tasks"); const snapShot = await collection.get(); return snapShot.docs.map(doc => doc.data() as Task); };そして、
TaskLane
の親コンポーネント`に以下の関数を定義します。const [tasks, setTasks] = useState<Task[]>([]); const load = async () => { const tasksData = await fetchTasks(); setTasks(taskData); };
fetchTasks()
によってタスク一覧を再取得すれば、tasks
というstate
が更新されるので、画面を再レンダリングすることができます。
Cloud Firestore
上でデータが更新されたあとに画面を更新するためには、データが更新されたことを検知して、fetchTasks()
を実行すれば良さそうです。
Cloud Firestore
上でデータの更新を検知するためには、以下のようにonSnapshot
を使います。type Task = { id: number; title: string; content: string; labels: string[]; status: TaskStatus; createdAt: firestore.Timestamp | null; updatedAt: firestore.Timestamp | null; }; export const updateTask = async ({ updateTaskAttribute, targetId, callback }: { updateTaskAttribute: Partial<Pick<Task, 'title' | 'content' | 'labels' | 'status'>>; targetId: number; callback?: () => void; }) => { const doc = firestore() .collection("tasks") .doc(targetId.toString()); await doc.update({ ...updateTaskAttribute, updatedAt: firestore.FieldValue.serverTimestamp(), }); doc.onSnapshot(() => callback && callback()); };ここでは、引数に
callback
を追加し、データの更新完了をトリガーに、callback
を実行します。あとは、
TaskLane
の親コンポーネントでupdateTasks
を実行する際に、タスク一覧の取得を実行する関数を引数のcallback
に渡すだけです。const [tasks, setTasks] = useState<Task[]>([]); const load = async () => { const tasksData = await fetchTasks(); setTasks(taskData); }; const [draggedId, setDraggedId] = useState(-1); const handleChangeDraggedId = useCallback((id: number) => setDraggedId(id), []); const handleUpdateTaskStatus = useCallback( (status: TaskStatus) => { const draggedTask = tasks.find(task => task.id === draggedId); if (!draggedTask || (!!draggedTask && draggedTask.status === status)) return; updateTask({ updateTaskAttribute: { status }, targetId: draggedId, callback: load }); }, [draggedId, tasks], ); return ( <> <TaskLane status="TODO" tasks={tasks.filter(t => t.status === "TODO")} onUpdateTaskStatus={handleUpdateTaskStatus} /> <TaskLane status="IN_PROGRESS" tasks={tasks.filter(t => t.status === "IN_PROGRESS")} onUpdateTaskStatus={handleUpdateTaskStatus} /> <TaskLane status="DONE" tasks={tasks.filter(t => t.status === "DONE")} onUpdateTaskStatus={handleUpdateTaskStatus} /> </> )ここまでで、ドロップ時に
Cloud Firestore
上での値を更新し、更新完了後にタスク一覧を再取得することで、リアルタイムで画面が更新されるようになります。おわりに
以上のように、
React
とFirebase
を使ってライブラリいらずで手軽にドラッグ&ドロップを実装することができました。
このサンプルコードの詳細部分はこのレポジトリにあるので、よろしければ参考にしてみてください。(スターもください?)また、Twitter上で技術に関するツイートもしてますので、ご興味があればフォローしてみてください><
Twitter: @kei_ffff
- 投稿日:2019-12-02T02:37:07+09:00
[React] 年末だしstyled-componentsで爆速レスポンシブ対応していこうな
はじめに
こんにちは、ねりこ @nerikosans と申します。もう2019年も終わりが近づいてきましたね。
皆様におかれましてはますますimport * as React from 'react';
の由、何よりと存じます。日頃は特別のexport default
にあずかり心より御礼申し上げます。さて、Styled Components は、DOMのスタイルを、その定義ファイル(
.jsx
,.tsx
)内で完結させてしまおうという思想のフレームワークです。コードの見通しの良さ、動的レンダリングのしやすさなどから昨今は人気になってい(ると感じてい)ます。そして、DOMスタイリングにはレスポンシブ対応がつきものですが、やっぱり爆速で書きたいですよね。
ということで、 styled-componentsでレスポンシブするときのメモ書きです。目標
最低限の記法で書きたいので、これ↓だけ書けば対応が済むように構築します。
const Box = styled.div` background: black; /* PC */ ${media.pc` background: red; `} /* Smartphones */ ${media.sp` background: red; `} `;Step1. styled-media-query
morajabi/styled-media-query
よく使うサイズをサクッと使いたい場合はこれだけで大丈夫。
small
,medium
,large
,huge
の4つの width breakpoint を備えていて、media queryを自動で生成してくれます。公式サンプルが以下の通り。
const Box = styled.div` background: black; ${media.lessThan("medium")` /* screen width is less than 768px (medium) */ background: red; `} ${media.between("medium", "large")` /* screen width is between 768px (medium) and 1170px (large) */ background: green; `} ${media.greaterThan("large")` /* screen width is greater than 1170px (large) */ background: blue; `} `;便利ですね。でも
.lessThan("medium")
って毎回書きたくないので、これをwrapします。
styled-media-query
の export されていない type を使用しているところがあります。media.tsximport media from 'styled-media-query'; import { ThemedStyledProps, InterpolationValue, FlattenInterpolation, } from 'styled-components'; /** * https://github.com/morajabi/styled-media-query/blob/master/src/index.d.ts */ type InterpolationFunction<Props, Theme> = ( props: ThemedStyledProps<Props, Theme> ) => InterpolationValue | FlattenInterpolation<ThemedStyledProps<Props, Theme>>; type GeneratorFunction<Props, Theme> = ( strings: TemplateStringsArray, ...interpolations: ( | InterpolationValue | InterpolationFunction<Props, Theme> | FlattenInterpolation<ThemedStyledProps<Props, Theme>> )[] ) => any; const rules: { [v: string]: GeneratorFunction<unknown, any> } = { pc: (...args) => media.greaterThan('medium')(...args), sp: (...args) => media.lessThan('medium')(...args), }; export default rules;よし、これでこのファイルを
media
として default importすれば、${media.pc` .... `}
だけでpc専用スタイルを書けるようになりました!Step2. カスタマイズ
さて、これだけでも便利ですが、styled-media-query では今のところ pre-defined な4つのサイズ以外は指定できないようなので、これに加えて自由なmedia queryを書きたい場合 (例えば、heightで区切りたい場合) は以下のようにすればOKです。
media.tsximport media from 'styled-media-query'; import { ThemedStyledProps, InterpolationValue, FlattenInterpolation, css, } from 'styled-components'; /* (... 中略) */ const rules: { [v: string]: GeneratorFunction<unknown, any> } = { pc: (...args) => media.greaterThan('medium')(...args), sp: (...args) => media.lessThan('medium')(...args), short: (...args) => css` @media screen and (max-height: 480px) { ${css(...args)} } `, }; export default rules;これで新たに
media.short`...`
が使えるようになりました!おわりに / 展望
以上で、晴れて簡潔にレスポンシブスタイルが書けるようになりました。最高ですね。
しかし、そもそもComponentを
render
するかどうかから出し分けたい場合などは、この方法では足りません。
例えばconst media = useMedia(); const isPC = media.pc;みたいに書けたら便利ですよね。これを実現する方法はまた今度書きたいと思います。
お読みいただきありがとうございました!