20200216のReactに関する記事は11件です。

まだMVCで消耗してるの?〜Django x Reactで始めるSPA開発〜

ここ最近JSフレームワークを使ったサイトが増えてきています。
とくにReactやVueなどのJSフレームワークはSPAというアプリケーション開発によく使われ、サイトを利用するユーザーだけでなく開発者にも多くのメリットをもたらします。

想定読者

  • Web開発経験者
  • APIを使ったWebアプリケーションを開発したことがある人
  • JavaScriptをそこそこ知っててPythonもそこそこ知ってる人
  • Djangoをちょっと知っている
  • MVCもしくはMTVを使った開発をしたことがある人

別記事にもっと詳細に書いた記事があるので、本記事で難しいと感じた方やもっと深いところまで学習したい方はこちらをご覧ください。
まだMVCで消耗してるの?〜React x Djangoで始める今時Web開発〜

この記事ではフロントエンドにReact、バックエンドにDjangoを使用してチュートリアルを進めていきます。
チュートリアルはToDoアプリを題材にして進めていきます。

SPAとは

SPAはSingle Page Applicationと呼ばれ、ユーザーエクスペリエンスを向上させるのに有効な手立てとなります。
また、データバインディング、仮想DOM、Componentの3つの特徴を兼ね備えています。

データバインディング

素のJavaScriptを使って値を変更する場合、DOMを指定して値を変更する処理を毎回動かさなければなりません。
ですが、JSフレームワークを使うと定義しておいた変数が更新されるたびに画面上の値も変更されます。

仮想DOM

JSフレームワークには、クライアントのブラウザで描画をするためのDOMとサーバーとDOMの間に存在する仮想DOMの2種類があります。
仮想DOMの役割は、新しくサーバーから吐き出された仮想DOMと現在存在する仮想DOMとの差分を取り、その差分をDOMに反映することです。
そのためDOMの更新は差分があった部分だけとなり、ページのレンダリングを高速にすることができます。

Component

JSフレームワークでは、ページの要素をコンポーネントと呼ばれる部品単位に分割することができます。 そうすることで、コンポーネントを再利用することができ同じコードを書かずに済みます。

このチュートリアルではページを1枚作るだけなので、ユーザーエクスペリエンスにつながるメリットを肌で感じることはできないかもしれないのですが、開発面でのメリットは感じることができると思います。

Django環境構築

まずはバックエンドから進めていきます。

以下のコマンドを順に実行してください。

mkdir todo-backend
cd todo-backend
python3 -m venv env
source env/bin/activate
pip install django djangorestframework django-cors-header
django-admin startproject project .
django-admin startapp todo
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver

環境が構築できたら127.0.0.1:8000にアクセスしてください。
初期画面が表示されるはずです。

Django環境の設定

settings.pyにプラグイン追加の設定とクロスオリジンの設定を追記していきます。
クロスオリジンの設定は、WebブラウザからAPIを実行するときにアクセス拒否されるのを防ぐために追記します。

settings.py
INSTALLED_APPS = [
   'django.contrib.admin',
   'django.contrib.auth',
   'django.contrib.contenttypes',
   'django.contrib.sessions',
   'django.contrib.messages',
   'django.contrib.staticfiles',
   'rest_framework',
   'corsheaders',
   'todo'
]

MIDDLEWARE = [
   'corsheaders.middleware.CorsMiddleware',
]

# 許可するオリジン
CORS_ORIGIN_WHITELIST = [
   'http://localhost:3000',
]

ついでにprojectディレクトリ内のurl設定ファイルに、APIのルーティングを設定します。

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

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

バックエンドの実装

todoアプリ内を実装していきます。

models.py
from django.db import models

class Todo(models.Model):
   name = models.CharField(max_length=64, blank=False, null=False)
   checked = models.BooleanField(default=False)

   def __str__(self):
       return self.name

マイグレーションを実行します。

python manage.py makemigrations
python manage.py migrate
admin.py
from django.contrib import admin
from .models import Todo

@admin.register(Todo)
class Todo(admin.ModelAdmin):
   pass
serializer.py
from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
   class Meta:
       model = Todo
       fields = ('id', 'name', 'checked')
views.py
from rest_framework import filters, generics, viewsets
from .models import Todo
from .serializer import TodoSerializer

class ToDoViewSet(viewsets.ModelViewSet):
   queryset = Todo.objects.all()
   serializer_class = TodoSerializer
   filter_fields = ('name',)
urls.py
from rest_framework import routers
from .views import ToDoViewSet
from django.urls import path, include

router = routers.DefaultRouter()
router.register(r'todo', ToDoViewSet)

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

ここまで終えたら、http://localhost:8000/admin にアクセスしてToDoをいくつか追加しておいてください。

React環境構築

Reactの環境立ち上げにはCreate React Appを使います。

yarn create react-app todo-frontend
cd todo-frontend
yarn start

http://localhost:3000にアクセスして画面が正常に表示されたら環境構築完了です。

ルーティング

Reactはルーティング機能を持たないので、別にプラグインをインストールします。

yarn add react-router-dom

srcディレクトリ直下にRouter.jsxを作成してください。

Router.jsx
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';
import Top from '../components/Top';

const Router = () => {
 return (
   <BrowserRouter>
   </BrowserRouter>
 );
};
export default Router;

App.jsにルーティングを読み込ませます。

App.js
import React from 'react';
import Router from './configs/Router';

function App() {
 return (
   <Router />
 );
}

export default App;

画面デザイン

画面のデザインにはMaterial UIというデザインフレームワークを使います。
Reactのプラグインとして提供されているので、yarn addでインストールしてください。

yarn add @material-ui/core

下の画像が出来上がり図です。

Screenshot from 2020-02-16 00-50-08.png

API実装

まずはAPIを実装していきます。
一つのコンポーネント内に含めると可読性が落ちるので、別ファイルに分けてAPI処理を実装します。
実装するAPI処理は、ToDoリスト取得、ToDo作成、ToDoのチェック、ToDo削除の4つです。

src/common/apiディレクトリを作り、その中にtodo.jsを作成してください。

todo.js
const originUrl = 'http://127.0.0.1:8000';

const getTodoList = (() => {
  const url = new URL('/api/todo/', originUrl);
  return new Promise( (resolve, reject) => {
    fetch(url.href)
    .then( res => res.json() )
    .then( json => resolve(json) )
    .catch( () => reject([]) );
  });
});
export default getTodoList;

