20201215のReactに関する記事は22件です。

Tailwind + FontAwesome + Nextでアイコン付きメニューを作る

完成品

Screen Shot 2020-12-15 at 23.17.21.png

環境

_app.jsを含め、今回の環境は以下のサンプルをベースに構築しています。
https://github.com/vercel/next.js/tree/canary/examples/cms-contentful

Font awesome以外の前提として、tailwindcssのバージョン2.0.1を使っています。一応パッケージ一覧を載せておきます。

package.json
package.json
  "dependencies": {
    "@contentful/rich-text-react-renderer": "^14.1.2",
    "@contentful/rich-text-types": "^14.1.2",
    "@fortawesome/fontawesome-svg-core": "^1.2.32",
    "@fortawesome/free-brands-svg-icons": "^5.15.1",
    "@fortawesome/free-solid-svg-icons": "^5.15.1",
    "@fortawesome/react-fontawesome": "^0.1.13",
    "autoprefixer": "^10.0.4",
    "classnames": "^2.2.6",
    "contentful": "^8.0.3",
    "date-fns": "^2.16.1",
    "next": "10.0.3",
    "postcss": "^8.1.14",
    "react": "17.0.1",
    "react-dom": "17.0.1",
    "tailwindcss": "^2.0.1"
  },
  "devDependencies": {
    "@fullhuman/postcss-purgecss": "^3.0.0"
  }


絶対パス

なお、以下のコンフィグで絶対パスを指定しています。

jsconfig.json
{
    "compilerOptions": {
      "baseUrl": ".",
      "paths": {
        "@/components/*": ["components/*"],
        "@/libs/*": ["libs/*"]
      }
    }
  }

Step1. react-fontawesomeとアイコン(fab/fas)のインストール

以下のチュートリアルが非常に親切ですので、参照してください。
https://fontawesome.com/how-to-use/on-the-web/using-with/react

この記事では以下が最低限必要です。

  • @fortawesome/react-fontawesome
  • @fortawesome/fontawesome-svg-core
  • @fortawesome/free-brands-svg-icons
  • @fortawesome/free-solid-svg-icons

なおFont Awesomeのアカウントは不要です

Step2. グローバル(_app.js)でフォントを宣言しておく

@fortawesome/react-fontawesomeはSVGの描画をするパッケージですので、<i class="...">は書きません。代わりに、@fortawesome/fontawesome-svg-corelibrary.add()関数で使うフォントを宣言します。

pages/_app.js
import '../styles/globals.css'

import { library } from '@fortawesome/fontawesome-svg-core'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'

library.add(fab, faExternalLinkAlt)

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

{fab}は全てのfree-brands-svg-iconsを一括で読み込みますが、{faExternalLinkAlt}は、アイコン「external-link-alt」のsolidスタイル版を単品で読み込んでいます。

Step3. メニューコンポーネントを作る

各コンポーネントでFontAwesomeIconをimportすれば、グローバルに宣言したアイコンが使えます。

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

あとはお好みでメニューを作ってください。「fab」「fas」のどちらのスタイルか、明確に指定する必要があるので注意。

メニューとボタン

components/menu/index.js
import Menu from './menu'
export default Menu
components/menu/button.js
import Link from 'next/link'

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

export default function Button({path, label, iconStyle, iconName}) {
    return(
    <Link href={path}>
      <a className="inline-block mr-0 md:mr-8 last:mr-0 my-2 md:my-4 no-underline p-4 border-2 transition duration-300 border-gray-100 hover:border-gray-500 rounded-lg">
        <b className="block font-bold text-xl">
          <FontAwesomeIcon className="w-5 mr-2 mb-1 inline" icon={[iconStyle, iconName]}/>{label}</b>
      </a>
    </Link>
    )
}

チュートリアルではsize=""としていますが、この環境では効きませんでした。(というか、チュートリアル通りだとsvgにfont-sizeを指定しちゃってない?)

代わりにtailwindのclassでサイズを指定しています。

components/menu/menu.js
import Button from './button'

const Menu = props => (
  <div className="flex flex-col md:flex-row mb-8 md:mb-12">
    {props.buttons.map(button => (
      <Button
        key={button.path}
        path={button.path}
        iconStyle={button.iconStyle}
        iconName={button.iconName}
        label={button.label}
        text={button.text}
      />
    ))}
  </div>
);

export default Menu;

メニューにデータを渡す

(以下、使い回す前提で別ファイルにURLを書いています)

libs/constants.js
export const CONST_TWITTER_URL = 'https://twitter.com/sasigume'
export const CONST_FB_URL = 'https://facebook.com/sasigume'
export const CONST_YOUTUBE_URL = 'https://www.youtube.com/channel/UCzOySm9dwHiu0d-bWIaInRA'
export const CONST_LAPRAS_URL = 'https://lapras.com/public/NG9RFWQ'

最後に、メニューを呼び出します。

components/intro.js
import {CONST_FB_URL, CONST_TWITTER_URL, CONST_YOUTUBE_URL, CONST_LAPRAS_URL} from '@/libs/constants'
import Menu from '@/components/menu'

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'

export default function Intro() {
  const menuButtons = [
      {
        label: "Facebook",
        iconStyle: "fab",
        iconName: "facebook",
        path: CONST_FB_URL,
      },
      {
        label: "YouTube",
        iconStyle: "fab",
        iconName: "youtube",
        path: CONST_YOUTUBE_URL,
      },
      {
        label: "Twitter",
        iconStyle: "fab",
        iconName: "twitter",
        path: CONST_TWITTER_URL,
      },
      {
        label: "LAPRAS",
        iconStyle: "fas",
        iconName: "external-link-alt",
        path: CONST_LAPRAS_URL,
      }
    ]
  return (
    <Menu buttons={menuButtons} />
  )
}

メニューに渡すデータ、もっとスマートに書けますね。雑ですいません。

ソースコード

(趣味で作ったポートフォリオです)
https://github.com/sasigume/sasigudotme

参考

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

create-react-app Advanced Configuration まとめ 2020年末版

進化し続けるReact。
React大好きなんですがちょっと離れてると機能が増えてたり、変更があったりと追いつくのがなかなか大変です。
今年はAdvanced Configurationで廃止された設定、新しく追加された設定に絞って書いてみます。
一部、勉強のためにcreate-react-appの修正箇所も紹介します。

2019年末版はこちら

Advanced Configurationの公式ドキュメント

GitHubのドキュメント

create-react-app

設定方法

shellで設定する場合には

