20201122のReactに関する記事は6件です。

【ReactNative・React Navigation】JWTを用いた認証と画面の出しわけの基本

認証で管理するStateの例

authSlice.ts
import { createSlice } from '@reduxjs/toolkit';

const authinInitialState = {
  isLoanching: true,
  isLoading: false,
  error: null,
  token: null,
};

export const authSlice = createSlice({
  name: 'login',
  initialState: authinInitialState,
  reducers: {
    loginStart: (state) => {
      state.isLoading = true;
    },
    loginSuccess: (state, { payload }) => {
      state.isLoanching = false;
      state.isLoading = false;
      state.error = null;
      state.token = payload;
    },
    loginFailure: (state, { payload }) => {
      state.isLoanching = false;
      state.isLoading = false;
      state.error = payload;
      state.token = null;
    },
    logout: (state) => {
      state.isLoading = false;
      state.error = null;
      state.token = null;
    },
  },
});

export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;

JWTを端末に保存するためのAsyncStrage

https://react-native-async-storage.github.io/async-storage/docs/install

yarn add @react-native-async-storage/async-storage

JWTの保存、読み込み、削除など一通り用意しておき、認証時の処理に差し込む。

services/deviceStrage.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

const JWT_KEY = 'jwt_key';

export const deviceStorage = {
  async saveItem(key: string, value: string) {
    try {
      await AsyncStorage.setItem(key, value);
    } catch (error) {
      console.log('AsyncStorage Error: ' + error.message);
    }
  },

  async saveJWT(token: string) {
    try {
      await AsyncStorage.setItem(JWT_KEY, token);
    } catch (error) {
      console.log('AsyncStorage Error: ' + error.message);
    }
  },

  async loadJWT() {
    try {
      const value = await AsyncStorage.getItem(JWT_KEY);
      return value;
    } catch (error) {
      console.log('AsyncStorage Error: ' + error.message);
      return error;
    }
  },

  async deleteJWT() {
    try {
      await AsyncStorage.removeItem(JWT_KEY);
    } catch (error) {
      console.log('AsyncStorage Error: ' + error.message);
    }
  },
};

スプラッシュ画面がすぐに終了しないようにする

ライブラリを使用する
https://github.com/crazycodeboy/react-native-splash-screen

スプラッシュ画面は、JWTの読み込みが完了したタイミングで明示的に終了させる。

yarn add react-native-splash-screen

Android

ReactNative(0.63)以降は、自動でライブラリとリンクするので、特に設定は必要ない。
以下、onCreate(アプリ起動時の処理)のみ追加する。

これによって、明示的に指示しない限りスプラッシュスクリーンが終了しなくなる。

package com.myapp;

import com.facebook.react.ReactActivity;

追加
++ import org.devio.rn.splashscreen.SplashScreen;


public class MainActivity extends ReactActivity {

追加
++  @Override
++  protected void onCreate(Bundle savedInstanceState) {
++    SplashScreen.show(this);
++    super.onCreate(savedInstanceState);
++  }

  @Override
  protected String getMainComponentName() {
    return "myApp";
  }
}

iOS

同じく起動時の処理に追加する。

これによって、明示的に指示しない限りスプラッシュスクリーンが終了しなくなる。

#import "AppDelegate.h"

#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
追加
++ #import "RNSplashScreen.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // ...other code
    [self.window makeKeyAndVisible];
追加
++  [RNSplashScreen show];

    return YES;
}

@end

Splashコンポーネントの作成

Splashコンポーネントといっても、画面には何も表示する必要はなく、
JWTの読み込みや、アプリ起動時に必要な処理を行う。

また、処理が終了した際にスプラッシュを非表示にする。

SplashScreen.tsx
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import SplashScreen from 'react-native-splash-screen';
import { deviceStorage } from 'src/services/deviceStrage';

export default function () {
  const dispatch = useDispatch();

  useEffect(() => {
    deviceStorage
      .loadJWT()
      .then((jwt) => {
        dispatch(loginSuccess(jwt)); // isLoanchingをfalseにする
      })
      .catch((error) => {
        dispatch(loginFailure(error)); // isLoanchingをfalseにする
      })
      .finally(() => {
        SplashScreen.hide(); // 
      });
  }, [dispatch]);

  return null;
}

React Navigation の設定