export const postCreateTodo = (name) => {
  const url = new URL('/api/todo/', originUrl);
  return new Promise( resolve => {
    fetch(url.href, {
      method: 'POST',
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        name: name
      })
    })
    .then( res => res.json() )
    .then( data => resolve(data) );
  });
};

export const patchCheckTodo = ((id, check) => {
  const url = new URL(`/api/todo/${id}/`, originUrl);
  fetch(url.href, {
    method: 'PATCH',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      checked: check
    })
  });
});

export const deleteTodo = ((id) => {
  const url = new URL(`/api/todo/${id}/`, originUrl);
  fetch(url.href, { method: 'DELETE' });
});

コンポーネント実装

次にコンポーネントを実装します。

index.jsx
import React, { useEffect, useState } from 'react';
import Button from '@material-ui/core/Button';
import Box from '@material-ui/core/Box';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import Container from '@material-ui/core/Container';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import getToDoList, { postCreateTodo, patchCheckTodo, deleteTodo } from '../../common/api/todo';

const useStyles = makeStyles(theme => ({
  todoTextField: {
    marginRight: theme.spacing(1)
  }
}));

const Top = () => {
  const classes = useStyles();
  const [todoList, setTodoList] = useState([]);
  const [todo, setTodo] = useState('');

  useEffect(() => {
    (async () => {
      const list = await getToDoList();
      setTodoList(list);
    })();
  }, []);

  const handleCreate = async () => {
    if ( todo === '' || todoList.some( value => todo === value.name ) ) return;
    const createTodoResponse = await postCreateTodo(todo);
    setTodoList(todoList.concat(createTodoResponse));
  };

  const handleSetTodo = (e) => {
    setTodo(e.target.value);
  };

  const handleCheck = (e) => {
    const todoId = e.target.value;
    const checked = e.target.checked;
    const list = todoList.map( (value, index) => {
      if (value.id.toString() === todoId) {
        todoList[index].checked = checked;
      }
      return todoList[index];
    });
    setTodoList(list)
    patchCheckTodo(todoId, checked);
  }

  const handleDelete = (e) => {
    const todoId = e.currentTarget.dataset.id;
    const list = todoList.filter( value => value['id'].toString() !== todoId);
    setTodoList(list);
    deleteTodo(todoId);
  };

  return (
    <Container maxWidth="xs">
      <Box display="flex" justifyContent="space-between" mt={4} mb={4}>
        <TextField className={classes.todoTextField} label="やること" variant="outlined" size="small" onChange={handleSetTodo} />
        <Button variant="contained" color="primary" onClick={handleCreate}>作成</Button>
      </Box>
      <FormGroup>
        {todoList.map((todo, index) => {
          return (
            <Box key={index} display="flex" justifyContent="space-between" mb={1}>
              <FormControlLabel
                control={
                  <Checkbox
                    checked={todo.checked}
                    onChange={handleCheck}
                    value={todo.id}
                    color="primary"
                  />
                }
                label={todo.name}
              />
              <Button variant="contained" color="secondary" data-id={todo.id} onClick={handleDelete}>削除</Button>
            </Box>
          )
        })}
      </FormGroup>
    </Container>
  )
};
export default Top;

最後に

ToDoアプリを一つ作りましたが、この記事の内容だけだとまだ実用はできないので、いずれホスティングに載せるところまでを紹介しようと思います。

誤字脱字や、間違いがあればご連絡ください。
ソースコードをGitHubに上げているので、必要であれば使ってください。

フロントエンド
https://github.com/uichi/todo-frontend

バックエンド
https://github.com/uichi/todo-backend

参考

React公式
Material UI公式
Django公式
Django REST framework公式

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

Reactで.envがどうしても読めなかった時に試してみるといいかもしれないこと

Reactで環境変数を.envファイルから読み込みたかった時の話です。
どうにかした軌跡を置いておきます。

TL;DR

  • webpack.config.js内に設定を追加した
  • dotenv-webpackはなんかのタイミングで入ってたけど、入ってなければ入れるとよいと思います
    • 僕はたぶんbabelかstorybook入れたとき入った
webpack.config.js
const Dotenv = require('dotenv-webpack');

plugins: [
  new Dotenv()
],

// なんか言われたら
node: {
  fs: 'empty'
}

ざっくり環境

  • create-react-appした
  • webpack-dev-serverで動かしてる
  • .envのREACT_APP_HOGE_KEYがundefinedになる

経緯

公式に従って設定してみたのですが、どう頑張ってもundefinedになりました。
プロジェクトのルートに .env ファイルを作り、REACT_APP_から始まるものを定義すればいいよと書いてあります。適当にググってもそんな感じの内容が出てきます。
https://create-react-app.dev/docs/adding-custom-environment-variables/#adding-development-environment-variables-in-env

でもダメでした。

デフォで入ってるのでダメなら改めて自分で入れてみるかぁと思い調査。

結論

明示的にdotenv-webpackを利用することに。
公式: https://www.npmjs.com/package/dotenv-webpack
参考: Can't resolve 'fs' https://megu-tech.hatenablog.com/entry/2019/10/09/170631

webpack.config.jsに設定を追加。

webpack.config.js
const Dotenv = require('dotenv-webpack');

plugins: [
  new Dotenv()
],

// なんか言われたら
node: {
  fs: 'empty'
}

で読めるようになりました。webpack絡みでなんか起きてるのかなと思いつつも学びきれてなさみです。

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

無理してReduxを使わずに、Custom Hookで楽に状態を管理する

はじめに

Reduxを使わなくても、ReactでSPAをつくることは可能です。

画面の初期化時に一度だけ叩くapiをreduxで管理したことはありませんか?
apiを一度叩いて画面に表示するだけなのに、reducer/action/actionCreatorを作ってconnectをしてませんか?
Reduxを無理して使っていませんか?

自分が参画したWebアプリケーション開発では、Reactが採用される場合、常にReduxもセットになっていました。

Reduxは要件によってはオーバーエンジニアリングかもなーと感じることが多いので、改めてどんな時にReduxが有効かのか自分なりに考えてみました。

SPAの設計で大事なこと

自分がフロントエンドの設計で大事だと思うのは、「ViewとStateの分離」です。複雑なアプリケーションでこれが出来ていないと可動性が低く変更しづらいです。

Viewはコンポーネントと呼ばれたりしますが見た目の部分です。
Stateはコンポーネントの状態です。本記事では、状態を変化させるロジックも含めStateと呼びます。

個人的には以下を実現することが重要です。

  • ViewとStateが密結合にならず、コードの可読性を保てる。

  • ユーザーが複雑な操作をしても、アプリケーションの状態が予測可能になっている。

