20200812のReactに関する記事は16件です。

VS CodeのスニペットでReactのコンポーネント雛形を作る

コンポーネントの雛形をVS Codeのスニペットに登録しておくと捗ります♪

スニペットの設定手順

  1. VS Codeの Code > Preferences > User Snippets
  2. tsxファイルのスニペットを作る場合typescriptreactと入力
    VS Codeではスニペットをファイル種別ごとにjsonファイルで管理します。
    スクリーンショット 2020-08-12 23.14.41.png

  3. 以下をコピペ
    以下はReact Nativeのコンポーネントの一例です。お好みで変更して下さい。

{
    "React Native Component": {
        "prefix": "react-native-component",
        "body": [
            "import React from \"react\";",
            "import { View, StyleSheet } from \"react-native\";",
            "",
            "type Props = {};",
            "",
            "export const ${TM_FILENAME_BASE}: React.FC<Props> = ({}) => {",
            "\treturn (",
            "\t\t<View style={styles.container}>",
            "\t\t\t<View />",
            "\t\t</View>",
            "\t)",
            "};",
            "",
            "const styles = StyleSheet.create({",
            "\tcontainer: {}",
            "});"
        ],
        "description": "React Native component"
    }
}

スニペットの利用

エディタでreact-native-componentと打ち込んでTabキー。

スニペットが挿入されます!
コンポーネント名はファイル名が自動的に反映されます。

9ab7fc4ed4df7c0297cd34d53750df23.gif

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

React Tutorial 「改良のアイデア」の実装

React Tutorial

React公式のチュートリアルページです。

三目並べゲームを一通りハンズオンで実装します。
その後、改良のアイデアを提案されます。

JavaScriptの練習がてら挑戦してみました。

React Tutorial

時間がある場合や、今回身につけた新しいスキルを練習してみたい場合に、あなたが挑戦できる改良のアイデアを以下にリストアップしています。後ろの方ほど難易度が上がります:

1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
2. 着手履歴のリスト中で現在選択されているアイテムをボールドにする。
3. Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
4. 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
5. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
6. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

ここから続きを実装しました。

ソースコード

開く
index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import classNames from "classnames";

function Square(props) {
  const squareClass = classNames("square", { "high-light": props.winnerSquare === 0 || props.winnerSquare });
  return (
    <button className={squareClass} onClick={props.onClick}>
      {props.value}
    </button>
  );
}

class Board extends React.Component {
  renderSquare(i) {
    const winnerSquare = this.props.winnerSquares && this.props.winnerSquares.includes(i) ? i : null;
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
        winnerSquare={winnerSquare}
      />
    );
  }

  render() {
    const boardRows = [];
    let k = 0;
    for (let i = 0; i < 3; i++) {
      const threeSquares = [];
      for (let j = 0; j < 3; j++) {
        threeSquares.push(this.renderSquare(k++));
      }
      boardRows.push(<div className="board-row">{threeSquares}</div>);
    }
    return <div>{boardRows}</div>;
  }
}

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [
        {
          squares: Array(9).fill(null),
          player: null,
          col: 0,
          row: 0,
        },
      ],
      stepNumber: 0,
      xIsNext: true,
      choosedHistory: null,
      isAsc: true,
    };
  }

  handleClick(i) {
    const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares)[0] || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? "X" : "O";
    const [col, row] = this.convertColAndRow(i);
    this.setState({
      history: history.concat([
        {
          squares: squares,
          player: squares[i],
          col: col,
          row: row,
        },
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
      choosedHistory: null,
    });
  }

  convertColAndRow(i) {
    const colAndRow = [];
    for (let i = 1; i <= 3; i++) {
      for (let j = 1; j <= 3; j++) {
        colAndRow.push([j, i]);
      }
    }
    return colAndRow[i];
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      choosedHistory: step,
      xIsNext: step % 2 === 0,
    });
  }

  changeOrder() {
    this.setState({
      isAsc: !this.state.isAsc,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const [winner, winnerSquares] = calculateWinner(current.squares);
    console.log(winnerSquares);

    const moves = history.map((step, move) => {
      const desc = move ? "Go to move #" + move : "Go to game start";
      const info = step.player ? `player: ${step.player}, col: ${step.col}, row: ${step.row}` : "";

      const button = (i) =>
        <button className={move === this.state.choosedHistory ? "bold" : ""} onClick={() => this.jumpTo(i)}>{desc}</button>

      return (
        <li key={move}>
          {button(move)}
          <span>{info}</span>
        </li>
      );
    });

    let status;
    if (winner) {
      status = "Winner: " + winner;
    } else {
      status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    }

    let drawMessage;
    if (!winner && !current.squares.includes(null)) {
      drawMessage = "The result is a draw.";
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board squares={current.squares} onClick={(i) => this.handleClick(i)} winnerSquares={winnerSquares} />
        </div>
        <div className="game-info">
          <div>
            {status}
            <button className="toggle-btn" onClick={() => this.changeOrder()}>
              {this.state.isAsc ? "Desc" : "Asc"}
            </button>
            <span>{drawMessage}</span>
          </div>

          <ol className={this.state.isAsc ? "asc-list" : "desc-list"}>{moves}</ol>
        </div>
      </div>
    );
  }
}

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

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

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return [squares[a], lines[i]];
    }
  }
  return [null, null];
}
index.css
body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol,
ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

span {
  margin-left: 6px;
}

.bold {
  font-weight: bold;
}

.toggle-btn {
  margin-left: 10px;
  color: white;
  background-color: black;
  border: none;
  border-radius: 7px;
}

ol {
  display: flex;
}

ol.asc-list {
  flex-direction: column;
}

ol.desc-list {
  flex-direction: column-reverse;
}

.high-light {
  background-color: yellow;
}

画面

ezgif.com-crop.gif

懸念点

this.props.winnerSquares && this.props.winnerSquares.includes(i) ? i : null;
props.winnerSquare === 0 || props.winnerSquare

もう少しいい書き方がありそう。

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

Reactで簡単にアコーディオンを実装できるライブラリ

概要

HTMLでアコーディオンを実装する方法はいくつかあります。CSSだけで実装する方法もあれば、Bootstrapなどのフレームワークを利用する方法も挙げられます。今回はReactのライブラリで簡単にアコーディオンを実装できるライブラリreact-accessible-accordionを紹介します。

サンプル

クリック前はこんな感じで、
スクリーンショット 2020-08-12 2.49.54.png

クリックすると中身が表示される
スクリーンショット 2020-08-12 2.50.02.png

アコーディオンの実装サンプルです。

SampleComponent.js
import React from "react";
import {
  Accordion,
  AccordionItem,
  AccordionItemHeading,
  AccordionItemButton,
  AccordionItemPanel,
} from "react-accessible-accordion";
import "react-accessible-accordion/dist/fancy-example.css";

export default function SampleComponent() {
  return (
    <div>
      <Accordion allowZeroExpanded>
        <AccordionItem>
          <AccordionItemHeading>
            <AccordionItemButton>ヘッダー</AccordionItemButton>
          </AccordionItemHeading>
          <AccordionItemPanel>内容</AccordionItemPanel>
        </AccordionItem>
      </Accordion>
    </div>
  );
}

その他出来ること

  • 詳細はこのデモページにまとめられています。
  • 今回の私のサンプルではヘッダーが一つですが、ヘッダーを複数設定して一つしか開けないようにする、といった制御もできます。
  • アコーディオンを開閉したときのイベントを拾えたり、アコーディオンのstateも取得できるようです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自分用メモ ライフサイクル

はじめに

ライフサイクルについてインプットしたのでアウトプットしておきます!!
reactを学び始めてこのライフサイクルが一番自分にとって難しく理解に苦しんでいます!!
まだまだ理解の途中ですが、アウトプットして行きながら理解を深めておきたいと思います!!

至らない点など数あると思いますが、アドバイスしていただけると幸いです!!

ライフサイクル

これはreactで学ぶ上で最初に教えてもらったことで、これであればなんとなく理解できました!

mounting

・constructor
・render
・componentDidMount
UI(ユーザー表示)のための準備
これらが主要メソッドで上からの順でメソッドが呼ばれる!!

updating

・render
・componentDidUpdate
ユーザーの処理などによりUIを更新、変化行う
ユーザーが操作できる箇所
ここの間はstate,propsが更新変更されるたびに何度もサイクルされる!!

unmounting

・componentWillUnmount
他のコンポーネントに切り替えたりする時に現在のコンポーネントを削除する箇所

注意)すいません!今記述した以外にもメソッドは多くあります。今自分がなんとか理解できてるメソッドのみ書かせていただきました。
詳しく知りたい方は是非こちらを見ていただくようお願いいたします!

ライフサイクルメソッド 

ユーザー表示するまでのサイクル

この中で必ず使うのはrenderのみになるので使わないのであれば使わなくてもいい!!

componentDidMount

mountingの時に呼び出されるメソッド
renderの後に呼ばれ、1回のみ呼ばれるメソッド
データを取ってくる場合や、タイマーやアニメーションをつけたりする時に使う!

render

mounting/updateの時に呼ばれるメソッドで一番呼ばれ、唯一必ず記載しなければならないメソッド!!
render内のコードがUI(ユーザー表示)になるもの
props,stateが変更されるたびに呼び出されるものになる!!
注意)renderの中で直接state,propsの変更を行ってはいけない!してしまうとrender内ではstate,propsを変更するとメソッドが呼ばれるのでrenderが無限にループされてしまう!!

componentDidUpdate

updateの時に使う
コンポーネント更新後に行われるメソッド
props,stateが変更時に呼び出される!!
多く呼び出されるメソッドなのでできるだけstate,propsの更新を行う思い処理は書かない!!

componentWillMount

unmountの時に呼ばれるメソッド
アニメーションやタイマーをセットしていた場合ここで解除する
リソースの解除を行う時に他のコンポーネントに影響を及ぼさないようにする!
ここでprops,stateの更新、変更はできない!unmount時はrenderが呼ばれないから!!

終わりに

以上が主要のメソッドになるのですが、他にも多くメソッドは存在しているため、必要になった場合勉強してここでも追加していこうと考えています!!

