20210129のReactに関する記事は13件です。

音のあるプッシュ通知を実装してみた(React.js)

はじめに

最近、個人開発でプッシュ通知を実装して、音をつけたいなと思ったので実装してみました。プッシュ通知はNotificationAPIを使っているので、残念ながらサイトを開いてないと通知されません。ServiceWorkerとweb-push使って、バックグラウンドで実装できるようにしたいと思ってます。
React使ってます。

参考:
通知APIの使用
Web Audio API

効果音探しに使わせていただきました↓
フリー音楽素材魔王魂

ソースコード

説明は簡単にコメントで記述しました!

src/App.tsx
import { useEffect, useState } from "react";

function App() {
  const [hasSound, setHasSound] = useState(true);
  useEffect(() => {
    if ("Notification" in window) {
      // 通知が許可されていたら早期リターン
      const permission = Notification.permission;
      if (permission === "denied" || permission === "granted") {
        return;
      }
      // 通知の許可を求める
      Notification.requestPermission().then(() => new Notification("テスト"));
    }
  }, []);

  const handlePushNotif = () => {
    if ("Notification" in window) {
      const notif = new Notification("こんにちは!");
      // プッシュ通知が表示された時に起きるイベント
      notif.addEventListener("show", () => {
        // 状態によって音の有無を変える
        if (hasSound) {
          // 音再生
          new Audio("./push.wav").play();
        }
      });
    }
  };

  return (
    <div>
      <button onClick={handlePushNotif}>PUSH</button>
      <button onClick={() => setHasSound((prev) => !prev)}>
        {hasSound ? "音なしにする" : "音ありにする"}
      </button>
    </div>
  );
}

export default App;

初回アクセス時は下の画像のようなものが出てくるので許可するとプッシュ通知が出るようになります。
image.png

creact-react-appで作られたApp.tsxを変えただけの簡単なコードです。
押したらプッシュ通知を出すボタンと音の有無を切り替えるボタンが表示されます。
image.png

一応完成のコード公開したので実際に試したい場合はクローンしてみてください!
https://github.com/NozomuTsuruta/simple-notification-audio

終わりに

ここまで読んでいただきありがとうございました!少しでもお役に立てれば嬉しいです
また、「もっといい方法あるよ」だったり、「こういう面白い技術があるよ」などなど気軽にコメントやTwitterで絡んでくれたりすると嬉しいです!
いろいろな技術を触りながら日々精進していきます!

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

Next.jsのプロジェクトにTypeScriptを導入する方法

Next.jsの公式チュートリアルにそって進めます

導入手順

①プロジェクトルートにtsconfig.jsonを追加。

$ touch tsconfig.json

②TypeScriptのパッケージをインストール。

# If you’re using npm
$ npm install --save-dev typescript @types/react @types/node

# If you’re using Yarn
$ yarn add --dev typescript @types/react @types/node

③ローカルの開発サーバの際立ち上げ。ここで、tsconfig.jsonの中身が記述され、next-env.d.tsが追加されます。

$ npm run dev or yarn dev

④JS ファイルを TS ファイルに変換します。

$ find pages -name "_app.js" -or -name "index.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv & \
  find pages/api -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv

以上です。

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

React でソフトウェアキーボードを作る

react 2年目がいろいろ学んだことやハマったことの記録。
今回は、「ランダムにキー配置するソフトウェアキーボード」を作った時のお話。

キー配列をシャッフル

キーボードは数字のみと英数字記号の2タイプ。さらにキー配置そのものを変えるものと、あるグルーピング単位で配置を変えるという仕様のキーボードを作ります。
ランダム→配列をシャッフルするから、Array.shuffle() かな?と思いましたが・・・ありませんでした。
みなさん自作なさってるようなのでそれについては真似して作りました。

続けてシャッフル対象のキー配列を作成します。まず「数字のみ」タイプから。
数字のみのほうはキー配置そのものを変える仕様のキーボードです。
もう1つのタイプでも数字は使うので、1〜9の数字と0の配列を1つ別に作っておきます。
数字タイプの、いわゆる10キーは3列4行の配置でレイアウトしたいため、ダミーの空文字を2つ加えて12個のキーを作り、シャッフルします。
シャッフルするタイミングはキーボードを開く時のみにしたいため、depsは未指定にします。

const numbers: string[] = React.useMemo(() => {
  return [...Array.from({length: 9}, (_, i) => (i + 1).toString()), '0'];
}, []);