どんな時にReduxが有効か

Reduxが実現しているFluxアーキテクチャは、状態をStoreに、ロジックをAction/ActionCreatorにすることで、アプリケーションの状態を予測可能にしています。

Reduxを使うことで「ViewとStateの分離」は達成出来ます。
ですが、Reduxを使わなくても「ViewとStateの分離」は達成出来ます。
Redux辛いみたいな話はわざわざReduxで解決しなくても良い問題をReduxの冗長な処理を書いて解決しているからだと思います。

では、Reduxを使わないと辛い時はどんな時でしょうか?
自分は以下だと考えます。

  • コンポーネントの階層が深い
  • 画面で発火するイベントが多い

componentの階層が深い

propsのバケツリレーと呼ばれる問題です。prop drillingの方が有名でしょうか。

ey25z0hvmy31xiiqqwgq.png
出典:Learn React Context in 5 Minutes - A Beginner's Tutorial

Reactではコンポーネントにデータを渡す時にpropsを使います。
コンポーネントの階層が深いと、親コンポーネントから子コンポーネント、さらに孫コンポーネントと渡したいコンポーネントまで延々とpropsをバケツリレーのように渡していく必要があります。
このバケツリレーを省略するためにReduxを使うのは有用です。
自分は3回を超えたらRedux導入を考えます。

画面で発火するイベントが多い

1つの画面でユーザーがやれることが多い画面はReduxが有効だと判断します。
例えば、SlackやGoogle Carenderは1つの画面で様々なイベントを発火でき、リアルタイムにUIを変更しつつapiリクエストを飛ばします。

様々なイベントを高速でハンドリングする場合は、アプリケーションの状態が予測しづらくなるのでReduxを使います。
Reduxはdevtoolが強力でstoreに対する変更を全て記録してくれるのでデバッグもしやすいです。

Custom Hookで「ViewとStateの分離」を実現する

一覧画面等で、画面初期化時に一度だけfetchしたデータを表示するのにReduxを使うのはオーバーエンジニアリングです。

自分はReactのCustom Hookを使います。

以下はユーザー情報を表示するだけの架空の画面の例です。

Custom HookにState(状態とロジック)を切り出します。

Custom HookはUtil的な使い方だけでなく、ドメイン固有の状態とロジックを管理するのにも有効です。

useUser.ts
export const useUser = (): [
  User | undefined,
  () => void
] => {
  const [user, setUser] = React.useState<User | undefined>(undefined);
  const [errorMessage, setErrorMessage] = React.useState<string | undefined>(undefined);

  const fetchUser = async (): Promise<void> => {
    try {
      const res = await axios.get<User>('/api/user');
      setUser(res.data);
    } catch (e) {
      setError(e.response.date.message)
    }
  };

  return [
    user,
    errorMessage,
    fetchUser,
  ];
};

stateとstateを操作するロジックをuseUserというCustom Hookに分離しています。
Reduxのreducer/action/actionCreatorをまとめたduckに近いですが、少ない記述量で「ViewとStateの分離」が出来ています。

container.ts
import { useUser } from 'useUser';

export const UserContainer: React.FC = () => {
  const [user, errorMessage, fetchUser] = useUser();
  React.useEffect(() => {
    fetchUser();
  }, [fetchUser]);

  return (
    <UserComponent
      user={user}
      errorMessage={errorMessage}
    />
  );
};

Custom Hookは任意のcontainer/component層でデータとロジックを注入できます。
自分はいつもcontainer層で注入します。

Reduxを使うならアプリケーション全体で、Stateの管理方法をReduxで統一したいと思ってた時もありましたが、最近は要件によってCustom HookとReduxを使い分けるのが良いかなと考えています。

Custom Hookの中でapiリクエストをする場合、middlewareを気にしなくて良いです。

今のところ上手くいってますが、同じ画面やドメインでCustom HookとReduxの両方を使わないようにしています。

まとめ

Reactでは「ViewとStateの分離」をいくつかの方法で実現出来ます。

とりえあずReduxにすると大きく外すことがないですが学習コストやコード量が大きいので、Reduxでオーバーエンジニアリングになりそうな時はCustom Hookがおすすめです。

参考

JavaScript: Reduxが必要なとき/不要なとき(翻訳)

Learn React Context in 5 Minutes - A Beginner's Tutorial

Fluxとはなんなのか

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

React のチュートリアルを Typescript でやってみた【環境構築編】

背景

React 触ってみたい。
Typescript もやってみたい。

作業環境

今回は docker を使用し環境を作る。

開発環境の準備

プロジェクトのディレクトリーを作成

作業するディレクトリを作成します。
ここではreact-tutorialとしておきます。

mkdir react-tutorial
cd react-tutorial

ディレクトリの中に以下のファイルを作成します。

  • Dockerfile
  • docker-compose.yml

以下それぞれのファイルの内容

Dockerfile

Dockerfile
FROM node:13.8.0-alpine3.11

ENV NODE_ENV=development

WORKDIR /app

docker-compose.yml

docker-compose.yml
version: "3"
services:
  webserver:
    container_name: react-tutorial
    image: react
    build: .
    volumes:
      - ./:/app
    tty: true
    ports:
     - 8080:8080

tree はこんな感じ

react-tutorial $tree
.
├── Dockerfile
└── docker-compose.yml

開発環境の作成

とりあえずビルドします。

docker-compose up --build -d

以下のコマンドでコンテナが起動していれば OK

react-tutorial $docker-compose ps

起動している場合以下のように表示される。

react-tutorial $docker-compose ps
     Name                 Command            State           Ports
---------------------------------------------------------------------------
react-tutorial   docker-entrypoint.sh node   Up      0.0.0.0:8080->8080/tcp

コンテナ内に移動する場合は以下

docker exec -it react-tutorial /bin/sh

package.json の作成

まずはコンテナ内に移動

docker exec -it react-tutorial /bin/sh

以下のコマンドで package.json を作成します。

npm init

設定は以下のように設定しました。

package name: (app) react-tutorial
version: (1.0.0)
description:
entry point: (index.js) webpack.config.js
test command:
git repository:
keywords:
author:humi3
license: (ISC)
About to write to /app/package.json:

ワークディレクトリに以下の package.json が以下の内容でされたと思います。

package.json
{
  "name": "react-tutorial",
  "version": "1.0.0",
  "description": "## コンテナビルド `docker-compose up --build`",
  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "humi",
  "license": "ISC"
}