そしてこれらの主要メソッドの代替方法としてreact hooks(useEffect)があるのですが、こちらについては別記事で書いております!!
私自身最初にこのサイクルをきちんと理解せずにhooks(useEffect)を使っていたのでそのありがたみを全く理解できていませんでした。
これからもおそらく便利なhooksを利用したいと思いますが、このサイクルとメソッドを構造的に理解していないとreactの深い理解はできませんのでライフサイクルについてはもっともっと理解を深めて行きたいと思います!!

参照

React(v16.4) コンポーネントライフサイクルメソッドまとめ
React.Component

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

ReactでCanvasを使う

ReactでCanvas APIを使う場合どういうタイミングで初期化したらいいかとか
どう状態を持ったらいいか慣れないと結構悩みどころなのでざっくりシンプルなサンプルで実装

app.js
import React,{useState,useEffect} from "react"
import ReactDOM from "react-dom"

const App = () => {
    // contextを状態として持つ
    const [context,setContext] = useState(null)
    // 画像読み込み完了トリガー
    const [loaded,setLoaded] = useState(false)
    // コンポーネントの初期化完了後コンポーネント状態にコンテキストを登録
    useEffect(()=>{
        const canvas = document.getElementById("canvas")
        const canvasContext = canvas.getContext("2d")
        setContext(canvasContext)
    },[])
    // 状態にコンテキストが登録されたらそれに対して操作できる
    useEffect(()=>{
        if(context!==null)
        {
            const img = new Image()
            img.src = "img.jpg" // 描画する画像など
            img.onload = () => {
                context.drawImage(img,0,0)
                // 更にこれに続いて何か処理をしたい場合
                setLoaded(true)
            }
        }
    },[context])
    useEffect(()=>{
        if(loaded)
        {
            // それに続く処理
        }
    },[loaded])
    return <canvas width="1280" height="720" id="canvas"></canvas>
}
ReactDOM.render(<App />, document.getElementById('root'))

のようにHooksのトリガーで順番に制御していけばタイミング問題でそれほど悩まずにReactでもCanvasが使えるはず

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

【30分以内で】新しいReact Hooksをnpmパッケージとして公開した話

npmパッケージの作成と公開/更新に便利(なはずの)boilerplateを作ったので
以前作ったuseStackRefをnpmパッケージとして公開してみる

最終結果
https://github.com/nariakiiwatani/react-use-stack-ref
https://www.npmjs.com/package/react-use-stack-ref

この記事にしたがって進めていく
https://qiita.com/nariakiiwatani/items/ac8acb1822ead4dfc429

ダウンロード

dev-npm-package-react-typescript-boilerplateをZIPでダウンロードして適当なフォルダに解凍する。

package.jsonを編集

package.json(編集部分のみ記載)
  "name": "react-use-stack-ref",
  "author": "nariakiiwatani <nariakiiwatani@annolab.com> (https://github.com/nariakiiwatani)",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/nariakiiwatani/react-use-stack-ref"
  },

開発環境のセットアップ

あとでyarn run setup:demoをやることになるのでここは単にyarn run setupでも良かった

yarn run setup:all

yarn run v1.22.4
$ yarn run setup && yarn setup:demo
$ yarn install
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...
success Saved lockfile.
$ cd demo && yarn install
warning package.json: No license field
warning No license field
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...
✨  Done in 15.39s.

srcフォルダ内を編集

  1. ModuleFile.ts削除
  2. useStackRef.tsを作成
  3. index.jsindex.d.tsを修正
index.js(index.d.tsも同じ)
import { useStackRef } from './dist/useStackRef'
export { useStackRef }

ビルドが通るか確認

yarn run build

yarn run v1.22.4
$ tsc
✨  Done in 1.74s.

distフォルダにビルド結果が出力されていることを確認

スクリーンショット 2020-08-12 16.19.08.png

コミットしておく

ここで、編集を始めるより前に初期状態をコミットしておいた方が良かったと後悔する

git init
git add .
git commit -m "initial commit"

[master (root-commit) 5ab56bf] initial commit including new module 'useStackRef'
 19 files changed, 8973 insertions(+)
 create mode 100644 .github/workflows/pages.yml
 create mode 100644 .github/workflows/release.yml
 create mode 100644 .gitignore
 create mode 100644 LICENSE.md
 create mode 100644 README.md
 create mode 100644 demo/.gitignore
 create mode 100644 demo/dist/index.html
 create mode 100644 demo/package.json
 create mode 100644 demo/src/components/App.tsx
 create mode 100644 demo/src/index.tsx
 create mode 100644 demo/tsconfig.json
 create mode 100644 demo/webpack.config.js
 create mode 100644 demo/yarn.lock
 create mode 100644 index.d.ts
 create mode 100644 index.js
 create mode 100644 package.json
 create mode 100644 src/useStackRef.ts
 create mode 100644 tsconfig.json
 create mode 100644 yarn.lock

デモページのセットアップ

demo/package.json(変更箇所のみ記載)
  "dependencies": {
    "react-use-stack-ref": "link:../"
  },
demo/src/components/App.tsx(変更箇所のみ記載)
import { useStackRef } from 'react-use-stack-ref'
yarn run setup:demo

yarn run v1.22.4
$ cd demo && yarn install
warning package.json: No license field
warning No license field
[1/4] ?  Resolving packages...
[2/4] ?  Fetching packages...
[3/4] ?  Linking dependencies...
[4/4] ?  Building fresh packages...
success Saved lockfile.
✨  Done in 9.92s.

デモページのビルドを確認

yarn run build:demo

yarn run v1.22.4
$ cd demo && yarn run build
warning package.json: No license field
$ webpack
Hash: 60733be7b91f6c1ca0c1
Version: webpack 4.44.1
Time: 2151ms
Built at: 2020-08-12 16:28:08
        Asset      Size  Chunks                   Chunk Names
    bundle.js   947 KiB    main  [emitted]        main
bundle.js.map  1.08 MiB    main  [emitted] [dev]  main
Entrypoint main = bundle.js bundle.js.map
[0] multi ./src/index.tsx 28 bytes {main} [built]
[./src/index.tsx] 179 bytes {main} [built]
    + 12 hidden modules
✨  Done in 3.35s.

demo/distフォルダに結果が出力される

スクリーンショット 2020-08-12 16.28.47.png

開発用サーバーでデモページが動作することを確認する

yarn run dev

yarn run v1.22.4
$ yarn run build
$ tsc
$ cd demo && yarn run start
warning package.json: No license field
$ webpack-dev-server --open
ℹ 「wds」: Project is running at http://localhost:8080/
ℹ 「wds」: webpack output is served from /
ℹ 「wds」: Content not from webpack is served from ./dist
ℹ 「wdm」: wait until bundle finished: /
ℹ 「wdm」: Hash: e51f0a781a90c74c170f
Version: webpack 4.44.1
Time: 2518ms
Built at: 2020-08-12 16:29:27
        Asset      Size  Chunks                   Chunk Names
    bundle.js  1.26 MiB    main  [emitted]        main
bundle.js.map  1.46 MiB    main  [emitted] [dev]  main
Entrypoint main = bundle.js bundle.js.map
[0] multi (webpack)-dev-server/client?http://localhost:8080 ./src/index.tsx 40 bytes {main} [built]
[../node_modules/react-dom/cjs/react-dom.development.js] 839 KiB {main} [built]
[../node_modules/react-dom/index.js] 1.33 KiB {main} [built]
[../node_modules/react/index.js] 190 bytes {main} [built]
[./node_modules/webpack-dev-server/client/index.js?http://localhost:8080] (webpack)-dev-server/client?http://localhost:8080 4.29 KiB {main} [built]
[./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.51 KiB {main} [built]
[./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.53 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/createSocketUrl.js] (webpack)-dev-server/client/utils/createSocketUrl.js 2.91 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/log.js] (webpack)-dev-server/client/utils/log.js 964 bytes {main} [built]
[./node_modules/webpack-dev-server/client/utils/reloadApp.js] (webpack)-dev-server/client/utils/reloadApp.js 1.59 KiB {main} [built]
[./node_modules/webpack-dev-server/client/utils/sendMessage.js] (webpack)-dev-server/client/utils/sendMessage.js 402 bytes {main} [built]
[./node_modules/webpack-dev-server/node_modules/strip-ansi/index.js] (webpack)-dev-server/node_modules/strip-ansi/index.js 161 bytes {main} [built]
[./node_modules/webpack/hot sync ^\.\/log$] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {main} [built]
[./src/components/App.tsx] 135 bytes {main} [built]
[./src/index.tsx] 179 bytes {main} [built]
    + 31 hidden modules
ℹ 「wdm」: Compiled successfully.

空っぽのページがブラウザで表示される。コンソールにエラーもなし。

スクリーンショット 2020-08-12 16.30.14.png

デモページを作成

demo/src/component/App.tsx
import React, { useState, useCallback, ChangeEvent } from 'react'
import { useStackRef } from 'react-use-stack-ref'

const App = (props: {}) => {
    const stack = useStackRef()
    const [input, setInput] = useState("")
    const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
        setInput(e.target.value)
    }
    const handlePush = useCallback(e => {
        stack.push(input)
        setInput("")
    }, [stack])
    const handlePop = useCallback(e => {
        const value = stack.pop()
        value && setInput(value as string)
    }, [stack])
    return (<>
        <h1>live demo for useStackRef</h1>
        <input type="text" value={input} onChange={handleChange} />
        <button onClick={handlePush} disabled={input.length === 0}>push</button>
        <button onClick={handlePop} disabled={stack.size === 0} >pop</button>
        <div>current value: {stack.value}</div>
        <div>stack depth: {stack.size}</div>
    </>)
}

export default App;

動作確認

スクリーンショット 2020-08-12 16.44.37.png

コミットしておく

git add .
git commit -m "create demo"

[master 5d27f8f] create demo
 3 files changed, 34 insertions(+), 14 deletions(-)
 rewrite demo/src/components/App.tsx (65%)

GitHubリポジトリを設定

react-use-stack-refという名前のリポジトリを作成

