20201019のReactに関する記事は9件です。

【ミライトデザイン社内勉強会#6】Reactの基礎

JSX の導入 – React

  • JSX と React Element について · react-hands-on
  • トランスパイルしてブラウザに表示する
  • 最終的にはjsのファイルに変換できるのでjsでできることはなんでもできる
    • {}で囲めばjsの処理がなんでもかける
    • XMLの記法が追加されただけ
  • JSXを使うことによって同じファイルに表示とロジックがかける
  • JSXの属性はキャメルケースにしないといけない
    • HTMLならtab_indexとかくところをtabIndexと書かないといけない
  • タグがからの場合は<img src={user.avatarUrl} />閉じタグが必要
  • Babelって何?
    • コンパイラ、constをvarに変換してくれたりする
    • https://babeljs.io/
      • ここでbabelが試せるドン

要素のレンダー – React

  • <div id="root"></div> はidじゃなくてもセレクターならなんでもいい
  • reactは要素が変更した部分だけ再描画してくれる
    • 普通のwebサービスではそこまでわからないが、重たい処理をしたりすると違いがでてくる。

コンポーネントと props – React

  • クラスでも関数でもコンポーネントを作れる
  • propsは引数みたいな理解でいいと思う
  • コンポーネントは全て大文字から始める
  • > コンポーネントをより小さなコンポーネントに分割することを恐れないでください。
    • サーバーサイドのクラスの分割と考え方は同じ
  • > 自分自身の props は決して変更してはいけません。
    • 変更しようと思ったらできるの?
      • propsに代入する式はかけるけど、実際には値は変更されない。
      • 外から受け取った時点から変更はできない。

state とライフサイクル – React

  • componentWillUnmount() { clearInterval(this.timerID); }でタイマーを削除する必要あるの?
    • reactはSPAなので削除しないと残り続けてしまう。
      • stateが変更されると再描画してくれる。
    • stateを直接変更した場合や、ただの変数に入れただけでは再描画はしてくれない。
    • setStateでstateを更新した時だけreactは再描画してくれる。
      • setStateにコールバック関数を渡すと、reactがstateを更新した後に処理をしてくれる
    • this.stateを使うと、更新される前のstateが使われることがある
    • 参考

データは下方向に伝わる

  • 親コンポーネントでも子コンポーネントでもステートフルやステートレス等を気にすることはない。
    • カプセル化

イベント処理 – React

  • React のイベントは小文字ではなく camelCase で名付けられています。
  • reactではonClickにreturn falseをしても動作を抑止できない
    • aタグの遷移を抑止できない。
    • reactではpreventDefault()を使用する

条件付きレンダー – React

  • if文で使用するコンポーネントを制御できる
  • JSXでインラインで条件を記述できる
    • 条件部分がtrueであれば&&の後の要素が出力される
function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&
        <h2>
          You have {unreadMessages.length} unread messages.
        </h2>
      }
    </div>
  );
}
  • 他のコンポーネントがレンダーされているときに、自身を非表示にしたい時はnullを返すようにする
    • 表示/非表示の判断を子自身がする時
    • この処理をやる機会は少なそう
    • nullを返してもコンポーネントのライフサイクルメソッドは呼び出される

リストと key – React

  • リストには識別できるようにkeyを与えるべきである
    • 一意に識別ができるものを設定する
    • リストのindexをkeyにすることもできるが、要素の並び順が変更される可能性がある場合は使用しない方がいい
  • コンポーネントに抽出した場合はコンポーネントにkeyを持たせる
  • keyは1つのリストで一意であればよい
    • 2つの異なる配列の場合は同一のkeyが使用されていても問題ない
  • mapの結果をインラインで書くこともできる
function NumberList(props) {
  const numbers = props.numbers;
  return (
    <ul>
      {numbers.map((number) =>
        <ListItem key={number.toString()}
                  value={number} />
      )}
    </ul>
  );
}

フォーム – React

  • 制御されたコンポーネント
    • React によって値が制御される入力フォーム要素
    • こっちの役の方がわかりやすいかも
      • React の状態を「真実の単一ソース」とすることで、この 2 つを組み合わせることができます。そうすると、フォームをレンダリングするReactコンポーネントは、その後のユーザー入力時にそのフォームで何が起こるかも制御します。このようにReactによって値が制御される入力フォーム要素は、「制御されたコンポーネント」と呼ばれます。
  • <input type="file"> はhtmlと何が違うの?
    • 読み取り専用。htmlと同じ。

state のリフトアップ – React

  • チュートリアル:React の導入 – React チュートリアルにも同じようなのがあった
  • リフトアップとは子コンポーネントでstateを共有する場合に、直近の共通の祖先コンポーネントに移動すること
  • stateを変更する時は親コンポーネントからsetStateのメソッドを受け取り、onChangeで実行する
  • 宣言的にかけるプログラミング言語だからリフトアップができている。
    • jQueryだと宣言的には書けない。
    • 【宣言的】foo * 3 みたいな、fooの値が変われば表示される値も変わる。

コンポジション vs 継承 – React

  • jsxをコンポーネントのタグで囲めば、コンポーネントに渡すことができる。
    • childrenで受け取ることができる
  • 継承は使わなくていい