※今回は 1 から環境を構築しますが、以下のコマンドで React + Typescript の環境できます。
my-appの箇所にプロジェクト名

npx create-react-app my-app --typescript

必要なライブラリのインストール

Webpack

npm install --save-dev webpack webpack-cli

Typescript で必要なライブラリ

npm install --save-dev typescript awesome-typescript-loader source-map-loader

react & react-dom

npm install --save-dev react react-dom @types/react @types/react-dom

ディレクトリとファイルの準備

以下のディレクトリの構成になるようにファイルを作成します。

react-tutorial
├── dist
│   ├── index.html
│   └── bundle.js
├── node_modules
├── src
│   └──index.tsx
├── Dockerfile
├── docker-compose.yml
├── package-lock.json
└── package.json

bundle.js 及び index.tsx はファイル作成だけでいいです。
index.html だけ以下のような内容にしておきます。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <title>TypeScript HelloWorld</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="bundle.js"></script>
  </body>
</html>

tsconfig.json の作成

以下のコマンドでtsconfig.jsonを作成します。

npx tsc --init

設定は以下のようにしました。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
    "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
    "outDir": "./dist/" /* Redirect output structure to the directory. */,
    "strict": true /* Enable all strict type-checking options. */,
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
    "sourceMap": true /* Emit a single file with source maps instead of having a separate file. */,
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
    "jsx": "react",
    "lib": ["es2015", "dom"]
  }
}

現在のディレクトリ

react-tutorial
├── dist
│   ├── index.html
│   └── bundle.js
├── node_modules
├── src
│   └──index.tsx
├── Dockerfile
├── docker-compose.yml
├── package-lock.json
├── package.json
└── tsconfig.json

webpack.config.js の作成

webpack.config.jsを作成します。

react-tutorial
├── dist
│   ├── index.html
│   └── bundle.js
├── node_modules
├── src
│   └──index.tsx
├── Dockerfile
├── docker-compose.yml
├── package-lock.json
├── package.json
├── tsconfig.json
└── webpack.config.json

webpack.config.jsの内容

webpack.config.js
const path = require("path");

module.exports = {
  entry: path.resolve(__dirname, "./src/index.tsx"),
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js"
  },
  devtool: "source-map",
  target: "node",
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".json"]
  },
  module: {
    rules: [
      { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
      {
        enforce: "pre",
        test: /\.tsx?$/,
        loader: "source-map-loader"
      }
    ]
  }
};

以上でファイルの準備が完成しました。

動作確認

index.tsx の内容を以下へ

index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";

export default class App extends React.Component<{}, {}> {
  render(): JSX.Element {
    return <div>HelloWorld!</div>;
  }
}

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

package.json に build 用の script を追加

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build":"webpack"
  },

以下のコマンドで build を行います。

npm run build

buildがうまく行ったことを確認します。
スクリーンショット 2020-02-16 21.30.12.png

index.html をブラウザで開きHelloWorld!が表示されたら OK です。

index.png

今回は、環境構築の部分をやってみました。
次回は、公式のチュートリアルを Typescript にしてやっていきたいと思います。

参考

https://speakerdeck.com/takepo/reactxreduxniokerutypescriptru-men
https://qiita.com/soarflat/items/28bf799f7e0335b68186
https://qiita.com/one-kelvin/items/b810aafb6b5ef90789a3

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

2020年版 Node.js+Reactのインストール

1.概要

MacbookにNode.jsとReactをインストールし、アプリを開発するための環境を構築するための手順について。

2.前提条件

事前作業

作業日時

  • 2020年2月

環境

  • MacBook Pro
  • macOS Catalina

ソフトウェアのバージョン

ソフトウェア バージョン
nodebrew 1.0.1
Homebrew 2.1.11
yarn 1.21.1

3.インストール手順

Node.jsをインストールする

以下の流れでインストールする。

  1. Homebrewのインストール
  2. nodebrewのインストール
  3. Node.jsのインストール

Homebrewのインストール

以下のコマンドでHomebrewをインストールする。

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

nodebrewのインストール

Homebrewを使用してインストールする。

brew install nodebrew

インストール後は以下コマンドでnodebrewのバージョンが確認できます。

nodebrew -v
nodebrew 1.0.1

node.jsのインストール

$ mkdir -p ~/.nodebrew/src

最新版を取得する際は

$ nodebrew install-binary latest

安定版を取得する際は

$ nodebrew install-binary stable

インストールされたnodeを有効化

$ nodebrew ls

上記、コマンドでインストールされたバージョンが一覧できる。

v7.1.0

current: none

インストール直後はcurrent: noneとなっているため、必要なバージョンを有効化する。

$ nodebrew use v7.1.0

もう一度nodebrew lsを試すと

v7.1.0

current: v7.1.0

v7.1.0が設定されました。

参考記事

ちなみに、Windowsの場合は、以下のインストーラでインストール可能。

https://nodejs.org/en/download/

yarnのインストール

npmの代わりのパッケージマネージャ。
yarn addでパッケージをインストールできる。

$ npm install -g yarn

Reactのアプリの作成

以下コマンドでReactアプリのフォルダが作成される。
Typescriptで開発を行うため、--template typescriptのオプションを設定する。

npx create-react-app <app name> --template typescript

各種ライブラリのインストール

作成したフォルダに移動して、各種ライブラリをインストールする。

$ cd my-react-app

Reduxのインストール

プロジェクトルートフォルダで以下コマンドを実行する。
reduxはreduxそのもの、react-reduxはreactとreduxをつなぐライブラリです

$ yarn add redux react-redux typescript-fsa typescript-fsa-reducers

typescript-fsaはAction側、typescript-fsa-reducersがReducer側で利用するライブラリです。

  • typescript-fsa → ActionCreatorを簡単に生成するライブラリ
  • typescript-fsa-reducers → Reducerを簡単に作成するためのライブラリ

開発ツール(redux-devtools)のインストール

redux-devtoolsは、Reduxで開発する際に利用できる便利な開発ツールで、アクション実行時のstoreの状態を確認するのに利用する。
これに加えてChromeの拡張機能でRedux DevToolsを追加するとツールでRedux上のStoreの状態が確認でき、非常にデバッグしやすくなります。

yarn add --dev redux-devtools

Prettierのインストール

整形ツールのPrettier(-Dで開発版のみにインストールされる)
設定はこちら
https://qiita.com/awakia/items/3a05edfa135762d7952c

# yarn add -D prettier

Eslintのインストール