npmのサイトでnpmトークンをRead and Publishの権限で作成
クリップボードにコピーしておく

スクリーンショット 2020-08-12 16.48.44.png

GitHubリポジトリのSettings -> Secretsに新たな変数を追加
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxの部分はコピーしたトークン文字列

スクリーンショット 2020-08-12 16.47.38.png

追加後の画面

スクリーンショット 2020-08-12 16.47.57.png

README.mdを編集してコミット

git add .
git commit -m "update readme"

[master 5c9508f] update readme
 1 file changed, 37 insertions(+), 95 deletions(-)
 rewrite README.md (99%)

git push

git remote add origin git@github.com:nariakiiwatani/react-use-stack-ref.git
git push -u origin master

Enumerating objects: 38, done.
Counting objects: 100% (38/38), done.
Delta compression using up to 8 threads
Compressing objects: 100% (31/31), done.
Writing objects: 100% (38/38), 148.32 KiB | 898.00 KiB/s, done.
Total 38 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), done.
To github.com:nariakiiwatani/react-use-stack-ref.git
 * [new branch]      master -> master
Branch 'master' set up to track remote branch 'master' from 'origin'.

GitHub Pagesの設定

Publish demo pageというGitHub Actionが自動で実行されて、デモページのビルド結果がgh-pagesブランチのpushされる。
するとリポジトリのSettings -> Options -> GitHub Pagesgh-pagesが選べるようになるので選択して、ディレクトリは/(root)のままでSaveボタンを押す

スクリーンショット 2020-08-12 16.57.50.png

動作確認

