【5分で動く】Reactで作るイケてるWEBアプリケーション

![react.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272803/e7cdcdfa-0661-0ab2-79eb-0c5603396671.gif)

# 背景
社内でReactを使ったWEBアプリケーションを開発するにあたって、UIフレームワークの導入から認証までやってくれるようなサンプルが見当たらなかったので、今回作成したものをテンプレっぽくして共有します。
これさえあれば、とりあえず簡単なWEBアプリは作れるのでぜひ活用していただければと思います。

# 概要
https://github.com/tonio0720/React-App

## 起動方法
```{bash}
git clone https://github.com/tonio0720/React-App

cd React-App

# react 起動
cd frontend
npm i
npm start

# express 起動
cd backend
npm i
npm start
````

## 今回利用したもの
* React (Frontend Framework)
* Create React App
* Ant Design (UI Framework)
* Axios (HTTP client)
* echarts (Chart Library)
* Express (Backend Framework)

# 解説
Reactアプリ自体は`create react app`を使って作成しました。
ただ色々と拡張する必要があったので、`react-app-rewired`と`customize-cra`を使いました。
UIフレームワークには`Ant Design`を使用しています。

バックエンドは認証処理をしたかったのでおまけ程度に書いています。
`express-generator`を使って作りました。
認証には`express-jwt`を使用しています。

空の画面だけでさみしかったので、echartsでダッシュボードっぽくしてみました。
データは僕のQiitaのダッシュボードから持ってきました。（APIではなく直書きです。）
グラフには`echarts`を利用しています。

↓で詳細について説明してみます。

## Ant Designの導入
`Ant Design`はLESSで作られているので、Webpackで読み込める状態にする必要があります。
`webpack`のバージョン次第では、`localIdentName`が云々とエラーが出てしまうので、そちらの対応もしています。

```{javascript:config-overrides.js}
const path = require('path');
const {
    override,
    disableEsLint,
    fixBabelImports,
    addLessLoader,
    addWebpackAlias,
} = require('customize-cra');
const theme = require('./src/theme');

const modifyVars = {};
Object.keys(theme).forEach((key) => {
    modifyVars[`@${key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}`] = theme[key];
});

const config = {
    webpack: override(
        disableEsLint(),
        addWebpackAlias({
            '@': path.resolve(__dirname, 'src')
        }),
        fixBabelImports('import', {
            libraryName: 'antd',
            libraryDirectory: 'es',
            style: true,
        }),
        // ★ここから
        addLessLoader({
            javascriptEnabled: true,
            modifyVars
        }),
        ((config) => {
            config.module.rules.forEach((rule) => {
                if (!rule.oneOf) {
                    return;
                }
                rule.oneOf.forEach((rule) => {
                    if (!rule.use) {
                        return;
                    }
                    rule.use.forEach((loader) => {
                        if (loader.options && loader.options.localIdentName) {
                            const { localIdentName } = loader.options;
                            delete loader.options.localIdentName;
                            loader.options.modules = { localIdentName };
                        }
                    });
                });
            });

            return config;
        }),
        // ★ここまで
    ),
    devServer: (configFunction) => {
        return (proxy, allowedHost) => {
            const config = configFunction(proxy, allowedHost);
            config.proxy = {
                '/api': {
                    target: 'http://localhost:3030',
                    pathRewrite: { '^/api': '' }
                }
            };
            return config;
        };
    },
};

module.exports = config;

```

## JWT認証
Reactの認証処理は`ContextProvider`を使って実装しています。
`ContextProvider`は下の階層にプロパティを引き渡すことができます。

ページ遷移の度に、バックエンドの`/user/info`というところにリクエストを送り、検証をします。
成功の場合、`userid`とトークンを保存します。
失敗の場合、ログインページに戻します。

```{jsx:./src/contexts/Auth.js}
import React, { useEffect, useState } from 'react';
import useReactRouter from 'use-react-router';

import { getToken, setToken, removeToken, gotoLogin } from '@/utils/auth';
import request from '@/utils/request';

export const AuthContext = React.createContext({});

export const AuthProvider = ({
    children
}) => {
    const { location: { pathname } } = useReactRouter();
    const [isLoggedIn, setIsLoggedIn] = useState(false);
    const [userid, setUserid] = useState(null);

    const checkAuth = () => {
        return request.post('/user/info', {});
    };

    useEffect(() => {
        setIsLoggedIn(false);

        if (pathname === '/login') {
            return;
        }

        const token = getToken();

        if (token) {
            checkAuth().then(({
                token,
                userid
            }) => {
                setToken(token);
                setIsLoggedIn(true);
                setUserid(userid);
            }).catch(() => {
                removeToken();
                gotoLogin();
            });
        } else {
            gotoLogin();
        }
    }, [pathname]);

    return (
        <AuthContext.Provider
            value={{
                isLoggedIn,
                userid
            }}
        >
            {(isLoggedIn || pathname === '/login') && children}
        </AuthContext.Provider>
    );
};

```

ログインページからは`/user/login`にリクエストを送り、usernameとpasswordを検証します。
成功した場合は、tokenが返ってくるのでCookieに保存します。

```{jsx:./src/pages/Login/LoginForm.js}
import React, { useState } from 'react';
import useReactRouter from 'use-react-router';
import {
    Form,
    Icon,
    Input,
    Button,
    Checkbox,
    Alert
} from 'antd';

import { setToken } from '@/utils/auth';
import request from '@/utils/request';

import styles from './index.module.less';

const LoginForm = ({
    form
}) => {
    const { history } = useReactRouter();
    const [error, setError] = useState(false);

    const handleSubmit = (e) => {
        e.preventDefault();
        form.validateFields((err, values) => {
            setError(false);
            if (!err) {
                request.post('/user/login', values).then(({ token }) => {
                    setToken(token);
                    history.push('/');
                }).catch(() => {
                    setError(true);
                });
            }
        });
    };

    const { getFieldDecorator } = form;
    return (
        <Form onSubmit={handleSubmit}>
            {error && (
                <Alert
                    description="Password Incorrect."
                    type="error"
                    showIcon
                    style={{ marginBottom: 16 }}
                />
            )}
            <span>username: admin, password: admin</span>
            <Form.Item>
                {getFieldDecorator('username', {
                    rules: [{ required: true, message: 'Please input your username!' }],
                })(
                    <Input
                        autocomplete="off"
                        prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        placeholder="Username"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                {getFieldDecorator('password', {
                    rules: [{ required: true, message: 'Please input your Password!' }],
                })(
                    <Input
                        prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
                        type="password"
                        placeholder="Password"
                    />,
                )}
            </Form.Item>
            <Form.Item>
                {getFieldDecorator('remember', {
                    valuePropName: 'checked',
                    initialValue: true,
                })(<Checkbox>Remember me</Checkbox>)}
                <Button
                    type="primary"
                    htmlType="submit"
                    className={styles.loginFormButton}
                >
                    Log in
                </Button>
            </Form.Item>
        </Form>
    );
};

export default Form.create({ name: 'login' })(LoginForm);

```

# 終わりに
Reactは便利ですが、部分的なサンプルが多くまとまったものが少ないので不便に感じている人も多いのではないでしょうか。
Reactを始めるきっかけにしてもらえればうれしいです！