https://reactnavigation.org/docs/auth-flow/

アプリ起動時、未ログイン(JWTなし)、ログイン後(JWTあり)の画面を出し分ける。

例はReduxでTokenを管理している場合。

navigation.tsx
export const RootNavigator = () => {
  const { isLoanching, token } = useSelector((state: RootState) => state.auth);

  if (isLoanching) return <SplashScreen />;

  return (
    <NavigationContainer>
      <RootStack.Navigator headerMode="none">
        {!token ? (
          <>
            <RootStack.Screen name="SignIn" component={SignInScreen} />
            <RootStack.Screen name="SignUp" component={SignUpScreen} />
          </>
        ) : (
          <>
            <RootStack.Screen name="AppBottomTab" component={AppBottomTabNavigator} />
            <RootStack.Screen name="ChatRooms" component={ChatRoomScreen} />
          </>
        )}
      </RootStack.Navigator>
    </NavigationContainer>
  );
};
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テストツールjestのインストール〜実行の流れ

jestインストール〜テスト実行まで

参照先:https://github.com/facebook/jest

①jestをインストール

npm install --save-dev jest

②基本設定ファイルの作成

1.node_modules/.binに対してinitで作成。

./node_modules/.bin/jest --init

2.基本的にEnter連打でok

The following questions will help Jest to create a suitable configuration for your project

✔ Would you like to use Jest when running "test" script in "package.json"? … yes
✔ Would you like to use Typescript for the configuration file? … no
✔ Choose the test environment that will be used for testing › node
✔ Do you want Jest to add coverage reports? … no
✔ Which provider should be used to instrument code for coverage? › v8
✔ Automatically clear mock calls and instances between every test? … no

✏️  Modified /Users/masahiro/Documents/react-router-test/project/package.json

📝  Configuration file created at /Users/masahiro/Documents/react-router-test/project/jest.config.js

③babelのインストール

1.import,exportをテスト環境で使用する場合、babelのインストールが必要。
下記コマンドでインストールする

npm install --save-dev babel-jest @babel/core @babel/preset-env

2.{}package.jsonの"devDependencies"に下記が追加されていればok

 "devDependencies": {
    "@babel/core": "^7.12.7",
    "@babel/preset-env": "^7.12.7",
    "babel-jest": "^26.6.3",
    "jest": "^26.6.3"
  }
}

④基本ファイルの作成

1.babel.config.jsファイルを作成し、下記のコードを追加

module.exports = {
  presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};

⑤testフォルダの作成

1.__test__フォルダの作成し、適当なコードを入力

import QuizFetcher from '../../../src/data_fetchers/QuizFetcher';

describe('QuizFetcherのテスト', () => {
  it('クラスチェック', () => {
    console.log('@@@@@@');
  });
});

⑥テスト実行

1.npm testを実行

npm test

2.下記の表示が出ればok

 PASS  __tests__/src/data_fetchers/QuizFetcher.js
  QuizFetcherのテスト
    ✓ クラスチェック (11 ms)

  console.log
    @@@@@@

      at Object.<anonymous> (__tests__/src/data_fetchers/QuizFetcher.js:5:13)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.775 s, estimated 1 s
Ran all test suites.

以上になります。

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

AWS Amplifyで環境変数を設定し、Reactアプリで読み込む

GitHubなどからWebアプリのCI/CDができるAWS Amplifyですが、
今回は環境変数の設定方法と、AmplifyでホストしているReactアプリから読み込む方法を備忘録として残しておきます。
(あと以外と日本語のドキュメントがなかった)

ReactなどのWebアプリでは.envファイルなどに環境変数を定義しておいて、
process.env.{任意の名前}で読み込むことがあると思いますが、
認証情報などを記載している場合はソース管理には載せられませんし、Amplifyで環境変数を設定して読み込む必要があります。

ちなみにAmplifyではバックエンドの構築も可能ですが、環境変数の利用はフロントエンドのみのアプリでもできます。
ちょっとしたフロントエンドだけのアプリ作って環境変数の読み込みだけしたい場合、Amplify CLIを使ったバックエンドの構築などはせずに環境変数の設定、利用が可能です。

環境変数の設定

環境変数の登録

Amplifyで環境変数を定義するにはまずAWS ConsoleでAmplifyのページに飛び、
サイドメニューから「環境変数」を選択します。
tempsnip.png

