20191202のReactに関する記事は11件です。

ReactでChrome拡張を開発して公開したお話

はじめに

この記事は、KIT Developer Advent Calendar 2019 2日目になります。
Advent Calendarの時期が来ると、今年ももう少しで終わるな〜っていう実感が湧いてきますね。

それでは、ReactでChrome拡張を開発して公開したお話について書いていきます!

今回開発したもの

タブを管理するActive Tab ListerというChrome拡張になります。

tab.jpg

開発背景

Chromeを使っていて、以下のことに困っていました。

  1. 前に見ていたページを探したいと思った時に、開いているタブが多いと、ページタイトルが見切れてしまい切り替えることが困難
  2. 見ているページをスマートフォンやiPadですぐに見たいときに、Slackなどで送るのは手間
  3. Chromeのタブをたくさん開いていると、メモリを沢山消費しているので開いているが不要なタブをサクッと削除したい
    • しかし、タブを沢山開いているとFaviconしか表示されないような状態になってしまい、不要なタブの判断がしづらい

これらの問題を解決するため、

  1. 開いているタブの一覧を表示して、タップするとそのページに遷移できるようにする
  2. 開いているタブのQRコードを読み取ることで、スマートフォン・iPadでも別のサービスを経由することなく瞬時に開くことができるようにする
  3. 複数ページを開いている際に「いくつタブを開いているのか」すぐに見ることができ、タイトルを見て不要だと思ったタブはDELETEボタンで削除できるようにする

という仕様を満たすように開発を進めていきました。

manifestファイルの書き方

Chrome拡張の開発において記入が必須のマニフェストファイル(manifest.json)の書き方については、以下の記事が大変分かりやすかったです。

Chrome拡張の実行方法

  1. npm run buildをすると、ルートディレクトリにbuildディレクトリを作成
  2. chrome://extensions/へアクセスし、パッケージ化されていない拡張機能を読み込むをクリックします。
  3. 拡張機能のディレクトリを選択できるので、先ほど作成されたbuildディレクトリを追加します。 (manifest.jsonはpublicディレクトリにもありますが、buildディレクトリを選びます)

※ 普段のフロントエンド開発の要領でnpm start, yarn startをしてもChrome拡張の動作確認は出来ないので注意です。

デバッグについて

Webアプリ開発と同じ点

  • Chrome拡張のアプリでもChrome DevToolsを使って検証することが出来ます

異なる点

  • React Developer Toolsを使用することが出来ない
    • そのため、Stateの確認などは少々手間になってしまいます

公開手順

公開手順は以下のようになっています。
Publish in the Chrome Web Storeより

  1. 作成されたbuildディレクトリをzip形式に圧縮する
  2. ChromeDeveloperDashboardにアクセスし、5ドルの登録料を払います(年会費制ではないので一度支払えばOKな点も良いですね)
  3. 「新しいアイテムを追加する」をクリックして、zipファイルをアップロードします
  4. アプリケーションの詳細な説明を追加します。

また、少なくとも以下の画像が必要です。

  • ストアに表示する128x128のアイコン(ファビコンを再利用できます)
  • アプリの動作を示すために、1280x800または640x400のスクリーンショット,YouTubeビデオのいずれか
  • Chromeウェブストアの壁に表示される440x280のタイルアイコン

(ここがちょっと大変ですが公開まであと少しなので、もうひと頑張りです!!)
その後、1~2日ほど待って審査が通れば、公開されます。

終わりに

Chrome拡張は公開する際に厳しい審査もなく、自分のアイデアを簡単に形にして届けることが出来ます。
普段Chromeを使っていて、こんなものがあれば便利だなって思うことがあれば作成してみるのも良いですね!

開発リポジトリはこちらになります。
IssuesやPR大歓迎です😄

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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.tsx
import 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;

動作確認

login-app.gif

Twitterのアカウントでログインして画像と名前を表示するだけのアプリケーションができました。
このUIはそのままに、読みやすいソースコードに分割できないか考えます。

クラス設計のベストエフォートがわからないので本当に自己流です...。ご容赦!

Firebaseの初期化処理

firebase.initializeApp()は最初に必ず通過してほしく、かつプロジェクトが変わってもAPIKeyを書き換えるだけで済むようにファイルひとつにしておきます。

src/firebase.ts
import 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.ts
import 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.ts
import 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.tsx
import 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.tsxProviderで囲うルートのコンポーネントです。

src/App.tsx
import 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クラスなんかを作ります。

まとめ

ソースの再利用がしやすいコードを意識してみましたがいかがでしょうか。
優しくマサカリぶん投げてくれる方がいらっしゃればご指摘お願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 cleardb

Herokuでは、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.js
connection.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"
  },
--- 省略 ---

デプロイ

最後にデプロイをして終了です。

実際に使ってみましょう。

スクリーンショット 2019-12-02 18.28.30.png

実際にPOSTを送ってみた際のスクショです。
いい感じですね。

終わり

次回は、このAPIを使用してTODOアプリを開発していきます。
何か間違いがある際はおしらせください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

頑張って書きます

Reactについて

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.spMediaQueryList になっており、

media.sp.addListener((e) => { ... })

することでメディアクエリの変更を動的に検知することもできるみたいです。

react-media-hook

lessmess-dev/react-media-hook が近い思想で存在してます。これをつかっても良いですし、どちらにせよwrapしたくなるので、上記のように自分で書いてもいいと思います。

以上です! ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 を表示することが出来ます。