# yarn add -D eslint  \
   @typescript-eslint/eslint-plugin  \
   eslint-plugin-prettier  \
   eslint-config-prettier

Material-ui

Materialデザインを利用するためのパッケージ。
タッチ、タップ、クリックなどのイベントを使うためにreact-tap-event-pluginのインストールも必要。

# yarn add material-ui react-tap-event-plugin 
# yarn add @material-ui/core @material-ui/icons

react-router-dom

react-routerの代わりにConnected React Routerを使う。
URLに応じて、表示するコンテンツを変更するルーティングを行うためにインストールする。

# yarn add react-router-dom connected-react-router
# yarn add -D @types/react-router-dom

date-fns

dateオブジェクトのライブラリ。
momentより軽いらしい。

$ yarn add date-fns

4.コマンド

ローカルでサーバーを起動する

以下コマンドでローカルでサーバーを起動できる。
http://localhost:8080/でアクセスできる。

$ yarn start

ビルドする

$ yarn build

5.終わりに

  • firebaseの設定は別途。

以上。

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

create-react-appで作ったTypeScriptプロジェクトで絶対パスでのimportをしたい

概要

絶対パスでimportしたい。

import Post from  '../../components/Post' 

import Post from  'components/Post'

やり方

.envの設定

.envの中を下記のようにすることで、Webpack側でaliasが効くようになります。

NODE_PATH=src

ref) https://github.com/facebook/create-react-app/issues/2188#issuecomment-302040874

.tsconfigの設定

これだとTypeScript側が認識してくれないため、.tsconfigでもpathを貼る必要があります。

tsconfig.json
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "*": [ "*"]
    }
  }

しかし、ただ書き加えてもreact-scripts startすると消されてしまいます...。
どうやらreact-scriptsが勝手に消してくるらしい。

そのため別ファイルに定義し、それをextendsすることでそれを回避します。

paths.json
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "*": [ "*"]
    }
  }
tsconfig.json
{
  "extends": "./paths.json",
   ...
}

ref) https://stackoverflow.com/questions/53794875/how-to-configure-react-script-so-that-it-doesnt-override-tsconfig-json-on-star

おわり

これで絶対パスで読み取れるようになりました!

import Post from  'components/Post'

Webpackerもそうですが、こういうのは導入は便利でも改変しづらいのが面倒ですね。
生が一番です。

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

create-react-appで作ったTypeScriptプロジェクトで相対パスでのimportをしたい

概要

相対パスでimportしたい。

import Post from  '../../components/Post' 

import Post from  'components/Post'

やり方

.envの設定

.envの中を下記のようにすることで、Webpack側でaliasが効くようになります。

NODE_PATH=src

ref) https://github.com/facebook/create-react-app/issues/2188#issuecomment-302040874

.tsconfigの設定

これだとTypeScript側が認識してくれないため、.tsconfigでもpathを貼る必要があります。

tsconfig.json
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "*": [ "*"]
    }
  }

しかし、ただ書き加えてもreact-scripts startすると消されてしまいます...。

そのため別ファイルに定義し、それをextendsする形で定義する必要があります。

paths.json
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "*": [ "*"]
    }
  }
tsconfig.json
{
  "extends": "./paths.json",
   ...
}

ref) https://stackoverflow.com/questions/53794875/how-to-configure-react-script-so-that-it-doesnt-override-tsconfig-json-on-star

おわり

これで相対パスで読み取れるようになりました!

import Post from  'components/Post'

Webpackerもそうですが、こういうのは導入は便利でも改変しづらいのが面倒ですね。
生が一番です。

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

雑に知ってしまったDockerを知り直す ~アプリケーション持ち込み編~

おさらい

第2回です。
前回の投稿はこちら
前回はnginxのイメージをpullして来てコンテナ起動。
ポートフォワードしてウェルカムページを表示させるまでやりました。

今回やりたいこと

  1. ローカルで適当にアプリケーションを用意する
  2. どうにかしてそのアプリケーションをコンテナの中にぶちこむ
  3. ホスト側から起動したアプリケーションへアクセスできるようにする

アプリケーションを用意

Nodeとgithubを勉強したときに作った、first-stepっていうオシャレ気取った名前のリポジトリが放置されていたので、これを再利用します。
せっかくなので触ったこと無いReactへ転生。

この辺のステップは本題と外れてしまうので割愛。
$ npm start
image.png

これをどうにかしてコンテナへ持っていきたい。

コンテナを用意

前回はnginxのイメージを利用しましたが、今回はnodeのイメージを利用します。
現在のnodeの最新バージョンはv13.8.0なんですが、ホストにインストールしてるのは安定版のv12.16.0なのでこれをインストールしたい。

バージョン指定してイメージをpullするのどうやるんだろう・・と思いつつとりあえずイメージを探します。
コマンドでも探せるみたいなんですが、初めてなのでGUIに頼ります。

https://hub.docker.com/
image.png

nodeのページを見つけた
image.png

下の方にズラーっとなんか並んでる
image.png

試しに検索窓にバージョン情報入れてみた
image.png

なるほどなるほど、なんかそれっぽいですね。

調べてみるとこれはTagと呼ばれるもので、古いのバージョンのイメージが欲しい場合はこのTagを指定してpullする必要があると。ただ、同じバージョン(v12.16.0)でもstretchとかslimとか更に違いがあるのが気になる。。
で更に調べると、例えばnodeの場合だとバージョンは同じでも無駄なファイルを省いて最適化されたslimバージョンがあったり、Debianのバージョンが異なるもの(stretchとかbusterとかはDebianのコードネーム)があったりと結構細かい違いがあるということが判明。
このあたりもちゃんと意識して適切なイメージを選択する必要があるってことか・・
で、更に気になったのがTag指定しない場合はどうなるんだ?とかlatest指定すると結局どれを引っ張ってくるのよとか、そもそもこれ誰が作ったイメージなん?安全なん?とか・・
TODO: これも色々調べてみたんですけど、今回の趣旨からは外れそうなのとボリューム膨らみそうなので別記事でまたやります。

今回はとりあえず名前がかっこいいのでbusterでやります。

$ docker pull node:12.16.0-buster
12.16.0-buster: Pulling from library/node
dc65f448a2e2: Pull complete
346ffb2b67d7: Pull complete
dea4ecac934f: Pull complete
8ac92ddf84b3: Pull complete
a3ca60abc08a: Pull complete
ab849ba5abe0: Pull complete
b9e0c215e971: Pull complete
766774022603: Pull complete
f199c0426b2a: Pull complete
Digest: sha256:66d5994de29952fe982729ef7c7f8d4c50d528279db386efbf373451f534fa16
Status: Downloaded newer image for node:12.16.0-buster