公開用URL( https://nariakiiwatani.github.io/react-use-stack-ref/ )でデモページが動いていることを確認

スクリーンショット 2020-08-12 16.59.49.png

デモのページをREADMEに追記しておく

[live demo](https://nariakiiwatani.github.io/react-use-stack-ref)

git add .
git commit -m "add live demo url in readme"

[master f156883] add live demo url in readme
 1 file changed, 2 insertions(+)
git push

Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 361 bytes | 361.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:nariakiiwatani/react-use-stack-ref.git
   5c9508f..f156883  master -> master

npmのサイトに行ってパッケージが公開されていることを確認

...ない!

スクリーンショット 2020-08-12 17.05.06.png

GitHub Actionのログを確認

semactic-releaseが新しいバージョンがないと判断してpublishしなかったらしい

[8:02:52 AM] [semantic-release] › ✔  Completed step "analyzeCommits" of plugin "@semantic-release/commit-analyzer"
[8:02:52 AM] [semantic-release] › ℹ  There are no relevant changes, so no new version is released.

semantic-releaseのために何かコミットしてpush

READMEにnpmパッケージページへのリンクを記載した。
コミットメッセージを"fix:"で始めるのを忘れない。

git add .
git commit -m "fix: add npm site in readme"

[master fbb5d05] fix: add npm site in readme
 1 file changed, 2 insertions(+)
git push

Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 336 bytes | 336.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:nariakiiwatani/react-use-stack-ref.git
   f156883..fbb5d05  master -> master

公開確認!

スクリーンショット 2020-08-12 17.08.40.png

終わりに

ということで、semantic-releaseの仕様を忘れていた以外は特につまづくことなく公開できました。
今回はデモページの作成やこの記事のための記録があってトータルで30〜40分くらいかかりましたが、新しいパッケージの開発環境構築と公開に限って言えば余裕で30分を切るのではないでしょうか。

みなさんもどうぞ自分のライブラリを公開する際は使ってみてください。

dev-npm-package-react-typescript-boilerplate

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

【備忘録】日本一わかりやすいReact-Redux講座 実践編 #9 「ネイティブアプリ風ヘッダーを作ろう」

はじめに

概要

この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

前回の講座では、Products の 詳細情報ページを作りました。

今回の講座では、Material-UI の <AppBar/>コンポーネントを利用して、ネイティブアプリ風のヘッダーを作成します。

※ 前回記事: https://qiita.com/ddpmntcpbr/items/1b1acf97a62cd5101884

動画URL

ネイティブアプリ風ヘッダーを作ろう【日本一わかりやすいReact-Redux講座 実践編#9】

要点

  • Material-UIの<AppBar>コンポーネントで、ヘッダーを作成できる。
  • Material-UIの<Badge>コンポーネントで、アイコンの肩に数字を表示できる。

完成系イメージ

http://localhost:3000

image.png

任意のページ上部にヘッダーが表示され、アプリ名ロゴと、各メニューアイコンが表示されています。

メニューアイコンはまだダミーの状態で、クリックしても何も起きません。また、カート右肩のバッジ数も、ダミーの値を表示させています。

メニューアイコンは、ログイン状態のときのみ表示させるため、サインイン画面では表示されません。

http://localhost:3000/signin

image.png

メイン

ヘッダーを作ろう

今回はとにかくMaterial-UIを用いて、ビューファイルをゴリゴリ書いていくだけです。

ヘッダーはアプリ内の任意のページ上部で表示をさせるため、App.jsxを親コンポーネントとして定義していきます。

src/components/直下に、新たにHeaderディレクトリを作り、

  • ヘッダー全体のコンポーネントとしてHeader.jsx
  • ヘッダー右側のメニューアイコン群を表示するコンポーネントしてHeaderMenu.jsx

を作成します。

image.png

実装ファイル(ヘッダー)
1.src/components/Header/Header.jsx
2.src/components/Header/HeaderMenus.jsx
3.src/components/Header/index.js
4.src/App.jsx
1.src/components/Header/Header.jsx
import React from 'react';
import {makeStyles} from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import logo from "../../assets/img/icons/logo.png";
import { useDispatch, useSelector } from 'react-redux';
import { getSignedIn } from '../../reducks/users/selectors';
import {push} from "connected-react-router"
import {HeaderMenus} from "./index"

const useStyles = makeStyles({
  root: {
    flexGrow: 1,
  },
  menuBar: {
    backgroundColor: "#fff",
    color: "#444",
  },
  toolBar: {
    margin: "0 auto",
    maxWidth: 1024,
    width: "100%"
  },
  iconButtons: {
    margin: "0 0 0 auto"
  }
})

const Header = () => {
  const classes = useStyles();
  const dispatch = useDispatch();
  const selector = useSelector((state)=>state)
  const isSignedIn = getSignedIn(selector)

  return (
    <div className={classes.root}>
      <AppBar position="fixed" className={classes.menuBar}>
        <Toolbar className={classes.toolBar}>
          <img
            src={logo} alt="Torahack Logo" width="128px"
            onClick={()=>dispatch(push("/"))}
          />
          {isSignedIn && (
            <div className={classes.iconButtons}>
              <HeaderMenus />
            </div>
          )}
        </Toolbar>
      </AppBar>
    </div>
  )
}

export default Header
  • <AppBar>の中に <Toolbar>を記述することで、ヘッダーを実装できます。
  • メニューアイコンを表示する<HeaderMenus>要素はログイン状態でのみ表示させたいので、{isSignedIn && ...で条件分岐しています。
2.src/components/Header/HeaderMenus.jsx
import React from "react";
import IconButton from "@material-ui/core/IconButton";
import Badge from "@material-ui/core/Badge";
import ShoppingCartIcon from "@material-ui/icons/ShoppingCart";
import FavoriteBorderIcon from "@material-ui/icons/FavoriteBorder";
import MenuIcon from "@material-ui/icons/Menu"

const HeaderMenu = () => {
  return (
    <>
      <IconButton>
        <Badge badgeContent={3} color="secondary">
          <ShoppingCartIcon />
        </Badge>
      </IconButton>
      <IconButton>
        <FavoriteBorderIcon />
      </IconButton>
      <IconButton>
        <MenuIcon />
      </IconButton>
    </>
  )
}

export default HeaderMenu
  • return文の中身は1つのHTML要素である必要があるので、<>...</>で全体を囲っています。
  • <Badge>コンポーネントで Icon をラッピングすることで、Icon の右肩に数値を表示させることができます。現在は、badgeContent={3}としてダミーの数値を表示させていますが、最終的にはここに「カートに入れた商品数」が入ります。
3.src/components/Header/index.js
export {default as Header} from "./Header"
export {default as HeaderMenus} from "./HeaderMenus"
  • エントリーポイントに追加します。
4.src/App.jsx
import React from 'react'
import Router from './Router'
import "./assets/reset.css"
import "./assets/style.css"
import {Header} from './components/Header'

const App = () => {
  return (
    <>
      <Header />
      <main className="c-main">
          <Router />
      </main>
    </>
  )
}

export default App;
  • App.jsxで Header を読み込み、全ページで表示させるようにします。

 動作確認

「完成系イメージ」と同様のため割愛します。

さいごに

今回の要点をおさらいすると、

  • Material-UIの<AppBar>コンポーネントで、ヘッダーを作成できる。
  • Material-UIの<Badge>コンポーネントで、アイコンの肩に数字を表示できる。

以上です!次回の講座で<MenuIcon />の中身を作っていく予定です。

このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。

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

開発中のnpmパッケージをローカルで参照する際に気をつけること

npmパッケージを開発中、デモページも同じリポジトリで開発してたら思わぬところでハマったので経緯と解決法を記録しておく。

経緯

ReactでシンプルなJSONエディタがほしくて作って

次回別のコンポーネントを作って公開する際にやることを最小化できるように、boilerplateとして切り出してまとめて

せっかくなのでreact-plain-json-editorもこのboilerplateにしたがって編集しなおしたところ、デモページが動かなくなったので

修正した!

現象

デモページを開くとブラウザで下記のエラーが出現した。
(非常に丁寧なエラーメッセージですね)

Uncaught Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.
    at resolveDispatcher (react.development.js:1465)
    at Object.useState (react.development.js:1496)
    at ../dist/PlainJsonEditor.js.exports.PlainJsonEditor (PlainJsonEditor.js:38)
    at renderWithHooks (react-dom.development.js:14803)
    at mountIndeterminateComponent (react-dom.development.js:17482)
    at beginWork (react-dom.development.js:18596)
    at HTMLUnknownElement.callCallback (react-dom.development.js:188)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:237)
    at invokeGuardedCallback (react-dom.development.js:292)
    at beginWork$1 (react-dom.development.js:23203)

よくある原因として挙げてくれている3点のうち、1,2は大丈夫そうだったので、3が怪しい。

3. You might have more than one copy of React in the same app
(意訳)Reactが複数含まれているかもしれません

ざっくり現象の概要をまとめてしまうと、「ライブラリ本体とデモページとで別々のReactを参照しているので不整合が起きている」ということになるかと思う。

とりあえず結論だけ知りたい方はこちらへ。

ちなみに、現象としてはこちらのページに書かれているものとおそらく同じだが、少し違う対処をした部分もあるのでこちらはこちらで記しておく。
Reactコンポーネントをnpmパッケージとして開発する

状況の整理

  • ライブラリ本体の開発
    • 開発フォルダをルートディレクトリ(/)と呼ぶことにする
    • ビルドツールはtsc
    • ビルドに関連するファイルはpackage.json
  • デモページの開発
    • 開発フォルダはdemoディレクトリ
    • ビルドツールはwebpack
    • ビルドに関連するファイルはdemo/package.jsondemo/webpack.config.js

問題と対処

問題1: ライブラリ本体のdependenciesにreactが入っている

これはそもそもよくない。普通は

  • 動作に必要なパッケージ => dependencies
  • 開発に必要なパッケージ => devDependencies

だが、パッケージとして公開することが前提であれば、使用側との競合を避けるため

  • 動作に必要なパッケージ => peerDependencies
  • 開発に必要なパッケージ => devDependencies

とするべきだ。
ただし、これだけではpeerDependenciesyarn install(またはnpm install)に無視されるので、開発時にビルドできなくなる。

問題1への対処

  • 動作に必要なパッケージ => peerDependenciesdevDependenciesの両方
  • 開発に必要なパッケージ => devDependencies

とするのが良さそうだ。

問題2: node_modulesdemo/node_modulesの両方にreactが含まれている

問題1に対処しても、まだreactの競合は避けられていないので、
どちらかだけに含むようにする(またはどちらかだけを参照するようにする)必要がある。

修正前

demo/package.json(修正前/一部抜粋)
"dependencies": {
    "react": "^16.13.1"
}
/demo/src/component/App.tsx(修正前/一部抜粋)
// ルートディレクトリを直接参照
import { PlainJsonEditor } from '../../../'
demo/webpack.config.js(修正前/一部抜粋)
// 実際にはこれは書いていなかったが、説明のためにデフォルト値で記載。
resolve: {
    modules: ['node_modules']
}

これらの指定により、デモページ側ではdemo/node_modulesにあるreactが参照されてしまう。

修正方針

実際にパッケージが使用される環境を想定すると、reactはライブラリ側ではなくdemo側にインストールされる方が正しくはあるが、開発環境が不便になるので、ライブラリ側だけにインストールし、demo側からルートのnode_modulesを参照できるようにする。

問題2への対処

yarn linkまたはnpm linkを使っておいてから"react-plain-json-editor": "latest"などを指定しても良いが、誰か別の人が使うときに管理する状態を極力増やさないようにしたいので今回は"link:../"を使った。

demo/package.json(修正後/一部抜粋)
"dependencies": {
    "react-plain-json-editor": "link:../"
}
demo/src/components/App.tsx(修正後/一部抜粋)
// dependenciesでlinkしたので、通常のパッケージをimportする時と同じ記法で使用可能
import { PlainJsonEditor } from 'react-plain-json-editor'
demo/webpack.config.js(修正後/一部抜粋)
resolve: {
    // ルート側を優先して参照するようにする
    modules: ['../node_modules', 'node_modules']
}

ちなみにこのdemo/webpack.config.jsonの修正後も、demo/node_modulesreactが含まれていると同様のエラーが起きた。
ここはwebpackの仕様の話になるのだろうが、これ以上は調べていない。

問題3: package.jsonにバージョン指定がない

※今回の問題とは直接の関係はない

パッケージのリリースはGitHub Actionsに登録したsemantic-releaseに任せており、バージョン番号はsemantic-releaseが管理するので、package.jsonにバージョンを書くと冗長だったり公開バージョンとの間に見た目上の不整合が生じたりすると思って、versionの指定自体を削除していた。

するとdemoのyarn install時に下記エラーが発生した。

error Can't add "react-plain-json-editor": invalid package version "".

問題3への対処

package.json"version": "0.0.0"を追加


以上で問題なくデモページが動くようになった。

成果物はこちらです。
react-plain-json-editor
dev-npm-package-react-typescript-boilerplate

まとめ

react(及びライブラリ本体が他に依存するパッケージ)はpackege.jsonpeerDependenciesdevDependenciesの両方に記載し、dependenciesには記載しない。

  • peerDependencies: このパッケージをinstallする際に必要に応じてWarningを出す
  • devDependencies: 開発時のためにnode_modulesにインストールされる

demo/node_modulesreactを含めない

  • demo/package.jsondependenciesreactを書かない

デモページのビルド時にルートのnode_modulesを参照するようにする

  • demo/webpack.config.jsresolve.modules['../node_modules', 'node_modules']を指定する
    • ../node_modules: ルートディレクトリ側のnode_modulesのこと
    • node_modules: デモページ側のdemo/node_modulesのこと

デモページから開発中のライブラリを使用できるようにする

  • demo/package.jsondependencies"react-plain-json-editor": "link:../"を記載
  • またはyarn linkを設定後"react-plain-json-editor": "latest"を記載
ex)yarn/linkを使う方法
# まずはルートディレクトリで
yarn link # このパッケージをlinkコマンドで参照可能にする(`~/.config/yarn/link`にシンボリックリンクが貼られる)
cd demo # demoへ移動
yarn link react-plain-json-editor # linkで登録したパッケージをこのプロジェクト(demo)で使えるようにする

参考にしたページ

Reactコンポーネントをnpmパッケージとして開発する
Yarnでローカルのパッケージをaddする方法
npm linkの基本的な使い方まとめ
0000-link-dependency-type.md
依存関係の種類
Webpackを使う時は、resolve.modulesの設定に気を付けよう。(特にModule not found: Error: Can't resolve~が表示された時)
Resolve

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

jestとreact-testing-libraryでreactのテストをする

はじめに

1回目のReactを使用してWeb画面を作成するでは、reactの簡単なサンプルと共に説明をまとめました。
2回目のReduxとReduxToolkitを使用してReact内でデータを管理するでは、reduxのの簡単なサンプルと共に説明をまとめました。
3回目のReactのRedux内でaxiosを使用した通信をするでは、axiosを使用してREST APIから情報を取得するサンプルと説明をまとめました。
今回は、jestとreact-testing-libraryで仮想DOMのテストをしてみます。

環境

  • node.js: v12.18.2
  • webpack: 4.44.1
  • React: 16.13.1
  • Redux: 7.2.1
  • axios: 0.19.2
  • jest: 26.3.0
  • react-testing-library: 10.4.8

環境作成

今までの続きとなります。
環境構築などはそちらを見てください。

jest

テストをするために、その枠組みを提供するjestというライブラリを使用します。

jestのインストール

yarn add --dev babel-jest react-test-renderer
npm install -g jest

react-testing-library

reactが生成する仮想DOMの操作をするためにreact-testing-libraryというライブラリを使用します。

react-testing-libraryのインストール

npm install --save-dev @testing-library/react

axios-mock-adapter

axiosを使用して通信を行っていますが、テストのたびにREST APIのサーバを起動するのは手間なのでモックを使用してaxiosの処理を置き換えます。

axios-mock-adapterのインストール

npm install --save-dev axios-mock-adapter

テストソースを作成する

テストソースを入れるフォルダの作成

テスト用ソースを入れるフォルダとしてルートフォルダの直下にtestフォルダを作成します。

project_root
├─dist   // ビルド後のファイルを格納 
├─public // htmlを格納
├─src    // reactのJavaScriptファイルやCSSファイルを格納
├─test    // reactのテストファイルを格納

テストファイルの作成

jestは×××.spec.jsや×××.test.jsというファイルを読み込んで実行するため、それに沿ったファイル名にしてください。

Message.spec.js
import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

const mockAxios = new MockAdapter(axios);

mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
// テスト実行後にDOMをunmount, cleanupします
afterEach(cleanup)

describe("Messageテスト", () => {
    it("初期値:メッセージが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // メッセージが2つ(テキストとボタンにあるか)
        expect(messageRender.getAllByText("メッセージ")).toHaveLength(2);
        // こんにちはが1つ(ボタンにあるか)
        expect(messageRender.getAllByText("こんにちは")).toHaveLength(1);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("通信")).toHaveLength(1);
        // ボタンが2つあるか)
        expect(messageRender.getAllByRole("button")).toHaveLength(3);
    });

    it("メッセージを押すと入力したものが表示される", () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // こんにちはが1つ(ボタンにあるか)
        const inputElement = messageRender.getByRole("textbox");
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // こんにちはが1つ(ボタンにあるか)
        const messageElement = messageRender.getAllByText("メッセージ");
        // 通信が1つ(ボタンにあるか)
        fireEvent.click(messageElement[0]);
        // 通信が1つ(ボタンにあるか)
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
    });

    test("通信のテスト", async () => {
        const messageRender = render(<Provider store={store}><Message /></Provider>);
        // 通信ボタンを取得
        const messageElement = messageRender.getAllByText("通信");
        // 通信ボタンをクリック。失敗すればエラーになる
        await fireEvent.click(messageElement[0]);
    });
});

jestの形式

jestは指定されたフォーマットにしたがって書くことによってテストの実行をしてくれます。

Message.spec.js
インポート

// afterEach内の関数はテスト後に実行される
afterEach(cleanup)

// テストの塊
describe("Messageテスト", () => {
    // テスト
    it("初期値:メッセージが表示される", () => {
        ~~~ テストの内容 ~~~ 
    });
});

jestのフォーマットはこのような形になります。他にもテスト前やテストスイート前後にする処理を登録できます。

仮想DOMの生成

