20200527のReactに関する記事は8件です。

Fluent UI を React で Assets License を回避しつつ使用する

はじめに

今回は Microsoft が出している「Fluent UI」をReactで使ってみる、という記事です。

https://github.com/microsoft/fluentui

もともとFluent UIは office-ui-fabric という名前だったそうで、
Qiitaで検索してみると2015年あたりの記事がちらほら出てきますね。

デモサイトもとってもリッチでワクワクしますね。
https://developer.microsoft.com/en-us/fluentui/#/controls/web

しかし、Microsoftのブランドなども関係することから、ライセンスがややこしいです。

ライセンスについて

Fluent UI React 自体は MIT ライセンス と、とても使いやすいです。

が、但し書きがあります。以下、2020/05/27時点での内容

All files on the Fluent UI React GitHub repository are subject to the MIT license. Please read the License file at the root of the project.
Usage of the fonts and icons referenced in Fluent UI React React is subject to the terms of the assets license agreement.
(https://github.com/microsoft/fluentui#licenses より引用。)

はて、 assets license agreement ?となり見てみると…

“Fabric Assets” means
a. Segoe font
b. Microsoft Office icons
c. Microsoft Fabric icons

上記 Fabric Assets は Sugoeフォント/Microsoft Office icons/Microsoft Fabric icons からなり、
それらの使用は

a. In connection with the use of a Microsoft API within the development of a software
application, website, or product you create or a service you offer designed to provide access
or interact with a Microsoft service or application (“Application”)
b. To illustrate that Application integrates with one or more Microsoft products and services.

の場合にのみ許可されるとあります。
すなわち、Microsoft製品のAPIを叩いたりする場合のみに限られそうです。

By downloading the Fabric Assets (defined below) from the Content Delivery Network, you represent and
warrant to Microsoft that you have the authority to accept this Agreement on behalf of yourself, a company,
and/or other entity, as applicable.

とあるように、CDNからダウンロードするか否かが問題になりそうです。

個人の「感想」です。実際の使用にあたっては知財部門など専門家の意見を仰ぐべきです。
本記事に従った結果発生するいかなる問題に対しても責任を負いかねます

回避方法

Segoe fontの使用を回避する方法と、Fabric Iconの使用を回避する方法を記載します。

Segoe font

回避方法についてはGitHub Issueでも議論されています。

https://github.com/microsoft/fluentui/issues/7416

「非MITライセンスなものをオプトアウトするオプションがほしい」という質問に対し、
デフォルトフォントを変更する方法が提案されています。

import { loadTheme, createTheme } from 'office-ui-fabric-react';

loadTheme(createTheme({
  defaultFontStyle: { fontFamily: 'Comic Sans MS', fontWeight: 'bold' },
  fonts: {
    medium: { fontSize: 50 }
  }
}));

今のバージョンでは office-ui-fabric-react でなく、 @fluentui/react ですね。

Fabric Icon

iconの利用回避についてはGitHubのwikiに書いてあります。

https://github.com/microsoft/fluentui/wiki/Using-icons

import { registerIcons } from '@uifabric/styling';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faFilter } from '@fortawesome/free-solid-svg-icons';

registerIcons({
  icons: {
    Filter: <FontAwesomeIcon icon={faFilter} />
  }
});

これについてはドキュメントが古く、
@uifabric/styling となっている部分は @fluentui/react とすることで実現できます。

使用するときは

<Button iconProps={{ iconName: "Filter" }}/>

のように、registerIconsのキーを iconProps.iconName に指定します。

やってみた

上記の通り実施してみました。ドキュメントにあるColorPickerBasicExampleを表示してみています。

image.png

いいぞ。

ログからもわかるように、文字やアイコンを表示しているにも関わらず、
MicrosoftのCDNからはコンテンツをダウンロードしている様子はありません。

普段使用しているWindows 10環境では、Segoeフォントは入ってしまっているので、
今回はDockerコンテナ(Debianベース)にGoogle Chromeを導入して見てみました。

おまけ

こっそりREADMEが experiences for Microsoft 365 から web experiences に変更になってますね。

https://github.com/microsoft/fluentui/commit/394367a5aec7b7efb0bbe68a74e5dc54241b7e69

image.png

終わりに

いざ記事を書いてみると、Fluent UIユーザーが少ないようでびっくりしました。
最近のMicrosoftっぽいUIって好きなんですが、あまり人気がないんでしょうか。

非MITライセンスのオプトアウトについては、このIssueの行方も見守りたいところです。
https://github.com/microsoft/fluentui/issues/12488

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

StateとPropsの違い(React)

ReactのStateとPropsの違いがよくわからなかったのでメモ

State

:point_up_tone1:変更可能
:point_up_tone1:そのコンポーネント自体が値を持っている
:point_up_tone1:setStateで更新することができる

*Propsだけでできなかった時に使うようにする(なるべく使わない)

Props

:star:外部からもらう値
:star:Readオンリー(変更不可)→通信が早い

「mapStateToProps」と「mapDispatchToProps」

:airplane:mapStateToProps
・stateの値をcompornentのpropsに入れる

:airplane:mapDispatchToProps
・グローバルステート:React-reduxのstate
・React コンポーネントのツリーの一番上流(Buttonとか)

:helmet_with_cross::helmet_with_cross::helmet_with_cross:connect:helmet_with_cross::helmet_with_cross::helmet_with_cross:
connectを使うことで、親→子ではなく親→孫、ひ孫...にも渡すことができるようになる

const mapStateToProps = ({ app, taskSubscription }) => ({
  isLoading: app.isLoading,
  taskSubscription: taskSubscription.selected
})

const mapDispatchToProps = dispatch => ({
 //ページタイトルはこのサービス内全てで共通
  setPageTitle: pageTitle => dispatch(setPageTitle(pageTitle)),
})