VRoid Hub

※ 本エントリで利用している VRM モデルは製作者から許可を得ています

VRM の準備

まずはじめに、VRoid Hub から VRM をダウンロードする必要があります。

  1. タグで VRM モデルを検索
  2. お気に入りのモデルを選択
  3. モデルのページに移動して「このモデルを利用する」をクリックしてダウンロード

プロジェクトのセットアップ

$ npx create-react-app three-vrm-sample
$ cd three-vrm-sample/
$ yarn add @pixiv/three-vrm three react-three-fiber
index.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.js
import 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>
    </>
  )
}

結果:

Result

見た目をいい感じにする

表示されている VRM モデルは小さくて向こう側を向いています。もっと拡大して、こっちを向いてもらいましょう。

1. 新しいカメラを追加する

Note:
useThree() を使うと glscenecameraclock のような 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. モデルを回転させてカメラを見てもらう

cameravrm.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.js
import { 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'))

結果:

Result

いい感じですね。

おわりに

VRM は今後より広い場面で利用されることが予測されます。読者が React で VRM を扱う場面でこの記事が助けになれば嬉しいです。また @pixiv/three-vrm はもっと多くの機能を持っているので、もし興味があればドキュメントを読んで試してみてください。
最後になりますが、もし問題点や質問があればコメントや僕の Twitter アカウントで伝えていただけると幸いです。

Sample Repository: saitoeku3/three-vrm-sample

5日目は @arakappa さんが Expo の Camera と Firebase Storage について書かれるのでお楽しみに! (4日目は埋まらなかった…)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactでDatePicker(react-datepicker)+ reactstrap + formikを使う

やりたいこと

datepickerを使うと、そこだけCSSが適用されなかったり、独自バリデーションとなるのを、ReactStrapで見た目を整え、Formikでバリデーションしたい。

完成

以下のような感じを目指す。

スクリーンショット 2019-12-02 13.09.29.png

準備

作業場所と必要なモジュールをインストール。

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.js
import 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;

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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は競合するのか否か。

lint系

Prettierを使ったことがないので、使ってみたい。

その他

  • Redux(定番そう?)
  • Jest + enzyme(テスト書きたい)
  • TypeScript(考え中)
  • React Hook(使いたい)
  • Ant Design(できれば)
  • Containerize(できれば)
  • GitHub Actions に載せる(できれば)
  • PWA(できれば)
  • Clean Architecture(必要そうなら)
  • React Native でアプリに(できれば)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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というライブラリです。

ライブラリのドキュメントサイトはこちらです。

https://react-tracked.js.org

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ブログ記事なども合わせてご参照ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React×Firebaseでタスク管理アプリのドラッグ&ドロップ機能を実装する

はじめに

都内の企業でWebエンジニアとして働いているKei(@kei_ffff)と申します。
最近、Firebaseの練習がてらタスク管理アプリを作ってみました。

その中でも↓のような「ドラッグ&ドロップ時にタスクの状態(ステータス)を更新し、リアルタイムで画面に反映する」という機能をReactとFirebaseを使って実装できたのでサンプルコードを交えて説明したいと思います。

877cf-8sgyd.gif

ざっくりとしたコンポーネントの構成 & 実装案

ページは主にそれぞれのステータスが付与されている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にドロップできるようにする。
  • ドロップされたら、そのTaskCardstatusという値はドロップ先のTaskLaneが持つstatusの値で更新する。
  • statusの更新はFirebaseCloud FirestoreというDBにセットされた値を書き換えるかたちで更新する。
  • Cloud Firestore上で、DBの値が書き換わったことを検知して、画面を再レンダリングする。

順に説明します。

ドラッグされたTaskCardを、別のステータスを持つTaskLaneにドロップできるようにする。

まず、画面上の要素をドラッグできるようにするためには、要素にdraggableという属性を指定します。ドラッグ開始のイベントハンドラは、JSX要素のonDragに渡すことができます。

また、現在どのTaskCardがドラッグされているのかという状態を考える必要があります。TaskLaneコンポーネントの親コンポーネントで、以下の状態を定義します。ここでは、各taskに割り振られたid(number)を使って、どのidTaskCardがドラッグされているのかを考えます。そして、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参照)
(ここではstyleemotionで定義しています。)

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を初期値に戻します。

ここまでで、ドラッグ&ドロップの動きを実現できました。

ドロップされたら、そのTaskCardstatusという値はドロップ先の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の更新はFirebaseCloud 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は更新対象のtaskidであり、ドキュメントidと同じ値を指定しています。
docの部分は更新対象のtaskのドキュメントであり、doc.updateの中で、フィールドの値を更新することができます。
ここでは、updateTaskAttributeupdateAtというフィールドの値を更新します。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}
     />
   </>
  )

ここまでで、ドロップ時にドラッグされたtaskCloud 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上での値を更新し、更新完了後にタスク一覧を再取得することで、リアルタイムで画面が更新されるようになります。

おわりに

以上のように、ReactFirebaseを使ってライブラリいらずで手軽にドラッグ&ドロップを実装することができました。
このサンプルコードの詳細部分はこのレポジトリにあるので、よろしければ参考にしてみてください。(スターもください🙏)

また、Twitter上で技術に関するツイートもしてますので、ご興味があればフォローしてみてください><
Twitter: @kei_ffff

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.tsx
import 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.tsx
import 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;

みたいに書けたら便利ですよね。これを実現する方法はまた今度書きたいと思います。

お読みいただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む