package.json
  "scripts": {
    "start": "CI=true react-scripts start",

のようにするか、.envファイルで

.env.development
CI=true

のように環境ごとに設定します。

廃止されたAdvanced Configuration

NODE_PATH

react-scripts@4.0.0で廃止。
今後はjsconfig.jsonで設定してねってこと。
もともと、baseUrlを設定してない場合には、NODE_PATHを使うようになってたのでやっとなくなったのかという設定。

https://github.com/facebook/create-react-app/releases/tag/v4.0.0

EXTEND_ESLINT

これも同じくreact-scripts@4.0.0で廃止。
この設定がなくても拡張できるようになったとのこと。

修正PR

https://github.com/facebook/create-react-app/releases/tag/v4.0.0

追加されたAdvanced Configuration

WDS_SOCKET_HOST

WDSwebpack-dev-serverから。
Hot Module Replacement用にカスタムのwebsocketホスト名で開発サーバーを実行。
webpack-dev-serverのデフォルトはwindow.location.hostnameで、SockJSのホスト名を指定します。
この変数を使用して、一度に複数のプロジェクトでローカル開発を行える。

https://webpack.js.org/configuration/dev-server/#devserversockhost

WDS_SOCKET_PATH

webpack-dev-serverのデフォルトは、SockJSパス名の/sockjs-node
この変数を使用して、一度に複数のプロジェクトでローカル開発を行える。

https://webpack.js.org/configuration/dev-server/#devserversockpath

WDS_SOCKET_PORT

webpack-dev-server のデフォルトは、SockJS ポートの window.location.port です。
この変数を使用して、一度に複数のプロジェクトでローカル開発を開始を行える。

https://webpack.js.org/configuration/dev-server/#devserversockport

FAST_REFRESH

Fast Refreshという実験段階の機能。
アプリ全体をリロードするわけではなく、修正したコンポーネントのみを更新することにより高速開発が可能になる。
問題があったらfalseに設定してオフにしましょう。

DISABLE_NEW_JSX_TRANSFORM

React 17 で導入され、React 16.14.015.7.00.14.10 にバックポートされたNEW JSX TRANSFORMを無効にします。
古いReactプロジェクトでReactをバージョンアップできなく問題になる場合に使用する。

NEW JSX TRANSFORMについて

まとめ

どうでしたか?
設定の廃止、追加をみるだけでも今年1年のReactの変化が見れて面白いですね!

今年追加された設定は、自分の開発ではあまり使う機会はないかも。
廃止された設定は使っていたのでそっと削除しておこうと思います。

来年もReactはどのように変化していくのか楽しみ!

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

React-pdfで作る!バス運行系統図

バスマップAdventCalendar12日目の記事です?

ちなみに3日遅刻です?
突然仕事と資格勉強とクリスマス準備で忙しくなってつらい?

はじめに

どれだけWebにおけるバスマップの試みが流行しようと、なかなかバスマップと切り離せないもの。それが紙です。
そして、紙に印刷しやすいフォーマットの代表格、PDFもまた、バスマップを語る上で外せません。

デザインソフトからPDF形式でバスマップを出力する際も、Excelで作成した時刻表をPDFにしてネットで公開する際も、バス事業者が利用者向けにプレスリリースを用意する際も、PDFが主に使われます。

幅広く使われているので、もしプログラミング的にバスマップをPDFで生成できたら、とても便利です。
が、正直な所、日本語PDFとプログラミングは、かなり噛み合わせにくいです。

自分の卒業論文は、プログラミング的にバスマップを生成する試作アプリ制作なのですが、その時もひどい文字化けに悩まされ続けました。結局、「1度画像としてバスマップを生成し、それをPDFに張り付ける」という荒業で無理やり形にした苦い思い出があります。(今考えると、技術力不足の影響が一番大きかったですが)

そんな中、偶然最近、reactとPDFを組み合わせるライブラリ「react-pdf」で日本語が簡単に使えるらしいぞ、ということを知りました。実際に触ってみると、フォントをソースフォルダに置いて、あとはコードをゴリゴリ書けばOK。超便利!

ということで今回は、卒業研究のリベンジも兼ねて、React-pdfで運行系統図を作成してみたいと思います。

サンプル

王57系統の運行系統図PDFアプリを作ってみました?
https://kotodu.io/react-route-guide/

出力PDF例-1

image.png

出力PDF例-2

image.png

設定画面

image.png

操作説明

サンプルを見ていただければ分かると思いますが、完成度が中途半端です?
予定が急変し、作業時間を確保できず、予想していたクオリティまで到達できませんでした?

ざっくり説明すると、

  1. consumerKeyを入れる
  2. getDataを押してAPI取得
  3. 停留所を選択する
  4. 通過停留所は灰色で、これから通る停留所は黒色で、系統パターンの運行系統図を表示する

というものです。

今回は東京公共交通オープンデータチャレンジよりデータを利用させていただきました。

(即興クオリティとはいえ、利用者に入力してもらう、という大変頭の悪いことをしたのは正直反省している?恥ずかしいから直したい?)

react-pdfの強み

エンジニア向け

ReactコンポーネントとしてPDFのコンテンツを記載できます。
ですので、繰り返し何かを表示したり、コンポーネントを使いまわしたり、といった辺りは非常に得意です。

例えば、系統情報から系統名を表示するのは

const a = (routePatterns: OdptBusroutePattern[])=>{
return (
    <>
            {routePatterns.map(pattern => {
                return (
                    <View>
                        <Text>{pattern["dc:title"]}</Text>
                    </View>
                );
            })}
    </>
)}

などの形で、系統名をPDF内に列挙できます。

また、画像配置も気楽です。今回の停留所画像は、画像で行っています。
stopGray.png
stop.png
line.png
lineGray.png

Reactコンポーネントを組み合わせてPDFを気軽に作成できるのは、React-pdfの最大の強みだと思います。

利用者・デザイナー・バス事業者向け

Web上でバス停を選択したら、そのバス停にカスタマイズされたバスマップがPDFで出力される、といったアプリなどができます。

今回試作したアプリでも、各バス停に合わせて、作成されるPDF表示を変更できました。
データさえあれば、例えばそのバス停からの所要時間表・運賃表・時刻表・周辺の地図などもPDFに埋め込めます。(っていうサンプルを作りたかったのですが時間がなくてパスです?)

運行系統図だけでなく、例えば地図ベースのバスマップのうち、付近を通るバス停だけ着色する、なんて処理もできます。いずれ作ってみたい。

終わりに

夢は広がりますが、時間は有限なので、予定日に間に合わなかったのは素直に反省です?

技術情報

内部的には、react-pdfはpdfkitをうまい具合にreactでできるようにしているものなので、たぶんpdfkitでも、使い方によっては日本語PDFが作成できるはずです。那須や源真ゴシック以外日本語フォントが使えなかったと記すGitHubでのコメントもあり、実際、自分も他のフォントだとうまくいきませんでした。対応しているフォント/していないフォントがあるようなので、注意が必要です。
また、react-pdfはそろそろv2の正式版が出ます。制作者さんが多忙で、1年以上ベータ版を作成している様子でしたが、あとはドキュメント程度、というのが近況だそうです(GitHubのv2へのタスクを確認のこと)。SVG対応など、欲しかったけど微妙に今まで使えなかった機能が目白押しなので、こちらも注目です!

今回主に活用したreact-pdfはこちら

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

GitHub Actionsを使ってNext.jsで作られているアプリケーションをfirebase deployをするまで

Increments × cyma (Ateam Inc.) Advent Calendar 2020 の17日目は、
Increments株式会社の @mziyut が担当します!

はじめに

  • 最近ハッカソンに参加した際にGitHub Actionsを利用し、master(main)ブランチにmerge等新規コミットが行われたことをトリガーに、本番環境に反映されるように設定しました。
  • この記事では、最短でfirebase deployするまでの手順を記載します。

今回用いる環境

% sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.7
BuildVersion:   19H15
% node -v
v12.16.0

また、firebaseのプロジェクト作成や、クレジットカード登録1 などはこの記事では記載しません。

Projectの準備

npx create-next-app --example with-firebase-hosting mziyut-advent-calendar-2020

起動確認

  • 最低限作成したProjectが動作することを確認しておきましょう。
cd mziyut-advent-calendar-2020 && yarn dev

localhost_3000_(Laptop with touch).png

.firebaserc の設定

  • Next.js with Firebase Hosting example でプロジェクトを作成した場合 .firebaserc のプロジェクト設定が <project-name-here> となっているので適当なプロジェクト名を設定しましょう。
.firebaserc
 {
   "projects": {
-    "default": "<project-name-here>"
+    "default": "mziyut-advent-calendar-2020"
   }
 }

GitHub Actionsを用いてデプロイする

GitHub Actions についてリンク先を確認してください。
デプロイするためにFirebase tokenが必要となります。

デプロイ用トークン生成とSecretsに登録

Firebase CLI リファレンス - CI システムで CLI を使用する を参照しTokenを生成しましょう

生成出来たら https://github.com/<YOUR_NAME>/<YOUR_REPO_NAME>/settings/secrets/actionsにアクセスしトークンを登録しましょう

今回はFIREBASE_TOKENと登録しました。

github.com_mziyut_mziyut-advent-calendar-2020_settings_secrets_actions(Laptop with touch).png

package.jsonにCI用のデプロイタスクを定義

デプロイ時に実行するコマンドは以下です。

firebase deploy --token "$FIREBASE_TOKEN"

そのままでも使用することは出来ますがworkflowに簡単に乗せることができるようにpackage.jsonに定義していきましょう。

package.json
     "serve": "npm run build && firebase emulators:start --only functions,hosting",
     "shell": "npm run build && firebase functions:shell",
     "deploy": "firebase deploy --only functions,hosting",
+    "deploy:token": "firebase deploy --only functions,hosting --token ${FIREBASE_TOKEN}",
     "logs": "firebase functions:log"
   },
   "dependencies": {

workflowsの定義

「デプロイ用トークン生成とSecretsに登録」と「package.jsonにCI用のデプロイタスクを定義」で設定した内容を元にworkflowsを定義していきます。

.github/workflows/deploy.yml
name: Deploy to firebase

on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [10.x]
    steps:
      - uses: actions/checkout@v2
      - name: yarn install
        uses: borales/actions-yarn@v2.0.0
        with:
          cmd: install
      - name: yarn run deploy:token
        uses: borales/actions-yarn@v2.0.0
        with:
          cmd: deploy:token
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

上記内容を記述したらGitHubのmaster(main)ブランチにPushしましょう。
Actionsの画面からworkflowの状態を確認することが出来ます。

github.com_mziyut_mziyut-advent-calendar-2020_runs_1551507367_check_suite_focus=true(Laptop with touch).png

まとめ

今回はサンプルプロジェクトをGitHub Actionsを使いFirebaseにデプロイするまでを記載しました。
実際に使ったコードは mziyut/mziyut-advent-calendar-2020 にPushしておきました。

最後に

Increments × cyma (Ateam Inc.) Advent Calendar 2020の18日目は@mi-noがお送りします!!


  1. 使用するNodeバージョンによって異なります。 

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

React + Material-UI でタグのような複数指定できるフォームを作るには

React + Material-UI で次のような感じのタグフォームを作るのに、
必要なオプション指定の仕方がすぐに分からなかったので簡単にメモ。

react-material-ui-tags.png

※公式ドキュメントはこちら

React Autocomplete component - Material-UI
https://material-ui.com/ja/components/autocomplete/#creatable

Autocomplete API - Material-UI
https://material-ui.com/ja/api/autocomplete/#main-content

前提

"@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.57",
"react": "^16.13.1",

必要なオプション

Autocomplete Componentに
- multiple ・・複数可にする
- freeSolo ・・自由入力可にする
- clearOnBlur ・・新規選択肢を追加可にする
- onChange={(e,v) => setTags(v)} ・・入力値をuseStateを使ってセットする

TexstField Componentに
- {(e) => {setTags(e.target.value)}} ・・入力値をuseStateを使ってセットする

コード全体

import { useState } from "react";
import AddIcon from '@material-ui/icons/Add';
import Autocomplete, { createFilterOptions } from '@material-ui/lab/Autocomplete';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import IconButton from '@material-ui/core/IconButton';
import TextField from '@material-ui/core/TextField';

const filter = createFilterOptions();

export default function TaskCreateForm(props) {
  const [tags, setTags] = useState([]);
  const [addFormOpen, setAddFormOpen] = useState(false);


  const handleClickAddFormOpen = () => {
    setAddFormOpen(true);
  };

  const handleAddFormClose = () => {
    setAddFormOpen(false);
  };

  const add = () => {
    setTags([]);
    console.log(tags);
    handleAddFormClose();
  }

  return (
    <div>
      <IconButton aria-label="add" color="primary" onClick={handleClickAddFormOpen}>
        <AddIcon />
      </IconButton>

      <Dialog open={addFormOpen} onClose={handleAddFormClose} aria-labelledby="form-dialog-title">
        <DialogContent style={{ minWidth: 400 }}>
          <form className={classes.formRoot} noValidate autoComplete="off">
            <div>
              <Autocomplete
                id="tags"
                size="small"
                options={[]}
                getOptionLabel={(option) => {
                  // Value selected with enter, right from the input
                  if (typeof option === 'string') {
                    return option;
                  }
                  // Add "xxx" option created dynamically
                  if (option.inputValue) {
                    return option.inputValue;
                  }
                  // Regular option
                  return option.title;
                }}
                multiple // 複数可
                freeSolo // 自由入力可
                clearOnBlur // 新規選択肢を追加可
                filterOptions={(options, params) => {
                  const filtered = filter(options, params);
                  // Suggest the creation of a new value
                  if (params.inputValue !== '') {
                    filtered.push({
                      inputValue: params.inputValue,
                      title: params.inputValue,
                    });
                  }
                  return filtered;
                }}
                onChange={(e,v) => setTags(v)} //★ ←複数のテキストを取得するにはココと
                renderInput={(params) => (
                  <TextField
                    {...params}
                    label="tag"
                    variant="standard"
                    onChange={(e) => {setTags(e.target.value)}} //★ ←ココ
                  />
                )}
              />
            </div>
          </form>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleAddFormClose} color="primary">
            Cancel
          </Button>
          <Button onClick={add} color="primary" variant="contained">
            Add
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  );
}

参考

マテリアルUIからのオートコンプリートコンポーネントの変更の処理
https://qastack.jp/programming/58684579/handle-change-on-autocomplete-component-from-material-ui

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

GatsbyでSEO対策をする

メタデータを追加するための準備

yarn add gatsby-plugin-react-helmet react-helmet

gatsby-config.jsにgatsby-plugin-react-helmetの設定を追加する

gatsby-plugin-react-helmet

このプラグインを使って、タイトルやメタ属性などの属性をコンポーネントに追加すると、それらがGatsbyが構築する静的なHTMLページに追加されます。これはサイト閲覧者にとってだけでなく、SEOにとっても重要です。

react-helmet

headタグの管理をするコンポーネント

メタデータの設定を記述するファイルを準備

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

class Application extends React.Component {
  render () {
    return (
        <div className="application">
            <Helmet>
               <html lang="言語の種類" />
         <meta charSet="utf-8" />
               <title>My Title</title>
         <meta name="description" content="説明" />
               <link rel="canonical" href="http://mysite.com/example" />
            </Helmet>
            ...
        </div>
    );
  }
};

サイト全体で使用するメタデータの値を用意する

サイト全体で繰り返し使用したいメタデータは、siteMetadataとして用意しておくとGraphQLのクエリで取得できるようになる

gatsby-config.js