React の流儀 – React

  • Step 1: UI をコンポーネントの階層構造に落とし込む
    • 分割しすぎはそれはそれでファイル数も増えて大変な気がする
    • まだそこまで分割するメリットはわかってない

 image.png (15.3 kB)

  • ここまでは分割しないこともある

    • Searchの部分としたの部分とか、多くても3つくらい
  • Step 4: state をどこに配置するべきなのかを明確にする

    • いろんなところで使うStateは専用のコンポーネントをつくってもいい
      • そのコンポーネントでしか使わないStateをリフトアップする必要はない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Recoil公式ドキュメント 翻訳

Recoilの公式ドキュメントをgoogle翻訳するとコードまで翻訳されてしまうのが面倒なのでQiitaにまとめてみます。

色々すっ飛ばしてとりあえず入門から。(自分が読みたかった)
追々リスト化して追加していきます。(多分)

公式ドキュメント

目次

  • 前書き
    • 動機
    • コアコンセプト
    • インストール
    • 入門
  • 基本チュートリアル
  • ガイド
  • APIリファレンス

入門

Create React App

RecoilはReactの状態管理ライブラリであるため、Recoilを使用するには、Reactをインストールして実行する必要があります。Reactアプリケーションをブートストラップするための最も簡単で推奨される方法は、Create ReactAppを使用することです。

npx create-react-app my-app

npxはnpm5.2以降に付属するパッケージランナーツールです。古いnpmバージョンの手順を参照してください。

Create React Appをインストールするその他の方法については、公式ドキュメントを参照してください。

Installation

Recoilパッケージはnpmとyarnにあります。
最新の安定バージョンをインストールするには、次のコマンドを実行します。

npmを使用している場合:

npm install recoil

yarnを使用している場合:

yarn add recoil

RecoilRoot

Recoil State を使用するコンポーネントでは、親ツリーのどこかにRecoilRootが表示される必要があります。
これはルートコンポーネントに置くとよいでしょう。

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

次のセクションでは、CharacterCounterコンポーネントを実装します。

Atom

Atomはstateを表し、任意のコンポーネントから読み取りおよび書き込みを行うことができます。
Atomの値を読み取るコンポーネントは暗黙的にそのAtomに登録されているため、Atomを更新すると、そのAtomに登録されているすべてのコンポーネントが再レンダリングされます。

const textState = atom({
  key: 'textState', // unique ID (他の atoms/selectors に関して)
  default: '', // default value (初期値として)
});

Atomからの読み取りとAtomへの書き込みが必要なコンポーネントは、useRecoilState()以下に示すように使用する必要があります。

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

Selector

Selectorは、derived state(派生状態?)を表します。derived statestateの変換です。
指定された状態を何らかの方法で変換する純粋関数に状態を渡す出力と考えることができます。