仮想DOMの生成にはreact-testing-libraryのrenderを使用します。この中に、テストしたいアプリのソースを登録します。
今回はMessageコンポーネントのテストをしたいのでrenderに入れていますが、storeも使っているためそれも適用しています。

Message.spec.js
import React from "react";
import { render, cleanup, fireEvent } from '@testing-library/react';

import { Message } from "../src/Message";

import { Provider } from 'react-redux'
import store from '../store/store'

        ~~~ 省略 ~~~ 

        // 仮想DOMの生成
        const messageRender = render(<Provider store={store}><Message /></Provider>);
    });

仮想DOMの操作

生成した仮想DOMに対してgetByXXXやfindByXXXといった関数を実行すると仮想DOM内の要素を抜き出すことができます。要素のイベントを発火させるためにはfireEvent.change(DOMElement);で発火させます。
今回は入力するためにgetByRole("textbox")で抜き出して、値を入れた後にfireEvent.change(inputElement);で反映させています。
さらにボタンを押すためにgetAllByText("メッセージ")で抜き出した後に、メッセージという文字列が2つあり最初の要素がボタンであるため、そっちのイベントを発火させます。

Message.spec.js
        ~~~ 省略 ~~~ 

        // 入力するテキストボックスを取得
        const inputElement = messageRender.getByRole("textbox");
        // テキストの入力と反映。
        inputElement.innerHTML = "テスト";
        fireEvent.change(inputElement);
        // ボタンの取得
        const messageElement = messageRender.getAllByText("メッセージ");
        // ボタンのクリック
        fireEvent.click(messageElement[0]);
        // ボタンを押した結果を確認
        expect(messageRender.getAllByText("テスト")).toHaveLength(1);
        ~~~ 省略 ~~~ 

axiosのモックを生成

モックを使用するときは、MockAdapterを使用して、モックを生成した後に、モック内の処理を記載します。
今回はgetのリクエストを置き換えるのでonGet(URL).reply(httpステータス,レスポンスボディ)を使用することにより、モック内の処理を定義します。

Message.spec.js
        ~~~ 省略 ~~~ 
import MockAdapter from "axios-mock-adapter";
import axios from 'axios';

// mockの生成
const mockAxios = new MockAdapter(axios);

// mock内容を定義
mockAxios.onGet("http://localhost:3000/mess_api").reply(200, 
    [{ id: 1, message: "mock hello" }],
);
        ~~~ 省略 ~~~ 

テストソースの実行

プロジェクトルートでjestコマンドを実行することでテストできます。

jest

終わりに

厳密にテストする場合、action内のテストやstateのテストといった細かい単位でテストすることが可能になります。
それによってテスト結果がNGになった場合に、原因となる場所が限定されるので調査がしやすくなったり、修正後のテストの範囲を限定できたりと様々なメリットがあります。
しかし、そこら辺のテストをちゃんと書くにはReactのことを理解する必要がある上に時間がかかってしまうため、
Reactへのなじみのない人が実施すると時間だけがかかってしまい、テストソースがバグだらけとなったりしてしまいます。
厳密にテストするか大きな枠でテストするかは理解度と時間、テストの影響のトレードオフかなと思っています。

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

【備忘録】日本一わかりやすいReact-Redux講座 実践編 #8 「Swiperで画像スライダーを作ろう」

はじめに

この記事は、トラハック氏(@torahack_)が運営するYoutubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

前回まで講座で、Products の CRUD 機能を一通り実装しました。

今回の講座では、
- React-swiperを用いた商品情報詳細ページ(CRUD の R)
- MuiThemeProviderによる Material-UI のテーマカラー設定

について実装します。

※ 前回記事: 【備忘録】日本一わかりやすいReact-Redux講座 実践編 #7 「商品を一覧表示しよう」

要点

  • <MuiThemeProvider>で、Material-UI のテーマカラーを設定できる。
  • react-swiperで画像スワイプ機能を実装できる。
  • Material-UIの<Table>コンポーネントでテーブルを作成できる。

完成系イメージ

http://localhost:3000/product/jJV3RsxCgFllZeo771wR

image.png

商品情報の詳細ページを作成します。

左側が画像スライダー、右側がサイズ表示用のテーブルになっています。

サイズ表示用テーブルは、Material-UIの<Table>コンポーネントを使うことで比較的容易に作成できます。

画像スライダーは、react-id-swiperというnpmパッケージをインストールすることで、非常に簡単に実装することができます。

メイン

MuiThemeProvider によるテーマカラー設定

Material-UI において、アプリ内で使用するテーマカラーを設定する機能としてMuiThemeProviderというものがあります。

これを用いることで、例えばコンポーネント内のフォント一つ一つにカラーコードを定義するような必要がなくなります。

また、「primaryが赤、secondaryが青」のように限られた数の色をテーマカラーを設定して運用するため、無駄な色の使いすぎを自然と防ぐことにも繋がります。

今回は既存のthemeファイルを読み込むだけですが、一連の流れが理解できれば、テーマカラーを簡単に変更することができます。

実装ファイル(theme関連)
1.src/assets/theme.js  ## themeカラーが定義されたファイル
2.src/index.js  ## theme.jsを読み込み、アプリ全体に適用
src/assets/theme.js
import { createMuiTheme } from '@material-ui/core/styles';

// Pick colors on https://material.io/resources/color/#!/

export const theme = createMuiTheme({
  palette: {
    primary: {
      light: '#88ffff',
      main: '#4dd0e1',
      dark: '#009faf',
      contrastText: '#000',
    },
    secondary: {
      light: '#ffff81',
      main: '#ffd54f',
      dark: '#c8a415',
      contrastText: '#000',
    },
    grey: {
      50: "#fafafa",
      100: "#f5f5f5",
      200: "#eeeeee",
      300: "#e0e0e0",
      400: "#bdbdbd",
      500: "#9e9e9e",
      600: "#757575",
      700: "#616161",
      800: "#424242",
      900: "#212121",
      A100: "#d5d5d5",
      A200: "#aaaaaa",
      A400: "#303030",
      A700: "#616161"
    }
  },
});

themeカラーをカラーコードとして定義します。これを、アプリ全体に適用させます。

src/index.js
import React from 'react';
.
.
.
import {MuiThemeProvider} from "@material-ui/core";
import {theme} from "./assets/theme"
.
.
.
ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <MuiThemeProvider theme={theme}>
        <App />
      </MuiThemeProvider>
    </ConnectedRouter>
  </Provider>,
  document.getElementById('root')
);
.
.
.

<MuiThemeProvider><App />を丸ごとラッピングして、設定完了です。

これにより、各コンポーネントのmakeStyle()内において、

任意のコンポーネントファイル
const useStyles = makeStyles((theme) => ({
  root: {
    color: theme.palette.primary.main
  }
  .
  .
  .
}));

theme.jsで定義した色を適用することができます。

今回のtheme.jsでは、primaryが水色系、secondaryが黄色系で配色されています。

詳細ページの大枠作成

商品情報詳細ページの大枠を作成します。

ひとまずは、URLに含まれるid情報を元に正しいproductsをCloud Firstoreから取り出すところまで進めます。テンプレート、及びパスは、

  • テンプレート: ProductDetail.jsx
  • path: /product/:id

として実装します。

実装ファイル(詳細ページ大枠)
1.src/Router.jsx
2.src/templates/ProductDetail.jsx
3.src/templates/index.js
1.src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {ProductDetail,ProductEdit,ProductList,Reset,SignIn,SignUp} from "./templates";
import Auth from "./Auth"

const Router = () => {
  return (
    <Switch>
      <Route exact path={"/signup"} component={SignUp} />
      <Route exact path={"/signin"} component={SignIn} />
      <Route exact path={"/signin/reset"} component={Reset} />

      <Auth>
        <Route exact path={"(/)?"} component={ProductList} />
        <Route exact path={"/product/:id"} component={ProductDetail} /> {/*追記*/}
        <Route path={"/product/edit(/:id)?"} component={ProductEdit} />
      </Auth>
    </Switch>
  );
};

export default Router
2.src/templates/ProductDetail.jsx
import React,{ useState,useEffect,useCallback } from "react";
import {useSelector} from "react-redux"
import {db} from "../firebase"
import { makeStyles } from "@material-ui/core";
import HTMLReactParser from "html-react-parser"

const useStyles = makeStyles((theme)=>({
  sliderBox: {
    [theme.breakpoints.down("sm")]: {
      margin: "0 auto 24px auto",
      height: 320,
      width: 320
    },
    [theme.breakpoints.up("sm")]:{
      margin: "0 auto",
      height: 400,
      width: 400
    }
  },
  detail: {
    [theme.breakpoints.down("sm")]: {
      margin: "0 auto 16px auto",
      height: "auto",
      width: 320
    },
    [theme.breakpoints.up("sm")]:{
      margin: "0 auto",
      height: "auth",
      width: 400
    }
  },
  price: {
    fontSize: 36
  }
}));

const returnCodeToBr = (text) => {
  if (text === "") {
      return text
  } else {
      return HTMLReactParser(text.replace(/\r?\n/g, '<br/>'))
  }
};

const ProductDetail = () => {
  const classes = useStyles();
  const selector = useSelector((state)=>state);
  const path = selector.router.location.pathname;
  const id = path.split("/product/")[1];

  const [product,setProduct] = useState();

  useEffect(()=>{
    db.collection("products").doc(id).get()
    .then(doc => {
      const data = doc.data();
      setProduct(data)
    })
  },[]);

  return (
    <section className="c-sention-wrapin">
      {product && (
        <div className="p-grid__row" >
          <div className={classes.sliderBox}>

          </div>
          <div className={classes.detail}>
            <h2 className="u-text__headline">{product.name}</h2>
            <p className={classes.price}>{product.price.toLocaleString()}</p>
            <div className="module-spacer--small"></div>
            <div className="module-spacer--small"></div>
            <p>{returnCodeToBr(product.description)}</p>
          </div>
        </div>
      )}
    </section>
  )
};