const tenkeys: string[] = React.useMemo(() => {
  return shuffle([...numbers, '', '']);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

グループ配列をシャッフル

残りのキーボードは「英字」「数字」「記号」のグルーピング単位で配置を変えます。
例えば「数字」「英字」「記号」とか、「記号」「英字」「数字」とか、そういう配置換えです。
この場合だと、3つの数字をシャッフルすればよいだろう、ということで2次元配列を作成してみました。
こちらも10キーと同様、シャッフルするタイミングは開くときのみです。

const alphabets: string[] = 'ABCDEF...'.split(''); // アルファベット文字列は省略してます
const symbols: string[] = '\"#$%...'.split(''); // 記号文字列も省略してます

const charkeys: stinrg[][] = React.useMemo(() => {
    return [[...numbers], [...alphabets], [...symbols]];
}, [numbers, alphabets, symbols]);

const groups: number[] = React.useMemo(() => {
  return shuffle([...Array(charKeys.length).keys()]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

実際の10キーのキーボードはこんな感じで作ってみました。

const tenKeyboard = React.useMemo(() => {
  return (
    <>
      <div className="tenkey">
        <div className="fixed">
          {tenkeys.map((key, index) => (
            <div className="keycell" key={index} onClick={() => onClick(key)}>
              {key}
            </div>
          ))}
        </div>
      </div>
    </>
  );
}, [tenkeys, onClick]);

英数字記号のほうも同様な感じで、ただしこちらは2次元配列のため2回ぐるぐるまわします。

const alphanumKeybord = React.useMemo(() => {
  return groups.map((key, i) => {
    const charKey = charKeys[key];
    return (
      <React.Fragment key={i}>
        <div className="keySection">
          {charKey.map((key, index) => (
            <div className="keycell" key={index} onClick={() => onClick(key.toString())}>
              {key}
            </div>
          ))}
        </div>
      </React.Fragment>
    );
  });
}, [keyboards, charKeys, onClick]);

シャッフルしまくる

今回のハマりどころは、メモ化のdeps指定でした。
初めはあまり深く考えずに、メモ化してeslintに言われるままdepsに影響する変数を書き込んでました。
すると、キーをクリックするたびにシャッフルが走ってしまうキーボードが出来上がってしまいました。

シャッフルは最初の1回だけなのだから、ということで前述した空配列指定になったわけです。ただしこのままだとeslintに怒られますので、無効化にするコメントも一緒に記載します。

メモ化はどのタイミングでメモの内容を書き換えるべきか?をよく考えて使わないといけない、と勉強した作業となりました。

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

Next.js + Typescript + TailwindCSSのつまずかない環境構築

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

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

> npx create-next-app MyApp

Typescript

Typescriptのインスコ

> touch tsconfig.json 
> yarn dev

yarn devするとtsconfig.jsonあるのにts関連のパッケージないよ!って言われるので
言われた通りのパッケージをインスコ

_app.jsとindex.jsを_app.tsx,index.tsxに変える

_app.tsx
import "../styles/globals.css";
import { AppProps } from "next/app";

export default function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}

index.tsxは自動生成されるもの消して終わり

TailwindCSS

TailwindCSSのインスコ

> yarn add tailwindcss@latest postcss@latest autoprefixer@latest postcss-preset-env
> touch postcss.config.js
> npx tailwindcss init --full // fullにするとstyle全部ぶっ込んでくれる

設定ファイル書き換え

tsxファイルに適用させる設定を書く

tailwind.config.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const colors = require("tailwindcss/colors");

module.exports = {
  + purge: ["./pages/**/*.tsx", "./components/**/*.tsx", "./public/**/*.html"],
  presets: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
      // 略
postcss.config.js
module.exports = {
    plugins: { tailwindcss: {}, "postcss-preset-env": {} },
  };

styleの適用

style/global.cssを書き換える

style/global.css
/* ./styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

test用ボタン

これにstyleが適用されて表示できたら完了

<button id="sendMoneyButton" className="px-4 py-1 text-sm text-purple-600 font-semibold rounded-full border border-purple-200 hover:text-white hover:bg-purple-600 hover:border-transparent focus:outline-none focus:ring-2 focus:ring-purple-600 focus:ring-offset-2">Send</button>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Reducerでstateを直接変更してはならないことの背景

Reducerのルール

Reducerにはいくつかの決まりごとがあり、よくあるミスとして「引数として渡されたstateそのものに変更を加えてはならない」ところを、直接変更を加える処理をreducerに記述してしまうというものがあります。

ルールの背景

各reducerをまとめてexportするときにcombineReducers関数を用いますが、このcombineReducersのコード( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts )を確認すると、終盤辺りに以下のようなコードがあります。

combineReducers.ts
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
  const key = finalReducerKeys[i]
  const reducer = finalReducers[key]
  const previousStateForKey = state[key]
  const nextStateForKey = reducer(previousStateForKey, action)
  if (typeof nextStateForKey === 'undefined') {
    const errorMessage = getUndefinedStateErrorMessage(key, action)
    throw new Error(errorMessage)
  }
  nextState[key] = nextStateForKey
  hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length
return hasChanged ? nextState : state

このコードは、受け取ったactionを各reducerに送り、reducerから返ってきたstateが更新されているかどうかを判断して、更新されていれば更新後のstateを、そうでなければ元々のstateを返すというものです。

forループでは、各reducerについてfor文内の処理が行われます。
for文内では、更新前のstateがpreviousStateForKeyに代入されます。
次に、reducerが実行され、reducerが返した更新後のstateがnextStateForKeyに代入されます。

そして、次の1行でstateの更新の有無が判定されます。

hasChanged = hasChanged || nextStateForKey !== previousStateForKey

この行では、previousStateForKeyとnextStateForKeyの比較が行われ、両者が異なっている(previousStateForKey !== nextStateForKey がTrueである)ならば、hasChangedはTrueとなり、更新後のstateが返されることになります。Falseであれば、更新前のstateがそのまま返されます。

ここで、次のようなケースを考えます。

const state = {name:"Taro",nation:"USA"};
const previousState = state;
const reducer = (state) => {
    state.nation = "Japan";
    return state;
};
const newState = reducer(state);

上のreducerは、stateを直接変更してしまっています。(実際のreducerではactionも引数として渡されますが、ここでは簡略化のために省いています。)

このような場合、次の真偽値はfalseとなります。これは、オブジェクトは参照渡しであるということから、newStateとpreviousStateは同じアドレスを参照しているためです。

newState !== previousState  //false

この結果、先程のcombineReducersのコードにおけるhasChangedはFalseになり、stateを更新したはずなのに更新されていないというような問題が起こるという訳です。

実は、2019年頃に次の一行が加わっており( https://github.com/reduxjs/redux/pull/3490/commits/001a1979372dbd9cf431805f439a179eb05e20be )combineReducersの変更検知の方法も少し変わったようですが、reducerでstateを直接変更しないという習慣は今後も続けた方が間違いはないと思います。

hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length

参考文献

・Redux combineReducers.ts( https://github.com/reduxjs/redux/blob/master/src/combineReducers.ts

・Udemy講座「 Modern React with Redux [2020 Update] 」( https://www.udemy.com/course/react-redux/
この講座は、Reactにおける様々な決まりごとの背景なども教えてくれます。
今回、reducerの決まりごとに関してここで学んだ内容について、備忘録を兼ねて共有させていただきました。

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

React + react-use: useKeyフックでキーボードイベントを扱う

react-useは、便利なフックを詰め合わせたライブラリです。本稿はその中から、キーボードイベントが簡単に扱えるuseKeyをご紹介します。作例はCodeSandboxに公開しました。

Create React Appでカウンターアプリケーションをつくる

サンプルにするのは、よくとり上げられるカウンターです。React公式サイトの「ステートフックの利用法」でも、useStateフックの説明に使われています。

Reactアプリケーションのひな型は、モジュール分けできるCreate Reac Appでつくることにしましょう(「Create React App 入門 01: 3×3マスのゲーム盤をつくる」01「Reactアプリケーションのひな形をつくる」参照)。

npx create-react-app react-usekey

ルートコンポーネントのモジュールsrc/App.jsは、つぎのコード001のように書き改めます。useStateでカウンター数値の状態変数(count)をひとつ定め、減算(decrement)と加算(increment)のハンドラを加えました。CounterDisplayは、このあと新たにつくるカウンター表示のコンポーネントです。

コード001■useStateとプロパティを用いたルートモジュール

src/App.js
import React, { useState } from 'react';
import CounterDisplay from './CounterDisplay';

const initialCount = 0;
function App() {
    const [count, setCount] = useState(initialCount);
    const decrement = () => setCount((count) => --count);
    const increment = () => setCount((count) => ++count);
    return (
        <div className="App">
            <CounterDisplay counter={{ count, decrement, increment }} />
        </div>
    );
}

export default App;

カウンターを表示するコンポーネントのモジュールsrc/CounterDisplay.jsは、つぎのコード002のように定めました。コンポーネントが受け取ったプロパティ(counter)から、カウンター数値(count)と減算(decrement)・加算(increment)のハンドラを取り出して、JSXのテキストとボタンに与えましょう。

コード002■カウンター表示のモジュール

src/CounterDisplay.js
import React from "react";

const CounterDisplay = ({ counter }) => {
    return (
        <div>
            <button onClick={counter.decrement}>-</button>
            <span>{counter.count}</span>
            <button onClick={counter.increment}>+</button>
        </div>
    );
}
export default CounterDisplay;

これで、数値をボタンで増減できるカウンターのでき上がりです(図001)。まだ、キーボードイベントには対応していません。

図001■でき上がったカウンター

2009001_001.png

react-useのuseKeyフックを使う

では、いよいよuseKeyフックを使います。まずは、アプリケーションへのreact-useのインストールです。

npm install react-use

そうしたら、キーボードイベントを扱いたいコンポーネントに、useKeyimportしてください。上下の矢印キーで、カウンターを増減したいと思います。

useKeyの使い方は、つぎのように気が抜けるほど簡単です。第1引数にキーを示すKeyboardEvent.keyプロパティの値、第2引数にイベントハンドラを渡します。JSXのどの要素にイベントハンドラを加えるか、などと悩まなくて済むのです。

src/App.js
import { useKey } from 'react-use';

function App() {

    useKey('ArrowDown', decrement);
    useKey('ArrowUp', increment);

}

ここまでのルートモジュールsrc/App.jsの記述全体を、つぎのコード003にまとめました。

コード003■上下矢印キーでカウンターを増減させるルートモジュール

src/App.js
import React, { useState } from 'react';
import {useKey} from 'react-use';
import CounterDisplay from './CounterDisplay';

const initialCount = 0;
function App() {
    const [count, setCount] = useState(initialCount);
    const decrement = () => setCount((count) => --count);
    const increment = () => setCount((count) => ++count);
    useKey('ArrowDown', decrement);
    useKey('ArrowUp', increment);
    return (
        <div className="App">
            <CounterDisplay counter={{ count, decrement, increment }} />
        </div>
    );
}

export default App;

[control]/[Ctrl] + [esc]キーでカウンターをリセットする

キーボードイベントの処理をもうひとつ加えます。[control]/[Ctrl] + [esc]キーを押したら、カウンターの数値を0にリセットしましょう。[esc]キーのKeyboardEvent.keyプロパティの値はEscapeです。[control]/[Ctrl]キーを押しているかどうかは、KeyboardEvent.ctrlKeyで調べられます。

useKeyのつぎの構文で、第1引数はキー判定する関数です。キーイベントを受け取るので、ハンドラを呼び出すキーかどうか論理値で返します。第3引数のeventプロパティに定めるのは、keydown/keypress/keyupのいずれかのキーボードイベントです。

const キー判定関数 = (event) => 判定した論理値;
useKey(キー判定関数, イベントハンドラ, {event: イベント});

キー判定関数では、つぎのように[control]/[Ctrl] + [esc]キーをイベント(event)のプロパティから調べます。キーボードイベントはkeyupとしました。ルートモジュールの記述は、以下のコード004にまとめたとおりです。作例はCodeSandboxに公開しましたので、動きやモジュールごとのコードはこちらでお確かめください。

src/App.js
function App() {

    const predicate = (event) => event.ctrlKey && event.key === 'Escape';
    const escKeyUpHandler = () => setCount(0);
    useKey(predicate, escKeyUpHandler, {event: 'keyup'});

}

コード004■[control]/[Ctrl] + [esc]キーでカウンターをリセットするルートモジュール

src/App.js
import React, { useState } from 'react';
import {useKey} from 'react-use';
import CounterDisplay from './CounterDisplay';

const initialCount = 0;
function App() {
    const [count, setCount] = useState(initialCount);
    const decrement = () => setCount((count) => --count);
    const increment = () => setCount((count) => ++count);
    useKey('ArrowDown', decrement);
    useKey('ArrowUp', increment);
    const predicate = (event) => event.ctrlKey && event.key === 'Escape';
    const escKeyUpHandler = () => setCount(0);
    useKey(predicate, escKeyUpHandler, {event: 'keyup'});
    return (
        <div className="App">
            <CounterDisplay counter={{ count, decrement, increment }} />
        </div>
    );
}

export default App;

オブジェクトの分割代入を使う

ここで、オブジェクトの分割代入の構文をご紹介しましょう。前掲コード004のキー判定関数(predicate())で、引数のキーボードイベントからプロパティを取り出すコードです。オブジェクトの分割代入を使えば、つぎのように引数からプロパティが簡単に取り出せます。

// const predicate = (event) => event.ctrlKey && event.key === "Escape";
const predicate = ({ ctrlKey, key }) => ctrlKey && key === "Escape";

[追記: 2021年01月30日]「オブジェクトの分割代入を使う」の項を追加。

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

github actionsで自動テストし、ReactのアプリをApp Engineに自動デプロイする

この記事ではgithub actionsを用いて、React+TypeScriptで作成したアプリを自動テストして、App Engineに自動デプロイする方法の一例を紹介します。僕自身、Circle CIに挑戦してみたけど挫折して、試しにGithub actionsを利用してみたら結構すんなりできたので、そういう人にもおすすめかもしれません。

以下、自動デプロイ→CD、自動テスト→CIとします。

また、想定している読者は以下の通りです。

  • Circle CIに挑戦してみたけど挫折した人
  • github actionsが何なのかなんとなくわかる人
  • 既に手動であればテストもデプロイもできる人

ちなみに「github actions?なにそれ?」となる人は参考記事を紹介しておきます。

Github Actionsの使い方メモ
Github Actionsが使えるようになったので使ってみる

どのタイミングでCI/CDが動くのか

まずは、どのタイミングでCICDが走るのか確認しておきましょう。

以下の表をご覧ください。

タイミング
CI Pull Request(以下PR)が作成されたタイミング
CD PRがmasterブランチにmergeされたタイミング

今回は、これらのタイミングでCICDが走るようにコードを書きます。

最終的に完成したコード(CI)

github actionsでは.github/workflows/配下の1つのyamlファイルが1つのワークフローとなります。

今回は、CI用で1つ、CD用で1つのファイルを用意しました。

まずはCI用のtest.yamlから。

test.yaml
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Node.js CI

on: pull_request

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [15.x]
        # See supported Node.js release schedule at https://nodejs.org/en/about/releases/

    steps:
      - uses: actions/checkout@v2
      - name: Cache Node.js modules
        uses: actions/cache@v2
        with:
          path: ~/.npm
          key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.OS }}-node-
            ${{ runner.OS }}-
      - name: docker-compose up
        run: docker-compose up -d firestore
      - run: sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share}
      - run: npm install -g npm@latest
      - run: rm package-lock.json
      - run: rm -rf node_modules
      - run: sed -i 's/babel-jest//g' package.json
      - run: npm install
      - run: CI=false npm run build
      - run: npm test

ファイルの中身について

github actionsのnew workflowにあるテンプレートに自分なりに付け足していく感じになります。
そのテンプレートに手動でテストするときのコマンドを記載していきます。

僕の場合、手動でテストする場合は、

  1. firebase emulatorを起動(docker-compose)
  2. npm run test

という手順をとっているので、まずはdocker-compose up -d firestorenpm run testというコマンドを記述したのですが、実際にPRを作成してみると様々なエラーでワークフローが止まります。これに関しては人によって違うと思うのですが、僕の場合はエラーメッセージを調べつつ、エラーを一つ一つ解消するようにコマンドを追加していった結果が上記のようになりました。

最終的に完成したコード(CD)

次にapp engineに自動デプロイするためのdeploy.yamlになります。

deploy.yaml
name: CD

# Controls when the action will run.
on:
  pull_request:
    types: [closed]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2
      - uses: google-github-actions/setup-gcloud@master
        with:
          project_id: ${{ secrets.GCP_PROJECT_ID }}
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true

      # Runs a single command using the runners shell
      - run: rm package-lock.json
      - run: rm -rf node_modules
      - name: npm install, build
        run: |
          npm install
          CI=false npm run build
        env:
          FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

      # Runs a set of commands using the runners shell
      - name: GAE deploy
        run: |
          echo ${{ secrets.GCP_SA_EMAIL }} | 
          gcloud app deploy --quiet

ファイルの中身について

デプロイは手動の場合は、

  1. npm run build
  2. gcloud app deploy

で行っています。test同様これだけをyamlに記述してもうまくいきません。エラーが色々起こります。
それを一つ一つ解決していき、最終的に上記のようなファイルになっています。

また、

deploy.yaml
on:
  pull_request:
    types: [closed]

としているのですが、これはPRがクローズされたタイミング、つまりmasterブランチにmergeされたタイミングでファイルに記述したワークフローが動くというものです。

実行環境(runs-onの部分)はubuntuを利用していますが、これはgcloudコマンドがデフォルトで利用できるからです。circle CIだとpathを通さないといけなく、そこで結局挫折したのですが、それがデフォルトで使えるとなるとubuntuを利用しない手はありません。

また、uses: google-github-actions/setup-gcloud@masterの部分では、githubのsecretsに登録しておいたgcloudのサービスアカウントの認証情報を読み込んでいます。

まとめ

github actionsはcircle CIに比べて、参考となる記事や自分と同じような環境で実行している人の情報が少ないため、色々手探りのような感じになると思いますが、環境ごとのワークフローのテンプレートが豊富に用意されているので、個人的にはcircle CIよりも使いやすい印象を受けました。デプロイでは認証の部分で少してこずりましたが、思いのほか簡単にできたので、github actionsに挑戦してみて良かったと思っています。

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

React Redux Firebase 環境構築

【Reactのローカル環境構築】

ターミナル
% cd [フォルダ名]
% npx create-react-app[フォルダ名]

【Firebaseのプロジェクトを作成】

・アプリを追加
・プロジェクトの設定からリソースロケーションを「asia-northeast1」へ変更
・データベースの作成(本番環境)
・注意:プランを従量制にすること

【Reactのローカル開発環境とFirebaseのプロジェクトをコネクトする】

ターミナル
% firebase login
% Firebase init

どのサービスを使うか聞かれるので選択

ターミナル
・Database
・Firestore
・Functions
・Hosting
・Storage
・Emulator

既存のプロジェクトを使いますかと聞かれるので

ターミナル
=> use an existing project

どのプロジェクトか聞かれるので

ターミナル
=> [Firebaseのプロジェクト名]

その後色々質問あって...基本デフォルトでいいのでエンター

どのパブリックディレクトリを使いますかと聞かれるので

ターミナル
build

シングルアプリケーションとして設定しますかと聞かれるので

ターミナル
Yes

デフォルトはNoなので注意

【必要なパッケージをインストール】

ターミナル
% npm install --save [パッケージ]

【Firebaseのデプロイ】

まずBuildしてReactアプリとして公開できる状態にする必要があるので

ターミナル
% npm run build

この状態で

ターミナル
% firebase deploy

注意:Firebaseが従量制でないと失敗する
Hosting URLに表示された内容にアクセスするとアプリを見ることができる

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

【Django REST Framework チュートリアル】#4 ReactとAPIサーバー繋ぐ【入門】

DjangoでAPIサーバー作成⇨React(axios)で取得して表示

Django REST Frameworkの初心者入門として、前回記事までで、ReactでJSON PlaceholderでAPIをデータを取得しましたので、JSON Placeholderで取得できるのと同じデータをAPIサーバーを作って表示Reactで表示させようと思います。

/posts: 記事全部取得

https://jsonplaceholder.typicode.com/posts

/posts/:id: 個別記事取得

https://jsonplaceholder.typicode.com/posts/1

これと同じデータ取得できるようなAPIサーバーをDjango REST Frameworkで作ります。

シリーズ記事

Django導入

環境の準備が難しい場合は、anaconda入れるといいと思います。

私は今回、pipenvで作ろうと思います。

> Pipenv 環境構築方法

好きなところにディレクトリ作って開始します。

$ pip install pipenv
$ pipenv --python 3.8.7

Pythonのバージョンを指定してます。
2020年の年末Djnagoを少しいじってたら、3.9だと動かないライブラリあったので3.8系を使ってます。

※ 以下pipenvを使用しますが、anacondaやDockerなど他の環境でやるときはpip installしてください。

$ pipenv install django
$ pipenv install djangorestframework

仮想環境pipenvで作業

pipfileが置いてあるところで、仮想環境の中に入ります。
普段、作業を開始するときはここから開始。

$ pipenv shell

一応確認。

$ python -V
Python 3.8.7

djangoのプロジェクトとアプリを作成

backendというプロジェクトを作成。
最後の.はカレントディレクトリにプロジェクト作成すると言う意味。
アプリの名前はapiという名前にします。

プロジェクト名も、アプリ名も好きに決めてください。

$ django-admin startproject backend .
$ django-admin startapp api

サーバー起動

manage.pyがあるところで、以下のコマンド実行

$ python manage.py runserver

デフォルトでは以下でサーバーが起動します。

http://127.0.0.1:8000/

ポート変更したかったら、
python manage.py runserver 7000

ブラウザでURLにアクセスして、ロケットが飛んだらOK

スクリーンショット 2021-01-16 20.48.33.png

設定変更

日本語表示と、タイムゾーンを変更します。

settings.py
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'
settings.py
INSTALLED_APPS = [
    ...
    'rest_framework',
    'api.apps.ApiConfig',
]

あとは、シークレットキーなど隠す必要ある方は、一番最後の
補足: python-dotenvで環境変数を設定を見て設定してみてください。

ルーティング urls.py

アプリごとにurls.pyを作って管理した方が良いのでurls.pyを作ります。

$ touch api/urls.py

↓まだ記述が足りないです。後で付け足します。

api/urls.py
from django.urls import path, include
from rest_framework import routers

router = routers.DefaultRouter()

urlpatterns = [
    path('', include(router.urls)),
]

includeを足してアプリ(api)側に作ったurls.pyのルーティングを反映させる。

backend/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('api.urls')),
]

model作成

今回とりあえず必要なのは、
id, title, bodyです。
idはデフォルトで作られるので、title, bodyを作ります。

api/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=50, blank=False)
    body = models.CharField(max_length=4000, blank=False)

    def __str__(self):
        return self.title

max_lengthは最大何文字にするかです。テキトウに決めました。

def __str__(self)self.titleと書くことで、
あとで、どんなデータか、タイトルのデータ表示で見やすくなるので設定しておいた方がいいと思います。
必須ではないです。

モデルにDBの構造を書いたので、makemigrationsでマイグレーションファイルを作成して、
マイグレーションファイルの記述通りにDBを作るため、migrateします。

$ python manage.py makemigrations
$ python manage.py migrate

これで、DBにテーブルが作成されました。

シリアライザーを作る

データの加工とか、形式正しいかとかの処理をするのがシリアライザーです。
とりあえず、やってみないとピンとこないと思うので作ってみましょう。

$ touch api/serializers.py
django-react-material/api/serializers.py
from rest_framework import serializers
from .models import Post

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ['id', 'title', 'body']

使用するモデルがPostなのでmodel = Post
id, title, bodyをReactに渡すので、
fields = ['id', 'title', 'body']

例えば、Reactでid, titleだけでいいなら
fields = ['id', 'title']になります。
必要なデータだけ選択するようにします。

views.py設定

DjangoはMVC(Model View Controller)モデルでなくMVT(Model View Template)です。

DB関係を扱う「Model」、
HTMLっぽい記述をする「Template」
「Views」は、その二つの間でデータを処理・加工など両者の受け渡しというか、、、
そんな感じです。

ViewsがMVCのControllerに当たるもので、なんか紛らわしいですね。
とりあえず、ModelViewsetというのを使うと、CRUD(Create Read Update Delete)を少ない記述で簡単に作れますので使ってみます。
(記述量増えるけど、細かく処理させたいこと決めたいならAPIviewとか使う)

とりあえず、Postテーブルに登録されているデータを全部JSONとして返すコードを書きます。

django-react-material/api/views.py
from django.shortcuts import render
from rest_framework import viewsets
from django.http import HttpResponse
from .serializers import PostSerializer
from .models import Post


class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer

admin管理画面に入る

情報を取得しようとしても、今は、DBに何もデータがありません。
これでは、データが取得できたかわからないですね。

ということで、管理画面を設定します。

Djangoの管理画面を作る

Djangoの管理画面は最初から付属しています。
簡単にデータを追加・修正したり、ユーザーを追加したりすることができます。

django-react-material/api/admin.py
from django.contrib import admin

from .models import Post

admin.site.register(Post)

作成したmodelをインポートして登録しましょう。
これで管理画面で確認、追加、編集、。削除などできます。

管理画面に入る

スクリーンショット 2021-01-17 1.04.38.png

ユーザーを作っていないと入れないですね。
コマンドでスーパーユーザーを作成します。

$ python manage.py createsuperuser  

ユーザー名、メール、パスワードを設定してユーザーを作成します。
テストですので、簡単なパスワードでもいいと思います。

Bypass password validation and create user anyway? [y/N]:
みたいにパスワード単純すぎるよ!みたいなこと言われるけど、無視してy入れていいです。
※ デプロイするときは、単純なパスワードは避ける

作成したユーザーでログインしてください。

モデルの名前である、
「POST」をクリック ⇨ 右上の「Postを追加」

押して、データを追加します。
そうしたら、以下のような登録フォーム出るのでデータを追加します。

スクリーンショット 2021-01-17 1.14.47.png

データを何個か追加してみてください。
DBにデータが追加されます。

ルーターに追加

最後に、ルートを追加します。

djnago-react-material/django-react-material/api/urls.py
from django.urls import path, include
from rest_framework import routers
from api.views import PostViewSet

router = routers.DefaultRouter()
router.register('posts', PostViewSet)

urlpatterns = [
    path('', include(router.urls)),
]

router.register('posts', PostViewSet) postsというURLに割り当てるのは、
views.pyに書いた、PostViewSetの処理という意味です。

紛らわしいですが、元からあったurls.pyには
path('api/', include('api.urls')),と書いたので、
api/というURL以降にapiフォルダの方にある、urls.pyの記述を足します。

ということで、api/ + postsなので

http://127.0.0.1:8000/api/posts/

にアクセスするとAPIサーバーからデータを取れることになります。

処理の流れ

  • APIを叩かれる。URLがhttp://127.0.0.1:8000/api/posts/
  • views.pyに書いてあるPostViewSetの処理が実行
  • queryset = Post.objects.all()でPostテーブルからデータを全て取得
    • SQLなら SELECT * FROM POSTみたいな感じ。正確にはDBのテーブル名違うけど
  • シリアライザーでデータを整える。
    • 今回は、単純な処理なので、処理はほぼしていない。
    • いらないカラムデータを削除(fieldsで指定したりする)、データを加工してJSONにするとかできる
    • 例えば、2021-01-17 12:00:00が見にくいから、2021年1月17日にしてJSONデータ作るとか
  • JSON形式でDjangoのAPIサーバーがデータを送信

少し長くなりましたが、これでできました。
URLにアクセスすると、

http://127.0.0.1:8000/api/posts

以下のように先ほど管理画面で登録したデータを取得できているのがわかります。

スクリーンショット 2021-01-17 1.48.10.png

Postmanで取得してみる

インストールしてない人はここから

https://www.postman.com/downloads/

APIを叩く(APIサーバーからデータもらう)ときによく使われるツールです。
先ほど確認できたので、大丈夫ですが、一応体験しましょう。

今回はGetで取得しますね。

手順

create request ⇨ URL入れる ⇨ Send

スクリーンショット 2021-01-17 1.59.07.png

認証のトークンをつけてAPIを叩いたり、
POSTデータを送信することもできるなどなど便利ツールです。
(今回はあまり使う意味なかったけど、WEBアプリ開発するなら入れておきましょう。使います。)

これだけだと、Reactで読み込めない

一見良さそうなんですけど、セキュリティとかの関係で今のままだと、
ReactではAPIを叩いてもデータを取得できません。
必要なものをインストールして、設定をします。

$ pipenv install django-cors-headers

設定が必要なので、以下を追加します。

settings.py
INSTALLED_APPS = [
    'corsheaders', # 追加
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', # 追加
]

CORS_ALLOWED_ORIGINS = [
    'http://127.0.0.1:3000',
    'http://localhost:3000',
]

CORS_ALLOWED_ORIGINSは通信を許可するIPとポートを指定する設定です。
Reactのデフォルトで起動するのはhttp://localhost:3000ですのでこのように書いてます。

localhostのIPの設定は意図的に変更しない限り、127.0.0.1なので一応書いてます。

CORS_ORIGIN_WHITELISTで指定するという情報がありました。
最近のバージョンではCORS_ALLOWED_ORIGINSですので注意。

一応ちゃんと確認したい方は、リポジトリを見てください

https://github.com/adamchainz/django-cors-headers

Djangoはこれで完成です!?

ReactからDjangoのAPIサーバーにアクセス

※ こちらは前回作ったReactの続きです。

こちらはそれほどやることはなくて、URLを変更すれば良いだけです。

material-react/src/components/Content.js
function Content() {
    const [post, setPosts] = useState([])

    useEffect(() => {
        // axios.get('https://jsonplaceholder.typicode.com/posts')
        axios.get('http://127.0.0.1:8000/api/posts/')
        .then(res => {
            setPosts(res.data)
        })
    }, [])
    const getCardContent = getObj => {
        const bodyCardContent = {...getObj, ...cardContent};
        return (
            <Grid item xs={12} sm={4} key={getObj.id}>
                <BodyCard {...bodyCardContent} />
            </Grid>
        );
    };
    return (
        <Grid container spacing={2}>
            {post.map(contentObj => getCardContent(contentObj))}
        </Grid>
    )
}

BodyCard.jsconst { userId, id, title, body, avatarUrl, imageUrl } = props;と書いてあって、ここにデータが渡ります。

userIdは今回作っていませんが、もともと使っていなかったので表示には問題ないです。
(※ 消してもよかったですね…)

Reactサーバー起動

Reactを開発したディレクトリにて、サーバーを起動して、

$ npm start

URLにアクセスすると、

http://127.0.0.1:3000

うまくいけば、表示されます。

スクリーンショット 2021-01-17 2.32.50.png

Djangoの管理画面でデータを登録したら増えますので試してください。

APIで個別記事のデータ取得

次は、「詳細をみる」ボタンで表示される個別の詳細記事です。
実は、ModelViewSetを使うと一通りの機能を作ってくれているので、すでに完成しています。

http://localhost:8000/api/posts/1

とブラウザのURLのところに入れれば、データが表示されます。
スクリーンショット 2021-01-18 0.17.07.png

ついでに、コード書いてないですが、削除、変更もすでに実装されています。
DELETE、PUTのボタンありますよね。この画面のボタンを押しても、編集できますし、

Postmanで、HTTPの通信を使って、
消したり、変更したりを、所定の形式に従って送ってやれば実行されるAPIが完成してるんですね。

まあ、今のうちは、ModelViewSet使うと簡単にCRUD作れるんだねー
くらいで良いと思います。

Reactからこの個別記事詳細データを取得

前回の
[【初心者】React #3 ルーティング・ページ遷移 react-router-dom]
ですでにボタンから個別の記事に飛べるようにもなっていますので
API叩くためのURLを変えるだけでOK

material-react/src/components/PostContent.js
import React, {useState, useEffect} from 'react';
import axios from 'axios';
import { useParams } from 'react-router-dom'

import { Grid } from '@material-ui/core'
import BodyCard from './BodyCard'


const cardContent = 
    {
        avatarUrl: "https://joeschmoe.io/api/v1/random",
        imageUrl: "https://picsum.photos/150"
    }

function PostContent() {
    const { id } = useParams();
    const [post, setPosts] = useState([])

    useEffect(() => {
        // axios.get(`https://jsonplaceholder.typicode.com/posts/${id}`)
        axios.get(`http://127.0.0.1:8000/api/posts/${id}`)
        .then(res => {
            setPosts(res.data)
        })
    }, [])

    return (
        <Grid container spacing={2}>
            <Grid item xs={12} key={post.id}>
                <BodyCard {...{...post, ...cardContent}} />
            </Grid>
        </Grid>
    )
}

export default PostContent

URLを変えるだけで詳細ページはOKです。

スクリーンショット 2021-01-18 2.29.29.png

一覧表示のBodyCardコンポーネントをそのまま使ったので
「詳細をみる」ボタンはここには必要ないし、画面の引き伸ばし感がひどいですが、
個別記事のルーティングもできていますね

まとめ

  • Django Rest Frameworkを使ってAPIサーバーを作成
  • Reactから作成したAPIサーバーにアクセスできるようにDjangoで設定
  • Reactで作成したAPIサーバーからデータが取得できることを確認

補足: python-dotenvで環境変数を設定

必須ではないです。必要な人はやってみましょう。

githubなどにpublicで公開するとき、APIキーなど公開すると危険な情報はを.envに書き、.gitignoreで除外しましょう

やり方よく忘れるので、メモしときます。

デプロイするものなどは、githubのpublicにしないか、
公開したらやばいものは公開しないようにしてください。

環境変数を.envから読めるようにします。

$ pipenv install python-dotenv

プロジェクト直下、manage.pyとかと同じところに.envファイル作って、ここにみられたえらやばい情報を追加します。

$ vim .env

SECRET_KEYと、DEBUGも一応追加しておきます。

setting.pyに書いてある、SECRET_KEYと、
ついでにDEBUGの値を以下のように貼り付け。

.env
SECRET_KEY=1234567890asdfghjklwertyzx
DEBUG=True

.envから読み込む記述と、
SECRET_KEY, DEBUGos.getenv('ここに.envに書いた名前書く')にする。

backend/settings.py
import os
from dotenv import load_dotenv

load_dotenv(verbose=True)
dotenv_path = os.path.join(BASE_DIR, ".env")
load_dotenv(dotenv_path)

SECRET_KEY = os.getenv('SECRET_KEY')
DEBUG = os.getenv('DEBUG')

.gitignore作ってなかったら、.envと同じプロジェクト直下に作成して以下の記述を足してください。

ついでに.githubのリポジトリ作るときに、.gitignoreを追加⇨python選べば、
こんな感じの記述はすでに書かれていると思うのでその場合は不要です。

.gitignore
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

.gitignoreに追加すれば、変更を追跡しなくなるので、
うっかり、やばい情報をgithubに公開してしまった!
ということはなくなります。

今回はDjangoのSECRET_KEYでしたが、APIキー、
特にAWS関係の情報を晒したら、悪用されて多額の請求来ます!
githubに公開しないといけないときには使いましょう。

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

reactのmaterial-tableで一部カラムのみを編集可能にする

Material-table

https://material-table.com/#/
Material-uiに則ったテーブルレイアウトを簡単に作成できるライブラリ。一部のカラムを編集できるデータの一覧表を実装したくて採用。

編集可能にしてみる

https://material-table.com/#/docs/features/editable
には「Row Editing」(行単位の一括編集)と「Cell Editing」(セル単位での編集)について記載があった。「Row Editing」の方は行の全カラムが編集可能になってしまい、編集可能にする必要のないカラムがあることから、「Cell Editing」の方かなと思い実装してみる。

const columnList = [
  { title: "ID", field: "id" },
  { title: "name", field: "name" },
  { title: "description", field: "description" },
];

const dataList = [
  { id: 1, name: "aaa", description: "xxxxxxxxxx" },
  { id: 2, name: "bbb", description: "yyyyyyyyyy" },
  { id: 3, name: "ccc", description: "zzzzzzzzzz" },
];

<MaterialTable
  columns={columnList}
  data={dataList}
  cellEditable={{
    onCellEditApproved: (newValue, oldValue, rowData, columnDef) => {
      return new Promise(() => {
        // do something
      });
    },
  }}
></MaterialTable>

結果、セル単位での編集にはなったものの、全セルが編集可能になってしまった。(ID列は編集させたくない)

image.png

一部のセルだけ編集可能にするには…

const columnList = [
  { title: "ID", field: "id", editable: "never" },
  { title: "name", field: "name" },
  { title: "description", field: "description" },
];

editable: "never"

をカラムの定義につけてあげると、そのカラムは編集不可になった。(ID列をクリックしても編集欄が表示されないけど、それ以外の列は編集可能)

image.png

参考になれば幸いです。

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

typescript-fsa-redux-thunkの紹介

はじめに

redux-thunkをtypescriptで使っていた時に、色々調べた結果typescript-fsa-rudx-thunkというパッケージが便利だったので紹介します。
非同期処理のactionを作るactionCreatorを提供しています。

typescript-fsaやredux-thunkの説明を詳しくすると長くなるので端折りながら。

typescript-fsaについて

そこでReduxの型定義をよしなにしてくれるパッケージはtypescript-fsaが有名。

import actionCreatorFactory from 'typescript-fsa';

const actionCreator = actionCreatorFactory();
//{foo: string}型がパラメータの型になる。
const somethingHappend = actionCreator<{foo: string}>('SOMETHING_HAPPENED');
console.log(somethigHappend({foo:"hoge"}));
// { type: 'SOMETHING_HAPPENED', payload: {foo:"hoge"})

payloadの型がジェネリックスで指定した型になります。
このようにactionCreatorを通して型の定義ができます。

redux-thunkについて

redux-thunkはreduxで非同期処理を行う時に良く使用されるパッケージです。

redux-thunkを使わない場合

const somethingHappend = actionCreator<{foo: string}>('SOMETHING_HAPPENED');

fetch("http://example.com")
  .then(response => response.text())
  .then(text => {
    store.dispatch(somthigHappend({foo:string});
  });

redux-thunkを使った場合

const somethingHappend = actionCreator<{foo: string}>('SOMETHING_HAPPENED');

const asyncSomethingHappend = () => (dispatch: any) => any{
  fetch("http://example.com")
    .then(response => response.text())
    .then(text => {
      dispatch(somthigHappend({foo:string});
    });
}
//asyncSomethingHappendはreduxのactionと同じようにdispatchできます。
store.dispatch(asyncSomethingHappend());

redux-thunkを使うと非同期処理そのもの(thunkと呼ぶらしい)をdispatchできるようになりました。
redux-thunkはdispatchの引数に関数を入れれるようにしてくれます。

typescript-fsa-redux-thunk

今までの説明では理解しやすくするために簡単な処理を書いていましたが、私が実際に非同期処理をする時はいつも複雑になります。
通信が始まったときの処理だとか、終わったときの処理だとか、エラー時の処理だとか、
ただでさえ、reduxやredux-thunkを理解するのが大変なのに、、、

そこで、便利だったのがtypescript-fsa-redux-thunk。
簡単に説明すると非同期処理のaction(thunk)を作るactionCreaterを提供してくれています。

import actionCreatorFactory from 'typescript-fsa';
import { asyncFactory } from 'typescript-fsa-redux-thunk';

//ジェネリックスで渡しているStateはReduxのstoreのstateと同じ型です。
const createAsync = asyncFactory<State>(create);

interface LoginParams {
  email: string;
  password: string;
}

const login = createAsync<LoginParam,UserToken,CustomError>(
  'Login',
  // 引数で取っているparamsがLoginPramになります。
  async(params,dispatch) => {
    const res = await fetch("http://example.com/login",{
     method: 'POST',
     body: JSON.stringfy(params)
    })
    if(!res.ok) {
     throw new CustomError();
    }
    //UserToken型を返します。
    retrun res.json() as UserToken
  }
);
const loginParam:LoginParam = {email:'hoge@example.om',password:'fuga'}
store.dispatch(login(loginParam));
//login.asyncの中
//{
//  started: f LOGIN_STARTED
//  done: f LOGIN_DONE
//  failed: f LOGIN_FAILED
//  type: 'LOGIN'
//}

typescript-fsa-redux-thunkのcreateAsyncは非同期処理のaction(thunk)を作るactionCreaterになります。

createAsyncで作られたthunkはdispatchされると、
started:非同期処理が始まったとき
done:非同期処理が完了したとき
faild:非同期処理が失敗したとき
上記の三つのactionがdispatchされる。
これらの三つのactionはlogin.asyncの中に定義されている。
いちいち自分で記述しなくてよいので、楽です。

createAsyncの三つのジェネリックは順番に
1.createAsyncが作ったthunkの引数の型・createAsync第二引数の関数の第一引数
2.createAsyncの第2引数の返り値・doneアクションのparameter
3.エラーの型
になっている。

一番うれしいのが、started・done・faildのアクションを定義してくれていること。

最後に

説明難しい。

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

Context / useContext を書きながら学ぶ

Reactの組み込みフックであるContextuseContextの説明をします。

また、useContextは、ReactのContextと併用するので、Contextの解説もします。

Contextとは

useContextは、ContextというReactの仕組みを利用するために必要なフックです。

なので、まずはContextについて説明します。

Contextとは...と言っても様々な言い方ができるでしょう。

  • 「状態」と「状態を変更するメソッド」を、propsを用いず、アプリケーション全体で取り回すことができるやつ
  • Propsを利用せずに、様々な階層のコンポーネントに値を共有するReactのAPI

などなどですね。図で説明しましょう。

本をReactで表現するとします。

IMG_56E1E941470B-1.jpeg

ホントは、もっといろいろなコンポネントがありえますが、今回は、Bookのデータ(仮にbookDataとする)を<Page /><Title />で、使いたいとします。

IMG_1BF3BF95601E-1.jpeg

すると、このようにpropsをバケツリレーして渡す方法もあります。ですが、<Body /><Cover />はそんなデータを使用しない。とか、もっと多階層で中間のコンポネントはbookDataをまったく使わないので、バケツリレーで渡す意味がない時ありますよね?

もしくは、グローバルにどこでも取りまわせるstateを持ちたい時がありますよね?

そんな時は、Contextの出番です。

IMG_9AACCBB1127C-1.jpeg

Contextがデータストアの役割をはたし、propsを使わず直接bookDataを下層階層のコンポネントが使うことができるようにしたものです。

useContextは、Contextの機能をさらにシンプルに使うことができるやつです。

Contextを利用するために必要なもの

  • Contextオブジェクト: React.createContextというReactのAPIの戻り値
  • Provider: Contextオブジェクトが保持しているコンポネント
  • Consumer: Contextオブジェクトから値を取得しているコンポネント

サンプルコード

基本の状態は以下にします。(Bookを表すには雑すぎますが...ツリー構造さえわかればOKなので許してください)

スクリーンショット 2021-01-28 23.10.42.png

Contextを利用して、下層コンポネントに、データを受け渡します。
以下の順番で書いていきます。

  • createContext(BookContext)してexportしておく(Book コンポネントで実装)
  • Providerコンポネントを作成し、valueにオブジェクト(state)をセットします(Book コンポネントで実装)
  • 下層コンポネントで、先ほど作成したBookContextをimportします(Title コンポネントで実装)
  • importしたBookContextを使ってconsumerを作成(Title コンポネントで実装)
    • Consumerコンポネント内でbook state の中身にアクセスできます

赤枠が、変更点です?

Context___useContext_を書きながら学ぶ_-_CodeSandbox.png

実行結果はこんな感じです。

スクリーンショット 2021-01-28 23.40.37.png

propsで上の階層から渡していないのに、下階層でcreateContextしたデータが引っ張れていることがわかります。

それでは次に、useContextを使っていきましょう。

useContextとは

useContextを使った場合でも、Providerを使って値を渡す点は同じです。

構文はこんな感じです。

const Contextオブジェクトの値 = useContext(Contextオブジェクト)

Contextオブジェクトから、取得できる値は、Contextオブジェクトが保持しているProviderのvalueプロパティに指定された値です。

サンプルコード

<Cover />とその子供の<Title />は、createContextされたBookContextAuthorContextコンテキストをPrivideされているので、それぞれのデータを引っ張ることができます。このように、複数のコンテキストを扱うことも可能です。

サンプルコード
/components/Book.js
import React, { createContext, useState } from "react";
import Cover from "./Cover";
import Body from "./Body";

const bookData = {
  author: "ryosuketter",
  title: "how to use React context",
  isbn: 12345,
  yearOfPublication: 2021
};

const authorData = {
  name: "ryosuketter",
  age: 35,
  gender: "male"
};

export const BookContext = createContext();
export const AuthorContext = createContext();

const Book = () => {
  const [book, setBook] = useState(bookData);
  const [author, setAuthor] = useState(authorData);
  return (
    <>
      <BookContext.Provider value={book}>
        <AuthorContext.Provider value={author}>
          <Cover />
        </AuthorContext.Provider>
        <Body />
      </BookContext.Provider>
    </>
  );
};

export default Book;
components/Cover/Title.js
import React, { useContext } from "react";
import { BookContext, AuthorContext } from "../../Book";

const Title = () => {
  const book = useContext(BookContext);
  const author = useContext(AuthorContext);

  return (
    <div>
      <p>this is {book.title}</p>
      <p>author is {author.name}</p>
      <p>age is {author.age}</p>
    </div>
  );
};

export default Title;

特定のコンポネントで、Contextの追加や上書きをしたデータを作成して使うことも可能です。

サンプルコード
/components/Book.js
// 上と同じ
components/Body/Page.js
import Reac, { useContext } from "react";
import { BookContext } from "../../Book";

const Page = () => {
  const book = useContext(BookContext);
  const customedBookContext = {
    ...book,
    author: "ryosuke",
    publisher: "Qiita publications"
  };

  return (
    <div>
      <p>this is page</p>
      <p>author is {customedBookContext.author}</p>
      <p>publisher is {customedBookContext.publisher}</p>
    </div>
  );
};

export default Page;

上2つのコードの実行結果

スクリーンショット 2021-01-29 1.04.52.png

Context利用時の注意点(Context更新時に不要な再レンダーを招く)

Contextは使い方によっては、パフォーマンスの問題を引き起こす可能性があります。

なぜから、Provider内のすべてのConsumerは、Proverのvalueプロパティが更新するたびに再レンダリングするからです。

特に、以下の場合は注意です

  • 再レンダリングされる Consumer の数が多い場合
  • Consumerの子コンポネントのレンダリングコストが高い場合

不要な再レンダリングを防ぐ方法は、次の3つです。

  • Contextオブジェクトの分割
  • React.memoの利用
  • useMemoの利用

ぜひ、試しながらやってみてください。

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

Reactのmap処理とkeyの使い方

Reactの配列処理

Reactを使うとフロントエンドで同じ繰り返しの値の処理を簡単に実装することができます。

事前知識

  • HTML基礎
  • JavaScriptの基礎
  • Reactの基礎

これらの前提知識がある定で話していきます。
今回はReactのフレームワークのNext.jsで実装していきます!!

npm init next-app アプリ名

まずさくっとNext.jsを立ち上げます。
エラー出た方は

node -v
yarn -v
npm -v

こちらを確かめましょう

次にcomponetsディレクトリーを作成し、その配下にitemsのコンポーネントのディレクトリを作成します。

cd アプリ名
mkdir components
cd conmponents
mkdir items
cd items
touch index.jsx
items/index.jsx
import React from "react";
const ITEMS = [
  {
    id: 1,
    title: "一番最初のタイトルが入ります",
    body: "テキストが入りますテキストが入りますテキストが入ります"
  },
  {
    id: 2,
    title: "2番目のタイトルが入ります",
    body: "テキストが入りますテキストが入りますテキストが入ります"
  },
  {
    id: 3,
    title: "3番目のタイトルが入ります",
    body: "テキストが入りますテキストが入りますテキストが入ります"
  },
]
const Items = () => {
  return (
    <React.Fragment>
      {ITEMS.map((item) => {
        return (
        <div>
          <p>{ item.title }</p>
          <p>{ item.body }</p>
        </div>
        )
      })}
    </React.Fragment>

  )
}
export default Items

いったん実装します。
説明としてITEMSの配列を定義し、その中でループさせたい処理を書いていきます。
その後mapメソッドでITEMSの中身を返します。
(この時自分はmapの後のreturnを忘れて、永遠と表示されない自体がおきました、、、見逃さずに)

Itemsコンポーネントはしっかりexportしましょう。

pages/index.js
import Items from "../components/items"

export default function Home() {
  return (
    <>
      <Items></Items>
    </>
  )
}


Itemsコンポーネントをインポートします。
<></>はの省略記法です。
どちらでも大丈夫ですが、書く際はどちらかに統一しましょう。

スクリーンショット 2021-01-29 0.37.25.png
できたーー!!!
、、、、、、、むむっ
スクリーンショット 2021-01-29 0.37.42.png
エラーが出ていますね。

Warning: Each child in a list should have a unique "key" prop
keyがないよーと警告が出ています。

https://reactjs.org/docs/lists-and-keys.html#keys-must-only-be-unique-among-siblings
参考記事
Keys used within arrays should be unique among their siblings. However they don’t need to be globally unique. We can use the same keys when we produce two different arrays:

配列の中のキーは兄弟間で一意である必要があります。

結論→親要素にkeyを設定すると解決する

pages/index.jsx
import React from "react";
const ITEMS = [
  {
    id: 1,
    title: "一番最初のタイトルが入ります",
    body: "テキストが入りますテキストが入りますテキストが入ります"
  },
  {
    id: 2,
    title: "2番目のタイトルが入ります",
    body: "テキストが入りますテキストが入りますテキストが入ります"
  },
  {
    id: 3,
    title: "3番目のタイトルが入ります",
    body: "テキストが入りますテキストが入りますテキストが入ります"
  },
]
const Items = () => {
  return (
    <React.Fragment>
      {ITEMS.map((item) => {
        return (
        <div key={item.id}>
          <p>{ item.title }</p>
          <p>{ item.body }</p>
        </div>
        )
      })}
    </React.Fragment>

  )
}
export default Items

ちなみにキーはindexでも渡すことができますが、こちらはあまり推奨されていないようです。

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