export default connect(  //ここで渡す
  mapStateToProps,
  mapDispatchToProps)

:writing_hand_tone1:(メモ)exportとは?
定義したものを外部からインポートできるようにする
defaultありとなしではimportの仕方が異なる。
:exclamation:defaultは1つのファイルに一つしか定義できない

export default AAA
import AAA from './sample.js'

export BBB
import { BBB } from './sample.ls'
*{}をつける

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

Androidの時にテキストが真ん中寄せにならない

どんな問題か

こんな感じで縦方向にテキストを中央配置しようとしたが、
IOSではうまくいくものおの、Androidは

<View style={{flexDirection: 'row', justifyContent: 'space-between', alignContent: 'center', alignItems: 'center', height: 40}}>
  <Text style={{fontSize: 14, fontFamily: 'NotoSansJP-Regular'}}>テキスト</Text>
</View>

IOSではうまくいっているが、Androidではうまくいかない
微妙に上に寄ってしまう

解決策

https://react-mongolia.github.io/react-native/docs/0.40/text-style-props#includefontpadding
公式を見に行ったら解決策が書いてあった

Set to false to remove extra font padding intended to make space for certain ascenders / descenders. With some fonts, this padding can make text look slightly misaligned when centered vertically. For best results also set textAlignVertical to center. Default is true.

どうやらAndroidだとfontによってはpaddingがあたってしまうみたい
-> includeFontPadding: falseに設定することでpaddingを取り除く

<View style={{flexDirection: 'row', justifyContent: 'space-between', alignContent: 'center', alignItems: 'center', height: 40}}>
  <Text style={{fontSize: 14, fontFamily: 'NotoSansJP-Regular', includeFontPadding: false}}>テキスト</Text>
</View>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

日本一わかりやすいReact入門【実践編】#1~5 学習備忘録

概要

この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact入門【実践編】』の自分用学習備忘録です。

本動画シリーズが、react初心者である私にとって非常に分かりやすいものでしたので、少しでも世の中に広めたいという想いを込めて公開します!

学習初期での筆者習熟レベル

#1...Reactでチャットボットを作ろう

開発アプリ概要

  • チャットボットアプリを開発
  • 予めセンテンス用のデータセットは用意しておき、それらを決められたルールで返信をする

使用技術

  • React
    • Class
    • Functional
    • create-react-app
    • Hooks
  • Firebase
    • Hosting
    • Firebase
    • Clound Functions
  • Material-UI
  • Slack(Incoming Webhook)

Material-UI, Slack Webhookについては完全初見です。

#2...サクッと環境構築しよう

create-react-appで環境を構築。今回のアプリ名は『chatbot-demo』。

terminal
$ npx create-react-app chatbot-demo

アプリのディレクトリに移動して、npmサーバーを起動。

terminal
$ npm start

localhost:3000へブラウザでアクセスし、いつもの画面が出てくれば、無事react環境で出来上がっています!

image.png

続いて、Material-UIをnpmへインストール。

terminal
$ npm install --save @material-ui/core @material-ui/icons @material-ui/system

今回は、Material-UIのRoboto FontFont Iconsを使用したいので、headタグ内に追記。

public/index.html
<head>
...
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
</head>

これで環境構築は完了!

#3...Firebaseプロジェクトの作成と初めてのデプロイ

Firebaseとは?

  • Googleが提供するmBaaS
  • DB管理からアプリのデプロイまで、バックエンド側の諸々をお任せできる、兎に角なんかすごいサービス
  • Firebaseを利用することで、エンジニア自身はフロントエンド側のコーディングに集中できる

Firebase -ブラウザ上の設定-

アプリを作成 -> コンソールに移動。
image.png

Settings -> リロケーションリソースをasia-northeast1(要するに日本のこと)に設定。
image.png

Webアプリとして使用するよう設定。
image.png

Database -> Cloud Firestoreでデータベースを作成(データベースは本番環境用の方に設定したが、セキュリティールールは別途設定するので、テスト環境用にしても恐らく関係ない)
image.png

Firebase -ターミナル上での設定-

ローカル全体にfirebase-toolsをインストール。

terminal
$ npm install -g firebase-tools

今回作成したアプリディレクトリ内に、firebaseをインストール。これによりpackage.json内に"firebase"が追加される。

terminal
$ npm install --save firebase            

firebaseにログインする。

terminal
$ firebase login

create-react-appで作成したローカル上のアプリと、ブラウザから作成したfirebase上のアプリを接続する。

terminal
$ firebase init
  • Firebase CLI featuresからは、Firestore、Functions、Hostingを選択。
  • Cloud Functionsの言語はTypescriptを選択。
  • ? What do you want to use as your public directory?はデフォルトではpublicになっているが、buildに変更する。なぜなら、create-react-appにおいては、publicは雛形用のディレクトリであり、本番用ディレクトリはbuildと決められているから。
  • ? Configure as a single-page app (rewrite all urls to /index.html)?はYesを選択。今回はSPAとして開発するため。
  • 上記以外はデフォルトでOK

Firebaseセキュリティルールの設定

現時点では、誰でもDBへの書き込みができる危険な状態になっているので、認証済みユーザーのみが書き込みできるよう設定。

firestore.rules.json
...
allow read;
allow write: if request.auth.uid != null;
...

Firebaseへデプロイ

アプリの本番環境用ディレクトリを作成。

terminal
$ npm run build

コンパイルエラーを避けるため、下記をコメントアウト

functions/src/index.ts
// import * as functions from 'firebase-functions';
...

Firebaseへデプロイする。

terminal
$ firebase deploy