export default ProductDetail
  • [theme.breakpoints.down("sm")]: {...}とすることで、smサイズの幅をブレイクポイントとして、適用するスタイルを変更しています。
  • useEffect()を用いて、初期レンダー時に、DBから該当するidの情報を取得します。
  • returnCodeToBr()で、改行コード\nを、改行のHTMLタグ<br />に変換しています。この関数に用いているhtml-react-parserは、この時点ではインストールしていないはずなので、npm からインストールします。
html-react-parserのインストール
$ npm install --save html-react-parser
3.src/templates/index.js
export {default as Home} from './Home'
export {default as ProductDetail} from './ProductDetail'
export {default as ProductEdit} from './ProductEdit'
export {default as ProductList} from './ProductList'
export {default as Reset} from './Reset'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'

いったん動作確認をします。すでにProductCard.jsxにおいて、リストアイテムをクリックすることで/product/:idへ遷移されるよう定義されているため、そこからアクセスします。

http://localhost:3000

image.png

スカートをクリックすると、

http://localhost:3000/product/ewzuLXPK2Dr0owCG3dt7

image.png

スカートの商品情報が取得できていることが分かります!

画像スライダーの設置

このProductDetailテンプレートに対して、画像スライダーを設置します。

画像スライダーの実装には、react-id-swiperというnpmパッケージを利用します。依存関係にあるswiperと合わせて、インストールしておきます。

swiperとreact-id-swiperをインストール
$  npm install -S swiper@5.4.2 react-id-swiper@3.0.0  

画像スライダー用のコンポーネントしてImageSwiper.jsxを定義し、それをProductDetail.jsxで読み込みます。

実装ファイル(画像スライダー)
1.src/components/Products/ImageSwiper.jsx
2.src/components/Products/index.js
3.src/templates/ProductDetail.jsx
1.src/components/Products/ImageSwiper.jsx
import React, { useState } from "react";
import Swiper from "react-id-swiper";
import NoImage from "../../assets/img/src/no_image.png"
import 'swiper/css/swiper.css'

const ImageSwiper = (props) => {
  const [params] = useState({
    pagination: {
      el: ".swiper-pagination",
      type: "bullets",
      clickable: true,
      dynamicBullets: true
    },
    navigation: {
      nextEl: ".swiper-button-next",
      prevEl: ".swiper-button-prev"
    },
    loop: true
  })

  const images = props.images

  return (
    <Swiper {...params}>
      {images.length === 0 ? (
        <div className="p-media__thumb">
          <img src={NoImage} alt="no images"/>
        </div>
      ) : (
        images.map(image => (
          <div className="p-media__thumb" key={image.id}>
            <img src={image.path} alt="商品情報"/>
          </div>
        ))
      )}
    </Swiper>
  );
};

export default ImageSwiper;

react-id-swiperは、

1.import 'swiper/css/swiper.css'でスライダー用のcssを読み込む。
1. スライダーの性質をparamsとして定義
2. <Swiper {...params}>としてJSXに展開

という流れで実装をします。

'swiper/css/swiper.css'は、npmインストールすることによって、node_modules下に保存されるcssファイルです。この中で、スライダーとしての動的な挙動が実装されているイメージです。

また、paramsの中で定義するパラメータを変えることで、様々な形式のスライダーを実装できるようです(公式リファレンス参照)

export {default as ImageArea} from "./ImageArea"
export {default as ImagePreview} from "./ImagePreview"
export {default as ImageSwiper} from "./ImageSwiper" //追記
export {default as ProductCard} from "./ProductCard"
export {default as SetSizeArea} from "./SetSizeArea"
3.src/templates/ProductDetail.jsx
.
.
.
import {ImageSwiper} from "../components/Products" {*追記*}
.
.
.
const ProductDetail = () => {
  .
  .
  .
  return (
    <section className="c-sention-wrapin">
      {product && (
        <div className="p-grid__row" >
          <div className={classes.sliderBox}>
            <ImageSwiper images={product.images} /> {*追記*}
          </div>
          <div className={classes.detail}>
            <h2 className="u-text__headline">{product.name}</h2>
            <p className={classes.price}>{product.price.toLocaleString()}</p>
            <div className="module-spacer--small"></div>
            <div className="module-spacer--small"></div>
            <p>{returnCodeToBr(product.description)}</p>
          </div>
        </div>
      )}
    </section>
  )
};

export default ProductDetail

画像スライダー動作確認

http://localhost:3000/product/jJV3RsxCgFllZeo771wR

8-1.gif

コード量も少なく簡単に画像スライダーが実装できるのはすばらしいですね!

サイズ表示テーブル

最後に、商品情報のサイズを表示するテーブルを作成します。

<SizeTable>コンポーネントを作成し、<ProductDetail>で読み込みます。

実装ファイル(サイズ表示テーブル)
1.src/components/Products/SizeTable.jsx
2.src/components/Products/index.js
3.src/templates/ProductDetail.jsx
1.src/components/Products/SizeTable.jsx
import React from 'react';
import Table from "@material-ui/core/Table";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import IconButton from "@material-ui/core/IconButton";
import TableContainer from "@material-ui/core/TableContainer";
import ShoppingCartIcon from '@material-ui/icons/ShoppingCart';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import {makeStyles} from "@material-ui/styles";

const useStyles = makeStyles({
  iconCell: {
    padding: 0,
    height: 48,
    width: 48
  }
})

const SizeTable = (props) => {
  const classes = useStyles();

  const sizes = props.sizes;

  return (
    <TableContainer>
      <Table>
        <TableBody>
          {sizes.length > 0 && (
            sizes.map(size => (
              <TableRow key={size.size}>
                <TableCell component="th" scope="row">{size.size}</TableCell>
                <TableCell>残り{size.quantity}</TableCell>
                <TableCell className={classes.iconCell}>
                  {size.quantity > 0 ? (
                    <IconButton>
                      <ShoppingCartIcon />
                    </IconButton>
                  ) : (
                    <div>売切</div>
                  )}
                </TableCell>
                <TableCell className={classes.iconCell}>
                  <IconButton>
                    <FavoriteBorderIcon />
                  </IconButton>
                </TableCell>
            </TableRow>
            ))
          )}
        </TableBody>
      </Table>
    </TableContainer>
  );
};

export default SizeTable;

Material-UIの

  • <TableContainer>
  • <Table>
  • <TableBody>
  • <TableRow>
  • <TableCell>

で、テーブルを実装しています。

2.src/components/Products/index.js
export {default as ImageArea} from "./ImageArea"
export {default as ImagePreview} from "./ImagePreview"
export {default as ImageSwiper} from "./ImageSwiper"
export {default as ProductCard} from "./ProductCard"
export {default as SetSizeArea} from "./SetSizeArea"
export {default as SizeTable} from "./SizeTable" //追記
3.src/templates/ProductDetail.jsx
.
.
.
import {SizeTable} from "../components/Products" {*追記*}
.
.
.
const ProductDetail = () => {
  .
  .
  .
  return (
    <section className="c-sention-wrapin">
      {product && (
        <div className="p-grid__row" >
          <div className={classes.sliderBox}>
            <ImageSwiper images={product.images} />
          </div>
          <div className={classes.detail}>
            <h2 className="u-text__headline">{product.name}</h2>
            <p className={classes.price}>{product.price.toLocaleString()}</p>
            <div className="module-spacer--small"></div>
            <SizeTable sizes={product.sizes}/>  {*追記*}
            <div className="module-spacer--small"></div>
            <p>{returnCodeToBr(product.description)}</p>
          </div>
        </div>
      )}
    </section>
  )
};

export default ProductDetail

<SizeTable>コンポーネントをインポートします。

 動作確認

http://localhost:3000/product/jJV3RsxCgFllZeo771wR

image.png

左に画像スライダー、右にサイズテーブルが設置されています!

さいごに

今回の要点をおさらいすると、
- <MuiThemeProvider>で、Material-UI のテーマカラーを設定できる。
- react-swiperで画像スワイプ機能を実装できる。
- Material-UIの<Table>コンポーネントでテーブルを作成できる。

以上です!

このような学習内容を日々呟いていますので、よろしければTwitter(@ddpmntcpbr)のフォローもよろしくお願いします。

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

Recoilの簡単な説明

Recoilをつかってみた

Qiita投稿2回目です。

今回はRecoilを軽く利用したのでRecoilについて簡単に説明します。

私が利用したのrecoil 0.0.10です。

Recoilとは

RecoilとはFacebookにより作成されたReact専用の状態管理ライブラリのこと。

状態管理とは

状態とはフロントの保持するデータのこと。

アプリケーションの規模が大きくなるほどコンポーネントでのデータの管理が複雑になるのでデータの状態を管理する必要があり、データを集中管理することを状態管理という。

Reactの場合、Redux Vueの場合はVuexなどが存在する。

状態管理についてfluxアーキテクチャを調べるとより理解が深まると考えています。

Recoilの特徴

  • 簡単

  • React専用の状態管理ライブラリ

  • Reduxよりコンパクトな状態管理を行うことが可能

  • カスタムフックとの相性が良い

  • 現時点(2020/08/12)では正式リリースはされていない

上記がRecoilの簡単な説明です。

Recoilの良い点

カスタムフックとの相性がとても良かったです。

状態管理を容易に行えるので、Reactを最近利用し始めた私でも簡単に利用できました。

Reactの状態管理ライブラリのなかでTypeScriptとの相性が良い。

Recoilについて

  • Atoms

  • Selectors

上記2つがRecoilの主機能です。

Atoms

Atomとはデータを管理する場所です。

Atomにはどのコンポーネントからでもアクセスすることが可能で、全てのコンポーネントにデータを渡すことができます。

Selectors

Atomに格納されたデータを取り出す機能です。

データを全て取り出す場合やデータを元のまま取り出す場合はAtomのみでも可能ですがデータを指定して取り出す場合やデータを加工して取り出す場合にはSelectorを利用します。

また、Selectorには非同期の処理を行うことが可能なので大量のデータを処理する場合にはSelectorを利用します。

Selectorはエラーのハンドリングを行うことが可能でAtomからデータを抽出した際に失敗した場合にどう対処するかを指定することもできます。