module.exports = {
/* Your site config here */
siteMetadata: {
title: `Example`,
description: `Example description`,
lang: `ja`,
siteUrl: `https://*******.netlify.app`
},
plugins: [
...

ページごとにメタデータの値を変える

example.js

export default ({data}) => (
  <Layout>
    <SEO
      pagetitle="example"
      pagedesc="example description"
    />
   <div>
   ...

参考文献:
Webサイト高速化のための静的サイトジェネレーター活用入門
react-helmet: https://github.com/nfl/react-helmet
gatsby-plugin-react-hemet: https://www.gatsbyjs.com/plugins/gatsby-plugin-react-helmet/

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

デザイナーでもわかる!Next.jsのはじめかた

Ateam Lifestyle Advent Calendar 2020の16日目は、株式会社エイチームライフスタイル デザイナー @kmfj が担当します!

はじめに

業務でReactを使う機会が増えてきたので、Reactを使ってサイトを作ってみたいなーとぼんやり思っていました。
Reactのフレームワークで有名どころ1はGatsbyとNext.jsですが、GatsbyではLPを作る経験をしたので、Next.jsとどう違うのか勉強ついでに触ってみたかったのと、next/imageを試してみたかったため、今回使ってみることにしました。チュートリアルを通して簡単なページを作成してみた覚え書きです。

Next.jsとは?

Next.jsはReactベースのフレームワークです。
「バンドル」「コンパイル」「ルーティング」などデザイナーが訳わからん部分の機能が内臓されていて、所定のディレクトリ構造に沿ってファイルを設置するだけでサイトを簡単に作成できます。SSRもSSGもできてすごいヤツらしい。

手順

まずは作業ディレクトリ内に、Next.jsをインストールします。

$ npx create-next-app 任意のディレクトリ名 --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"

今回はディレクトリ名をnextjs-blogとしました。このディレクトリに移動します。

$ cd nextjs-blog

サーバーを立ち上げるために下記のコマンドを実行します。

$ npm run dev

http://localhost:3000にアクセスすると下記のようなテンプレートページが表示されます。
スクリーンショット 2020-12-15 3.44.09.png

チュートリアルを進めると、pages配下にposts/first-post.jsを作るよう言われるので作成し、下記の内容を記述します。

export default function FirstPost() {
  return <h1>First Post</h1>
}

http://localhost:3000/posts/first-postにアクセスすると、先ほど作成した"First Post"のページが表示されます!
pagesディレクトリにファイルを置くだけで、ファイルへのパスがURLのパスになります。

そのままチュートリアルを進めると下記のようなブログページが完成します。
スクリーンショット 2020-12-15 3.48.40.png

next/imageによる画像最適化

Next.js 10へのアップデートで画像最適化が簡単にできるようになったそうなので使ってみます。
- 参考:next/image | Next.js

public配下に対象の画像を配置し、画像を追加したいファイルに下記を追加します。

import Image from 'next/image'
// 中略 //
<Image
  src="/main.jpg" // publicに配置した画像
  alt="mainImage"
  width={2000} //widthとheightはアスペクト比を画像と合わせる
  height={700}
/>

これだけで完了です!
スクリーンショット 2020-12-15 14.47.07.png
上が<img>タグで読み込んだもので、下がImageコンポーネントの画像です。
サイズがかなり小さくなっていることがわかります。
objectFitなどにも対応していて便利そう。

その後好きにスタイルを調整し、簡単なブログページを作ることができました。
スクリーンショット 2020-12-15 16.14.08.png

ちなみにNext.jsはCSS Modulesを推奨しているため2、今回はCSS Modulesでスタイルを当てていきました。
ですが、styled-componentやemotionなどのCSS-in-JSにも対応しているので利用できない訳ではありません。3

Gatsbyと比較して

Next.jsはシンプルで自由度が高く、拡張性がとても高いと感じました。
また、画像の最適化はNext.jsの方が簡単でした。Gatsbyの画像最適化はGraphQLの記述などで苦戦したので、画像サイズの指定だけですんだのはありがたかったです。

テーマやプラグインの充実度はGatsbyが圧倒的だと思いました。
スターターのテンプレートサイトも見ているだけで楽しいです。
デザイナーが気軽にサイトを作ろうと思ったらGatsbyの方が適しているのかもしれません。

おわりに

本当に簡単にサイトが作成できるので、私と同じ初心者の人の背中を押せれば幸いです。
自分用のサイトページが作れたので、これからはstorybookを入れたり勉強の場として使っていこうと思います。
最後まで読んでいただきありがとうございました!

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

React Hook Formを初めて使う人へ

開発のプロジェクトで「ReactHookFormで実装して」と言われて触る機会があり,初心者なりに何度か詰まってから理解したので,つまらない用に超絶簡易ドキュメントを書く.

React Hook Formとは

Home

特徴

  • onChangeを走らせずにすみパフォーマンスがつおい
  • inputやselectのvalueをsubmitのときに拾ってくれる

多分他にもっとある


install

npm

npm install react-hook-form

yarn

yarn add react-hook-form

普通に使う

公式から持ってきたサンプルを説明します.

import React from "react"
import { useForm } from "react-hook-form"

export default function App() {
  const { register, handleSubmit, watch, errors } = useForm()
  const onSubmit = data => console.log(data)

  console.log(watch("example")); // watch input value by passing the name of it

  return (
    {/* "handleSubmit" will validate your inputs before invoking "onSubmit" */}
    <form onSubmit={handleSubmit(onSubmit)}>
    {/* register your input into the hook by invoking the "register" function */}
      <input name="example" defaultValue="test" ref={register} />

      {/* include validation with required or other standard HTML validation rules */}
      <input name="exampleRequired" ref={register({ required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}

      <input type="submit" />
    </form>
  );
}

useFormをimportする

import { useForm } from "react-hook-form"

必要なものを分割代入して使う

const { register, handleSubmit, watch, errors } = useForm()

最低限registerhandleSubmitは必要になるかな

監視対象にregisterを設定

<input name="example" defaultValue="test" ref={register} />

ref属性がregisterがであるものを監視

registerは以下のように引数にオブジェクトを渡すことで,バリデーションの設定ができる

ref={register({ required: true })}

その他の詳細設定はここを参照↓

https://react-hook-form.com/jp/api#register

送信の設定

<form onSubmit={handleSubmit(onSubmit)}>

formタグのonSubmit属性にhandleSubmit()を設定.この引数にフォームを送信する関数を渡す

今回は,const onSubmit = data => console.log(data)が実行される

仕組みは,refにregisterが設定された要素のvalueが,その要素のnameがプロパティとなり,dataにオブジェクトとして渡される.

今回であれば,

data = {example: "入力されたvalue",exampleRequired: "入力されたvalue"}と言った具合

バリデーションのエラーメッセージ

registerに関するエラーは,

const { register, handleSubmit, watch, errors } = useForm()

で定義したerrorsに返ってくるので

errors.exampleRequiredとすれば呼び出せる


外部ライブラリに対応した使い方

さて,便利なHookFormだが,Material-UIなどのツールと共存すると正常に動作しないので困る.

ここをよくわかっていないと,「Material-UIと共存できないとかwww 使う意味なくね?wwww」と言われる.

制御ができない場合はControllerコンポーネントを使って制御する

制御ができない場合とはref属性を設定することができないもののようです.

Controllerをimportする

import { useForm, Controller } from "react-hook-form";

もちろんuseFormも忘れずに

controlを定義しておく

const { handleSubmit, register, errors, control } = useForm();

こんな感じに,普通に使い場合に加えて,Controllerコンポーネントとcontrolを準備する.

外部ライブラリのコンポーネントを制御する書き方

今回のサンプルはみんな大好きMaterialUI

<Controller
  name="gender"
  // control属性忘れずに
  control={control}  
  // component
  as={
    <RadioGroup aria-label="gender" name="gender1" >
      <FormControlLabel value="female" control={<Radio />} label="Female" />
      <FormControlLabel value="male" control={<Radio />} label="Male" />
      <FormControlLabel value="other" control={<Radio />} label="Other" />
      <FormControlLabel value="disabled" disabled control={<Radio />} label="(Disabled option)" />
    </RadioGroup>
  // validation
    rules={{ required: true }}
  }
/>

オブジェクトのプロパティとなるname属性をセットしましょう

name="gender"

先程定義したcontrol属性もこんな感じにセットします

control={control}

そして制御したいコンポーネントをasに入れます.

今回はMaterialUIのラジオボタンです.

また,このControllerコンポーネントを使用した場合はregisterを使わないので,バリデーションどーすんの?となる.この場合はrules属性を使う.ここがドキュメントちゃんと読んでいなかったので詰まった?

rules={{ required: true }}

こんな感じでOK

欠点

TypeScriptとの相性がわるい(かもしれない)

試してはないし,よくわかっていない?

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

[React]ドラッグでリサイズできるコンポーネントを作る

react-rndを使いましょう。
以下の記事を参考に。
https://medium.com/ruffnote/react-rnd%E3%81%A7%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%82%92%E3%81%90%E3%82%8A%E3%81%90%E3%82%8A%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B-781c59a6ee83

ポイント

  • 親コンポーネントにrelativeを追加
  • 画面の半分にしたいときは以下のように指定
x: document.documentElement.offsetWidth / 2
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【重要】非同期通信でOptimizeを使いA/Bテストをする方法

こんにちは、シンヤです!
本日は「【重要】非同期通信でOptimizeを使いA/Bテストをする方法」というテーマでお話しいたします。


概要

非同期通信の場合Google OptimizeでA/Bテストを行うには、以下のような特殊な設定が必要です。

  1. 「アクティベーションイベント」を設定する
  2. テスト有効化のタイミングを「アクティベーションイベント発火後」に設定する

React製のフレームワーク「Next.js」を使ってOptimizeの設定を書き込んでいきます。

Google Optimizeとは

Google Optimizeとは、Googleが提供しているA/Bテストを行えるツールです。

主な特徴として・・・

  • 無料
  • Google Analyticsとの連携機能
  • 過去の測定データの蓄積と分析
  • 「多変量テスト」「リダイレクトテスト」も可能
  • 簡単にデザインとHTMLを変更できる

があります。

どのテストも行いたいことは、基本的には「仮説検証」です。
考え出した仮説を基にGoogle Optimizeを使い、画面の見た目を変更して、2週間〜1ヶ月程テストします。


やること整理

環境構築〜テスト完了まで、全て解説します。
具体的には以下の順番で解説します。

  1. 「Node.js」のインストール
  2. 「Next.js」のプロジェクト作成
  3. 「_document.js」の設定とテストページの作成
  4. 「アクティベーションイベント」の書き込み
  5. 「Google Analytics(GA4)」のアカウント作成
  6. 「Google Optimize」のアカウント作成
  7. 「Google Analytics」「Google Optimize」の連結
  8. 「Next.js」にGAとOptimizeのIDを書き込む
  9. 「Netlify」の設定
  10. 「Google Optimize」でのテスト

1. 「Node.js」のインストール

以下はMacでの環境構築の方法となります。

ご使用中のMacに「Node.js」の環境を構築していきます。
既に環境構築済みの方は、こちらの項目は飛ばして読んでください。

ターミナルを起動する

まずは、ターミナルの起動を行います。
Mac下部の「Launchpad」で、「terminal」と入力して出てきたアイコンを、クリックしてください。

Homebrewのインストール

以下のコマンドを入力して、Homebrewをインストールします。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストール後以下のコマンドを入力して、Homebrewのバージョンが表示されたら、成功しています。

$ brew -v
#> Homebrew 1.2.3
#> Homebrew/homebrew-core (git revision 7212; last commit 2017-07-02)

Nodebrewのインストール

以下のコマンドを入力して、Nodebrewをインストールします。

$ brew install nodebrew

インストール後、以下のコマンドを入力して、Nodebrewのバージョンが表示されたら、成功しています。

$ nodebrew -v
#> nodebrew 1.0.0

Nodebrewを使える様にする

以下のコマンドをターミナル上で入力して、環境変数を追加します。

$ vi ~/.bash_profile

ターミナルの画面が変わったら、

  1. キーボードの「I」を押す
  2. 「export PATH=$HOME/.nodebrew/current/bin:$PATH」のコードを追加する。
上記の画像のようになっていれば成功です。

コードを追加したら、

  1. 「esc」キーを押す
  2. 「:wq」と入力する
  3. 「Enter」を押す

コードの保存が終わり、エディターが終了します。

Nodebrewのセットアップ

以下のコマンドを入力して、先ほど作ったbash_profileを反映させます。

$ source ~/.bash_profile

次に以下のコマンドを入力して、Nodebrewをセットアップします。

$ nodebrew setup

Node.jsのインストール

以下のコマンドを入力して「Node.js」をインストールします。

$ nodebrew install-binary v12.13.1

次に以下のコマンドを入力して、インストールした「Node.js」を使える様にします。

$ nodebrew use v12.13.1

インストール後以下のコマンドを入力して、「Node.js」のバージョンが表示されたら、成功しています。

$ node -v
#> v12.13.1

2. 「Next.js」のプロジェクト作成

以下のコマンドを入力して、「Next.js」のプロジェクトを作ります。

$ npx create-next-app
✔ What is your project named? … next-app-optimize

What is your project named?に好きな名前を入れます。
これがディレクトリ名になります。

上記では仮に「next-app-optimize」を名付けました。

上記の画像のようになっていれば成功です。

次に以下のコマンドを入力して、作成した「next-app-optimize」に移動します。

$ cd next-app-optimize

以下のコマンドを入力して、ローカルで「Next.js」を起動させます。

$ yarn dev
実行結果
http://localhost:3000にアクセスして、上記の画面が表示されていれば成功です。

「index.js」を書き換える

pagesディレクトリにあるindex.jsを以下のように書き換えて、不要なものを全て削除します。

import React from "react";
import Head from 'next/head';
import Link from 'next/link';
import styles from '../styles/Home.module.css';

const Home = () => {

  return (
    <div className={styles.container}>
      <Head>
        <title>Create Next App</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>Top</h1>

        <Link href="/lp">
          <a style={{ textDecoration:'underline' }} className={styles.title}>LPへのリンク</a>
        </Link>
      </main>
    </div>
  )
}

export default Home;

/lpのページはこれから作成します。

上記の画像のようになっていれば成功です。

3. 「_document.js」の設定とテストページの作成

以下のコマンドを入力して、プロジェクトのルートディレクトリにいるか確認します。

$ ls
> README.md node_modules    package.json    pages       public      styles      yarn.lock

pagesディレクトリがあればOKです。
以下のコマンドを入力して、pagesディレクトリに移動します。

$ cd pages

以下のコマンドを入力して、_document.jsファイルを作成します。

$ touch _document.js

作成した_document.jsファイルを、以下のように書き換えます。

import React, { Fragment } from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const isProduction = process.env.NODE_ENV === 'production';

    const initialProps = await Document.getInitialProps(ctx);

    return { ...initialProps, isProduction };
  }

  render() {
    const { isProduction } = this.props;

    return (
      <Html>
        <Head>
          {isProduction && (
            <Fragment>
              <script async src="https://www.googletagmanager.com/gtag/js?id={{AnalyticsのID}}" />
              <script
                dangerouslySetInnerHTML={{
                  __html: `
                  window.dataLayer = window.dataLayer || [];
                  function gtag(){dataLayer.push(arguments);}
                  gtag('js', new Date());
                  gtag('config', '{{AnalyticsのID}}', { 'optimize_id': '{{OptimizeのID}}'});
                `
                }}
              />
            </Fragment>
          )}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument;

{{AnalyticsのID}}{{OptimizeのID}}は、後ほど書き換えます。

テストページの作成

以下のコマンドを入力して、pagesディレクトリにいるか確認します。

$ ls
_app.js     _document.js    api     index.js

以下のコマンドを入力して、OptimizeでA/Bテストを行うためのページを作成します。

$ touch lp.js

作成したlp.jsファイルを、以下のように書き換えます。

import React from "react";
import Head from 'next/head';
import Link from 'next/link';
import styles from '../styles/Home.module.css';

const lp = () => {

  return (
    <div className={styles.container}>
      <Head>
        <title>LP</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>LP</h1>

        <Link href="/">
          <a style={{ textDecoration:'underline' }} className={styles.title}>Topへのリンク</a>
        </Link>
        <span style={{ background: '#222222', color: '#ffffff', padding: '16px', marginTop: '16px', borderRadius: '8px', cursor: 'pointer' }}>
          LPのボタン
        </span>
      </main>
    </div>
  )
}

export default lp;
上記画像のデザインになっていて、かつトップページからテストページへの遷移が出来る様になっていれば成功です。

4. 「アクティベーションイベント」の書き込み

非同期通信の場合、テスト有効化のタイミングをアクティベーションイベント発火後にする必要があります。
なのでまず、テストページでイベントが発火するように設定します。

dataLayer.push({'event': 'optimize.activate'});

上記のコードをlp.jsに書き込みます。

import React, { useEffect } from "react";
import Head from 'next/head';
import Link from 'next/link';
import styles from '../styles/Home.module.css';

const lp = ( location ) => {

  useEffect(() => {
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push({'event': 'optimize.activate'});
  }, [location.pathname])

  return (
    <div className={styles.container}>
      <Head>
        <title>LP</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main className={styles.main}>
        <h1 className={styles.title}>LP</h1>

        <Link href="/">
          <a style={{ textDecoration:'underline' }} className={styles.title}>Topへのリンク</a>
        </Link>
        <span style={{ background: '#222222', color: '#ffffff', padding: '16px', marginTop: '16px', borderRadius: '8px', cursor: 'pointer' }}>
          LPのボタン
        </span>
      </main>
    </div>
  )
}

export default lp;

5. 「Google Analytics(GA4)」のアカウント作成

事前にGoogle Analyticsのダッシュボードにログインしてください。

Step1
サイドバーの【管理】をクリックし【アカウント作成】をクリックします。
Step2
「アカウント名」を入力して【次へ】をクリックします。
Step3
【詳細オプションを表示】をクリックします。
Step4
「ユニバーサルアナリティクスプロパティの作成」のトグルをONにします。
URLを入力して【次へ】をクリックします。

ユニバーサルアナリティクスプロパティでないと、Google Optimizeは使えないので、必ずトグルをONにしてください。

Step5
「ビジネスの概要」の入力は任意なので【作成】をクリックします。
Step6
利用規約を読み、チェックを入れて【同意する】をクリックします。

6. 「Google Optimize」のアカウント作成

Step1
Google Optimizeの公式ページにアクセスして【無料で利用する】をクリックします。
Step2
【利用を開始】をクリックします。
Step3
【次へ】をクリックします。
Step4
チェックを入れて【完了】をクリックします。
Step5
【アカウントを作成】をクリックします。
Step6
「アカウント名」を入力し、チェックを入れて【次へ】をクリックします。
Step7
「コンテナ名」を入力し【作成】をクリックします。

7. 「Google Analytics」と「Google Optimize」の連結


Step1
コンテナのダッシュボードにアクセス出来ている事を確認し【設定】をクリックします。
Step2
【アナリティクスへリンクする】をクリックします。
Step3
作成したGoogle Analyticsのプロパティを選択します。

現状(2020/12/09時点)は、GA4のプロパティは表示されません。
プロパティとリンクさせないとOptimizeは使えないので、ユニバーサルアナリティクスプロパティでないと、Optimizeは使えないことになります。


8. 「Next.js」にGAとOptimizeのIDを書き込む

GAとOptimizeのリンクに成功するとIDが分かるので、これを_document.jsに書き込みます。

import React, { Fragment } from 'react';
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const isProduction = process.env.NODE_ENV === 'production';

    const initialProps = await Document.getInitialProps(ctx);

    return { ...initialProps, isProduction };
  }

  render() {
    const { isProduction } = this.props;

    return (
      <Html>
        <Head>
          {isProduction && (
            <Fragment>
              <script async src="https://www.googletagmanager.com/gtag/js?id=UA-185122844-1" />
              <script
                dangerouslySetInnerHTML={{
                  __html: `
                  window.dataLayer = window.dataLayer || [];
                  function gtag(){dataLayer.push(arguments);}
                  gtag('js', new Date());
                  gtag('config', 'UA-185122844-1', { 'optimize_id': 'OPT-MZN2N7K'});
                `
                }}
              />
            </Fragment>
          )}
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

export default MyDocument;

package.jsonの設定を変更する

後述のNetlifyでのbuildをするため、package.jsonのscriptを以下のように変更します。

"scripts": {
  "dev": "next",
  "build": "next build",
  "export": "next export",
  "deploy": "npm run build && npm run export",
  "start": "next start",
  "test": "echo \"No test specified\" && exit 0"
},

RepositoryにPushする

「Next.js」の設定は全て完了したので、

  • GitHub
  • Bitbucket
  • GitLab

いずれかにRepositoryを作り、Pushしておきます。


9. 「Netlify」の設定

事前にNetlifyのダッシュボードにログインしてください。

Step1
【New site from Git】をクリックします。
Step2
PushしたGitのサービスをクリックします。
Step3
PushしたRepositoryを選択します。
Step4
Build command → npm run deploy
Publish directory → out/
と入力し【Deploy site】をクリックします。
Step5
Deploy中のログをクリックします。
Step6
Deployが終了し「Site is live ✨」と表示されるか確認します。
Step7
【Site overview】をクリックし、URLが表示されていれば成功です。

10. 「Google Optimize」でのテスト

  1. 「エクスペリエンス」の作成
  2. デザインの変更
  3. 「パターンの比重」の変更
  4. 「測定と目標」の変更
  5. Optimizeの動作確認
  6. 「アクティベーションイベント」の設定
  7. テスト開始

の7つに分解して解説します。

1. 「エクスペリエンス」の作成

Step1
コンテナを選択して【開始】をクリックして、エクスペリエンスを作成します。
Step2
テスト名とテストするページのURLを入力し【作成】をクリックします。

2. デザインの変更

Step1
【パターンを追加】をクリックします。
Step2
「パターン名」を入力し【完了】をクリックします。
Step3
【編集】をクリックします。
Step4
ボタンを選択し【要素を編集】【HTMLを編集】をクリックします。
Step5
↑ Before ↑
↑ After ↑
こちらのソースコードに変更して【適用】をクリックします。

以下コピペ用です。
<span style="background: rgb(255, 0, 255); color: rgb(255, 255, 255); padding: 16px; margin-top: 16px; border-radius: 8px; cursor: pointer;">LPのボタンを変更しました</span>

Step6
デザインが変更されているのを確認し【保存】【完了】をクリックします。

3. 「パターンの比重」の変更

Optimizeでの動作テストを行いたいので、オリジナルのデザインを表示させないために、追加したパターンの表示比重を100%にします。

実際のA/Bテストでは、オリジナルと変更したデザイン半分ずつ表示させてテストを行うので、比重を変更する必要はございません。

Step1
【比重】をクリックします。
Step2
【カスタムの割合】に変更して、追加したパターンの比重を100%にして【完了】をクリックします。

4. 「測定と目標」の変更

Step1
「測定と目標」【テスト目標を追加】をクリックします。
Step2
【リストから選択】をクリックします。
Step3
メインとなる目標を選択します。

実際のテストでは、目標となる項目がない場合【カスタム目標を作成】から目標を新規作成します。

5. Optimizeの動作確認

Step1
「設定」【インストールを確認】をクリックします。
Step2
正しくインストールされていることを確認し【ウェブサイト エクスペリエンスに戻る】をクリックします。

アクティベーションイベント後にテストをする場合、アンチフリッカースニペットを設定する必要はございません。
注意書きは無視していただいて構いません。

6. 「アクティベーションイベント」の設定

Step1
「設定」「アクティベーションイベント」から【ページの読み込み】をクリックします。
Step2
【カスタムイベント】をクリックします。
Step3
イベント名がoptimize.activeになっていることを確認し【完了】をクリックします。

7. テスト開始



【開始】をクリックし「実行中」のステータスに切り替わったら、テストが開始されます。

テストが正常に反映されていたら完了です。

ご注意

テストの確認はGoogle Chromeのシークレットブラウザで行ってください。
キャッシュの影響でテストが正常に反映されない可能性があります。


初期設定のままテストを行った場合

結論からいうと、ページ読み込み時に一瞬テスト結果が反映され、その後JavaScriptがテスト結果を上書きします

また非同期通信はページ遷移時に読み込みが発生しないので、ページ遷移した際テスト結果が反映されません。

初期設定のままテストを行うと、テスト結果が反映されない。

その為テスト有効化のタイミングを、アクティベーションイベント発火後に設定するのです。


どのライブラリでもイベント発火後にテストを実施しないといけない



フロントエンドのライブラリを支える技術を「Single Page Application(SPA)」といいます。
非同期通信の技術を使って、JavaScriptがDOMを操作してページを切り替えています。

基本的にはどのライブラリでもA/Bテストを行う場合「アクティベーションイベント発火後」に設定する必要があります。

「非同期通信」という共通の技術を使っているからです。
JavaScriptがテスト結果を上書きしてしまうので、必ずイベント発火後にテストを実施しないといけません。


余談:Google Tag Manager(GTM)は公式非推奨

前述のアンチフリッカースニペットの項目にもありますが、Google Tag Managerを使ってOptimizeタグを配信することは、公式から非推奨の勧告が出ています。

GTMを使う場合、Optimizeコンテナを読み込む前にGTMコンテナを読み込みます。
両方読み込むまでテストが開始されないので、数値に誤差が出る可能性があります。

正確な測定結果が重要な「仮説検証」で、数値の誤差が出るのは致命的です。
その為OptimizeのタグはGTMを使わず、直接埋め込みOptimizeコンテナを先に読み込ませます。


最後に

SPAとOptimizeの設定を解説した記事はいくつかあります。
私も過去にGatsbyでOptimizeを使う方法を解説した記事を書きました。

ですが、環境構築からテストまで一気通貫で、かつ画像入りで解説した記事はなかったので、自分で執筆してみました。

SPAでOptimizeを使い始めた際は、アクティベーションイベントの件は全く知らなかったので、数値の誤差が出ているテストを、自分が気付かない内に執り行っていました。

テスト結果が反映されなくても、Optimizeではデザインを変更したBパターンとして記録されます。
なので気付きにくいのです。

今回は以上になります。

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

React未経験者がUdemy学習で3週間で「すげえサイト」が公開できてしまった・・・

「すげえサイト」ってどないやねん!

とやかく述べる前に、たった一人で3週間かけて構築したサイトをご紹介します。

百聞は一見に如かず!!

まずはこちらのURLからご覧ください。

https://smaphonavi-9evcv2z6z.vercel.app/

※12月中にもっと進化させる予定です。お楽しみに

ページプレビュー

「いちいちページ遷移して見に行くの面倒臭いねん!」って人向けに概要を説明します。
今回制作したWebアプリは中古フリマサイトで取引されているスマートフォンの品質別の相場把握や、有名所のフリマサイト横断で価格比較や商品検索ができる便利ツール的なアプリケーションをコンセプトとして制作しました。
今やスマホは一人一台の時代になったので、”高く売りたい人”や”安く買いたい人”にとって中古品の直近相場や品質の程度による価格の幅を知れるのは嬉しいかなと思い作成してみました。

image.png

image.png

Reactのスゴいところは何と言ってもページの切替なく、ヌルヌルと動きのあるWebアプリを簡単に作成できるところですね。
条件変更に伴うグラフの再表示も俊敏にカッコよく実現できたりします。

Image from Gyazo

1週間目:とにかくインプット on Udemy

Reactを習得するために今回受講した講座はこちらです。

講座のタイトル
Modern React with Redux [2020 Update]

URL
https://www.udemy.com/course/react-redux/

受講してよかった点

  • ボリューム満点。全52時間なので小規模から大規模なReactアプリの開発手法が学べる
  • とにかく説明が丁寧。「こうすれば動きます」ではなく「なぜこうすれば動くのか」の説明が深い理解を促してくれる
  • ミニアプリを作りながら学べるので単元毎に達成感がありモチベーションが下がりにくい教材設計となっている
  • 講師のコーディングを見て実際に自分でもコードを書きながらハンズオンで学べるので、コードを書く力が養われる
  • アメリカのトップ企業で働いているエンジニアが講師なので、変数の命名規則やお作法など細かい部分も実務で役立つ観点を取り入れて説明されている

image.png

「英語圏の講座かい。英語なんて無理。」 と諦めかけたそこの読者の方、ちょっとお待ちください!
私も3週間前まではそんな一人でしたが、この講座を受講してから英語教材が第一選択肢となる程、考えが一変しました。
理由は明白で、日本に比べて英語圏はUdemyの市場競争が激しい為に、教材・講師の質がとにかく高いんです!

僕も高校卒業して数十年来、英語なんて触れていませんでしたが、それでも問題なく理解できました。
どうしても苦手な方の場合でも、Google翻訳を使って日本語字幕に変換もできるので、英語学習のハードルはかなり低くなっていると思います。詳しくは以下の記事が非常に参考になるので興味のある方はご覧ください。

【Udemy】50講座以上取った筆者がおすすめ英語字幕講座と勉強法を解説してみる

52時間の動画教材なので盛沢山でありますが、今回制作したレベルであれば10.4時間分の学習内容で事足ります。
ECサイトなど規模の大きいWebアプリの制作となると色々なライブラリを駆使することになるのでもっと学習が必要です。
実際に私もまだ半分しか見終わってません。

教材の中ではアプリを作りながらハンズオンで学べるので、動画を見進める内に自ずとコードが書けるようになっています。
1週間目はとにかく動画を見ながら、講師と一緒にコードを書いてインプット作業に専念しました。

2週間目:作りたいプロダクトの構成を考える

1週間本気で動画を見進めると、ある程度の簡単なWebアプリであれば作れるスキルが身に着いています。
というより、得たスキルで何か自分でも作ってみたい衝動に勝てなくなります笑

もちろん動画で学んだことだけでアプリ制作が出来るわけではないので、作りたいプロダクトを先に頭に描いて、足りない部分はGoogle検索でつまみ食いで勉強する形になります。今回のプロダクトの例では、グラフ表示のロジックはネット上の記事を漁って実装しました。

元々、分析・研究目的でフリマサイトから収集したデータが手元にあったことに着想を得て今回のアプリを作りたいと思いました。

インフラやバックエンドはAWSを組み合わせて構築してます。⇩が最終的なシステムアーキテクチャーです。
この辺の構成は最初から描いたわけではなく、作りながら拡張・微調整して今の形に落ち着いたという感じですね。
image.png

  1. データ収集レイヤー (EC2+RDS)
    EC2上でPythonでスクレイピングコードを定期スケジュール実行し、RDS上のテーブルにデータを保持する流れを構築しました。無料利用期間であればt2.microインスタンスであれば起動しっぱなしでも無料で使えます。

  2. バッチ集計レイヤー(RDS + Lambda + CloudWatch)
    グラフに表示するような集計データは、アプリでの表示の都度、集計や計算を行うとパフォーマンスが最悪なので、毎夜間にクエリを実行して明細データが蓄積されたテーブルを基に集計データ用のテーブルを再作成する仕掛けを構築しました。

  3. APIレイヤー(Lambda + API Gateway)
    アプリで表示したい結果セットをRDSからSELECTするコードをLamdaで関数を定義しました。関数実行のトリガーをAPI Gatewayに設定することで、HTTP通信を介してアプリ側からデータを受け取ることが可能になります。「APIとの連携がよく分からない」という方も講座内で詳しく説明されているので大丈夫です。

  4. Hosting
     Webアプリの公開はVercelを利用しました。無料で使えて、マジで10分くらいあれば世界中に自分の作ったWebアプリを公開できて非常に便利です。コマンドたった1行書いてEnter押すだけでURLが発行されて世界中に公開できるなんて夢のようですね。この辺の使い方も講座内でしっかり触れられています。

AWSについてもUdemyの動画教材を利用して学習しました。以下の教材はAWSのサービスに加え、インフラの知識も補填して話を進めてくれるので特におススメです。

AWS:ゼロから実践するAmazon Web Services。手を動かしながらインフラの基礎を習得

image.png

3週間目:実装してとりあえず公開! そして振り返り!

1~2週目で作るための力と作りたい物が決まったので3週目はひたすら実装です。

ここで大事なのは完璧を求めすぎないこと

完璧にキレイに実装しようとすると時間がかかりすぎます。また、難しいドキュメントに辿り着き、意味が分からなくなってモチベーションが下がって頓挫、なんて良くあることです。とりあえず動くものを作る これが大事です。

僕の作ったアプリも一見良さげに見えますがコードはまだまだ汚かったりします。パフォーマンスも最適とは言えないです。
それでも一旦作ってしまえば達成感に溢れ、「もっと良くしたい」という欲が出てくるものです。

その時に立ち止まって見直してリファクタリングすればいいんですね。改めて見直してみると、「何でこんな汚いコードの書き方したんだろ...」とか出てきます。その気付きが本当に大事で、効率的で可読性の高いコードの書き方を定着させ成長に繋がるんですね。エンジニアの成長ってこういう積み重ねなんだなあと思ったりもします。

まとめ

とまあ偉そうに書き連ねましたが、インフラ・バックエンド・フロントエンド通しでアプリ構築する経験は本当に勉強になりました。

プログラミングスクールに通っていた時期がありましたが、個人的にはUdemyの講座で独学でも十分だと思います。
今回はスクールでの知識は一切使わず、実際にUdemyで学習した知識だけでアプリ構築できましたし。
それくらい質が高いですし、費用対効果が断然違うと思います。コミュニティにも今やネット上で簡単に参加できますし、スクールに通うメリットって強制感くらいしかないのかなあなんて思ったりします。

エンジニアとして技術を高めたいと考えている方は、是非Udemy学習やってみてください。セール中であれば1講座 ~2000円ぽっきりでいつでも学習できるので。

僕も現在、無職なの年明けには仕事に就けるように頑張りたいと思います!
記事の内容についてご指摘・ご意見・ご質問等あれば是非とも宜しくお願いします。

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

公式Reactチュートリアルの解説vol.2

公式Reactチュートリアルの解説vol.1のつづきです。
公式Reactチュートリアルの解説vol.1はこちらです。

「データを Props 経由で渡す」の解説

公式チュートリアルのここの部分からの解説です(+Ctrlで別タブでひらく)

ここでは、propsという大事な概念について解説していきます。

この章の最終目的を先に言うと、こちらのようにマス目に数字を表示させることです。
image

1マス=1<Square/>コンポーネントです。
<Square/>コンポーネントに数字を渡して、それを表示させます。

<Square/>コンポーネントの親(呼び出し元)である<Board/>コンポーネントの以下のところで、自身のrenderSquareメソッドに引数として数字を渡していましたね。

      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>

renderSquareメソッド内で引数として受け取ったiに対して特に何もしていませんでしたが、今回は、受け取ったiをvalueという名前をつけたpropsとして子コンポーネントSquareに渡すように以下のように変更します。

コンポーネント呼び出しのときに、属性のような形で設定すると、呼ばれたコンポーネントのほうで値を取り出すことができるのです。

class Board extends React.Component {
  renderSquare(i) {
    return <Square value={i} />;
  }
}

Squareコンポーネントで渡された値を表示するように、{/* TODO */}{this.props.value}に書き換えます。

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {this.props.value}
      </button>
    );
  }
}

これで、親であるBoardコンポーネントから子であるSquareコンポーネントに「propsを渡す」ことができました。
表示内容をコンポーネントにベタ書きせず、呼び出し元から値を渡してあげることによって、コンポーネントの再利用が可能になるんですね。

※この章はほとんど追加の解説は必要なかったです。

次回は「インタラクティブなコンポーネントを作る」の解説をしたいと思います。

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

公式Reactチュートリアルの解説vol.1

公式Reactチュートリアルとは

Reactを勉強したいと思ったら、公式のチュートリアルがおすすめです。
実際にコードをいじりながら勉強ができるので、とても分かりやすいです。
とはいえ、初心者には難解な部分も多々あります。
この記事は、公式チュートリアルの内容をかみくだいて解説しています。
該当する公式チュートリアルの部分へのリンクを載せてあるので、並行して読んでいくとより理解が深まるのでおすすめですが、この記事単独だけでも進められるようにしてあります。

「これから作るもの」の解説

公式チュートリアルのここの部分からの解説です(+Ctrlで別タブでひらく)

公式チュートリアルでは、ゲームを作りながらReactを学んでいきます。
よくあるOXゲームです。
まず、完成版で一度遊んでみてください。
三目並べゲーム (tic-tac-toe) (+Ctrlで別タブで開く)

このゲームは、以下のようなルールと機能があります。

  • プレイヤーはXとOの2人で、交互に順番が回ってくる。
  • マス目をクリックすると、プレイヤーがXのときはX、OのときはOでマスが埋められる。
  • すでに埋まっているマス目は埋めれられない。
  • 縦、横、斜めのいずれかが1列揃った時点で終了。
  • 盤面の右上部に、次のプレイヤーが表示される。勝負がついた場合には勝者が表示される。
  • 盤面の右下部に、操作履歴がボタンとして表示され、ボタンをクリックするとその時点にさかのぼることができる。

image

「React とは?」の解説

公式チュートリアルのここの部分からの解説です(+Ctrlで別タブでひらく)

Reactは複雑なUIを、「コンポーネント」と呼ばれる小さく独立した部品から組み立てることができます。
大きなコンポーネントは小さなコンポーネントの組み合わせで作成します。
コンポーネントは、props(“properties” の略)と呼ばれるパラメータを受け取り、renderメソッドを通じて、画面上に表示したいものの説明書きである“React要素” を返します。
たいていのReact開発者は、簡単にReact要素を生成できる “JSX” と呼ばれる構文を使用しています。
そして、React DOMが、React要素に合致するようにDOMを更新する作業をします。
JSXについてはのちほどまた詳しく学ぶとして、とりあえずそういうものだということで進めていきましょう。

「スターターコードの中身を確認する」の解説

公式チュートリアルのここの部分からの解説です(+Ctrlで別タブでひらく)

完成版は閉じて、まずはスターターコードを開いてみてください。
スターターコード(+Ctrlで別タブで開く)

最低限のコードが書いてあります。
このコードに少しずつ肉付けして完成させていく流れで学んでいきます。

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

↓表示は今こんな感じですね。
image

公式チュートリアルでは、このコードを見ると以下のことが分かります、と書いてあります。
分かる人は、これ以降の記事は読まなくても大丈夫です。
分からないよ!という人は、いっしょにコードを追ってみましょう。

コードを見てみると、3 つの React コンポーネントがあることが分かります。

- Square(正方形のマス目)
- Board(盤面)
- Game

Square(マス目)コンポーネントは 1 つの <button> をレンダーし、Board(盤面)が9 個のマス目をレンダーしています。
Game コンポーネントは盤面と、後ほど埋めることになるプレースホルダーを描画しています。
この時点ではインタラクティブなコンポーネントはありません。

さて、このコード、どこから追えばいいのかな?と悩むところですが、描画は一番下のここでしていますので、ここからたどってみましょう。

// ========================================

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

ReactDOM.render()メソッドを使って、コンポーネントを レンダリング(描画)しています。

以下は ReactDOM.render()メソッドの書式です。

ReactDOM.render(element, container[, callback])
- element:React要素やコンポーネント
- container:描画先となる実際のDOMノード(コンテナ)
- callback:オプションのコールバック関数

参考:ReactDOM.renderの公式リファレンス(+Ctrlで別タブで開く)

今回の場合は、
- element:<Game />
- container:document.getElementById('root')
- callback:なし

ですね。

スターターコードのHTMLを見てみると、9行目にrootというidのdiv要素があります。
(スターターコードを開いているツール「CodePen」の画面左の「HTML」と表示されている部分を開くと見られます。)

<Game />コンポーネントは、このdiv要素の子要素として差し込まれます。

<div id="root"></div>

ReactDOM.render()メソッドの第1引数(<Game />の部分)を変更すると表示が変わりますので、ちょっと試してみてください(あとで元に戻してくださいね)。

次に、<Game />コンポーネントのコードを見てみましょう。

class Game extends React.Component {
  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

<Board />コンポーネントが埋め込まれています。
<div><ol>などもありますね。
一見HTMLに見えますが、これもJSXです。

ここで、JSXについて少し説明しておきます。

JSXはFacebook社が考案したHTMLやXMLに似たJavaScript構文の拡張です。
以下で仕様(Draft)が公開されています。
Draft: JSX Specification / XML-LIKE SYNTAX EXTENSION TO ECMASCRIPT

JSXではHTMLやXMLのタグのように見えるJSX独自の構文を使って画面に描画したい内容を記述します。

//JSX を使った記述
const element = <h1>Hello world!</h1>

JSXはHTMLタグのように見えますが React要素(オブジェクト)を生成するJavaScriptの式です。
内部ではオブジェクトを受け取り、ReactのメソッドcreateElement()を実行しています。
ビルドの際に Babel(コンパイラ)はJSXを以下のようなReact.createElement()メソッドを使った普通のJavascriptに変換します。

//JSXを使わずReact.createElement()を使った記述
const element = React.createElement('h1', null, 'Hello world!')

つまり、JSXはReact.createElement()のシンタックスシュガー(糖衣構文)にすぎません。
JSXを使うとReact.createElement()を直接呼ばすに HTML タグに似たような形式で記述できるので、見た目がわかりやすくなります。
ReactでJSXは必須ではありませんが、JSXを使った方が簡潔に記述することができます。
JSXがどのようにJavaScriptへ変換されるのかをテストしたい場合は、
オンラインBabelコンパイラ(+Ctrlで別タブで開く)で試すことができます。

では<Game />コンポーネントのコードの説明を続けましょう。

このコンポーネントに埋め込まれている<Board />だけでなく、<div><ol>もコンポーネントです。
小文字から始まるのは、組み込みの DOM コンポーネントで、これらはそれぞれ<div><ol>といった文字列に変換されてReact.createElementに渡されます。
一方で <Board /> のように大文字で始まる型はReact.createElement(Board)にコンパイルされ、JavaScriptファイルにおいて定義あるいはインポートされたコンポーネント(カスタムコンポーネント)を参照します。
参照:ユーザ定義のコンポーネントの名前は大文字で始めること(+Ctrlで別タブで開く)

<Game />コンポーネントのコードの以下の部分は、盤面右上部の、次は誰のターンかを表示することになる部分です。

<div>{/* status */}</div>

<Game />コンポーネントのコードの以下の部分は、盤面右下部の操作履歴を表示することになる部分です。

<ol>{/* TODO */}</ol>

次に、<Board />コンポーネントのコードを見てみましょう。

class Board extends React.Component {
  renderSquare(i) {
    return <Square />;
  }

  render() {
    const status = 'Next player: X';

    return (
      <div>
        <div className="status">{status}</div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

コードをあたまから見ていきましょう。

  renderSquare(i) {
    return <Square />;
  }

は、iを引数で受け取るrenderSquare()というメソッドです。
(このメソッドについてはまたあとで出てきます。)

<Game />コンポーネントの<Board />が埋め込まれたところに、その下のrender()の中身が展開されます。

まず、statusという変数が定義されていますね。中身は「Next player: X」というテキストです。
どのプレイヤーのターンかという表示です。

const status = 'Next player: X';

<Board />コンポーネントのここで表示しています。

<div className="status">{status}</div>

その下へいくと、3行の<div className="board-row"></div>要素があります。
1行につき3回、渡す数字を変えてrenderSquare()メソッドを呼び出しています。

ここで、renderSquare()メソッドに戻ってみると、中で<Square />コンポーネントを呼び出しています。
なので、9個の<Square />コンポーネントを3行3列で描画する、ということになります。

最後に<Square />コンポーネントのコードを見てみましょう。

class Square extends React.Component {
  render() {
    return (
      <button className="square">
        {/* TODO */}
      </button>
    );
  }
}

表示がないので見た目ではわかりづらいのですが、ボタンを返しています。
いまのところボタンの表示は空のようですね。

これでコードにひと通り目を通すことができました。
何となく全体の構成がわかったと思います。

以上の説明を踏まえた上で、公式チュートリアルにある以下の文章が理解できるようになっているでしょうか?

コードを見てみると、3 つの React コンポーネントがあることが分かります。

- Square(正方形のマス目)
- Board(盤面)
- Game

Square(マス目)コンポーネントは 1 つの <button> をレンダーし、Board(盤面)が9 個のマス目をレンダーしています。
Game コンポーネントは盤面と、後ほど埋めることになるプレースホルダーを描画しています。
この時点ではインタラクティブなコンポーネントはありません。

だいたい理解できたら、チュートリアルの続きをやっていきましょう。
全く理解できない場合は、このレッスンをもう一度読み直してみることをおすすめします。

次回は「データを Props 経由で渡す」の解説をしたいと思います。

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

Redux ToolkitでLoadingの状態を管理する

何をやるか

SPAを扱う上で、どのタイミングでローディングの表示をするのか、どのように管理するのか。
結構考えるのめんどくさいと思います。
↓こんなやつ
b39f6ff59f51a613294a5f21ef9382de.gif

Redux Toolkitを使えばシンプルにかけたので備忘録として残しておきます:writing_hand:
どのようにローディング状態管理しよう…とお考えの方がいれば、その解決の一つになればと思います:eyes:

使用している技術

  • React (reactHooks)
  • Redux (Redux-toolkit)
  • TypeScript

機能

今回はAPIを叩いてpending中にloadingを表示、
処理が終了したらloadingを非表示という機能を想定して書いていきます。

redux toolkitが用意してくれているextraReducersという機能を使って実装します!

pending → 実行開始〜結果が帰ってくるまでの間
fulfilled → 正常終了
rejected → エラー

となっているので、以下のように、
pendingの時のみ、isLoadingという項目をtrueにすることで、
APIが実行開始〜結果が帰ってくるまでの間の状態を管理します。

sampleSlice.tsx
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
import { fetchGet } from "./common/fetch"

interface selectUserProps {
  isLogin: boolean
}

// ↓このAPIを叩く想定で進める
export const getInfo = createAsyncThunk("admin/post",async () => {
  const url = "叩くAPIのURL"
  const result = await fetchGet(url)
  return result
  }
)

const sampleSlice = createSlice({
  name: "sample",
  // ↓初期値
  initialState: {
    isLoading:false
  },

  // extraReducersで状態をLoading管理する
  extraReducers: (builder) => {
    builder.addCase(getInfo.pending, (state, action) => {
      // APIの処理が始まったら isLoadingをtrueに
      state.isLoading = true
    })
    builder.addCase(getInfo.rejected, (state, action) => {
      // 失敗したら isLoadingをfalseに 
      state.isLoading = false
    })
    builder.addCase(getInfo.fulfilled, (state, action) => {
      // 成功しても isLoadingをfalseに 
      state.isLoading = false
    })
  },
})


// ローディング状態をexport
export const selectLoading = (state: any): selectUserProps => state.sample.isLoading
export default sampleSlice.reducer

fetch.ts
// 別に切り分けた関数
import axios from "axios"
axios.defaults.withCredentials = true

export const getInfo = async(url:string) =>{
  const result = await axios(url, {
    method: 'GET',
    )
  return result.data
}

UI

UI上では、スピナーを作成して、
先程のloading状態をインポートし、trueの場合だけ作成したスピナーを表示させます。

スピナー

画面全体にLoadingを表示させるコンポーネントです。
(待っている間はユーザー側の入力ができないので、ユーザー操作できるようにするには調整必要です)

fixedSpinner.tsx
import React, { useEffect, useState } from 'react'
import { css } from '@emotion/core'
import { Spinner } from '../../components/atoms/Spinner'

export const FixedSpinner = () => {
  return (
    <div css={spinnerOverRay}>
      <div css={spinnerWrap}>
        <Spinner />
      </div>
    </div>
  )
}

const spinnerOverRay = css`
  position: fixed;
  -ms-transform: translate(-50%, -50%);
  height: 100vh;
  width: 100vw;
  z-index: 999;
  background-color: rgba(245, 245, 245, 0.5);
`
const spinnerWrap = css`
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
`
Spinner.tsx
import React from 'react'
import { css } from '@emotion/core'

export const Spinner = () => {
  return <div css={spinner} />
}


export const spinner = css`
  width: 50px;
  height: 50px;
  border: 10px solid #dddddd;
  border-top-color: #aaaaaa;
  border-radius: 50%;
  animation: 1s spin infinite linear;
  @keyframes spin {
    from {
      transform: rotateZ(0deg);
    }
    to {
      transform: rotateZ(360deg);
    }
  }
`

Loading表示

useSelectorを使って、loading状態を取得し、
trueの場合のみ、先程作成したスピナーを表示します。

sample.tsx
import React from 'react'

// components
import { FixedSpinner } from '../../components/block/FixedSpinner'

// store
import { getInfo, selectLoading } from '../../../stores/slices/sampleSlice'

export const Sample = ({ setPath }: any) => {
  const dispatch = useDispatch()
  const loading = useSelector(selectLoading)

  const handleSubmit = () => {
    dispatch(getInfo())
  }

  return (
    <div>
      // loading が true の時、FixedSpinnerを表示
      {loading && <FixedSpinner />}
       <button css={SubmitBtn} onClick={handleSubmit}>
         API叩く
       </button>
    </div>
  )
}

感想

Redux Toolkitを使えば簡単にloadingの管理ができてハッピーでした:clap:
コンポーネントも使いまわせるので、使える部分はこれ使っていこうかな〜と考えております。

もっといい方法あればご教示ください:pray:

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

Ant designでカスタマイズしたもの、使いやすくしたもの

Ant designとは

https://ant.design/
Reactコンポーネントを、すぐに使える形(デザインされたもの)で返してくれる。
同時に、基本的な機能を揃え、開発をスムーズにしてくれる。

カスタマイズしたもの

省略した文字+ツールチップ

// 省略文字(かつ長い時切り捨て)とツールチップの表示
export const EllipsisTooltip = (props) => {
  const { text, toolTipText, maxSize, children } = props;
  const flgTruncate = maxSize ? text.length > maxSize : false;
  return (
    <Space>
      {flgTruncate ? text.slice(0, maxSize) + '' : text}
      {(text !== toolTipText || flgTruncate) && <Tooltip title={toolTipText}>{children}</Tooltip>}
    </Space>
  );
};

// use
<Link href={url} target="blank">
  <EllipsisTooltip text="" toolTipText={url}>URL <ExportOutlined />
  </EllipsisTooltip>
</Link>

テーブルのpaddingを狭め、子要素を囲むようなスタイルに変更

export const TableList = styled(Table)`
  .ant-table-thead > tr > th,
  .ant-table-tbody > tr > td,
  .ant-table tfoot > tr > th,
  .ant-table tfoot > tr > td {
    padding: 10px 10px;
  }
  .ant-table-tbody > tr.ant-table-expanded-row {
    > td {
      padding: 0;
    }
  }
  .ant-table-column-sorters {
    padding: 0;
  }
  .ant-table-tbody > tr > td > .ant-table-wrapper:only-child .ant-table {
    margin: 0;
  }
`;

// 子スタイル
export const ChildTable = styled(Table)`
  background-color: #ccc;
  .ant-table {
    font-size: 90%;
  }
  .ant-spin-container {
    /* padding: 24px 22px 20px 22px; */
    padding: 10px;
    background-color: #ccc;
    position: relative;
    &:before {
      content: '';
      display: block;
      position: absolute;
      left: 72px;
      top: 5px;
      width: 0;
      height: 0;
      border-style: solid;
      border-width: 0 8px 14px 8px;
      border-color: transparent transparent #fafafa transparent;
    }
  }
  .ant-table-thead > tr > th,
  .ant-table-tbody > tr > td,
  .ant-table tfoot > tr > th,
  .ant-table tfoot > tr > td {
    padding: 8px 10px;
  }
  .ant-table-content {
    overflow: auto !important;
  }

テーブル > ドロップダウンフィルター

dataから存在するデータをドロップダウンで表示

export const getFilterOption = (datalist, dataIndex) => {
  if (!datalist) return false;
  let array = [],
    filters = [];
  datalist.forEach((value) => {
    if (array.indexOf(value[dataIndex]) === -1) {
      array.push(value[dataIndex]);
      filters.push({ text: value[dataIndex], value: value[dataIndex] });
    }
  });

  return {
    filters: filters,
    onFilter: (value, record) => record[dataIndex].indexOf(value) === 0,
  };
};

テーブル > 単語検索フィルター

編集中

export const getSearchOption = (dataIndex) => {
  let searchInput;
  let searchState = {
    searchText: '',
    columnIndex: '',
  };

  const handleSearch = (selectedKeys, confirm, dataIndex) => {
    confirm();
    return { searchText: selectedKeys[0], columnIndex: dataIndex };
  };

  const handleReset = (clearFilters) => {
    clearFilters();
    return { searchText: false, columnIndex: false };
  };

  return {
    // eslint-disable-next-line react/display-name
    filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
      <div style={{ padding: 8 }}>
        <Input
          ref={(node) => {
            searchInput = node;
          }}
          placeholder={`検索したいキワードを入力してください`}
          value={selectedKeys[0]}
          onChange={(e) => setSelectedKeys(e.target.value ? [e.target.value] : [])}
          onPressEnter={() => {
            searchState = handleSearch(selectedKeys, confirm, dataIndex);
          }}
          style={{ width: 188, marginBottom: 8, display: 'block' }}
        />
        <Space>
          <Button
            type="primary"
            onClick={() => {
              searchState = handleSearch(selectedKeys, confirm, dataIndex);
            }}
            icon={<SearchOutlined />}
            size="small"
            style={{ width: 90 }}>
            検索
          </Button>
          <Button onClick={() => handleReset(clearFilters)} size="small" style={{ width: 90 }}>
            リセット
          </Button>
        </Space>
      </div>
    ),
    // eslint-disable-next-line react/display-name
    filterIcon: (filtered) => (
      <SearchOutlined
        style={
          filtered
            ? { color: '#ffffff', backgroundColor: '#000', padding: '8px', borderRadius: '50%' }
            : { padding: '8px' }
        }
      />
    ),
    onFilter: (value, record) =>
      record[dataIndex] ? record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()) : '',
    onFilterDropdownVisibleChange: (visible) => {
      if (visible) {
        setTimeout(() => searchInput.select(), 100);
      }
    },
  };
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで神経衰弱を作ってみた

卒業できるかな?

See the Pen RwGKbRJ by kotani (@kotani) on CodePen.

はじめに

最近注目されているReactについて記事を書こうと思います。
Reactで検索してみると、既に入門的な情報が転がっていますが、それらを読んだだけでは
なかなかイメージがつきにくいこともあり、Reactを使って自分で作ってみることにしました。
Reactの公式ページには三目並べのゲームがチュートリアルで紹介されてましたが、
私は神経衰弱を作ってみることで、Reactを体験してみようと思います。

基本的な知識

Reactとは「ユーザインターフェース構築のための JavaScript ライブラリ」だそうです。

コンポーネントと呼ばれる小さい部品を組み合わせることでアプリケーションを構築します。
その1つ1つのコンポーネントは状態(State)を持っており、外部から渡された情報 props
に従ってその状態を変化させることで、そのコンポーネントの挙動の制御を行います。

コンポーネント同士を疎結合にすることで、再利用したり管理しやすくなり、
どちらかというと大規模アプリケーション開発に向いていると言われてます。
なので、アニメーションといった実装には向いてないと思います。

そのほかにも、仮想DOMという仕組みによって高速描画を行ったり、
JSXの記法でJavaScriptの中にHTML似の構文を使える技術も取り入れています。

シングルページアプリケーション(SPA)開発にも適しており、iPhoneアプリや
androidアプリのように、ReactNativeフレームワークを使うことで、どちらの環境でも
動くアプリ開発もできるようです。

神経衰弱の概要

ゲームのイメージは、下記の通りです。

  • 5種類の絵柄が書かれた2対のカード合計10枚が伏せてあります。
  • スタートボタンをクリックするとカウントダウンタイマーが作動しゲーム開始となります。
  • 任意のカード1枚目をクリックすると、カードが反転し絵柄が表示されます。
  • 続けて別の2枚目のカードをクリックすると、同様に反転し絵柄が表示されます。
  • 2枚のカードの絵柄が揃ったら、カードの色をピンク色に変更してそのままにします。
  • 2枚のカードの絵柄が揃わなかったら、1秒程度後に2枚とも元の状態へと伏せます。
  • 全てのカードの絵柄が揃った場合は「おめでとう!」のメッセージが表示されます。
  • 全てのカードの絵柄を揃えることができないままタイムアウトになってしまった場合は
    「ゲームオーバー」のメッセージを表示させます。
  • カードが揃ったかどうかの判定結果は、下の方に「あたり」「はずれ」のメッセージを表示させます。

このような内容のゲームをReactで実装してみます。

準備

Reactを使った本格的な開発は、create-react-app という環境構築ツールを使うようですが、
ここでは、下記jsを外部から取り込むだけで使えるようになるお手軽な方法で実装します。

<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

HTMLの body 終了タグの直前に上記3行を記述して、scriptタグにtype="text/babel" 属性を追加します。JSX構文はまだ今のブラウザには対応していないので、Babelというツールを使って後方互換バージョンへ変換することで使えるようになっています。また、カードの画像を1個用意して、10枚分のカードが並んだ状態にCSS調整しておきます。

コンポーネントの説明

ここでは、コンポーネントの少し具体的な内容をコードと共に紹介していきます。

1 構成

下記のように、3つのコンポーネントで構成されています。

①Gameコンポーネント(②の親コンポーネント)
      |
②Tableコンポーネント(③の親コンポーネント)
      |
③Cardコンポーネント

2 Gameコンポーネント

下記は、divタグのところにReactコンポーネントを組み込む処理の記述になります。

<div id="root"></div>

React.Component クラスを継承したGameクラスを定義し、render()メソッドの中でGameコンポーネントを記述します。Gameコンポーネントの中にTableコンポーネントが含まれていることが分かるかと思います。

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="main">
                    <Table />
                </div>
            </div>
        );
    }
}
ReactDOM.render(
    <Game />,
    document.getElementById('root')
);

3 Tableコンポーネント1(初期化)

コードが長いので分割します。TableコンポーネントもReact.Componentを継承します。
constructorの中では、getInitialState()をコールして初期化した各種状態のプロパティを持つオブジェクトをstateに格納します。shuffleは、毎回絵柄の並びが入れ替わるように配列内部をランダムに置換します。

class Table extends React.Component {
    constructor(props) {
        super(props);
        this.state = this.getInitialState();
    }
    getInitialState = () => {
        let arr = Array("", "", "", "", "", "", "", "", "", "");
        let sts = Array(10).fill(0);
        arr = this.shuffle(arr);
        return {
            cards: arr,
            status: sts,
            ready: -1,
            message: '',
            count: 15,
            timer: null,
            title:'',
            run:false,
            overlay: 'overlay'
        }
    }
    shuffle = ([...array]) => {
        for (let i = array.length - 1; i >= 0; i--) {
            const j = Math.floor(Math.random() * (i + 1));
            [array[i], array[j]] = [array[j], array[i]];
        }
        return array;
    }

    :
    :

}

4 Tableコンポーネント2(click時処理)

handleClickは、カードをクリックした時に呼ばれるメソッドです。
i はカードのindexであり、クリックされたカードに紐づいた数値(0~9)が渡されます。
stateで持っている状態は下記の通りです。

  • cards :絵柄 ("♡"や"♧"など)の配列
  • status : カードの状態を表す配列 0(初期値)、1(クリックされ絵柄が表示されている状態) 2(2枚の絵柄が揃った状態)、3(2枚の絵柄が異なった状態)
  • ready : クリックされたカードの状態 -1(初期値)、0~9(クリック1枚目のindex)
    -2(「はずれ」が表示された状態)
  • message : 判定後のメッセージ "" または "あたり" または "はずれ"
  • count : カウントダウンの数値(1づつ減算、残り:xx秒 のところに表示される)
  • timer : setInterval()の戻り値を格納
  • title : 画面中央に表示する文言 "" または "おめでとうございます。卒業です!" または "ゲームオーバー"
  • run : ゲームの状態 true(ゲーム実行中)、false(ゲーム停止中)
  • overlay : オーバーレイのクラス指定 "" または "overlay" または "overlay overlay-end"

readyの値を見て、1枚目がクリックされたのか?2枚目がクリックされたのか?の判定に使用しています。

handleClick(i) {
    const sts = this.state.status.slice();
    //未選択以外をクリックされた時は無視
    if (this.state.status[i] != 0) {
        return;
    }
    //ゲームスタートしてなければ無視
    let run = this.state.run;
    if (!run){
        return;
    }
    let ready = -1;
    let message = "";
    let title = "";
    let overlay = "";
    if (this.state.ready == -2) {
        return;
    } else if (this.state.ready == -1) {
        //1枚目をクリックした時の処理
        sts[i] = 1;
        ready = i;
    } else if (this.state.ready != i) {
        //2枚目をクリックした時の処理
        sts[i] = 1;
        //2枚揃ったかどうかを判定
        if (this.state.cards[this.state.ready] == this.state.cards[i]) {
            //2枚揃った!
            message = "あたり";
            sts[this.state.ready] = 2;
            sts[i] = 2;
            if (!this.isFinish(sts)) {
                setTimeout(() => {
                    this.cardClear();
                }, 800);
            } else {
                message = '';
                run = false;
                title = "おめでとうございます。卒業です!";
                overlay = 'overlay overlay-end';
                clearInterval(this.state.timer);
            }
        } else {
            //揃ってなかった!
            message = "はずれ";
            ready = -2;
            sts[this.state.ready] = 3;
            sts[i] = 3;
            //少し経過後に元に戻す
            const rollbacksts = this.state.status.slice();
            rollbacksts[this.state.ready] = 0;
            rollbacksts[i] = 0;
            setTimeout(() => {
                this.cardClear();
                this.cardReset(rollbacksts);
            }, 800);
        }
    }
    this.setState({
        status: sts,
        ready: ready,
        message: message,
        run: run,
        title: title,
        overlay: overlay
    });
}

5 Tableコンポーネント3(ゲーム開始)

gameStartは、スタートボタンが押された場合に呼ばれます。
runを参照して、既にゲーム実行中であれば何もせずにリターンして、
そうでない場合はsetInterval()を使ってカウントダウンタイマーを起動します。

gameStart= () => {
    if(this.state.run){
        return;
    }
    this.setState(this.getInitialState());
    const timer = setInterval(() => this.countDown(), 1000);
    this.setState({
        timer: timer,
        run: true,
        overlay: ''
    });
}

6 Tableコンポーネント4(その他)

カードの状態を元の伏せた状態に戻します。

cardReset = (sts) => {
    this.setState({
        status: sts,
        ready: -1
    });
}

わずかな時間「あたり」「はずれ」のメッセージを表示させたあとは表示削除します。

cardClear = () => {
    this.setState({
        message: ""
    });
}

全てのカードが揃ったかどうかを判定します。stsの値で2以外のものが1つでも存在したら、
まだ伏せてあるカードがあると判定します。

isFinish = (sts) => {
    let flg = true;
    for (let i = 0; i < sts.length; i++) {
        if (sts[i] != 2) {
            flg = false;
            break;
        }
    }
    return flg;
}

タイマーによって1秒ごとに呼ばれるメソッドです。
countの値を1づつ減らしていき、0になったらゲームオーバーと判定します。

countDown = () => {
    let nextCount = this.state.count-1;
    if(nextCount < 1) {
        this.setState({
            message: '',
            count: 0,
            run: false,
            title: 'ゲームオーバー',
            overlay: 'overlay overlay-end'
        });
        clearInterval(this.state.timer);
    } else {
        this.setState({
            count: nextCount
        });
    }
}

カードの並びを構成するHTMLをJSXで定義します。

renderCard(i) {
    return (
        <Card key={i}
            number={this.state.cards[i]}
            ready={this.state.status[i]}
            onClick={() => this.handleClick(i)}
        />
    );
}
render() {
    const cards = [];
    for (let i = 0; i < 10; i++) {
        cards.push(this.renderCard(i));
    }
    return (
        <div>
            <button className="start-button" onClick={this.gameStart}>スタート</button>
            <div className="count-number">残り:{this.state.count}</div>
            <div className="table">
                {cards}
            </div>
            <div className="status">{this.state.message}</div>
            <div className={this.state.overlay}><p className="title">{this.state.title}</p></div>
        </div>
    );
}

7 Cardコンポーネント

関数型コンポーネントとして定義しています。
propsで渡されたreadyの値に従ってカードのclassName属性を設定します。(「裏」「表」「当たり」)

function Card(props) {
    let cardStyle = 'card card-ura';
    let numStyle = 'omote';
    switch(props.ready){
        case 1:
            numStyle = 'ura';
            break;
        case 2:
            numStyle = "ura atari";
            break;
        case 3:
            numStyle = "ura hazure";
            break;
        default:
            cardStyle = 'card card-omote';
            break;
    }
    return (
        <button className={cardStyle} onClick={props.onClick}>
            <div className={numStyle}><span>{props.number}</span></div>
        </button>
    );
}

最後に

Reactは、少々学習コストが高く感じられました。経験の浅い方が、これにいきなり入るのは
ちょっとハードルが高いように思います。上手に設計し構築することが出来るようになれば、
大規模なアプリでも耐えうる実力が十分ありそうなライブラリだということがわかりました。

コンポーネント間のデータの受け渡しはpropsを通じて行われますが、コンポーネントの
数が増えたり親子関係が深くなると、親から子へ通知する処理やコールバック関数が増えてしまい、
複雑化するようです。

そこで、React-Reduxという状態(state)管理ライブラリをうまく活用することで、
コンポーネント間の情報の受け渡しや状態管理をより簡単に実現することができます。

この神経衰弱は少しstate管理が煩雑になっているようなので、React-Reduxで実装した場合、
どのような書き方に変わるのか?どのような効果があるのか?を次回試してみようと思います。


:christmas_tree: FORK Advent Calendar 2020
:arrow_left: 17日目 Reactアプリケーション開発入門 @sy12345
:arrow_right: 19日目 Vue.jsのSSGフレームワークGridsomeはすごいぞ!! @Kodak_tmo

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

ubuntuでreactの環境構築諦めた件

ubuntuでreactを使う。

プロジェクトディレクトリに移動して

npx create-react-app todoapp

を実行したら
not foundram Files/nodejs/npxと返ってきた。

恐らくubuntuにnode.jsを入れてないからである。

このサイトのステップ1の通りにいれた。

しかしまた同じエラーが返ってきた。

すいません。無理です。コマンドラインと仲良くなれません。
諦めました、、、(笑)

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

react基礎について

reactはUI構築のためのライブラリである。
そもそもライブラリ・フレームワーク・UIとは何か?

ライブラリ

一言で言うと「様々なプログラムを保存しているところ」
プログラムを組んでいく中で何度も目にするコードがある。
それらを一部修正をして汎用性の高いコードや関数をまとめておく。
けど、便利なコードや関数をまとめてるだけで、ライブラリだけ動かすことは出来ない

メリット
・プログラムの動作が軽くなる
・プログラムの開発スピードが上がる
・開発者の勉強になる

フレームワーク

端的にアプリやシステム開発のために必要な機能を用意した枠組み
経験が浅くてもプログラムを作成することが出来る。

メリット
・開発コストを抑える。
・バグを減らす
・コードがシンプルになる

フレームワークは、必要なものをまとめた枠組のこと
ライブラリは、便利なソースや関数の部品の集まり

https://xtech.nikkei.com/it/article/lecture/20070205/260697/

ユーザーインターフェース

窓口みたいなもの
何か2つのものの間で情報などのやりとりを行うときの方法や方式

https://techacademy.jp/magazine/8364

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

Next.jsでMDXを使ってみる

目次

  1. MDXとは?
  2. Next.jsで使ってみる
  3. MDXのシンタックスをいろいろ試してみる
  4. 所感
  5. 余談

MDXとは?

  • 公式サイト: https://mdxjs.com/
  • markdownとjsx両方を同時に書ける
  • markdownはjsxをimportできる
  • MDXはruntimeを持たない、ビルドで解決される

といった特徴があるようです。

# Hello, *world*!

Below is an example of JSX embedded in Markdown. <br /> **Try and change
the background color!**

<div style={{ padding: '20px', backgroundColor: 'tomato' }}>
  <h3>This is JSX</h3>
</div>

(MDXは流石にシンタックスハイライトされない...)

ドキュメントやブログを書く際に便利ですね。

Next.jsでMDXを使ったブログを作ってみる

https://mdxjs.com/getting-started/next 公式の通りに設定を入れると、あとはpages以下に好きなようにmdxファイルを作成するだけです。next devしましょう。

markdownが吐き出すタグを変更する

MDXの中に書いたmarkdownの構文はjsxに変換されますが、どのコンポーネントに変換するかを設定することができます。
https://mdxjs.com/getting-started/#mdxprovider
公式サイトにある通り、MDXProviderを使います。今回はNext.jsなので_app.jsに書いてしまいます。
例として、markdownのcode blockにシンタックスハイライトを効かせます。
https://mdxjs.com/guides/syntax-highlighting
(最近でたnext/imageなんかをここで使うと良さそうですね)

_app.js
import { MDXProvider } from "@mdx-js/react"
import { CodeBlock } from "../components/layouts/CodeBlock"

const mdComponents = {
  code: CodeBlock
}

const MyApp = ({ Component, pageProps }) => {
  return (
    <MDXProvider components={mdComponents}>
      <Component { ...pageProps } />
    </MDXProvider>
  )
}

export default MyApp

これでMDXに書いたcode blockはシンタックスハイライトが効くはずですね。
スクリーンショット 2020-12-15 3.22.01.png

Layoutを定義してみる

MDXだと、特定のページにレイアウトを適用するのが非常に簡単です。
今回は例として、タイトルタグを設定するレイアウトを作成します。

components/layout/BaseLayout.jsx
import Head from "next/head"

export const BaseLayout = ({ meta, children }) => (
  <>
    <Head>
      <title>{meta.title}</title>
    </Head>
    { children }
  </>
)

mdxでLayoutを適用します。

.mdx
import { BaseLayout } from "../components/layouts/BaseLayout"

export const meta = {
  title: "ホーム"
}

# レイアウトを適用したいMDX

export default ({ children }) => (
  <BaseLayout meta={meta}>
    {children}
  </BaseLayout>
)

MDX内でdefault exportすることができて、childrenにはこのMDXのJSXが渡ってきます。

MDXのシンタックスをいろいろ試してみる

index.mdx
import { Counter } from "../components/Counter"
import AboutContent, { meta as aboutMeta } from "./about.mdx"

# サンプルです

<div>
  { ["hoge", "fuga"].map(word => (
    <p>{word}</p>
  )) }
  # これはh1にならない
</div>

<!-- コメントアウト -->

<div>
  {/* コメントアウト */}
</div>

(qiitaのmarkdownとバッティングするのでいったん回避)
\```tsx
// コードブロックが書ける
type Props = { name: string }
export const Component: FC<Props> = ({ name }) => <>{name}</>
\```

- exportのない変数宣言はできない

export const hoge = "hoge"

- 文章とexportの間に空行がない場合は文章として扱われる
- constであればちゃんとMDX内でも再定義できない
export const hoge = "hoge"

<!--
export あああ
これはexport構文と解釈されてエラーとなる
-->

もちろんコンポーネントも使える
<Counter />

別のMDXをimportすることもできる(jsxなら当たり前ではある)
<AboutContent />
<div>
  title: { aboutMeta.title }
</div>

定義済みの変数があったりする
import MDXContent from "./about.mdx" // MDXContentはjsxで宣言される関数名ぽい
about.mdx
export const meta = {
  title: "about"
}

# aboutページです

vercelに↑のmdxをデプロイしたので、興味がある方は実際にどう表示されているか確認してみてください。
https://ikea-dless-mdx-sample.vercel.app/

所感

Next.js + MDXはドキュメントやブログでは結構使えるんじゃないかなと思っています。実際はfsでゴニョゴニョする部分があったり、検索機能を入れようと思うと一筋縄ではいかなさそうだったりすると思いますが...
gatsbyjsとの比較はしていないので、どちらが良いかはわかりませんが、vercelも込みで考えるとこの辺のエコシステムはNext.js強いよなーと思います。

余談

https://storybook.js.org/docs/react/writing-docs/mdx
以前、試験的に導入したのですが、storybookもMDXに対応していて、これが結構コンポーネントのドキュメントを書くのには向いている気がしています。仕様と実際のコンポーネントが同時に確認できて、非常に良いです。
加えて、MDXでもstoryshotsでドキュメント化したコンポーネントは漏れなくsnapshotテスト対象にできたりします。

button-story-default-docs-code.png

参考

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

Progate React学習

Reactとは

Reactとは、Javascriptのフレームワークの一つです。WebのUI(見た目や機能)をつくることが出来ます。

Reactの基本的な使い方

renderというメソッド内にreturnを記述することでプラウザに文字を表示させることが出来ます。

render(){
  return(
    <h1>Hello World</h1>
  );
 }

JSXは、HTMLと同じような記述をすることができます。複数の要素がある場合は、

タグで囲んで、1つの要素にまとめます。
JSXを{/* */}で囲むと、その部分はコメントになります。
render(){
  return(
   <div>
    <h1>h1です</h1>
    <h2>h2です</h2>
    <p>pです</p>
   </div>
  );
 }

{/*この部分はコメントです*/}

imgタグは、HTMLでは、<img src='画像のURL'>のように閉じタグが必要ありませんでしたが、一方、JSXでは閉じタグが必要になります。<img src='画像のURL' />のように、タグの終わりにスラッシュ/を記述します。

render(){
  return(
    <img src="URL"/>
  )
}

App.jsは以下の要素で構成されています。

import React from "react";

class App extends React.Component {
 render(){
  return (
      <h1>Hello React</h1>
    );
 }
}
 export default App;

最後に

洗練さんでインターンの課題(Progate 学習コース PythonⅠ~Ⅲ)です。
Progate React

最後に
現在ここでインターンしています。
まだまだ駆け出しですが頑張ります!やる気は人一倍です!
洗練

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

Create React Appで生成したプロジェクトで開発時のみESLintを(ほぼ)無効化する

元ネタはこのissueです。
https://github.com/facebook/create-react-app/issues/9929

Create React AppのESLintに対する姿勢

Create React Appのリポジトリでは度々ESLintを無効化したい、というissueが立ってきました。例えば#808はこの件について触れられています。
2020年12月現在の最新バージョン4.0.1でも、公式に対応するための設定は用意されていません。これは意図的なもので、disable eslint #2157によれば

Our rules are specifically picked to not enforce style, and to only find logical mistakes.

とのことです。しかしながら、ファイル数が膨大になっている場合、ESLint自体がホットリロードのパフォーマンスに深刻な影響を与え、DXを著しく低下させます。TypeScriptやPrettier周りのルールを多く設定している私のプロジェクトでは、保存のたびに10~20秒程度待たされていました。

.eslintrc.jsの利用

ESLintの設定ファイルは通常JSON形式で指定しますが、webpack等他の多くの例に漏れず、JavaScriptで記述することもできます。環境変数で分岐させれば、開発時のみESLintを無効化(無力化)させることが可能になりますね。

.eslintrc.js
/** @type {import('eslint').Linter.Config} */
const config = {
  // 設定の内容
};

if (process.env.DISABLE_ESLINT) {
  config.ignorePatterns = ["**/*.ts", "**/*.tsx"];
}

module.exports = config;

JSDocを記述しておくとIDEの補完が効くのでお勧めです。

package.json
{
  "scripts": {
    "start": "DISABLE_ESLINT=1 react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test"
  }
}

注意点として、すべてのファイルがignorePatternsにマッチするとLint対象がない、という旨のエラーになってしまいます。適当なglobパターンを指定するようにしましょう。

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

「Rails APIモードとReact Hooksを使ってToDoリストを作る」をdocker上に配置する

目的

これもやったことの備忘録が目的になります。
Know Howより、How to methodな内容にします

背景

バックエンドとフロントエンドを分けたアプリケーションをdockerにあと乗せする記事がなかったため。
せっかくやったのだから記事にしちゃおうって考えです。

アウトライン

  1. docker-compose.ymlを用意する
  2. Dockerfile(Backend用)を用意する
  3. entrypoint.shを用意する
  4. Dockerfile(Frontend用)を用意する
  5. rails側の調整
  6. axiosmaterial-uiのインストール

やってみよう!

docker-compose.yml

既存のrailsアプリ等に「後からdocker」を用意する場合でも、
アプリを作り始める時でも、やることはほとんど変わりません。

docker-compose.ymlはメインのディレクトリ上に配置します。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:5.6
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: root
    volumes:
      - mysql-data:/var/lib/mysql 
    ports:
      - "4306:3306" #別件で3306は使っていたので4306を指定しました

  app:
    build: 
      context: .
      dockerfile: Dockerfile_back
    command: /bin/sh -c "rm -f /myapp/tmp/pids/server.pid && bundle exec rails s -p 3001 -b '0.0.0.0'"
    image: rails:dev
    volumes:
      - .:/myapp    #myappというところは任意で設定してください
      - ./app/vendor/bundle:/myapp/vendor/bundle 
    environment:
      TZ: Asia/Tokyo
      RAILS_ENV: development
    ports:
      - "3001:3001"
    depends_on: 
      - db

  front:
    build: 
      context: todo_front
      dockerfile: Dockerfile_front
    volumes:
      - ./todo_front:/todo_front
    command: /bin/sh -c "cd todo_front && yarn && yarn start"
    ports:
      - "3000:3000"

volumes:
  mysql-data:
  bundle: 

Dockerfile(バックエンド用)を用意する

entrypoint.shを用意する

Rubyのバージョンを既存のrailsアプリのRubyバージョンを合わせました。
それとmyappというのは任意になります。合わせてもOKです!
SQLがmysql-clientと入力するとエラーになります。
いつしか、maliadb-clientに統一されたようです。

Dockerfile_backentrypoint.shdocker-compose.ymlと同じディレクトリ階層に配置しています。

Dockerfile_back
FROM ruby:2.6.3

RUN apt-get update && \
    apt-get install -y mariadb-client nodejs vim

RUN mkdir /myapp

WORKDIR /myapp

ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock

RUN gem install bundler
RUN bundle install

ADD . /myapp

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3001

CMD ["rails", "server", "-b", "0.0.0.0"]
entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

Dockerfile(frontend用)

todo_frontディレクトリ上に配置します。

Dockerfile_front
FROM node:14

Rails側の調整

database.yml'を調整してあげます。
socket通信になっているので、
host: db`に変えてあげます。

database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
- socket: /tmp/mysql.sock
+ host: db

axiosmaterial-uiのインストール

コンテナをバックグラウンドで立ち上げます。
そして、axiosやmaterial-uiをインストールします。
ちなみにexecはすでにコンテナが立ち上がっている状態で使えます。
コンテナが立ち上がっていない時はrunを使います。
終わったら、dbを作って、マイグレートして、植えて終わりになります。

console
$ docker-compose up -d #コンテナを立ち上げる
$ docker-compose exec front npm install axios
$ docker-compose exec front npm install @material-ui/core
$ docker-compose exec app bin/rails db:create
$ docker-compose exec app bin/rails db:migrate
$ docker-compose exec app bin/rails db:seed

localhost:3000にアクセスすると、前回と同じ画面が立ち上がるはずです。
終わり

終わりに

今回は、docker for Macの調子が悪く、何度もattachが発生しました。
再起動すると次のプロセスに行けました。
なんだろう、もう少しdockerの勉強した方がいいかなと思いました。

Zennに投稿した記事

https://zenn.dev/nicoryo/articles/2020121402tech

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