環境変数のメニューが開くので「変数の管理」から変数の登録・編集ができます。
今回はReactアプリで読み込むのでREACT_APP_を変数名にプレフィックスとしてつけておきます。
また、複数ブランチをホストしている場合には、ブランチごとに変数を設定することが可能です。
1.png

ビルドの設定

続いてメニューから「ビルドの設定」を選択し、以下の画面を開きます。
2.png
ここではデプロイ時のビルドの設定をyaml形式で定義することができるので、ここで環境変数のエクスポートをします。
buildのcommandsに↓こんな感じで書いておきます。
出力するパスはアプリに応じて変更します。今回はルートに書きだしています。

echo "REACT_APP_HOGE=$REACT_APP_HOGE" >> .env

アプリからの読み込み

あとはアプリから

process.env.REACT_APP_HOGE

で読み込めば完了です。

参考

Environment variables - AWS Amplify

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

Next.jsでログインフラグによるリダイレクト機能を実装してみた

前書き

Next.jsで自前のログイン機能を作成しようとして情報集めに苦労したのでその備忘録。

前提として、認証処理一式を実装できるライブラリは色々あります。但しSNSとの連携を前提としたモノしかなかった。(僕調べ)

  • 個人学習向けの簡単なログイン機能を実装したい
  • 自前の画面遷移時のリダイレクト判定処理を実装したい

みたいな方々には、もし良ければこんな実装もあるよっていう参考にしていただければ幸いです。

使用技術

  • Next.js(メイン)
    • axios:お馴染み、API叩く用に使用
    • nookies:Cookie制御用の外部ライブラリ
  • バックエンドAPI(なんらかのログイン成功フラグが返せればなんでも良い、自分はSpring Bootを使いました)

機能要件

大要件

  • ログイン済みか否かで遷移させる画面を制御する

小要件

  • ログイン済みフラグの取得のために、ログイン画面からIDとパスワードを入力してログイン用バックエンドAPIを叩く(固定値設定して等価判定しているだけの簡単なモノ)
  • ログイン済みフラグをクライアント側で保持(Cookieへ)
  • Cookieにログイン済みフラグがなければ、遷移時にログイン画面へリダイレクトさせる

実装

ポイント

  • _app.tsx(Jsなら_app.jsx) で画面制御処理を実装→全画面のコンポーネントで呼ばれるため

コード

  • ログイン画面コンポーネント
import Head from 'next/head';
import axios from 'axios';
import { useState } from 'react';
import { Container, Button, Form, Image } from 'react-bootstrap';
import { setCookie } from 'nookies';
import { useRouter } from 'next/router';

interface ILogin {
  userName: string;
  password: string;
}

const initialPayload: ILogin = {
  userName: '',
  password: '',
};

const Login = () => {
  const router = useRouter();

  const [payload, setPayload] = useState<ILogin>(initialPayload);

  const handleChange = (e) => {
    setPayload({
      ...payload,
      [e.target.name]: e.target.value,
    });
  };

  const onClickLogin = () => {
    axios
      .post('/api/login', payload)
      .then((res) => {
        // ログインフラグをクッキーへ、「auth」というキーで登録
        setCookie(null, 'auth', 'true', {
          maxAge: 30 * 24 * 60 * 60, // お好きな期限を
          path: '/',
        });
        router.push('/');
      })
      .catch((e) => {
        console.log('認証エラー');
      });
  };
  return (
    <Container>
      <Head>
        <title>ログイン画面例</title>
      </Head>
      <div className='login-container'>
        <Image
          src='https://placehold.jp/150x150.png'
          roundedCircle
          style={{ marginBottom: '20px' }}
        />
        <Form.Control
          type='text'
          placeholder='User Name'
          name='userName'
          value={payload.userName}
          onChange={handleChange}></Form.Control>
        <Form.Control
          type='password'
          placeholder='Password'
          name='password'
          value={payload.password}
          onChange={handleChange}></Form.Control>
        <Button variant='info' type='button' onClick={onClickLogin}>
          Login
        </Button>
      </div>
    </Container>
  );
};

export default Login;

※イメージこんな画面(若干自前CSS足しているので上記コピーだけでは再現されません)

  • _app.tsx(メイン)