$ docker image ls
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
node                12.16.0-buster      ce43ce61c1de        28 hours ago        882MB

$ docker run [イメージ名:TAG]
$ docker run -d --name node-react node:12.16.0-buster
505ba1fdefd2f705f90b083cee3653445256a0917e054147f8251d4e0eeef122

#前回はrunする前にイメージをpullしてなかったのでこのタイミングで色々ダウンロードしてましたが、
今回は事前にpull済なのでローカルのイメージを利用。カシコイ

$ docker ps -a
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS                     PORTS               NAMES
8a126cad2450        node:12.16.0-buster   "docker-entrypoint.s…"   5 seconds ago       Exited (0) 4 seconds ago                       node-react

で、ここで作成したコンテナのSTATUSを見るとUp(起動中)ではなく、Exitedになっちゃってる。
前回のnginxの場合サーバを立ち上げてUp状態になっていたけど、今回のイメージはあくまでnodeの実行環境を用意するだけなのでコンテナが立ち上がりっぱなしにはならないのかな、となんとなく推測。
環境を構築して後続の命令が無いと、プロセスが死ぬようになっているらしい。起動するアプリケーションも無いのに立ち上げておく意味ないもんね。
-itdってオプションつけるとコンテナが立ち上がりっぱなしになるらしいのでお試し。

$ docker run -itd
-i ホストの入力をコンテナの標準出力につなげる。コンテナ側へ正しくコマンドを渡せるようにする。
-t コンテナの標準出力をホストの標準出力につなげる。ttyをコンテナに割り当てて、対話できるようにする。
-d コンテナに入らずバックグランドで起動する

$ docker run -itd --name node-react node:12.16.0-buster
10235f0996b7c1b1e5f47c5b1c36651ff4fe20cd72b14f47dc5a11f0e8607780

$ docker ps -a
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS               NAMES
10235f0996b7        node:12.16.0-buster   "docker-entrypoint.s…"   3 seconds ago       Up 2 seconds                            node-react

うんうん、STATUSがUpになってる。
コンテナに入って作業したい場合は、とりあえず-itdって覚えても良いかもしれない。
TODO: 1個づつオプション外して試してみたけど、ここの挙動が上手く理解できていないので別の機会にちゃんと調べる。

# コンテナの中に入る
$ docker exec -it node-react /bin/bash
root@8bf7a6704bb9:/# node -v
v12.16.0

ちゃんと指定したバージョンで入ってますね

コンテナにアプリケーションをぶちこむ

最初は「ファイル転送でもすれば良いんかなぁ」とかかなり脳筋なこと考えていたんですけど、考えてみたらgithubからcloneしてきて起動すれば良いんじゃね?と思ったのでお試し。

root@8bf7a6704bb9:/# git --version
git version 2.20.1

git入ってる!勝った

# 適当なディレクトリへ移動して
root@8bf7a6704bb9:/# cd /usr/src/

# cloneして
root@8bf7a6704bb9:/usr/src# git clone https://github.com/wol-827/first-step.git
Cloning into 'first-step'...
remote: Enumerating objects: 22, done.
remote: Total 22 (delta 0), reused 0 (delta 0), pack-reused 22
Unpacking objects: 100% (22/22), done.

root@8bf7a6704bb9:/usr/src# cd first-step/

# アプリ起動に必要なモジュールインストールして
root@8bf7a6704bb9:/usr/src/first-step# npm i
found 0 vulnerabilities

# アプリケーション起動
root@8bf7a6704bb9:/usr/src/first-step# npm start
Compiled successfully!

You can now view sample-app in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://172.17.0.2:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

起動できてそう!良い感じ

コンテナ起動時にポートフォワード設定し忘れてしまったので、改めて設定。
アプリケーションは3000番で起動していて、8080へ飛ばしたいのでこんな感じ。

$ docker run -itd -p 8080:3000  --name node-react node:12.16.0-buster
  • 起動前
    image.png

  • 起動後
    image.png

できた。ウレシイ。

まとめ

Dockerについて調べていると、環境を汚すことなくアプリケーションの実行環境を用意できる、なんでことが書かれているのをよく見るんですが身を持ってそれを実感しました。
ホスト側にDockerイメージだけ用意してあれば、nodeやらReact用のモジュールやらはコンテナの中にだけ入れるのでホスト側はキレイな体を保てると。専用のコンテナを立てるのでnodeのバージョンの切り替えも不要。
イメージはこれで、githubのリポジトリはこれ、コマンド実行手順はこれ、っていう風に準備すれば、とりあえず環境は手に入る。便利便利。

とはいえ今回の場合、コンテナの中に入ってcloneしたり色々コマンド実行しているのが冗長。
次はこの辺りをコマンド1発で出来るようにしたいと思います。たぶん出来る。出来ろ。

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

ReactとAmplifyでCognitoとgoogleサインイン/サインナップ試してみた

一通り設定内容を把握したかったので、マニュアルで設定してみた記録です。先に感想ですが、サードパーティーのログイン機構が簡単に実装でき、だいぶ強力なサービスでした。

参考にしたページは、以下になります。
https://aws-amplify.github.io/docs/js/authentication

CognitoとGoogle APIsの設定

Mange User Pools

ユーザープールにアクセスします。
ユーザープールにアクセス

Create a user poolをクリックします。
Create a user poolをクリック

Pool nameを指定します。Pool nameは再設定ができない様なので、名前を決める時はよく考えて決めたほうが良いです。
Pool nameを指定

Review defaultsを選択します。
Review defaultsを指定

Review画面
Review画面

Attributesを設定します。一度設定すると再設定できないようなので、扱う情報をどうするか事前に考える必要があります。
Attributesを設定

今回は、Emailアドレスとユーザー名でサインインできるようにします。Standard attributesは、ログインに使用するものだけ指定します。必要に応じて変更するとよいと思いますが、最初に決める必があります。Custom attribe(後で新たに追加)もできるようなので、この辺りは慎重に考えて決めたほうが良いでしょう。
Attributesを設定

パスワードの強さの設定やPoliciesで、管理者のみ追加権限指定で、任意のユーザー登録を無効にできます。
Attributesを設定

後、MFAの設定などありますが、必要に応じて設定ができるのでスルーします。

最後に、App Clientsの設定をします。
App Clientsの設定