デプロイが完了したら、Hosting URL:のところに表示されているURLを開き、確認してみる。
image.png
create-react-appの初期画面で出ていればOK!! Firebaseは本当にデプロイが簡単ですねー

#4...stateの設計とクラスコンポーネントの作成

stateの設計

  • ルートで管理するstate(React側)とデータモデル(Firebase側)が1対1で対応するよう、設計する。
  • データモデルの設計はViewから始めるのが効率的。所謂「ワイヤーフレーム」から作り始める。

動画内ではstate設計の答えが最初から示されていました。ただ、なぜその答えに辿り着いたかを自分なりに納得したかったので、今回は自力でも考えてみました。

1頁目 2頁目
IMG_4714.JPG IMG_4715.JPG

結果、state設計をまとめると以下の通り。

answers: [{key: value}, {key, value}...]

answer: {
    content: string, //回答内容
    nextId: string, //次のcurentId
}

chats: [{key: value}, {key, value}...]

chat: {
    text: string, //チャット本文
    type: string //質問か回答か
}

currentId: string

dataset: {
    "currentId_1": {
        answers: [
            {content:"content_x", nextId:"currentId_x"}
             .
             .
             .
            {content:"content_y", nextId:"currentId_y"}
        ],
        question: "question content_x",
    }
    "currentId_2": {
...
}

open: boolean(true or false)

最後のopenはお問い合わせフォームのモーダル開閉に使用。

stateの定義

stateの設計をApp.jsxに定義する。create-react-appでは、App.jsはFunctional Componentで定義されているが、stateを扱いたいため、Class Componeneに書き換える(拡張子もjsからjsxに変更する)

src/App.jsx
import React from 'react';
import './App.css';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      answers: [],
      chats: [],
      currentId: "init",
      dataset: {},
      open: false
    }
  }
  render(){
    return (
      <div>
      </div>
    );
  }
}

datasetはgithubからコピーして、src/dataset.jsとして保存。