import { NextPageContext } from 'next';
import { AppProps } from 'next/app';
import { parseCookies } from 'nookies';
import { useEffect } from 'react';
import { useRouter } from 'next/router';

const MyApp = ({ Component, pageProps }: AppProps, ctx: NextPageContext) => {
  const router = useRouter();
  const cookies = parseCookies(ctx);

  // 第二引数に空配列を指定してマウント・アンマウント毎(CSRでの各画面遷移時)に呼ばれるようにする
  useEffect(() => {
    // CSR用認証チェック

    router.beforePopState(({ url, as, options }) => {
      // ログイン画面とエラー画面遷移時のみ認証チェックを行わない
      if (url !== '/login' && url !== '/_error') {
        if (typeof cookies.auth === 'undefined') {
          // CSR用リダイレクト処理
          window.location.href = '/login';
          return false;
        }
      }
      return true;
    });
  }, []);

  const component =
    typeof pageProps === 'undefined' ? null : <Component {...pageProps} />;

  return component;
};

MyApp.getInitialProps = async (appContext: any) => {
  // SSR用認証チェック

  const cookies = parseCookies(appContext.ctx);
  // ログイン画面とエラー画面遷移時のみ認証チェックを行わない
  if (
    appContext.ctx.pathname !== '/login' &&
    appContext.ctx.pathname !== '/_error'
  ) {
    if (typeof cookies.auth === 'undefined') {
     // SSR or CSRを判定
      const isServer = typeof window === 'undefined';
      if (isServer) {
        console.log('in ServerSide');
        appContext.ctx.res.statusCode = 302;
        appContext.ctx.res.setHeader('Location', '/login');
        return {};
      } else {
        console.log('in ClientSide');
      }
    }
  }
  return {
    pageProps: {
      ...(appContext.Component.getInitialProps
        ? await appContext.Component.getInitialProps(appContext.ctx)
        : {}),
      pathname: appContext.ctx.pathname,
    },
  };
};

export default MyApp;

後書き

Next(Nuxtも)はSSRとCSRそれぞれを考慮する必要があるからめんどい、、けど良い勉強になった。
Nextはほぼ英語記事しかないので日本語の参考記事も増えたらいいなあ(やっぱり日本ではNuxt・・・?)

参考

※リダイレクト周りはほぼ英語記事参考にしましたが散り散りで集められませんでしたすみません・・。

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

Next.jsまとめ

Next.jsを使ってブログ作ったが色々とだめすぎたので調べたことをまとめて作るために書く記事。
間違ってたりするかもだけど忘備録だからあんまり怒らないで。。。
てことで始めます。

Node.jsのインストール

Next.jsはNode.jsのReactのフレームワークみたいなやつなので色々入れます。
まずはNode.js
基本的にNode.jsの公式サイトからダウンロードすれば問題ない。
これは普通にexeとかからインストールしよう。
Node.js
バカでもこれくらいはできるはず

作業するディレクトリを作る

ディレクトリ内にNext.jsとReactをインストールしていくのでディレクトリを作成しましょう。
デスクトップとかにドメイン名のディレクトリとか作ったらいいんじゃないのかな
そんでcd 作ったディレクトリ名みたいなのをコマンドプロンプトとかで実行してディレクトリ移動してね

node_modulesを設定する

npm init -y

これを実行したらpackage.jsonってファイルができると思います。
それが設定ファイルみたいなのになります

ReactとNext.jsのインストール

npm install --save react react-dom next

これを実行したらReactとNext.jsがインストールされます。
簡単ですね^^

package.jsonに設定を記述する

設定を記述するとかなんかむずそうなこと言ってますが開発用のサーバーを起動する設定するだけです