Webのみで認証するので、以下の設定にします。
App Clientsの設定1

App Clientsが作成されると、App Client Idが発行されます。このIdは、後ほど使います。
App Clientsの設定2

作成完了します。ここで生成されたPool Idは、後ほど使います。
Cognito User Pool 設定完了

Manage Federated Identities

Federated Identitiesをクリックします。

Manage Federated Identities

Pool nameを指定して、Authentication providersを開き、赤く囲ったcognitoの設定項目に、先程作成した、User Pool IdとApp Client Idを指定します。そして、作成を押して進みます。
Identity poolの作成

最後に、Identity Pool Idが表示されるます。これも後ほど使いますので、記録してください。
Identity poolの作成

Identity Pool Idは、Edit Identity Poolから確認できます。
Identity poolの作成

Google APIsの設定

User PoolのIdのプロバイダーにGoogleも使うので、その設定をします。

Google Cloud Platformにアクセスして、

メニュー > APIs & Services > Credentials

にアクセスします。
GCP APIs

Create Credentialsを押します。
GCP APIs

OAuth client IDを選択します。
GCP APIs OAuth client ID

Web applicationを選択し、Restrictionsを以下の様に設定します。
GCP APIs OAuth client ID

Authorized JavaScript origins.

https://<your-user-pool-domain>

Authorized Redirect URIs.

https://<your-user-pool-domain>/oauth2/idpresponse

詳細はこちらにあります。
https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-social-idp.html

Your Client IDをプロバイダーGoogleの設定項目に設定します。
GCP APIs OAuth client ID

Federated IdentitiesUser Pools > Edit identity poolへ移動します。UnlockしてYour Client IDを入力して保存します。
GCP APIs OAuth client ID Google Provider

後から確認もできます。
OAuth 2.0 Client IDs
GCP APIs OAuth ID & Secret

赤丸囲いクリックして詳細を確認できます。
GCP APIs OAuth ID & Secret

Reactで使う情報まとめ

UserPoolId
User Pool作成時に生成されたIDで、フォーマットは見た目は、

ap-northeast-1_*******な感じ

IdentityPoolId
Identity Pool作成時に生成されたIDで、フォーマットは見た目は、

ap-northeast-1:********-****-****-****-************な感じ

  • ClientId: App Client作成時に生成されたIDで、フォーマットは見た目は、

7nq7sou3kac74bkfuoee92nes33な感じ

  • Google Client Id: Google APIsで作成されたIDで、フォーマットの見た目は、

********-************************.apps.googleusercontent.comな感じ

Reactでサインイン/サインナップ

Reactの準備

今回はMacOSを使います。コンソールを開き、開発用のディレクトリを作成しそこに移動します。

npmの準備

$ mkdir workingdir
$ cd workingdir/
$ brew install npm
$ npm install -g n
$ n stable  # ローカルのnode_modulesを参照する
$ npm install -g yarn

バージョン確認

$ npm -v
6.13.6
$ node -v
v12.15.0

Reactをインストール

$ npm install react

Reactアプリを作成

$ npx create-react-app my-app --template typescript
$ tree -I node_modules -L 2 my-app/
my-app/  # 以下のようなディレクトリが作成されます(node_modulesは省いてます)
├── README.md
├── package-lock.json
├── package.json
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── logo192.png
│   ├── logo512.png
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.test.tsx
│   ├── App.tsx
│   ├── index.css
│   ├── index.tsx
│   ├── logo.svg
│   ├── react-app-env.d.ts
│   ├── serviceWorker.ts
│   └── setupTests.ts
└── tsconfig.json
$ cd my-app
$ yarn add amazon-cognito-identity-js  # cognitoを使うためのモジュールを入れておく
$ npm start
Compiled successfully!

You can now view app in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://192.168.***.***:3000/

Note that the development build is not optimized.
To create a production build, use npm run build.

となれば、OKです。ブラウザーが起動しreact-appが表示されます。
react-app

設定ファイルの準備

$ cd my-app/src
$ touch appConfig.tsx
$ vi appConfig.tsx

以下の内容を記述します。

appConfig.tsx
const appConfig = {
    aws: {
        cognito: {
            region: 'ap-northeast-1',
            IdentityPoolId: 'ap-northeast-1:********-****-****-****-************',
            UserPoolId: 'ap-northeast-1_*******',
            ClientId: '***************************'
            GoogleClientID: '*****-**********.apps.googleusercontent.com'
        }
    }
}

export default appConfig

Amplifyで認証機構を準備

$ yarn add aws-amplify aws-amplify-react
App.tsx
import React, { Component } from 'react';

import logo from './logo.svg';
import Amplify from 'aws-amplify';
import { Authenticator } from 'aws-amplify-react';

import appConfig from './appConfig'

Amplify.configure({
    Auth: {
        identityPoolId: appConfig.aws.cognito.IdentityPoolId,
        region: appConfig.aws.cognito.region,
        userPoolId: appConfig.aws.cognito.UserPoolId,
        userPoolWebClientId: appConfig.aws.cognito.ClientId
    }
});

const federatedConfig = {
    google_client_id: appConfig.aws.cognito.GoogleClientID
};