私は公式ドキュメントをTypeScriptで記述したのですが公式ドキュメントのTodoアプリではAtomのみでもアプリの機能は乏しいですがアプリ開発ができると感じました。

Recoilの利用方法

今回は公式ドキュメントに記述されている内容をTypeScriptで記述し、説明します。

TypeScript App.tsx
import React from 'react'
import { RecoilRoot } from 'recoil'
import { Sample } from './Sample'

export function App() {
  <RecoilRoot>
    <Sample />
  </RecoilRoot>
}

まずはルートコンポーネントにRecoilRootを記述します。

これによりStateを格コンポーネントに受け渡すことができるようになります。

※ RecoilRootは必ずとしてルートコンポーネントに記述する必要はありません。今回はルートコンポーネントに記述しましたが状態管理をしたいコンポーネント郡の親コンポーネントであれば構いません。

Atoms

atom.ts
import { atom } from 'recoil';

type Sample = {
  id: number;
  title: string;
  content: string;
  completed: boolean;
}

const initialSample: Sample[] = [];

export const sampleListState = atom({
  key: 'sampleListState',
  default: initialSample,
});

上記がatomのサンプルコードです。

const initialSample: Sample[] = [];

まずデータを保存する配列を用意します。

export const sampleListState = atom({
  key: 'sampleListState',
  default: initialSample,
});

ここがatomのメインとなる部分です。

defaultで先ほど記述したデータ格納場所を指定します。

これでatomでデータの保持ができるようになりました。

useRecoilValueとuseSetRecoilState

useRecoilValueについて

const sampleList = useRecoilValue(sampleListState);

上記コードにより、先ほど作成したsampleListStateに格納されたデータがsampleListに移動されました。

useRecoilValueによってatomに存在するデータをコンポーネントに渡すことができるようになります。

const setSampleList = useSetRecoilState(sampleListState);

まずuseSetRecoilValueにsampleListStateにアクセスできるように設定します。

useSetRecoilStateによりatomに格納されているデータに追加することができます。

useRecoilValueとusseSetRecoilValueはReactに搭載されているuseStateの機能にとても似ていると思いました。

Selectors

Atomsでデータの取り出しとデータの格納方法がわかりましたが、条件を指定してデータを取り出したい場合にはAtomsではできません。

そこでSelectorsを利用します。Selectorsを使用することで、条件を指定したデータを取り出すことができます。

type Sample = {
  id: number;
  title: string;
  content: string;
  completed: boolean;
}

const initialSample: Sample[] = [];

Sampleデータにcompletedが格納されています。completedがtrueの場合とfalseの場合で取り出すデータを変えたい場合は

const sampleListFilterState = atom({
  key: 'sampleListFilterState',
  default: 'Show All',
});

まずはatomに全てのデータを参照できるようなコードを追加します。

ここからSelectorsの処理です。

selector.ts
import { selector } from 'recoil';
import { 
  sampleListState,
  sampleListFilterState
} from './atom';

type Sample = {
  id: number;
  title: string;
  content: string;
  completed: boolean;
}