const charCountState = selector({
  key: 'charCountState', // unique ID (他の atoms/selectors に関して)
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

useRecoilValue()フックを使用して、charCountStateの値を読み取ることができます。

function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

画面収録-2020-10-19-22.18.02.gif


参考サイト

公式ドキュメント
Pure function (純粋関数)とは
みらい翻訳


全目次

  • 前書き
    • 動機
    • コアコンセプト
    • インストール
    • 入門
  • 基本チュートリアル
    • イントロ
    • Atoms
    • Selectors
  • ガイド
    • 非同期データクエリ
    • 非同期状態(state)同期
    • 状態(state)の永続性
  • APIリファレンス
    • Core
    • 実用(utils)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Recoil公式ドキュメント 翻訳④

Recoilの公式ドキュメントをgoogle翻訳するとコードまで翻訳されてしまうのが面倒なのでQiitaにまとめてみます。

追々追加していきます。(多分)

公式ドキュメント

目次


入門

Create React App

RecoilはReactの状態管理ライブラリであるため、Recoilを使用するには、Reactをインストールして実行する必要があります。Reactアプリケーションをブートストラップするための最も簡単で推奨される方法は、Create ReactAppを使用することです。

npx create-react-app my-app

npxはnpm5.2以降に付属するパッケージランナーツールです。古いnpmバージョンの手順を参照してください。

Create React Appをインストールするその他の方法については、公式ドキュメントを参照してください。

Installation

Recoilパッケージはnpmとyarnにあります。
最新の安定バージョンをインストールするには、次のコマンドを実行します。

npmを使用している場合:

npm install recoil

yarnを使用している場合:

yarn add recoil

RecoilRoot

Recoil State を使用するコンポーネントでは、親ツリーのどこかにRecoilRootが表示される必要があります。
これはルートコンポーネントに置くとよいでしょう。

import React from 'react';
import {
  RecoilRoot,
  atom,
  selector,
  useRecoilState,
  useRecoilValue,
} from 'recoil';

function App() {
  return (
    <RecoilRoot>
      <CharacterCounter />
    </RecoilRoot>
  );
}

次のセクションでは、CharacterCounterコンポーネントを実装します。

Atom

Atomはstateを表し、任意のコンポーネントから読み取りおよび書き込みを行うことができます。
Atomの値を読み取るコンポーネントは暗黙的にそのAtomに登録されているため、Atomを更新すると、そのAtomに登録されているすべてのコンポーネントが再レンダリングされます。

const textState = atom({
  key: 'textState', // unique ID (他の atoms/selectors に関して)
  default: '', // default value (初期値として)
});

Atomからの読み取りとAtomへの書き込みが必要なコンポーネントは、useRecoilState()以下に示すように使用する必要があります。

function CharacterCounter() {
  return (
    <div>
      <TextInput />
      <CharacterCount />
    </div>
  );
}

function TextInput() {
  const [text, setText] = useRecoilState(textState);

  const onChange = (event) => {
    setText(event.target.value);
  };

  return (
    <div>
      <input type="text" value={text} onChange={onChange} />
      <br />
      Echo: {text}
    </div>
  );
}

Selector

Selectorは、derived state(派生状態?)を表します。derived statestateの変換です。
指定された状態を何らかの方法で変換する純粋関数に状態を渡す出力と考えることができます。

const charCountState = selector({
  key: 'charCountState', // unique ID (他の atoms/selectors に関して)
  get: ({get}) => {
    const text = get(textState);

    return text.length;
  },
});

useRecoilValue()フックを使用して、charCountStateの値を読み取ることができます。

function CharacterCount() {
  const count = useRecoilValue(charCountState);

  return <>Character Count: {count}</>;
}

画面収録-2020-10-19-22.18.02.gif


参考サイト

公式ドキュメント
Pure function (純粋関数)とは
みらい翻訳


全目次

  • 前書き
  • 基本チュートリアル
    • イントロ
    • Atoms
    • Selectors
  • ガイド
    • 非同期データクエリ
    • 非同期状態(state)同期
    • 状態(state)の永続性
  • APIリファレンス
    • Core
    • 実用(utils)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React②】JSX

JSXとは

XMLのようなタグを記述できるJavaScriptの拡張言語。これを使えば、短く直感的にわかりやすい記述ができる。
JSXはBabelによって暗黙的にJavaScriptに変換される。
以下のページの左側にXMLを入力すると、右側にリアルタイムでJavaScriptが表示されることが確認できる。
https://babeljs.io/

使い方

import React, { Component } from 'react';

するだけ。他には

  • 変数キャメルケースで書く
  • コンポネントには閉じタグ/>が必要

例えば、https://babeljs.io/ に以下のコードをかくと、

class App extends Component{
  render(){
    return <div>Foo</div>;
  }
}

暗黙的にBabbleが以下のようにReact.createElement()に変換してくれるのです。
HTMLのように、最終形に近い形で記述できることになります?

function render() {
  return React.createElement('div', null, 'Foo');
}

式をうめこむ

JSXを使えば、JavaScriptの式をJSX内で中括弧に囲んで使用できます。

const name = 'Tom';
const element = <h1>Hello, {name}</h1>;

ReactDOM.render(
  element,
  document.getElementById('root')
);

属性を指定

JSXではclass属性の指定はclassではなくclassNameで指定する。
例えば下の例だと、h1タグがトランスパイルされてdomという変数に代入されている。

class App extends Component{
  render(){ 
    const greeting = "Hi, Tom!";
    const dom = <h1 className="foo"> {greeting}</h1>;  
    return dom;
  }
}

タグの属性

HTMLタグの属性forはReactではhtmlForで指定できる。labelに付与することで、同じ内容のid属性を持つ要素と関連付ける。
下の例ではonChangeベントハンドラによりformへの入力が変化するとI am clickedが表示される。

class App extends Component{
  render(){ 
    return(
      <div>
         <label htmlFor="bar">bar</label>
         <input type="text" onChange={() => {console.log("I am clicked.")}}/>
          //条件がクリックの場合はonClick
      </div>
    )
  }
}

また、Reactのreturn内で返すタグは1個でないといけない。

//エラー文
index.js: Adjacent JSX elements must be wrapped in an enclosing tag

そこでreturn内を<div>で囲って対応、、、
するとReactのための意図せぬタグが増えてしまうので、<React.Fragment>が用意されている。
<div>ではなく、<React.Fragment>で囲うことで、最終的に画面に表示されるHTMLに余計なタグが出力されない。
<React.Fragment> ではkey={item.id}も指定可能。

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

以下のように<></>を使って単略化して書くこともできるが、key属性は指定できない。

class Columns extends React.Component {
  render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}

比較

JSXを使わずReactを使ってDOMを描画すると、こんな感じに長くなる。

 class App extends Component {
   render() {
     return React.createElement(
       "div",
        null,
       "Hello, world!!"
     );
   }
 }
export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのhooksのESLint対応

公式 見るべし。

// Your ESLint configuration の部分は .eslintrc.js 内の設定をかえる

一応、yarn版

yarn add eslint-plugin-react-hooks --dev
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js初期インストール時にエラーが発生する人向け対処法

自分が詰まりかけたため、同じような状態になってしまった方を対象にして回避方法を記入しておきます。

この問題はnpx create-next-app を実行した際に、
npm ERR! code ENOLOCAL
npm ERR! Could not install from "xxx\AppData\Roaming\npm-cache_npx\14184" as it does not contain a package.json file.

のような記述が返ってきてインストールできない人向けのものです。

この問題は使用者のユーザー名の中にスペースがあるのが原因です。
例えば私の場合ですと、苗字と名前の間にスペースが存在するため、このエラーが発生してしまいました。(修正してくれ…)

対処法ですが、インストールしたいフォルダーの所へ行って
npm config set cache "c:\tmp\nodejs\npm-cache --global"

を実行してください。「npm-cache」フォルダーが別のパスに変更されるので、これで回避可能のはずです。

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

React + Expressで作るWebアプリ制作 基本のキ

はじめに

普段はHTML/CSS/JSを用いたフロントエンド開発を中心に行っているのですが、Webアプリを作るためにはバックエンド(サーバーサイド)の知識も少なからず必要になってきます。

そこで、今回はExpressを使ってシンプルなAPIサーバーを作りながら、Webアプリの仕組みについて理解を深めていきたいと思います。

対象読者

  • マークアップやフロントエンドの技術は多少触れたことがあるが、バックエンド開発は初心者の方

使用技術 / バージョン

フロントエンド (UI)

バックエンド (APIサーバー / DB)

今回作成するもの

GitHubの「草」機能が好きです。徐々にマス目が埋まっていくのが継続のモチベーションになって楽しいですよね。

(参考画像)
kusa.png

そこで、なにか好きな「継続したい目標」を決めて、それに対して「草を生やす」ことで自分がどれくらい継続できたかを可視化できるようなアプリを作ってみました。

iphone.png

  • 継続目標のカレンダーを作って一覧表示する
  • 達成できたら「達成!」ボタンを押すことで、その日のマスに「草」が生える

機能はこれだけで、ユーザー登録等は今回の記事では考えません。

  1. Reactで作成したViewからHTTPリクエストをAPIサーバーに送信
  2. サーバーでデータの処理を行い、レスポンスを返す
  3. レスポンスを受け取って、Viewを書き換える

というシンプルなSPAを作成していきます。

必要なAPIを検討する

まずは、上記のアプリを動かすために必要なAPIについて考えてみましょう。

いきなりたくさんの機能を作ろうとすると大変なので、はじめはアプリの核となる基本機能に絞って検討するのがよいと思います。

今回は、最低限

  • [GET] カレンダーを取得する
  • [POST] カレンダーを作成する
  • [PUT] 作ったカレンダーに「達成」の記録を追加する

という3つのAPIがあればよさそうです。

データ構造について検討する

続いて、どのようなデータ構造でDBへの保存を行うか考えます。
今回はMongoDBを利用するので、JSON形式でのデータ保存を行います。

今回はcalendarsというコレクションに下記のような形式でドキュメントを格納していくことにしました(わかりやすくTypeScript風に書いています)。準備段階でこの辺りの設計をはっきりとさせておくことで、以降の実装がしやすくなるかと思います。

interface Post {
  date: string; // 達成した日付
  description: string; // ひとことコメント
}

interface Calendar {
  title: string; // カレンダーのタイトル
  posts: Post[]; // 達成した日付が配列で格納される
}

Expressで上記の機能を実装していく

いよいよ実装です。RESTful APIを意識して実装していきます。
MongoDBへのアクセスにはExpress用のライブラリmongooseを利用しています。

Expressサーバーの準備

index.ts
import express from 'express';
import mongoose from 'mongoose';
import router from './routes/v1';

// Expressサーバーの用意
const app = express();
const port = process.env.PORT || 5000;

mongoose.connect(process.env.MONGODB_URI || '(ローカルのMongoDB URI)', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

app.get('/', (_req, res) => {
  res.send('Hello Express.');
});

// routes/v1/index.tsをrouterとして宣言
app.use('/api/v1', router);

app.listen(port, () => {
  console.log(`listening at http://localhost:${port}`);
});

モデルの定義

models/calendar.ts
import mongoose from 'mongoose';

// インターフェースの定義 (TypeScript)
interface Post {
  date: string;
  description: string;
}

interface CalendarDocument extends mongoose.Document {
  title: string;
  posts: Post[];
}

// スキーマの定義 (mongoose)
const { Schema } = mongoose;

const CalendarSchema = new Schema({
  title: String,
  posts: Array,
})

// モデルの定義 (mongoose)
const Calendar: mongoose.Model<CalendarDocument> = mongoose.model('Calendar', CalendarSchema);

export default Calendar;

ルーティング

routes/v1/calendar.ts
import express from 'express';
import bodyParser from 'body-parser';
import Calendar from '../../models/calendar';

const router = express.Router();

// [GET] カレンダーの全件取得
router.get('/', (_req, res) => {
  Calendar.find((err, data) => {
    if (err) {
      res.status(500).send('post get failed.');
      return;
    }

    res.json(data);
  });
});

// [POST] 新規カレンダーの追加
router.post('/', bodyParser.json(), (req, res) => {
  if (!req.body) {
    res.status(400).send('request body is empty.');
    return;
  }

  const instance = new Calendar();
  instance.title = req.body.title;
  instance.posts = req.body.posts;

  instance.save((err) => {
    if (err) {
      res.status(500).send('calendar create failed.');
      return;
    }

    res.json({ message: 'calendar create succeed.' });
  });
});

// [PUT] 対象カレンダーに達成記録(post)を追加する
router.put('/:id/post', bodyParser.json(), (req, res) => {
  if (!req.body) {
    res.status(400).send('request body is empty.');
    return;
  }

  const targetId = req.params.id;
  Calendar.updateOne(
    { _id: targetId },
    {
      $push: {
        posts: {
          date: req.body.date,
          description: req.body.description,
        },
      },
    },
    (err) => {
      if (err) {
        res.status(500).send('calendar update failed.');
        return;
      }

      res.json({ message: 'calendar update succeed.' });
    }
  );
});

export default router;

Postman を使ってAPIのテストを行う

APIにリクエストを送信して動作をチェックするために、今回はPostmanを利用しました。
任意のリクエストを送信して、結果を確認できる便利なツールです。

postman.png

ReactでView部分を作成する

以上でバックエンド部分は完成です。
続いてView(ユーザーインターフェース)を作っていきましょう。

  • カレンダー
  • カレンダーごとに「目標達成」の送信ボタン
  • 新規カレンダーの作成ボタン

を作成し、ユーザーの操作に応じてリクエストを送信します。
カレンダーはdate-fnsを利用して、今月のものを動的に生成しています(今回は1ヶ月分のみ)。

カレンダー

src/components/calendar.tsx
import React from 'react';
import {
  getDate,
  getDay,
  eachDayOfInterval,
  endOfWeek,
  eachWeekOfInterval,
  startOfMonth,
  endOfMonth,
} from 'date-fns';
import { CalendarDocument } from '../interfaces';

/**
 * 引数にとったDateから1ヶ月ぶんのカレンダーを作成して返す
 * see: http://yucatio.hatenablog.com/entry/2019/12/23/172547
 * @param date new Date()を渡すと今月が返る
 */
const getCalendarArray = (date: Date) => {
  // eachWeekOfInterval は 指定範囲の週(日曜日)の配列を返す
  // ここでdateを含む月の全ての日曜日を取得
  const weeks = eachWeekOfInterval({
    start: startOfMonth(date),
    end: endOfMonth(date),
  });

  // eachDayOfInterval は指定範囲の日を返す
  // ここで全ての日曜日 ~ 週末 = 1ヶ月ぶん毎日を取得
  const calendar = weeks.map((sunday) =>
    eachDayOfInterval({
      start: sunday,
      end: endOfWeek(sunday),
    })
  );

  return calendar;
};

const Calendar: React.FC<CalendarDocument> = (props) => {
  const { title, posts } = props;
  const calendar = getCalendarArray(new Date());

  return (
    <div>
      <h2>{title}</h2>
      <table>
        <thead>
          <tr>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {calendar.map((week, index) => (
            <tr key={index}>
              {week.map((date) => {
                // 対象日が「達成」されているか判定
                const isPostedDay = posts.find(
                  (post) =>
                    getDate(new Date(post.date)) === getDate(date)
                );

                return (
                  // getDay は曜日(0 ~ 6)、getDate は日付を取得
                  <td
                    key={getDay(date)}
                    className={isPostedDay ? 'postedDay' : ''} // 達成日は色が変わる
                  >
                    {getDate(date)}
                  </td>
                );
              })}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
};

export default Calendar;

Redux-Sagaでの状態管理

非同期処理による副作用を伴う状態管理には、ReduxのミドルウェアであるRedux-Sagaが便利です。

  1. rootSagaの起動に伴い、特定のActionのdispatchを待ち受ける
  2. 対象のActionがdispatchされたときに「処理中」のActionをdispatchし、Redux-Saga内部で非同期処理を実行
  3. 非同期処理の成功/失敗時に、「成功/失敗」のActionをdispatch

といった処理が可能です。ジェネレーター関数を用いた記法が独特で最初は少しとっつきづらいですが、慣れると処理を追いやすいです。

src/sagas/index.ts
import { put, takeLatest, call, fork, all } from 'redux-saga/effects';
import types from '../modules/actionTypes';
import Action, {
  getAllCalendars,
  postNewCalendar,
  putNewPost,
} from '../modules/action';
import * as api from '../services/api';

// カレンダーを全件取得
function* runGetAllCalendars() {
  try {
    const result = yield call(api.getAllCalendars);
    yield put(getAllCalendars.succeed(result));
  } catch (error) {
    yield put(getAllCalendars.fail(error));
  }
}

// 新規カレンダーを作成
function* runPostNewCalendar(action: Action) {
  try {
    const { data } = action.payload;
    // カレンダーの追加
    yield api.postNewCalendar(data);
    // カレンダーを再取得してStoreを更新する
    // TODO: 全件取得する必要はないかも
    const result = yield call(api.getAllCalendars);
    yield put(postNewCalendar.succeed(result));
  } catch (error) {
    yield put(postNewCalendar.fail(error));
  }
}

// カレンダーに「達成記録」を追加
function* runPutNewPost(action: Action) {
  try {
    const { _id, data } = action.payload;
    // 「達成」の追加
    yield api.putNewPost(_id, data);
    // カレンダーを再取得してStoreを更新する
    // TODO: 全件取得する必要はないかも
    const result = yield call(api.getAllCalendars);
    yield put(putNewPost.succeed(result));
  } catch (error) {
    yield put(putNewPost.fail(error));
  }
}

// 特定のactionがdispatchされるのを監視
export function* watchGetAllCalendars() {
  yield takeLatest(types.GET_ALL_CALENDARS_START, runGetAllCalendars);
}

export function* watchPostNewCalendar() {
  yield takeLatest(types.POST_NEW_CALENDAR_START, runPostNewCalendar);
}

export function* watchPutNewPost() {
  yield takeLatest(types.PUT_NEW_POST_START, runPutNewPost);
}

// rootSagaはアプリ起動時に実行される
export default function* rootSaga() {
  yield all([
    fork(watchGetAllCalendars),
    fork(watchPostNewCalendar),
    fork(watchPutNewPost),
  ]);
}

APIサーバーに対してのリクエストはaxiosで行っています。

src/services/api.ts
import axios from 'axios';
import { Post, PostCalendar } from '../interfaces';

const url = '(APIサーバーのURL)';

/**
 * [GET] axios で APIサーバーからカレンダーを全件取得
 */
export const getAllCalendars = () =>
  axios({
    method: 'get',
    url,
  })
    .then((res) => {
      return res.data;
    })
    .catch((err) => {
      throw err;
    });

/**
 * [POST] axios で カレンダーを追加
 * @param data 送信する値
 */
export const postNewCalendar = (data: PostCalendar) =>
  axios({
    method: 'post',
    url,
    data,
  })
    .then((_res) => {
      alert('カレンダーを追加しました');
    })
    .catch((err) => {
      throw err;
    });

/**
 * [PUT] axios で 対象のカレンダーに「達成記録」を追加
 * @param _id 対象カレンダーのID
 * @param data 送信する値
 */
export const putNewPost = (_id: string, data: Post) =>
  axios({
    method: 'put',
    url: `${url}${_id}/post/`,
    data,
  })
    .then((_res) => {
      alert('達成を記録しました! えらい!');
    })
    .catch((err) => {
      throw err;
    });

完成

こんな感じで動きます。
output.gif

おわりに

今回のアプリはFirebase等を利用すれば簡単に作ることができそうですが、あえて自分で実装してみることでより理解を深めることができたと感じています。

領域にとらわれず幅広い知識を身に着けることで、FE/BE間の連携を意識した開発ができるようになっていきたいですね。

このアプリもさらに改良を加えていきたいです。

参考(公式ドキュメント以外)

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

Docker + Django + Reactの環境構築

Dockerfile(Django用)

FROM python:3.9.0
ENV PYTHONUNBUFFERED 1
RUN mkdir /backend
WORKDIR /backend
ADD requirements.txt /backend/
RUN pip install -r requirements.txt
ADD . /backend/

Dockerfile-nodejs(React用)

FROM node:12.19.0
RUN mkdir /frontend
WORKDIR /frontend
RUN npm install -g create-react-app

requirements.txt

Django==3.1
psycopg2

docker-compose.yml

version: '3'

services:
  db:
    image: postgres
    environment: 
      POSTGRES_PASSWORD: password
  django:
    build: .
    command: python3 manage.py runserver 0.0.0.0:8000
    volumes:
      - ./backend:/backend
    ports:
      - "8000:8000"
    depends_on:
      - db

  react:
    build:
      context: .
      dockerfile: "./Dockerfile-nodejs"
    volumes:
      - .:/frontend
    command: >
        cd frontend && yarn start
    ports:
      - "3000:3000"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript + React 覚書

TypeScript + Reactを使用してSPAを構築したので、作業メモと復習を兼ねて投稿します。
シンプルなアプリケーションですが、ページ遷移、状態管理、非同期処理が最低限確認できる構成にしています。

状態管理は選択肢が多く迷いましたが、Redux Toolkitが良さそうだったので使ってみました。
見た目はBootstrapで実装していきます。

環境構築

Create React AppのTypeScriptテンプレートを使用します。

$ npx create-react-app react-ts-app --template typescript

ディレクトリを移動してサーバを起動します。

$ cd react-ts-app
$ npm start

必要なライブラリをインストールします。
historyのみ、最新版だとエラーになるのでバージョンを指定しています。

npm i @reduxjs/toolkit react-redux @types/react-redux react-router-dom @types/react-router-dom connected-react-router history@4.10.1 bootstrap reactstrap @types/reactstrap

アプリケーション概要

ホーム、ログイン、Todoアプリの3画面で構成されます。
ホームはログインしていない場合はログインページへのリンクを、ログインしている場合はTodoアプリへのリンクを表示します。
Todoアプリは登録したタスクをリストで表示し、完了ボタンを押すことでリストから消すことができます。

モジュールの作成

src配下にmodulesディレクトリを作成し、この中にモジュールを定義していきます。
この構成は、Reduxにおけるディレクトリ構成の一つである「Ducksパターン」を参考にしました。

Ducksパターン

Ducksパターンは、Reduxに必要なActionType、Reducer、Actionを1つのファイルにまとめて見通しを良くすることが目的ですが、ファイルが肥大化することが欠点と言われています。これを解決するためにRe-Ducksパターンという構成もあリます。
今回は、Redux Toolkitを使用することでActionType、Reducer、Actionをまとめて定義できるので、Ducksパターンで作成していきます。

currentUserモジュール

ログイン状態に関してはcurrentUserステートのusernameの有無で判断することとします。
ダミーのAPIから非同期で値を取得し、ユーザ名とパスワードが合致した場合にログイン済みとして処理します。

src/modules/currentUser.ts
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'
import { push } from 'connected-react-router'

type User = {
  username?: string
  isAuthenticationError?: boolean
}

type UserAuthentication = {
  username?: string
}

type UserCredential = {
  username: string
  password: string
}

type ThunkApiConfig = {
  rejectValue: {
    errorCode: number
    errorMessage: string
  }
}

const initialState: User = {}

// ダミーの認証API
// username/password共にuser1の場合にログイン成功とする
const dummyAuthenticate = ({ username, password }: UserCredential) => {
  return new Promise<UserAuthentication>((resolve, reject) => {
    setTimeout(() => {
      if (username === 'user1' && password === 'user1') {
        resolve({ username: 'User1' })
      } else {
        reject()
      }
    }, 1000)
  })
}

// 非同期通信はcreateSliceで定義できないので、予めcreateAsyncThunkで作成しておく
// createAsyncThunkのジェネリクスは<返り値, 引数, thunkAPI>となる
export const fetchCurrentUser = createAsyncThunk<UserAuthentication, UserCredential, ThunkApiConfig>(
  'currentUser/fetch',
  // 第二引数でthunkAPIを受け取る
  async (userCredential, { dispatch, rejectWithValue }) => {
    try {
      const response = await dummyAuthenticate(userCredential)
      // ページ遷移は副作用なので非同期処理内で発火させる
      if (response.username != null) {
        dispatch(push('/'))
      }
      return response
    } catch (e) {
      return rejectWithValue({
        errorCode: 401,
        errorMessage: 'Unauthorized'
      })
    }
  }
)

// ActionType、Reducer、Actionをまとめて定義
const slice = createSlice({
  name: 'currentUser',
  initialState,
  reducers: {
    setCurrentUser(state, action: PayloadAction<User>) {
      // Reduxでは新しいstateをリターンしなければならないが、ReduxToolkit内では
      // Immerが使用されており、stateを直接変更するようにも記述できる
      state.username = action.payload.username
    }
  },
  extraReducers(builder) {
    // fetchCurrentUserの正常終了で発火
    builder.addCase(fetchCurrentUser.fulfilled, (state, action) => {
      state.username = action.payload.username
      state.isAuthenticationError = false
    })
    // fetchCurrentUser内でrejectWithValue関数が呼ばれると発火
    builder.addCase(fetchCurrentUser.rejected, (state, action) => {
      console.error(action.payload?.errorMessage)
      state.isAuthenticationError = true
    })
  }
})

export const {
  setCurrentUser
} = slice.actions

export const currentUserReducer = slice.reducer

todosモジュール

続いてtodosモジュール。こちらは非同期処理を行わないためシンプルです。

src/modules/todos.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

type Todo = {
  id: number
  task: string
}

const initialState: Todo[] = []

const slice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    addTask: (state, action: PayloadAction<Todo>) => {
      const newTodo = {
        id: action.payload.id,
        task: action.payload.task,
      }
      state.push(newTodo)
    },
    removeTask: (state, action: PayloadAction<number>) => {
      return state.filter(todo => todo.id !== action.payload)
    }
  }
})

export const {
  addTask,
  removeTask,
} = slice.actions

export const todosReducer = slice.reducer

Storeの作成

作成したモジュールからReducerをまとめてStoreを作成します。
Connected React Routerもミドルウェアとして登録します。

src/store.ts
import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { connectRouter, routerMiddleware } from 'connected-react-router'
import { createBrowserHistory } from 'history'

import { todosReducer } from './modules/todos'
import { currentUserReducer } from './modules/currentUser'

// Connected React Routerで共通のインスタンスを使用する
// 必要があるためエクスポートしておく
export const history = createBrowserHistory()

const reducer = combineReducers({
  router: connectRouter(history),
  todos: todosReducer,
  currentUser: currentUserReducer
})

// useSelectorでの型推論に必要なため、reducerの戻り値型をエクスポート
export type RootState = ReturnType<typeof reducer>

export const store = configureStore({
  reducer,
  middleware(getDefaultMiddleware) {
    return getDefaultMiddleware().concat(routerMiddleware(history))
  }
})

コンポーネントの作成

src/components配下にコンポーネントを作成していきます。

ホーム

src/components/Home.tsx
import React from 'react'
import { useSelector } from 'react-redux'
import { useHistory, Link } from 'react-router-dom'
import { Button, Jumbotron } from 'reactstrap'

import { RootState } from '../store'

export const Home: React.FC = () => {
  const currentUser = useSelector((state: RootState) => state.currentUser)
  const { push } = useHistory()

  const unauthorizedView = (
    <>
      <p>アプリケーションを利用するにはログインして下さい</p>
      <Link to="/login">ログインページへ</Link>
    </>
  )

  const authorizedView = (
    <>
      <p>利用するアプリケーションを選択して下さい</p>
      <Button color="primary" onClick={() => push('/todo')}>
        Todoアプリ
      </Button>
    </>
  )

  return (
    <div>
      <Jumbotron>
        <h1 className="py-2">Home</h1>
        {currentUser.username ? authorizedView : unauthorizedView}
      </Jumbotron>
    </div>
  )
}

ログイン

src/components/Login.tsx
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { RootState } from '../store'
import { fetchCurrentUser } from '../modules/currentUser'
import { Alert, Button, Form, FormGroup, Input, Label } from 'reactstrap'

export const Login: React.FC = () => {
  const dispatch = useDispatch()
  const currentUser = useSelector((state: RootState) => state.currentUser)
  const [username, setUsername] = useState('')
  const [password, setPassword] = useState('')

  const handleSetUsername = (e: React.ChangeEvent<HTMLInputElement>) => {
    setUsername(e.target.value)
  }

  const handleSetPassword = (e: React.ChangeEvent<HTMLInputElement>) => {
    setPassword(e.target.value)
  }

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    dispatch(fetchCurrentUser({username, password}))
  }

  const authenticationErrorView = <Alert color="danger">認証に失敗しました</Alert>

  return (
    <>
      <h2 className="py-2">ログイン</h2>
      { currentUser.isAuthenticationError && authenticationErrorView}
      <Form onSubmit={handleSubmit}>
        <FormGroup>
          <Label for="usernameInput">ユーザー名</Label>
          <Input type="text" id="usernameInput" value={username} required
            onChange={handleSetUsername}
          />
        </FormGroup>

        <FormGroup>
          <Label for="passwordInput">パスワード</Label>
          <Input id="passwordInput" type="password" value={password} required
            onChange={handleSetPassword}
          />
        </FormGroup>

        <Button color="primary" type="submit">
          ログイン
        </Button>
      </Form>
    </>
  )
}

Todoアプリ

src/components/Todo.tsx
import React, { useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { Alert, Button, InputGroup, InputGroupAddon, Input, ListGroup, ListGroupItem } from 'reactstrap'

import { RootState } from '../store'
import { addTask, removeTask } from '../modules/todos'

export const Todo: React.FC = () => {
  const todos = useSelector((state: RootState) => state.todos)
  const dispatch = useDispatch()
  const [newTask, setNewTask] = useState('')

  const onChangeNewTask = (e: React.ChangeEvent<HTMLInputElement>) => {
    setNewTask(e.currentTarget.value)
  }

  const handleAddTask = () => {
    if (newTask === '') {
      window.alert('タスクを入力して下さい')
      return
    }
    dispatch(addTask({
      id: Date.now(),
      task: newTask
    }))
    setNewTask('')
  };
  const handleRemoveTask = (id: number) => {
    dispatch(removeTask(id))
  }

  return (
    <>
      <h2>Todoアプリ</h2>
      <Link to="/" className="d-block px-2 pt-2 pb-4">Homeへ戻る</Link>
      <InputGroup className="mb-3">
        <Input value={newTask} onChange={onChangeNewTask} />
        <InputGroupAddon addonType="append">
          <Button color="primary" onClick={handleAddTask}>タスクを登録する</Button>
        </InputGroupAddon>
      </InputGroup>
      <ListGroup flush>
        {!todos.length
          ? <Alert color="info">登録されているタスクはありません</Alert>
          : todos.map(({ id, task }) => (
            <ListGroupItem key={id}>
              <Button color="outline-success" onClick={() => handleRemoveTask(id)}>
                完了
              </Button>
              <span className="pl-4">{task}</span>
            </ListGroupItem>
          ))}
      </ListGroup>
    </>
  )
}

PrivateRoute

Todoアプリはログイン済みでなければログインにリダイレクトさせたいので、リダイレクト用のコンポーネントも作成していきます。

src/components/PrivateRoute.tsx
import React from 'react'
import { Route, Redirect, RouteProps } from 'react-router-dom'
import { useSelector } from 'react-redux'

import { RootState } from '../store'

export const PrivateRoute: React.FC<RouteProps> = (props) => {
  const currentUser = useSelector((state: RootState) => state.currentUser)
  // usernameプロパティがfalsyの場合、ログインにリダイレクト
  return currentUser.username ? <Route {...props} /> : <Redirect to="/login" />
}

App.tsxに統合

App.tsxでStoreやRouteの設定を行っていきます。

src/App.tsx
import React from 'react'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import { Route, Switch } from 'react-router-dom'
import { Container } from 'reactstrap'

import { store, history } from './store'
import { PrivateRoute } from './components/PrivateRoute'
import { Todo } from './components/Todo'
import { Home } from './components/Home'
import { Login } from './components/Login'

const App: React.FC = () => {
  return (
    <Provider store={store}>
      <ConnectedRouter history={history}>
        <Container className="py-3">
          <Switch>
            <Route exact path="/" component={Home} />
            <Route exact path="/login" component={Login} />
            <PrivateRoute exact path="/todo" component={Todo} />
            <Route render={() => (<h1>Not Found...</h1>)} />
          </Switch>
        </Container>
      </ConnectedRouter>
    </Provider>
  )
}

export default App

index.tsxの修正

index.tsxでBootstrapのCSSを読み込みます。
またReactのStrictモード下でreactstrapがエラーを吐くので、Appを直接マウントしています。

src/index.tsx
import 'bootstrap/dist/css/bootstrap.min.css'

import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

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

まとめ

以上で、TypeScript + Reactでページ遷移、状態管理、非同期処理の動きが確認できるアプリケーションが作成できました。まだまだ勉強不足で拙い箇所もありますが、参考になれば幸いです。

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