const App = () => {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

class AppWithAuth extends Component {
  render(){
    return (
      <div>
          <Authenticator
              federated={federatedConfig}
          >
              <App />
          </Authenticator>
      </div>
    );
  }
}

export default AppWithAuth;

再度、起動します。

$ npm start

以下の様なページが保存されます。cognitoでgmailアドレスでアカウントを作って、

amplify coginito & google login

感想

大体動作が把握できたので、次はHosted UI使いたいので amplify init 使ってコンフィグつくって実装しようと思いました。

Googleのみで利用できるようにして、lambda triggerのpre-signupやauthentcation機能を使ってSignupできるユーザーを事前にUserPool側に登録したユーザーの使えるようにして、細かい制御をする予定です。

自由を手に入れた感じです。

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

ReactNavigation v.5 とReduxでタブのバッジ数を管理する

この記事でやること

Reduxで管理する通知バッジ数をReactNativeのボトムタブに表示させる記事です。
通知バッジ数はスクリーンをまたぐ変数なので、Reduxで管理するのが良いかと思います。

こんな感じのやつです ↓↓ (この記事ではこれの簡易版を作ります)
S__31916034.jpg

ReactNavigation v.4では、ボトムタブのレンダリングのタイミングに癖があった(?)ようです。
Reduxで状態を更新してもボトムタブに即時反映はされませんでした(僕の周りでも何人か言ってましたが、間違っていたら教えてください!)。
僕は無理やりテキトーな変数を入れたNavigationActionsをボトムタブにdispatchすることで、無理やり再レンダリングさせて、即時反映させていました(本当はこの記事はそれを書く予定だった)。

しかしなんと、v.5ではそんな必要がなくなってました…!
ありがてぇ…!

やりたいこと

画面から通知バッジ数を変更してボトムタブの数字に即時反映

主な環境

  • Expo 36.0.0
  • react 16.9.0
  • @react-navigation/bottom-tabs 5.0.5
  • @react-navigation/native 5.0.5
  • @react-navigation/stack 5.0.5
  • react-redux 7.1.3
  • redux 4.0.5

ReactNavigationはモジュールの移動が激しいですね。
公式ドキュメントを読んで、必要なライブラリをインストールしていってください。

画面構成

ホームスクリーンをStackにして、それをひとつのタブに対応させる単純な画面構成です。

コード

Redux

初期状態とreducerの定義をします

src/reducers/index.js
const INITIAL_STATE = {
    badgeNumber:0,
}

const reducer = (state=INITIAL_STATE, action) => {
    switch (action.type){
        case "SET_BADGE_NUMBER":
            return {...state, badgeNumber:action.badgeNumber}
        default:
            return state;
    }
}

export default reducer

actionの定義をします

src/actions/index.js
export const setBadgeNumber = badgeNumber => ({
    type:"SET_BADGE_NUMBER",
    badgeNumber
})

storeを作ります

src/store.js
import { createStore } from 'redux'
import reducers from './reducers'

export default createStore(reducers)

スクリーン

HomeScreenを作り、Reduxと繋げます。badgeNumber+1というテキストをタッチするとバッジ数がインクリメントされる仕様に。

src/screens/Home.js
import React from 'react';
import { Text, View, TouchableOpacity } from 'react-native';
import { setBadgeNumber } from '../../src/actions'
import { connect } from 'react-redux'

const HomeScreen =({ badgeNumber,setBadgeNumber})=>{
  return(
    <View>
      <TouchableOpacity onPress={()=>setBadgeNumber(badgeNumber+1)}>
        <Text>badgeNumber + 1</Text>
      </TouchableOpacity>
    </View>
  )
}
const mapStateToProps = state => ({
  badgeNumber: state.badgeNumber
})

const mapDispatchToProps = {
  setBadgeNumber
}

 const Home = connect(
    mapStateToProps,
    mapDispatchToProps
  )(HomeScreen)

  export default Home

ナビゲーション

ReactNavigationでナビゲーションを作ります。
StackとTabでHomeScreenをラップしていきます。

App.js
import React from 'react';
import { Text } from 'react-native';
import { createStackNavigator } from '@react-navigation/stack';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Provider, connect } from 'react-redux'
import { setBadgeNumber } from './src/actions'
import store from './src/store'
import Home from './src/screens/Home'

// ここでStackをつくります()今回は1画面だけだけど
const Stack = createStackNavigator();

const HomeStack=()=>{
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={Home} />
    </Stack.Navigator>
  );
}


// ここでBottomTabを作ります(今回は上で作ったStack1つだけ)
const BottomTab = createBottomTabNavigator();

const MyBottomTab=()=>{
  return (
    <BottomTab.Navigator>
      <BottomTab.Screen 
        name="Home" component={HomeStack}
        options={{
          tabBarLabel: 'Home',
          tabBarIcon: () => (
            <Text>{store.getState().badgeNumber}</Text>
          ),
        }} />
    </BottomTab.Navigator>
  );
}


const Main=()=>{
  return(
    <NavigationContainer>
      <MyBottomTab>
      </MyBottomTab>
    </NavigationContainer>
  )
}

const mapStateToProps = state => ({
  badgeNumber: state.badgeNumber
})

const mapDispatchToProps = {
  setBadgeNumber
}

const ConnectedMain = connect(
  mapStateToProps,
  mapDispatchToProps
)(Main)

const App=()=>{
    return (
        <Provider store={store}>
            <ConnectedMain />
        </Provider>
    )
}
export default App

これで、HomeScreenのbadgeNumber+1をタッチすれば、ボトムタブの数字も更新されていくと思います。

まとめ

基本に忠実なReduxの使い方、構成です。
上記のコードだけでタブに即時反映してくれるようになってとてもありがたいですね。
ReactNavigation v.5はv.4に比べて見通し良くなったと思います(前はcreateなんとかnavigatorみたいなのが何をやっているかわかりづらかった)。

今回は、HomeScreenから通知バッジ数を変更する仕様でしたが、プッシュ通知が来たらバッジ数をインクリメントすることも考えられるかなと。
僕も、プロジェクトにおいてWebsocket対応の優先順位はまだ低いと感じたときには、プッシュ通知が来たのをトリガーにバッジ数をインクリメントしています。
その場合、Focusされているのがどの画面か問わず、storeの状態を書き換えなければいけませんが、上記で言うMainコンポーネントの中でNotifications.addListenerを使ってハンドリングすると上手くいきます。

参考になれば幸いです!

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

ReactとVueとAngularの戦い。

日本ではReactとVueが競い合っているように思えますが、実際はどうなのかを調べてみました。

npm trends

npm trendsという場所がありまして、そこではnpmパッケージのダウンロード数をグラフに表示してくれます。

使い方は簡単、パッケージ名を一つづつ入力していくだけです。

調査結果

react vs @angular/core vs vue vs jquery vs typescript | npm trends

reactとvueと@angular/coreとjqueryとtypescriptの図
npmtrends.PNG

Reactの圧勝ですね。Vueは日本だと記事を色々見かけて人気なのはなぜなのでしょうか?

The State of JavaScript 2019

ついでにこちらでも調べてみます。
The State of JavaScript 2019でフレームワークを見てみます。

Front End Frameworksカテゴリ

React
https://2019.stateofjs.com/front-end-frameworks/
Vue
https://2019.stateofjs.com/front-end-frameworks/
Angular
https://2019.stateofjs.com/front-end-frameworks/angular/

濃いピンクが、以前使用したことがあり、また利用したい。
薄いピンクが、以前使用したことがあり、もう使わない。
濃い緑が、聞いたことがあり学びたいと思っている。
薄い緑が、聞いたことはあるが興味はない。

満足度(=濃いピンク/(濃いピンク+薄いピンク))はReactとVueは同じくらいのようです。

SvelteがVueを抜き第二位に入っていますね。
The State of JavaScript 2019: Front End Frameworks

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