package.json
{
  "name": "re-nixo-blog",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

package.jsonの中身はこんな感じになっているとおもうのでscriptsの中身を変更します
"test": "echo \"Error: no test specified\" && exit 1"
を削除して

"dev": "next",
"build": "next build",
"export": "next export",
"start": "next start -p 8010"

に変更します

トップページを作成する

next.jsのページはpagesというディレクトリにJavaScriptファイルを作成して作成します。
pagesというディレクトリをpackages.jsonと同じディレクトリに作成します。
そして、pagesの中にindex.jsというJavaScriptファイルを作成します。
これがトップページです。
トップページのコードを書いていきます。

pages/index.js
import Head from "next/head"
import Link from "next/link"

export default function Index() {
    var index = (
        <html>
            <Head>
                <meta charSet="UTF-8"/>
                <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0"/>
                <title>トップページ</title>
            </Head>
            <body>
                <h1>HelloWorld</h1>
            </body>
            <style jsx>{`
                * {
                    margin: 0;
                    padding: 0;
                    box-sizing: border-box;
                }
                h1 {
                    text-align: center;
                }
            `}</style>
        </html>
    )
    return index
}

これをindex.jsにコピペして、

npm run dev

を実行して、localhost:3000にアクセスするとHelloWorldと表示されると思います。

静的ファイルを配置する

今回は画像を読み込ませます。
Next.jsはpublicまたはsrcという名前のディレクトリに静的ファイルを配置します。
どちらでも動きますが、publicのほうがなんかいいね!
ということで静的ファイルを配置します。
package.jsonと同じディレクトリにpublicというディレクトリを作成します。
そこに画像とかを配置してください。
そして、index.jsbodyにhtmlと同じように書きます。
image.pngというファイル名の画像の場合は

<img src="/image.png" width="100%" height="auto"/>

のように記述します。
すると読み込まれます。

サーバーで公開する方法

本当に苦戦しました。
npm run exportでエクスポートしたものを公開したらアクセスはできるものの直URLでアクセスすると404になったり再読込すると404になったりするのでサーバーでnpm run buildをしたあとにnpm run startをし、リバースプロキシを使用し、公開することに成功しました。
えー、ずっと苦戦し、いろんな海外サイトを飛び回っていたら現在朝の6:31分です。
本当に疲れました。
なので忘れないように書いておきます。

まず、サーバーにプロジェクトファイルをアップロードし、サーバーでも

npm install --save react react-dom next

を実行します。
そして、npm run buildをし、screenを開きログアウトしても閉じないようにし、npm run startをします。
すると、localhost:8010でサーバーが開かれます。
8010というポートはpackage.jsonに書いた

"start": "next start -p 8010"

の8010です。ここを変えることでポートを変更することができます。
ポート8010で開いた、という体で進めます。
ポート開放が必要なのかわかりませんが一応しておきます。
ルーター側でもしておいたほうがいいかも

sudo ufw allow 8010
sudo ufw reload

そして、VirtualHostの設定に、

virtual-host.conf
<VirtualHost *:80>
    LoadModule ssl_module modules/mod_ssl.so
    LoadModule proxy_module modules/mod_proxy.so
    ServerName ドメイン名

    ProxyRequests Off
    ProxyPass / http://localhost:8010/
    ProxyPassReverse / http://localhost:8010/
</VirtualHost>

のように記述します。
これだけだとエラーが出ます。(4時間苦戦した)
なのでこの記事の通り色々有効化します。
https://stackoverflow.com/questions/23931987/apache-proxy-no-protocol-handler-was-valid

sudo a2enmod ssl
sudo a2enmod proxy
sudo a2enmod proxy_balancer
sudo a2enmod proxy_http

するとアクセスができるようになり、SSRも有効だと思います。
表示が早いね!

本当に疲れました。
ではまた。。。

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

create-react-app + TypeScript: 初っ端からnpm startでエラーが出たときの対処法

バージョン情報
- TypeScript: 4.1.2
- create-react-app: 4.0.0

エラー内容

Reactアプリをサクッと書きたいときに便利なcreate-react-app
TypeScriptで書きたいなと思ったときには、

npx create-react-app [app name] --template typescript

と書くことで簡単に作成してくれるのですが、create-react-appのバージョンを4.0.0にした途端、npm startをしてみると

path\to\app\node_modules\react-scripts\scripts\utils\verifyTypeScriptSetup.js:239
      appTsConfig.compilerOptions[option] = value;
                                          ^

TypeError: Cannot assign to read only property 'jsx' of object '#<Object>'

のように怒られてしまう問題が発生しました。

解決方法

  1. create-react-appによって作成されたtsconfig.jsonを削除
  2. その状態でnpm startまたはyarn startする。(ここで自動的にtsconfig.jsonが再作成される)
  3. なぜか動くようになる!!!

何が違うのか気になったため2つのtsconfig.jsonを比較しましたが、何も違いませんでした
なぜ動くんだ......

(参考: https://github.com/facebook/create-react-app/issues/10117)

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