const filterSampleListState = selector({
  key: 'filteredSampleListState',
  get: ({get}) => {
    const filter: string = get(sampleListFilterState);
    const list: Sample[] = get(sampleListState);

    switch (filter) {
      case 'Show Completed':
        return list.filter((item) => item.isComplete);
      case 'Show Uncompleted':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});

これによりfilterSampleListStateが選択された場合は、

Show Completed

が選択された場合は、completedがtrueのデータの取り出しが可能になり、

Show Uncompleted

が選択された場合は、completedがfalseのデータを取り出します。

では次はどちらのデータを取り出すかをコンポーネントで選択できるような処理を追加しましょう

Sample.tsx
import React from 'react';
import { useRecoilValue } from 'recoil';
import { filterSampleListState } from './selector.ts'
import { SampleFilter } from './SampleItem';
import { SampleItem } from './SampleItem';

export function Sample() {
  const sampleList = useRecoilValue(filterSampleListState);
  return(
    <>
      <SampleFilter />
      {sampleList.map((sampleItem) => (
        <SampleItem item={sampleItem} key={sampleItem.id} />
      ))}
    </>
  )
}

上記により、sampleFilterで選択されたデータをSampleItemコンポーネントに渡すように設定されました。

ではデータをどうやって選択するかについて説明します。

SampleFilter.tsx
import React from 'react';
import { useRecoilValue } from 'recoil';
import { sampleListFilterState } from './atom';


export function SampleFilter() {
  const [filters, setFilters] = useRecoilValue(sampleListFilterState);

  const updateFilter = (event: React.ChangeEvent<{ value: unknown }>) => {
    setFilter(event.target.value as string);
  }

  return (
    <select value={filter} onChange={updateFilter}>
      <option value="Show All">All</option>
      <option value="Show Completed">Completed</option>
      <option value="Show Uncompleted">Uncompleted</option>
    <select/>
  )
}

まずuseRecoilValuesampleListFilterStateを設定します。

selectで3個の要素を選択することができます。selectのvalueはfilterSampleListStateにて記述したswitchのcaseと一致します。

これで選んだデータがSampleItemコンポーネントにpropsで渡されるようになりました。


以上がSelectorsの簡単な使い方です。

参考サイト

Recoil

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

Ant Design で Table の columns の dataIndex の候補を表示する型

Ant Design で Table の columns の dataIndex の候補を表示する型

antd の Tablecolumns には表示する対象を指定する dataIndex がありますが、
これは手動で指定する必要があります。

no-suggestion
type Data = {
  id: number;
  name: string;
};

export const columns: TableProps<Data>['columns'] = [
  {
    title: 'ID',
    key: 'id',
    // これだと候補がでない
    dataIndex: '',
  }
];

no-suggestion.png

そこで以下のような型を定義することで dataIndex の型を指定してみます。

columns-type
import { ColumnGroupType, ColumnType } from 'antd/lib/table/interface';

type AnyData = Record<string, unknown>;

type RenderReturn<TRecord = AnyData> = ReturnType<
  NonNullable<ColumnType<TRecord>['render']>
>;

type Column<TRecord = AnyData> =
  | (Omit<ColumnGroupType<TRecord>, 'render'> & {
      render?: (
        value: TRecord,
        record: TRecord,
        index: number
      ) => RenderReturn<TRecord>;
    })
  | (ColumnType<TRecord> & {
      dataIndex?: keyof TRecord;
      render?: <T = TRecord>(
        value: T,
        record: TRecord,
        index: number
      ) => RenderReturn<TRecord>;
    });

type Columns<TRecord = AnyData> = Column<TRecord>[];

type Data = {
  id: number;
  name: string;
};

export const columns: Columns<Data> = [
  {
    title: 'ID',
    key: 'id',
    dataIndex: '',
  },
];

定義した Columns を指定することで dataIndex に候補が出るようになりました。

suggestion.png

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

ReactとFirebaseを使ってログインフォームを実装する①

ReactとFirebaseを使用してProgateのようなログインフォームを作成してみます。Google、Twitter、FacebookのソーシャルログインとEメール、パスワードを用いた一般的なログインを実装します。

ソーシャルログインはFirebaseを利用すれば比較的簡単ですが、結構大変でした。。私と同じように個人で開発している人のお役に立てれば幸いです。

React、Redux、react-router、Firebaseの基本的な使い方を理解している方を対象としています。全4回です。

使用するパッケージ

  • 状態管理にredux、、react-redux、
  • 非同期処理にredux-thunk
  • cssフレームワークにmaterial-ui
  • ルーティングにreact-router-dom
  • 完成品はGitHubにアップしています。

https://github.com/nineharker/react-firebase-login-form

プロジェクト作成

create-react-appで作成します。このままでもいいのですが、ESlintとPrettierを設定した方が便利なので設定しておきます。使うエディタはVS Codeです。自分のエディタがある人はそれを使ってください。

VS CodeでReact、ESlint、Prettierを設定する方法は下記の記事を参照してください。
https://harkerhack.com/vscode-react-eslint-prettier/
GitHubに設定済のプロジェクトがあるのでダウンロードしましょう。
https://github.com/nineharker/react-firebase-login-form

#install create-react-app
npm -g install create-react-app

# clone repo
git clone https://github.com/nineharker/react-vscode-eslint-prettier.git

cd react-vscode-eslint-prettier

yarn install

必要なファイルを作成する

まず最低限のページを作成します。必要なパッケージをインストールしましょう。

yarn add @material-ui/core redux react-redux react-router-dom redux-thunk

ランディングページとログイン後のページの二つを作ります。それと二つのページで共有するヘッダーを用意します。src/componentsディレクトリ作成し、 components以下にLandingPage.jsxとLoginedPage.jsx、NavBar.jsxを作成します。

src/components/LandingPage.jsx
import React, { Component } from 'react';

# ランディングページ
class LandingPage extends Component {
  render() {
    return <div>LandingPage</div>;
  }
}

export default LandingPage;
src/components/LoginedPage.jsx
import React, { Component } from 'react';

# ログイン後のページ
class LoginedPage extends Component {
  render() {
    return <div>LoginedPage</div>;
  }
}

export default LoginedPage;

ヘッダーはmateriaru-uiを使います。Reactのコンポーネントとして利用できます。

AppBar参考
https://material-ui.com/components/app-bar/

src/components/NavBar.jsx
import React, { Component } from 'react';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';

const useStyles = makeStyles(theme => ({
  root: {
    flexGrow: 1,
  },
  menuButton: {
    marginRight: theme.spacing(2),
  },
  title: {
    flexGrow: 1,
  },
}));

export default function NavBar() {
  const classes = useStyles();
  return (
    <AppBar position="static">
      <Toolbar>
        <Typography variant="h6" className={classes.title}>
          News
        </Typography>
        <Button color="inherit">ログイン</Button>
      </Toolbar>
    </AppBar>
  );
}

ルーティング

react-router-domを使用して二つのページをルーティングします。App.jsxを編集しましょう。の外にを書くことで、どちらのページでもヘッダーが表示されるようになります。

react-router参考
https://qiita.com/muiyama/items/b4ca1773580317e7112e

src/App
import React, { Component } from 'react';
import { Router, Route, Switch } from 'react-router-dom';

import history from './history';
import LandingPage from './components/LandingPage';
import LoginedPage from './components/LoginedPage';
import NavBar from './components/NavBar';

class App extends Component {
  render() {
    return (
      <Router history={history}>
        <NavBar />
        <Switch>
          <Route path="/" exact component={LandingPage} />
          <Route path="/logined" exact component={LoginedPage} />
        </Switch>
      </Router>
    );
  }
}

export default App;

react-router-domのBrowserRouterではなくRouterを使っているので、別に履歴を管理するhistory.jsが必要になります。 src以下に作成しましょう。historyが別にあることによってGoogleアナリティクスの設定などができるようになります。

src/history.js
const createHistory = require('history').createBrowserHistory;

export default createHistory();

それではyarn startでローカルサーバーを起動してみましょう。
http://localhost:3000/ に青色のヘッダーとLandingPageという文字、
http://localhost:3000/logined に青色のヘッダーとLoginedPageという文字があればルーティングは完了です!

おわり

今回で基本的なページを作成することができました! 全体のページ数はユーザーが最初に訪れるランディングページと、ログイン後に偏移するページの二つです。

次回はログインフォームを作っていきます。普通に作ると面倒ですが、フレームワークを使えば簡単に実装できます。

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

UikitとFontAwesomeをNext.js(React.js)上で動くようにする

本手順は、Next.js上で、UikitとFontAwesomeを動くようにする手順です。

前提

ツール バージョン ツール バージョン
React.js 16.13.1 Next.js 9.4.4
uikit 3.5.5 react-icons 3.10.1

背景

  • もともと以下のようなhtmlで、の中でcdnを使用してUikitとFontAwesomeを呼び出していた。
  • Next.js (React.js)に移行した途端に、uikitのアイコンとFontAwesomeのアイコンが読み込まれなかった。
  <!--UiKit-->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.3.1/css/uikit.min.css" integrity="***" crossorigin="anonymous" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.3.1/js/uikit.min.js" integrity="***" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.3.1/js/uikit-icons.min.js" integrity="***" crossorigin="anonymous"></script>
  <!--FontAwesome-->
  <script src="https://kit.fontawesome.com/1d615c07d9.js" crossorigin="anonymous"></script>
...
<!--FontAwesome-->
<i class="fas fa-user-shield fa-5x"></i>
<!--UiKit-->
<span uk-icon="icon: users" class="uk-margin-small-right uk-icon"></span>

原因

  1. Reactで読み込むための指定方法は下記のようになるらしい (参考) html × <span uk-icon="icon: users" class="uk-margin-small-right uk-icon"></span> ○ <span data-uk-icon="icon: users" class="uk-margin-small-right uk-icon"></span>
  2. そもそも、レンダー時にUikit, FontAwesome共にうまく読み込めていなかった。。 => そもそもCDNを使うべきかは未検討だったため、いったんローカルでインストールする方法を模索。 => npm install を使って必要なものをインストールする方針に変更

解決

  1. uk-***のところをdata-uk-***に変更
  2. UiKitの必要なものをインストールし、コードを修正 => uikit
$ npm install uikit
import UIkit from 'uikit';
import Icons from 'uikit/dist/js/uikit-icons';
...
UIkit.use(Icons);

  1. FontAwesomeの必要なものをインストールし、コードを修正 => react-icons
$ npm install react-icons
import { IconContext } from "react-icons";
import { FaUserShield , FaGlobe, FaUsers  } from 'react-icons/fa';
...
    render() {
        return (
            <IconContext.Provider value={{ style: { fontSize: '5em' } }}>
            <div>
                <FaUserShield />  
                <FaGlobe />
                <FaUsers />
            </div>
        );
    }
...

参考

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

create-react-appでのReact環境構築がつらいので新しい簡易版ツール作った

create-xxx-app

作った。

npx create-xxx-app プロジェクト名 --template react

でReact+TypeScriptのプロジェクト作られるはず。
インストールされるボイラープレート部分についてはいろいろ考え中なのでまだ中途半端です。後で大幅に変わる可能性があります。
でも一応動くReactのウェブアプリ作れるよ!

ちなみにxxxという文字の並びには英語圏ではエロい意味があるらしい。後で知った。

読みたくないから短くまとめろ

  • create-react-appつらい
    • 縛りきついから入れた瞬間にejectしたくなる
    • eject後のファイルが意味不明すぎて解読するより自分で始めから構築した方が早い
  • でもcreate-react-app捨てて生きていけない
    • 手動構築だるい!create-react-app無しの時代には戻れない!
  • だから新しいツール作ったよ
    • わりとニッチ向け
    • ボイラープレートをコピーしてnpm installするだけ
    • create-react-appと手動構築の中間ぐらいの存在

create-react-appつらい問題

create-react-app(CRA)、最初は便利だなーって思うんですけど、だんだんとブラックボックス化された部分に不満が溜まってきます。
あれができないとかこれができないとか、変なところでエラー出るとか、やり方は存在するけど遠回りとか。重要な部分を隠蔽しないでほしい。
たまにならいいけどほぼ毎プロジェクトで不満出る。公式的には不満あるならejectすりゃいいじゃん的な雰囲気を放ってますが、ejectするとwebpackの闇を煮詰めたようなファイルが出力されます。
これを解析するよりも自分で初めから環境構築した方が早い。

それに加えて同梱されてる初期ファイルが邪魔。ServiceWorkerいらねえ場面とかCSS Module使いたくない場面とかいっぱいある。カスタムテンプレート作るの面倒だから毎回ちまちま削除してる。

じゃあお前はCRA無しで生きていけんのって言われたら生きていけない。webpackの設定ファイル書きたくないしnpm install なんちゃら かんちゃら @types/ほんにゃら を打ち込みまくるのもつらい。もう完全にCRAに飼い慣らされてる。

カスタムテンプレートという手もあるけど、それも結局はCRAの手の上で踊るだけ。同じ結末になる。

それならお前はどうしたいのって言われたら別にどうもしたくなく、白馬の王子様がなんかうまい感じに助けてくれるの期待してました。

結局何が欲しいんだよ

結局自分は何が欲しいのかよくわからんまま半年ぐらい経過しました。CRAには不満。じゃあどうしてほしいのって聞かれると答えられない。どうしようもねえ。

でもやっぱり npm install アレ コレ ソレ @types/ドレ を打ち続ける日々には辟易してましたし、CRA使うと後で死ぬほど苦労するという経験則だけはたまってます。単純なReactアプリにnext使うのはなんか違うという気分もある。

snowpackとかも使ってみたいんだけど、create-snowpack-appの公式テンプレートの出来がかなりアレ。変なところから設定をextendsしてるから設定ファイルの中身をサクっと追えない。自分はどこに向かえばいいんだ。

もうcpとnpm installだけでよくないか

自分がReactプロジェクトを作るときによくやってた作業について考えてみると、結局コピーとnpm installしかしてない気がしてきました。tsconfig.jsonをどっかからコピーしてきてちょっといじって、reactと@types/reactと入れて、みたいなのの繰り返し。

じゃあもうここの部分だけ自動化してあとは手動でやれば良くね?

シェルスクリプトだとどこに置くの問題とか発生するし、ボイラープレートリポジトリ作ってもいちいちcloneするのすら面倒。yarn createは好きではないし、じゃあもうここツール化しちゃえばいいんじゃね?って思いつきました。npxで叩きたいからどこからでも使えるようにnpm publishしました。

create-xxx-appの誕生です。

正直なところとしてはcpしてnpm installしてくれればあとは勝手にやるんですよ。jest欲しかったら自分で入れるしeslintの設定欲しかったら自分で書く。とりあえずプロジェクト作る最初のcpとnpm installだけしてくれればいい。一生面倒見てくれる献身的なreact-scriptsとか完全に不要で、コピーとインストールだけしてくればそれでよかった。プロジェクト構築ツールはプロジェクト構築終わったら存在が完全に消えていてほしいという願いです。痕跡を残さないでほしい。

cxa-template-react

--template react で使うテンプレートはこんな感じでpublishしてあります。

https://www.npmjs.com/package/cxa-template-react

中身はここ。

https://github.com/kotofurumiya/cxa-template-react

中身見ると(2020/08/12時点では)reactとesbuild入れてbrowser-syncでグルグル回してるだけ。
ブラックボックス化された部分は作られないし、令和にもなってbrowser-sync使ってるのが丸わかりだし、変更入るたびにフルビルドが走るtools/devserver.jsのクソコードがフルオープンになってます。
これだよこういうの求めてたんだよ(自画自賛)。

create-xxx-appで作られたプロジェクトは簡単にカスタマイズできます。だってフォルダ作ってnpm installしてるだけだもん。browser-syncいらなかったらuninstallすれば終わるし、prettierほしかったらinstallすれば動く。

カスタムテンプレート

cxa-template-templatename の命名規則でpublishすれば npx create-xxx-app prjname --template templatename で取ってこれます。テンプレートパッケージのフォルダ構造とかはcxa-template-reactを見てパクってください。

ちなみにReactには依存してないので任意のNode.jsプロジェクトを作れます。

さいごに

CRAは素晴らしいけど、現実で使おうとすると割と結構いろんな問題出てきます。CRAは目的にマッチしないけど手動でやるのは嫌って人向けのツールがcreate-xxx-appです。

「CRAつらいけど手動でやるのはな〜」みたいな人がいたらぜひ使ってみてください。まだエラーハンドリングとかもろくにできてないので環境によっては動かないとかもあるかもですが、よろしくお願いします。

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

Parsing error: The keyword 'import' is reserved の解決法

解決法

eslintから怒られているので、.eslintrcに怒られないようにこちらを追記してください。

{
    ...
    "parserOptions": {
        "ecmaVersion": 7, 
        "sourceType": "module",
        "ecmaFeatures": {
            "jsx": true
        }
    }
   ...
}

参考
https://github.com/yannickcr/eslint-plugin-react/issues/447#issuecomment-208625730

eslintのプロパティについてはこちら
https://eslint.org/docs/user-guide/configuring

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