src/dataset.js
const defaultDataset = {
  "init": {
      answers: [
          {content: "仕事を依頼したい", nextId: "job_offer"},
          {content: "エンジニアのキャリアについて相談したい", nextId: "consultant"},
          {content: "学習コミュニティについて知りたい", nextId: "community"},
          {content: "お付き合いしたい", nextId: "dating"},
      ],
      question: "こんにちは!?トラハックへのご用件はなんでしょうか?",
  },
  "job_offer": {
      answers: [
          {content: "Webサイトを制作してほしい", nextId: "website"},
          {content: "Webアプリを開発してほしい", nextId: "webapp"},
          {content: "自動化ツールを作ってほしい", nextId: "automation_tool"},
          {content: "その他", nextId: "other_jobs"}
      ],
      question: "どのようなお仕事でしょうか?",
  },
  "website": {
      answers: [
          {content: "問い合わせる", nextId: "contact"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "Webサイト細作についてですね。コチラからお問い合わせできます。",
  },
  "webapp": {
      answers: [
          {content: "問い合わせる", nextId: "contact"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "Webアプリ開発についてですね。コチラからお問い合わせできます。",
  },
  "automation_tool": {
      answers: [
          {content: "問い合わせる", nextId: "contact"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "自動化ツール開発についてですね。コチラからお問い合わせできます。",
  },
  "other_jobs": {
      answers: [
          {content: "問い合わせる", nextId: "contact"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "その他についてですね。コチラからお問い合わせできます。",
  },
  "consultant": {
      answers: [
          {content: "YouTubeで動画を見る", nextId: "https://www.youtube.com/channel/UC-bOAxx-YOsviSmqh8COR0w"},
          {content: "学習コミュニティについて知りたい", nextId: "community"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "トラハックは普段からYouTubeでキャリアについて発信しています。また、僕が運営するエンジニア向け学習コミュニティ内でも相談に乗っていますよ。",
  },
  "community": {
      answers: [
          {content: "どんな活動をしているの?", nextId: "community_activity"},
          {content: "コミュニティに参加したい", nextId: "https://torahack.web.app/community/"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "2020年3月から学習コミュニティを始めました!?Webエンジニアへの転職を目指す人向けに、プログラミングを教えたりキャリアの相談に乗っています。",
  },
  "community_activity": {
      answers: [
          {content: "さらに詳細を知りたい", nextId: "https://youtu.be/tIzE7hUDbBM"},
          {content: "コミュニティに参加したい", nextId: "https://torahack.web.app/community/"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "フロントエンド向けの教材の提供、キャリアや勉強法に関するメルマガの配信、週1のオンライン作業会などを開催しています!\n詳細はYouTube動画で紹介しています。",
  },
  "dating": {
      answers: [
          {content: "DMする", nextId: "https://twitter.com/torahack_"},
          {content: "最初の質問に戻る", nextId: "init"}
      ],
      question: "まずは一緒にランチでもいかがですか?DMしてください?",
  },
}

export default defaultDataset

defaultDatasetという定数を定義し、export defaultでファイル外部から利用できるようしている。これをApp.jsにimportする。

src/App.js
import React from 'react';
import './App.css';
import defaultDataset from "./dataset"

...

css周りを設定

  • githubよりassets/styles/style.cssをコピー
  • App.cssを削除。App.jsxのcss importを上記ファイルに修正
  • index.cssをassets/stylesへ移動。index.jsのimport文を修正

css設定を終えたら、早速App.jsx内でjsxタグを書いてみる

src/App.jsx
import React from 'react';
import './assets/styles/style.css';
import defaultDataset from "./dataset"

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      answers: [],
      chats: [],
      currentId: "init",
      dataset: defaultDataset,
      open: false
    }
  }
  render() {
    return (
      <section className="c-section">
        <div className="c-box">
        </div>
      </section>
    );
  }
}

localhost:3000へ行くと、空の四角い箱が画面に表示されているはずです。

image.png

#5...繰り返し再利用できる関数コンポーネントを作ろう

画面下部に表示させる回答部分をコンポーネントで作る。コンポーネントの構造としては、

  • AnswersList
    • Answer
    • Answer
    • Answer
    • ...

のようなイメージ。選択肢ひとつひとつがAnswerコンポーネントであり、それらを束ねる親コンポーネントとしてAnswersListコンポーネントを定義する。

作成・修正するファイルは、
- src/App.jsx
- components/AnswersList.jsx
- components/Answer.jsx
- components/index.js

components/index.jsはいわゆるエントリーポイントの役割を担う。全てのコンポーネントを読み込む場所であり、コンポーネントをimportしたい場合は(コンポーネントの種類に依らず)参照先をここにしておけばいい

完成形はこちら。

src/App.jsx
import React from 'react';
import './assets/styles/style.css';
import defaultDataset from "./dataset"
import {AnswersList} from "./components/index"

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      answers: [],
      chats: [],
      currentId: "init",
      dataset: defaultDataset,
      open: false
    }
  }

  initAnswer = () => {
    const initDataset = this.state.dataset[this.state.currentId];
    const initAnswers = initDataset.answers;

    this.setState({
      answers: initAnswers
    })
  }

  componentDidMount() {
    this.initAnswer()
  }

  render() {
    return (
      <section className="c-section">
        <div className="c-box">
          <AnswersList answers={this.state.answers} />
        </div>
      </section>
    );
  }
}

  • AnswersListコンポーネントをimportしている。今回はコンポーネントファイルを直接読みに行かず、エントリーポイントであるcomponents/index.jsを経由している
  • AnswersListにdatasetの値が渡されるまでの時系列を整理すると、
    • App.jsxのMountingが開始。まず最初にconstructor()が走り、各stateに初期値が与えられる
    • constructor()の次はrender()が走り、JSX構造が作られる。この時点では、this.state.answersの中身はconstructor()で定義されたanswers: []となっている
    • render()の次はcomponentDidMount()が走る。initAnswer()関数が実行されることで、answersの中にdatasetの値が代入される
  • ライフサイクルのうち、「最初に1回だけ実行したい」という処理は、componentDidMount()に書く
src/components/AnswersList.jsx
import React from 'react'
import {Answer} from './index'

const AnswersList = (props) => {
  return (
    <div className="c-grid__answer">
      {props.answers.map((value, index) => {
        return <Answer content={value.content} key={index.toString()} />
      })}
    </div>
  )
}

export default AnswersList
  • App.jsxより受けとってprops.answersをmap関数を用いて繰り返し処理
components/Answers.jsx
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const useStyles = makeStyles((theme) => ({
  root: {
  },
}));

const Answer = (props) => {
  return (
    <Button variant="contained" color="primary">
      {props.content}
    </Button>
  )
}

export default Answer
components/index.js
export {default as AnswersList} from './AnswersList'
export {default as Answer} from './Answer'
  • 全てのコンポーネントを読み込ませる。

ここまでで、dataset内のテキストが含まれたボタンが表示されるようになる。

image.png

記事が長くなったのでいったんここまで!

続きは『日本一わかりやすいReact入門【実践編】#6~9 学習備忘録』です。

参考URL

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

apolloよりシンプルなgraph client, urqlの紹介

データフェッチをgraphqlにする場合、clientライブラリとしてapllo-graphqlを選ぶことは多いと思います。しかし、ややtoo muchな印象があります。そこで、よりシンプルでかつカスタマイズも柔軟にできる、urqlを紹介します。

urqlとは

urqlは、JS及びReactのコンサル企業であるFormidableによって開発された、新しいgrqphql clientです。apolloをご存知の方は、react-apollo相当と思ってもらえればよいです。メインメンテナはstyled-componentsのコアチームでもあるPhil Plückthun。

  • apolloより軽量なgraphql client
  • apolloよりシンプルなキャッシュ戦略
  • reduxやexpressのmiddlewareに似た"Exchange"による拡張性

https://formidable.com/open-source/urql/docs/
https://github.com/FormidableLabs/urql (何故か公式DocよりReadmeのほうが断然分かりやすい)

streamを扱うライブラリであるwonka、及びそのラッパーであるreact-wonkaといった技術の上に作られています。よって、中身のコードは、operation(=ネットワークリクエスト抽象)をstreamで扱うような構造になっており、そこにExchange(middleware相当)を注入することで拡張させることができるようになっています。

使い方(最小限のコード)

codesandboxで良いという人はこちら
https://codesandbox.io/s/simple-urql-example-wz6zw

セットアップ。apolloよりシンプル。

import { Provider, createClient } from 'urql'

const client = createClient({
  url: 'http://localhost:4000/graphql',
})

const YourApp = () => (
  <Provider value={client}>
    {/* ... */}
  </Provider>
)

QueryをリクエストするHooks

import React from 'react'
import { useQuery } from 'urql'

const getTodos = `
  query GetTodos($limit: Int!) {
    todos(limit: $limit) {
      id
      text
      isDone
    }
  }
`

const TodoList = ({ limit = 10 }) => {
  const [result] = useQuery({
    query: getTodos,
    variables: { limit },
  });

  if (result.fetching) return 'Loading...'
  if (result.error) return <div>{result.error.message}</div>

  return (
    <ul>
      {result.data.todos.map(({ id, text }) => (
        <li key={id}>{text}</li>
      ))}
    </ul>
  )
}

Refetching (再リクエスト)

再度リクエストは、第引数で得られるexecuteQueryでハンドルできます。

function() {
  const [res, executeQuery] = useQuery({
    query: getTodo,
    variables: { id }
  })

  return (
    <div>
      <div>{res.data.todo}</div>
      <button onClick={executeQuery}>refetch</button>
    </div>
  )
}

キャッシュ

デフォルトのキャッシュの仕組み

urqlは"document" cacheの仕組みを持っています。すべてのquery + variablesの組み合わせはキャッシュされるので、再度同じquery + variablesでリクエストが送られると、urqlはキャッシュされた結果を返します。ただし、同じ__typenameのmutationが送られたときはリソースが更新されたとみなされ、キャッシュはクリアされる仕組みになっています。

キャッシュをコントロールする - requestPolicy

urqlのQueryのキャッシュは、requestPolicyを設定することで自在に操ることができます。デフォルトの挙動は、cache-first(キャッシュがあればリクエストを送らずキャッシュデータを返す)です。

urqlのキャッシュ戦略(request policy)は4種類で、とてもシンプルです。例えばcache-and-networkは、キャッシュデータを返しながら、同時に最新をフェッチしに行く挙動をします。つまりユーザーはキャッシュがあればローディングを待たず即結果をみることができ、かつ少し遅れて最新のものを見ることができます。

useQuery({ query: q, requestPolicy: 'cache-and-network' });

4つのrequestPolicy

Policy 内容 使い所 
cache-first デフォルト。キャッシュがあれば必ずキャッシュを返す。 変更のすくないリソースへのクエリ
cache-only リクエストは投げず、常にキャッシュのみを返す SSR等、トリッキーな用途。通常は使わない
network-only キャッシュを使わず、常にリクエストを投げる 頻繁に変わるリソースへのクエリ
cache-and-network キャッシュがあればキャッシュを返し、その後リクエストを投げる 頻繁には変わらず、早く見せたいリソースへのクエリ

Exchange

urql特徴の一つが、Exchangeです。

urqlでは、リクエスト関数をoperationという単位で扱います。このoperation lifecycleは、リクエストが投げられてから返ってくるまでの「双方向」の流れをstream的に扱います。

例えば、リクエストとレスポンスを簡易的にコンソールに出力するようなExchangeはこのように書けます。

export const loggerExchange: Exchange = ({ forward }) => {
  if (!isDev) {
    return ops$ => forward(ops$)
  } else {
    return ops$ =>
      pipe(
        ops$,
        tap(op => {
          console.log(
            '[Exchange debug]: Incoming operation: ',
            op.query.definitions[0].name.value,
            `variables: ${JSON.stringify(op.variables)}`
          )
        }),
        forward,
        tap(result => {
          if (result.error) {
            console.log(
              `[Exchange debug]: Completed operation ${result.operation.query.definitions[0].name.value}`,
              result.error
            )
          } else {
            console.log(
              `[Exchange debug]: Completed operation ${result.operation.query.definitions[0].name.value}`,
              result.data
            )
          }
        })
      )
  }
}

その他の便利な機能

dev tool

Exchangesではコンソールにログを出力するサンプルを書きましたが、公式でリッチなデバッグツールも作られています。
https://formidable.com/open-source/urql/docs/advanced/debugging/

Graphcache

urqlのデフォルトのキャッシュ戦略は紹介したとおりですが、Graphcacheを使うことによりApollo ClientのようなNormalized Cacheを適用することもできます。

まとめ

今回紹介したurqlの他に、react-queryやSWRなど、ちょっとした群雄割拠状態となっているfetcherライブラリ界隈ですが、urqlはその中でもかなり特徴的な設計思想をもったライブラリです。

Graphqlの場合はApollo clientを使うケースが多いと思いますが、やや多機能すぎる嫌いがあります。特にアプリケーションがシンプルな場合はApolloの代替手段として、よりシンプルなurqlを検討してみてはいかがでしょうか。

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

apolloよりシンプルなGraphQL client, urqlの紹介

データフェッチをgraphqlにする場合、clientライブラリとしてapllo-graphqlを選ぶことは多いと思います。しかし便利な機能を多く抱え込みすぎており、特にキャッシュ戦略は複雑でtoo muchな印象があります。そこで、よりシンプルでかつカスタマイズも柔軟にできるurqlを紹介します。

urqlとは

urqlは、JS及びReactのコンサル企業であるFormidableによって開発された、新しいgrqphql clientです。apolloをご存知の方は、react-apollo相当と思ってもらえればよいです。メインメンテナはstyled-componentsのメンテナでもあるPhil Plückthun。

下記のような特徴をもっています。

  • apolloより軽量なgraphql client
  • apolloよりシンプルなキャッシュ戦略
  • reduxやexpressのmiddlewareに似た"Exchange"による拡張性

公式ドキュメント: https://formidable.com/open-source/urql/docs/

streamを扱うライブラリであるwonka、及びそのラッパーであるreact-wonkaといった技術の上に作られています。よって、中身のコードはoperation(ネットワークリクエスト抽象)をstreamで扱うような構造になっており、そこにExchange(middlewareのようなもの)を注入することで拡張させることができるようになっています。

使い方

codesandboxで良いという人はこちら(国の一覧と詳細を持つ簡単なデモアプリです)
https://codesandbox.io/s/simple-urql-example-wz6zw

セットアップ

apolloよりも簡単にセットアップできます

import { Provider, createClient } from 'urql'

const client = createClient({
  url: 'http://localhost:4000/graphql',
})

const YourApp = () => (
  <Provider value={client}>
    {/* ... */}
  </Provider>
)

useQueryによる基本的なリクエスト

useQueryを使った基本的なリクエストの例です。option引数にパラメターを渡すことで振る舞いをカスタマイズすることができるようになっています。

import React from 'react'
import { useQuery } from 'urql'

const getTodos = `
  query GetTodos($limit: Int!) {
    todos(limit: $limit) {
      id
      text
      isDone
    }
  }
`

const TodoList = ({ limit = 10 }) => {
  const [result] = useQuery({
    query: getTodos,
    variables: { limit },
  });

  if (result.fetching) return 'Loading...'
  if (result.error) return <div>{result.error.message}</div>

  return (
    <ul>
      {result.data.todos.map(({ id, text }) => (
        <li key={id}>{text}</li>
      ))}
    </ul>
  )
}

Refetching (再リクエスト)

再リクエストは、第引数で得られるexecuteQueryで実現できます。

function() {
  const [res, executeQuery] = useQuery({
    query: getTodo,
    variables: { id }
  })

  return (
    <div>
      <div>{res.data.todo}</div>
      <button onClick={executeQuery}>refetch</button>
    </div>
  )
}

条件付きリクエスト

何らかの条件が揃うまでリクエストを送りたくない場合はpauseオプションを設定します。
例えばidが別の非同期処理の結果を待たなければいけないときに使えます。

  const [result] = useQuery({
    query: getTodo,
    variables: { id },
    pause: id
  });

キャッシュ

デフォルトのキャッシュの仕組み

urqlは"document" cacheの仕組みを持っています。すべてのquery + variablesの組み合わせはハッシュ化されレスポンスとともにキャッシュされるので、再度同じquery + variablesでリクエストが送られると、urqlはキャッシュされた結果を返します。ただし、同じ__typenameのmutationが送られたときはリソースが更新されたとみなされ、キャッシュはクリアされる仕組みになっています。

キャッシュをコントロールする - requestPolicy

urqlのQueryのキャッシュは、requestPolicyを設定することで自在に操ることができます。デフォルトの挙動は、cache-first(キャッシュがあればリクエストを送らずキャッシュデータを返す)です。

urqlのキャッシュ戦略(request policy)は4種類で、とてもシンプルです。例えばcache-and-networkは、キャッシュデータを返しながら、同時に最新をフェッチしに行く挙動をします。つまりユーザーはキャッシュがあればローディングを待たず即結果をみることができ、かつ少し遅れて最新のものを見ることができます。

useQuery({ query: q, requestPolicy: 'cache-and-network' });

4つのrequestPolicy

Policy 内容 使い所 
cache-first デフォルト。キャッシュがあれば必ずキャッシュを返す。 変更のすくないリソースへのクエリ
cache-only リクエストは投げず、常にキャッシュのみを返す SSR等、トリッキーな用途。通常は使わない
network-only キャッシュを使わず、常にリクエストを投げる 頻繁に変わるリソースへのクエリ
cache-and-network キャッシュがあればキャッシュを返し、その後リクエストを投げる 頻繁には変わらず、早く見せたいリソースへのクエリ

Exchange

urql特徴の一つが、Exchangeです。

urqlでは、リクエスト関数をoperationという単位で扱います。このoperation lifecycleは、リクエストが投げられてから返ってくるまでの「双方向」の流れをstream的に扱います。

例えば、リクエストとレスポンスを簡易的にコンソールに出力するようなExchangeはこのように書けます。

export const loggerExchange: Exchange = ({ forward }) => {
  if (!isDev) {
    return ops$ => forward(ops$)
  } else {
    return ops$ =>
      pipe(
        ops$,
        tap(op => {
          console.log(
            '[Exchange debug]: Incoming operation: ',
            op.query.definitions[0].name.value,
            `variables: ${JSON.stringify(op.variables)}`
          )
        }),
        forward,
        tap(result => {
          if (result.error) {
            console.log(
              `[Exchange debug]: Completed operation ${result.operation.query.definitions[0].name.value}`,
              result.error
            )
          } else {
            console.log(
              `[Exchange debug]: Completed operation ${result.operation.query.definitions[0].name.value}`,
              result.data
            )
          }
        })
      )
  }
}

その他の便利な機能

dev tool

Exchangesではコンソールにログを出力するサンプルを書きましたが、公式でリッチなデバッグツールも作られています。
https://formidable.com/open-source/urql/docs/advanced/debugging/

Graphcache

urqlのデフォルトのキャッシュ戦略は紹介したとおりですが、Graphcacheを使うことによりApollo ClientのようなNormalized Cacheを適用することもできます。

まとめ

今回紹介したurqlの他に、react-queryやSWRなど、ちょっとした群雄割拠状態となっているfetcherライブラリ界隈ですが、urqlはその中でもかなり特徴的な設計思想をもったライブラリです。

Graphqlの場合はApollo clientを使うケースが多いと思いますが、やや多機能すぎる嫌いがあります。特にアプリケーションがシンプルな場合はApolloの代替手段として、よりシンプルなurqlを検討してみてはいかがでしょうか。

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

ReactでHTMLヘッダを変更する

概要

react-helmetを利用する。
どこからでもHTMLヘッダを変更できる。

使い方

導入

yarn add react-helmet @types/react-helmet

タイトルの変更

import React from 'react';
import { Helmet } from 'react-helmet';

const title = 'Help';

const Help = () => (
  <>
    <Helmet>
      <title>{title}</title>
    </Helmet>

    <h1>{title}</h1>
    <p>
      This is Help page ... 
    </p>
  </>
);

export default Help;

まとめ

簡単に変更できる。
SPAとかで、ページによってタイトルを変更したいときに便利。
他にも変更できるので、以下を確認する。
https://github.com/nfl/react-helmet

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

SPA(Single Page Application) を採用するメリット再考(2020年)

1. 2010~2020年にかけて、WebシステムはSPAで作るのが主流

 近年、WebシステムはJavascript(Typescript)を用いたSPA(Single Page Application)を作ることが主流になっている。
 とはいえ、本当に、一つのHTMLファイルだけで、全画面をJavascriptで描画するSPAがどれだけあるかは分からないが、少なくとも、SPAで"作ろう"とはしている。
 (2010年はSPAの黎明期で、Backbone.jsやknockout.jsが生まれた年)

(1) SPAの構成要素

 "Single Page"に着眼するならば、SPAは、以下を特徴とする。
 ページの基本となるHTMLファイルを1回のリクエストで取得し、その後の画面描画に関しては、大半を初回に取得したブラウザ上のリソースで行う。そして、適宜、必要なデータのみAPIへの問合せで取得する。

 そして、SPAを特徴づけるのは、描画のパーツを作成するComponent、そして、画面遷移を実現するRouterの存在であると思う。そして、状態の管理をするState Manager(VuexやRedux)によりアクションと描画が連結される。

a. Component:視覚要素の部品

 ReactやVueを使って作られる、視覚要素の部品、すなわち、Componentは、SPAにあっては画面の描画を行う基本となる。特に、データバインディングによりHTMLのタグとJavascriptのデータを双方向で対応付け、動的に互いの状態変化を伝達する仕組みをもつのが特徴的だと思う。
 そして、これらの要素を細かく管理し、適宜、必要な部品を描画することでページ全体をサーバから取得する必要がなくなる。
 SPAの心臓部分といっても過言ではないと思う。
 

b. Router:画面遷移の管理

 Routerは、画面というグループでComponentの配置換えを指示するコントローラー。そして、React RouterやVue Routerのように、URLの指定によって、画面を描画することで、既存のWebページと同じ画面遷移方法やUXを提供している。

c. State Manager(状態管理)

最小構成を考えた時、これがないとSPAが本当に成立しないかは別にして、実際にアプリケーションを作る上では、画面上のアクションやイベントをうけて、値や状態の変化をComponentの描画につなげていくときに、VuexやReduxのような状態管理モジュールが利用される。
 Componentは、タグとJavascriptの値の双方向バインディングを特徴とするが、データの流れを規制することで、意図しないデータ変更を防ぎ、バグの防止、バグの早期発見に役立つ。
 大量のComponentや状態を扱ううえでは、必要な構成要素であると思う。

2. jQuery不要論の背景

 SPAの登場と共に、jQuery不要論も散見されるようになった。
 ただ、それはSPAを作ろうとする上での話で、SPAをしないのであれば、jQueryは依然として便利なライブラリである。
 jQueryの特徴としては以下が挙げられる。

(1) jQueryの特徴、メリット

  • Javascriptのシンタックスシュガー、すなわち、既存の構文などを別の構文や記法で記述できるようにしたもの。
  • ツールプリセットであり、事前に様々な処理を関数として定義し手軽に利用できるようにしたもの。
  • 既存のHTML構造を外部から、HTMLを壊すことなく操作したり、イベントを設定できる。

(2) jQueryはSPA専用のライブラリではない

 jQueryはSPAを作るために作られたものではないので、SPAを作ろうとすると不要と言われるのは当然である。
 もちろん、jQueryは先に上げたように、Javascriptを便利に扱うためのAltJS的な存在でもあるので、それをつかってSPAを作れなくもない。

 ただ、それよりはすでにSPAの構成要素がすでに作られている、ReactやVueをつかって、Componentをつくったした方がSPAを作るには便利なのは確かである。

 jQueryは、先に上げた、SPAの構成要素(Component、Router、State Manager)を標準でもっているわけでもなく、自作する必要がある。そういった点で、SPAをするなら、jQueryを使うよりは、Vue、Reactを使うほうが遥かに工数削減ができてメリットがある。

(3) SPAをしないのであればjQueryを利用するメリットはあるのでは?

 つまり、jQuery不要論は、SPAを作るにあたっての話で、そうでないならば、jQueryは依然として有用なライブラリであると思われる。
 問題は、WebシステムをSPAにしているか否か、また、データバインディングしたコンポーネントを使うか否かではないだろうか。例えば、単純なDOM操作やイベントを少し設定するぐらいならjQueryでも十分だと思う。

 もちろん、SPAの中に、jQueryを使うのも問題はないが、ライブラリの数を抑えるためには不要と思われるものは除外しておくほうがよいのかもしれない。ただ、それは開発者の「好み」に依存するようにも思える。

3. SPAを利用するメリット

 SPAは、前述の構成要素を持つが、そのメリットとしては、以下が挙げられる。

  • ページ表示に関する通信回数やデータサイズを削減する。
  • 上記を通じて、サーバとの通信という描画上のオーバーヘッドを小さくし描画速度を上げる。

4. メリットを台無しにするSPAライブラリの利用方法(CSR:Client Side Renderingの速度低下)

 SPAを採用しているが、初回アクセスした際に、真っ白になったり、ローディング画面の表示が長いページがある。
 こういう場合、SPAを利用するメリットを享受していない。
 
 その背景としては、ページ内の構成要素をすべてComponentで、クライアントサイドで描画(CSR:Client Side Rendering)させすぎて、実行時間がかかっているケースもある。

5. Pre-Rendering(デプロイ前描画)、SSR(Server Side Rendring)というSPA以前の技術の採用

 CSRの速度改善策として、Nuxt.jsやNext.jsなどを利用したSSR(Server Side Rendering)などがある。要は、ページの要素であるComponentの描画をサーバサイドでしておこうというもの。
 
 また、サーバへデプロイする前にHTML部分を作成するPre-Renderingもあり、これはデータベースなどアプリケーション稼働時に動的なデータ結合を行わずにできる部分において行われる。

 たとえば、ヘッダやフッタなど構成上共通化した部分を各ページごとに結合することなどが代表的だろう。eleventy、Gatsbyなど静的サイトジェネレーターがこうした部分に利用されることがある。

 ここでSPA登場以前に、RailsやLaravelなどのフルスタックWebフレームワークを利用している人からすると、ビューのテンプレートエンジン使っているのと同じではという指摘があると思う。

 強いて、最近のPre-RenderingとSSRの特徴を挙げると、Javascriptベースで作られたComponentを描画するところにある。フロントの要素であるJavascriptで作成されたものを利用しているのが異なっている。

 サーバサイドはJSONを返すAPIに限定し、ビューやフロント側のリソースをサーバサイドと分けて管理するという観点では、JavascriptベースのPre-RenderingやSSRは良いのかもしれない。 

 尤も、RailsやLaravelがもつビューのテンプレートエンジンでSSR部分をしてしまえば、システム構成が少しシンプルになるのではという指摘もあるので、SSRの実現方法については、ソフトウェア実行上の問題よりは、開発方法という人間側の問題になるのではないだろうか。

 ビューの作成やレンダリングの過程について、以下に改めて整理しておく。

(1) ビュー作成、レンダリングの過程

a. Pre-Rendring:デプロイ前、コーディング時にHTML作成

 これはヘッダやフッタなど共通部分を分けて作成して、静的サイトジェネレーターなどでデプロイする前にHTMLコードやComponentを作成するもの。HTMLに関して言えばgo製のHugo、Javascript製のものだとeleventy(11ty)、Gatsbyなどのツールがある。これは実行時のデータベースの値などに依存しない箇所には適用できる。

b. SSR(Server Side Rendring):DBの値とビューをサーバサイドで結合する。

 アプリケーションが稼働後、ユーザーの操作に関係しない、DBなどサーバサイドで取得できる値とビューを結合する際には、SSRによってビューを作成する。
 ここは、Javascriptのコンポーネントから作成するだけでなく、RailsやLaravelなどWebフレームワークがもつテンプレートエンジンによっても、ビューを作成することができる。
 

c. CSR(Client Side Renderig):ユーザーの操作に応じて描画

 ブラウザ上で描画する。多くの場合、ユーザーの操作に応じて描画をする。ここはJavascriptによる描画が必要となってくる。SPAとして尤も特徴を表す箇所。

 

6. そもそもSPAにする意味、その適用範囲

 Pre-RenderingやSSRを見ていると、Javascriptによる動的なCSRは局所的なものとなり、SPAの構成要素の扱いも変わってっくる。
 フロントのソースコードは同じだとしても、SSRの仕組みを入れたりすると開発や実行環境での構成要素が変わってくる。
 端的に言えば、それだけ手間が発生する。実際には、コーディング上でも完全なCSRとは書き方が変わる。

 SPAが採用されだした2010年辺りは、2007年にiPhoneの初代が発売され、携帯電話でのWebブラウジングが本格的に普及してきた。当時は、端末自体の性能も低く、通信回線も3G回線(数Mbps~14Mbps)で低速であった。このような状況のなか、SPAにより通信回数を削減し、フロントサイドの描画速度の向上は意味があったかもしれない。

 2020年現在、通信回線は4G回線(75Mbps~100Mbps)になり、SPAが登場した当時より回線速度が5倍強になっている。もちろん、端末の性能も上がっている。
 
 こうしたなか、逆に、Javascriptを多用しない極力静的なサイトに近いほうが表示が速いケースもある。(画像のサイズなどが大きくないなどの条件付き)。

 もちろん、SPAだけが速度低下を招くわけではなく、画像のサイズ、広告など別サイトのAPIへの通信なども関係する。

 いずれにしろ、SPAが採用されだした当時とは、環境も変わっているし、行き過ぎたSPAライブラリの利用は本来のSPAのメリットを台無しにしかねないのではないだろうか。

 SPAの構成要素をみて、CSRにおいて、動的にJavascriptで実現しないといけない部分はどこなのかを改めて検討する必要があるのではないだろうか。

7. 局所的なComponentの利用(Router、State Managerの役割低減)

SPA登場以前は、ページ描画は頻繁にサーバとの通信が発生しており、それよりは動的に画面を描画したり、必要な箇所だけサーバと通信するSPAの方がユーザー操作の観点ではよいと思う。

 けれど、行き過ぎたSPA化、フルコンポーネント化はどうなのだろうか?

 必要な箇所だけ、Componentを利用し、Javascriptでの管理範囲を下げることで、State Managerの役割も限定可されてくる。

 複数の画面をまたいSPA化にすればするほど、状態管理も複雑になり、コーディングのストレスが発生するのはVuexやReduxを使った事がある人は感じるのではないだろうか。

 SSRなどをみていると、そもそもサイトやページ全体に亘って、JavascriptによるComponentで作成する必要があるのだろうかという疑問があり、実際、そうではないものが多い。

 であれば、局所的に、Componentを利用し、できるだけRouterは排除し、状態管理の役割も居所化シていくのがよいのではないだろうか。

 SPAにおけるComponentという構成要素は、データバインディングを念頭においた時、動的な画面描画にはなくてはならないものであるかもしれない。
 
 その良さは利用しつつも、行き過ぎたSPA化を抑制し、シンプルな構成にして、結果的、通信も描画もシンプルになり、開発者もユーザーのストレスも減るのではないだろうか。

 

技術選定は、時代ごとの環境、構成要素、工程など細分化して評価する

 Webシステムを作るなら、Javascript(Typescript)使って、SPAをするのが流行っているそれらの技術を使おうという安易な発想よりは、それが流行った理由、どの工程において威力を発揮するかを細かく評価する必要があると思う。

 勿論、分からなければ、とりあえず流行っているものにするというのはありだと思う。けれど、ある程度その道で製造を行えばいろいろなメリットとデメリットが分かる。
 その際には、より細かく物事を整理して評価することが必要ではないだろうか。特に、なんらかの問題がおきているのならば。

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