LoginSignup
8
8

More than 3 years have passed since last update.

React + TypeScript + vte.cxで簡単なWebアプリを作ってみた①基本的なCRUDアプリ

Posted at

はじめに

この記事はvte.cx(ブイテックス)というBaaSを使って初心者エンジニアがサーバ構築なしで
登録、編集、削除、一覧といったWebアプリケーションの基礎機能を備えたアプリを作ってみた記事です。

今回作ったアプリ

登録した情報が一覧として表示されるアプリを作りました。

会員情報一覧(UserInfo.tsx)

image.png

新規登録画面(Form.tsx)

image.png

情報編集画面(UserInfoEdit.tsx)

image.png

一般的なCRUDアプリケーションです。
ただこちらはvte.cxというBaasを使って、画面のリロードなしにサーバーレスに非同期処理で表示を行うシングルページアプリケーション(SPA)となっています。

シングルページアプリケーション is 何

単一のWebページでコンテンツ切り替えを行うことで、ページ遷移の必要がなく、ブラウザの挙動に縛られないWeb表現を可能にするのです。

引用元サイト

普通のWebページだとサーバーに情報を送った後にサーバーが欲しい情報を返してくれるのですが、その際に一旦全ての情報を書き換えないといけません。
でもこれって書き換える必要のある情報以外の情報も書き換える必要があり、そのせいでページを遷移するごとに時間がかかるので時間のない現代人にとってあまり得策ではありません。
じゃあどうするかというと、Javascriptを使ってAjax(非同期通信)という方法で必要な情報だけ画面の後ろ側で処理してもらいます。

Ajax(非同期通信) is 何

更新の前にサーバと通信を行う。
それによりページ遷移の画面真っ白を待つことなく、createアクション、destroyアクション、updateアクションなどなどの操作ができるんです!
もっと具体的に言うと、ページ遷移をすることなく、コメントができたり、いいねができたり、ユーザ編集ができたりするんです!

もしこのような技術がなかったらTwitterでいいねをした瞬間に毎回リロードが行われてしまい、イライラしてしまいます。

そして今回はfirebaseのようなBaaSのvte.cxを利用して情報を保存したり、実際にホスティングしています。

vte.cx is 何

vte.cx公式サイト

vte.cx(ブイテックス)はReactなどのJavaScriptフレームワークを利用して
Webサービスを作成することができるバックエンドサービス(BaaS)です。
サーバ構築は一切不要で、開発コストや運用コストを削減できます。

自分のようなフロントエンド専門でやってきたプログラマーにぴったりです!
「でもfirebaseも同じようなことができるけど何が違うん?」
そんな声がどこかから聞こえてきそうですのでvte.cxの利点を説明します!
firebaseではnpm run buildした後にfirebase deployをしてやっとデプロイができますがこちらのvte.cxはnpm run serveするだけで自動的にデプロイされます!なのでいちいち手動デプロイしなくて良いので時間のない現代人にもぴったりのBaaSになっています!

ではこのvte.cxを使ったプロジェクトの作り方ですがすでにチュートリアルがあるのでこちらをご参照ください

vte.cxによるバックエンドを不要にする開発(1.Getting Started)

アプリケーション機能

今回作るものはデータを登録、編集、削除、一覧表示ができるアプリケーションです。
新規登録でユーザーを登録してその項目に名前、メールアドレス、趣味などといったデータを持たせます。
ではさっそく登録したいデータをvte.cxのスキーマに登録してみます。

  1. vte.cxにログイン
  2. サービス管理から新規サービスを作成(名前は適当に)
  3. 作成したサービスから管理画面へ移動
  4. エンドポイント管理から任意のエンドポイントを作成(ここにデータは登録されていく今回はfoo)
  5. エントリスキーマ管理から新規エントリ項目追加していく(今回10項目)

このあたりは
vte.cxによるバックエンドを不要にする開発(2.データの登録と取得)
vte.cxによるバックエンドを不要にする開発(3.スキーマ定義と型の利用)
の記事が参考になります。

最終的にはエントリ項目一覧はこんな感じになりました。
image.png

データ構造は下記のような構造になっています。

[
    users: {
        name: string,
        gender:string,
        age:int,
        address:string,
        password:string,
        email:string,
        post_number:string,
        like_residence_type:string,
        position:string,
        language:string
    }
]

とりあえずこのエントリー項目の型を開発環境に反映させましょう。

npm run download:typings

をターミナルで打つことにより/typingsフォルダの下に、index.d.tsファイルが作成されます。
これはfirebaseにはない機能で、チーム開発や堅牢なシステムを作るためにTypescriptは必須だと思うのでかなり強みだと思いました。

index.d.ts
export = VtecxApp
export as namespace VtecxApp

declare namespace VtecxApp {
    interface Request {
        feed: Feed
    }
    interface Feed {
        entry: Entry[]
    }
    interface Entry {
        id?: string,
        title?: string,
        subtitle?: string,
        rights?: string,
        summary?: string,
        content?: Content[],
        link?: Link[],
        contributor?: Contributor[],
        users?:Users
    }
    interface Content {
        ______text: string
    }
    interface Link {
        ___href: string,
        ___rel: string,
    }

    interface Contributor {
        uri?: string,
        email?: string
    }

    interface Users {
        name?:string,
        gender?:string,
        age?:number,
        address?:string,
        password?:string,
        email?:string,
        post_number?:string,
        like_residence_type?:string,
        position?:string,
        language?:string
    }
}

中身はこのようになっていて、これを使うことで型を合わせることができます。

Reactを使った開発

app.tsx
import * as React from 'react'
import { useReducer, createContext } from 'react'
import { Router, Route, Switch, Link, useHistory } from 'react-router-dom'
import axios from 'axios'

import Index from './index'
import Form from './Form'
import UserInfo from './UserInfo'
import UserInfoEdit from './UserInfoEdit'

const initialState = {
    isShow: false,
}

type ContextType = {
    state: stateType
    dispatch: React.Dispatch<actionType>
}

type stateType = {
    isShow: boolean
}

type actionType = {
    type: string
}

// objをcase文の中で2回returnすることができないので、dispatchを時間差で渡している

const reducer = (state: stateType, action: actionType) => {
    const obj = { ...state }
    const SHOW_INDICATOR = 'SHOW_INDICATOR'
    const HIDE_INDICATOR = 'HIDE_INDICATOR'

    switch (action.type) {
        case SHOW_INDICATOR:
            console.log('SHOW_INDICATORが作動')
            obj.isShow = true
            console.log(obj)
            return obj
        case HIDE_INDICATOR:
            console.log('HIDE_INDICATORが作動')
            obj.isShow = false
            return obj
        default: throw new TypeError(`Illegal type of action: ${action.type}`);
    }
}

export const Store = createContext({} as ContextType)
//dispatchを渡したいがStoreには{isShow:boolean}が渡されているため、この中にはdispatchを入れることはできない

const app = () => {
    const [state, dispatch] = useReducer(reducer, initialState)
    const history = useHistory()

    const postFormData = async (name: string, gender: string, age: number, address: string, password: string, email: string, postNumber: string, likeResidenceType: string, position: string, language: string) => {
        const req: VtecxApp.Entry[] = [
            {
                // ここにはFormで入力した値が入ってくる
                "users": {
                    "name": name,
                    "gender": gender,
                    "age": age,
                    "address": address,
                    "password": password,
                    "email": email,
                    "post_number": postNumber,
                    "like_residence_type": likeResidenceType,
                    "position": position,
                    "language": language,
                },
            }
        ]
        try {
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await axios.post('/d/users', req).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
            })
            history.push('/')
        } catch (e) {
            alert('error:' + e)
        }
    }

    return (
        <Router history={history}>
            <Switch>
                <Store.Provider value={{ state, dispatch }}>
                    <Index />
                    <Link to="/"><button>登録情報一覧へ</button></Link>
                    <Link to="/form"><button>新規登録画面へ</button></Link>
                    <Route exact path='/form' render={() => <Form click={postFormData} />} />
                    <Route exact path='/' component={UserInfo} />
                    <Route exact path='/user-info-edit' component={UserInfoEdit} />
                </Store.Provider>
            </Switch>
        </Router>
    )
}

export default app


SPAでの画面遷移のためにreact-router-domを採用しています。
react-router-domの説明がめちゃめちゃわかりやすいサイト
上記のサイトを参考にしながら実装していきました。
コンポーネントの役割は

  • IndexコンポーネントはAjax通信をしている際のローディングインジケーターの表示
  • Formコンポーネントは新規登録画面
  • UserInfoコンポーネントは一覧表示、削除機能
  • UserInfoEditは編集画面
※注意! vte.cxでreact-router-domを使うときindex.htmlのheadに以下の記述が必要
index.html
<script src="https://unpkg.com/react-router-dom/umd/react-router-dom.min.js"></script>

通常はnpm installで追加されるのですが、reactやreact-router-dom、axiosといった共通コンポーネントについてはHTMLの方に追加する必要があります。

理由は、ビルド時にこれらのコンポーネントを除外することでパフォーマンスを向上させる目的からです。つまり、ブラウザで常に共通コンポーネントがキャッシュされるので、ビルド時に読む込みをするよりも速くなります。
すべてのコンポーネントをHTMLに追加しているわけではなく、上記3つの代表的な共通コンポーネントのみです。

Indexコンポーネント(ローディングインジケーター)

index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { useContext } from 'react'
import CircularProgress from '@material-ui/core/CircularProgress'
import App, { Store } from './App'
import { HashRouter as Router, } from 'react-router-dom'



const Index = () => {
    const { state } = useContext(Store)
    const isShow = state.isShow

    if (isShow) {
        return <CircularProgress />
    } else {
        return (
            <></>
        )
    }
}

export default Index

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

このコンポーネントではapp.tsxで操作しているisShowというstateを監視してisShowがtrueの時にローディングインジケータを表示するコンポーネントです。

ここで使っているのは

index.tsx
//useContext
import { useContext } from 'react' 
// マテリアルUIのローディングインジケータ
import CircularProgress from '@material-ui/core/CircularProgress'
// Appコンポーネントとstateとdispatch情報が格納されているグローバルな情報
import App, { Store } from './App'
// ルーティングする対象を決めるHashRouter。BrowserRouterが一般的ですが、HashRouterの方がvte.cxと相性が良いのでHashRouterを使います
import { HashRouter as Router, } from 'react-router-dom'

ローディングインジケータ is 何

image.png
こんな感じで通信中にクルクル回っているものです。

地味なやつですがこれがあるのとないのとではSPAノ使いやすさがかなり違ってきます。
Ajaxを使用するにあたってローディングインジケータがないと通信が終わったのか終わってないのかわからないので使います。

マテリアルUIからimportすることですぐに使うことができます。
またvte.cxのプロジェクトには最初からmaterail-ui/coreがインストールされているため、新たにインストールする必要はありません。

またapp.tsxにあるstateを違うコンポーネントから参照するためにここではpropsではなく、useContextを使って実装しています

useContext is 何

useContextを利用することで様々なコンポーネントから参照できるステートを作成することができます。

createContext()を利用することで、ステートの状態管理が可能なProviderコンポーネントが作成でき、value属性で管理したいステートを指定することができ、Provider内の子孫コンポーネントではuseContextを利用して管理しているステートにアクセスできます。
useContextの説明がめちゃめちゃわかりやすいサイト

stateを渡す時にpropsを使うと親から子に渡す分には良いのですが、親=>子=>孫=>ひ孫
渡す対象を深くしてしまうとバケツリレーのようになってしまい、可読性や拡張性がかなり悪いアプリになってしまいます。

useContextを使うことで親=>ひ孫のように途中に必要なステップをすっ飛ばして実装することができます。

useContextを使うための流れ

step1. 親コンポーネントで渡したいstateをcreateContextというものを使って定数として設定します。また、設定した定数のタグ + .Providerで子孫コンポーネントをラップします。またタグ内のvalueに子孫に渡したい値を入れます。今回はstateとdispatchです(この中で出てくるuseReducerは後述します)

app.tsx
import { useReducer,createContext } from 'react'

const initialState = {
    isShow: false,
}

type ContextType = {
    state: stateType
    dispatch: React.Dispatch<actionType>
}

type stateType = {
    isShow: boolean
}

export const Store = createContext({} as ContextType)

const app = () => {
  const [state, dispatch] = useReducer(reducer, initialState)
  return (
        <Router history={history}>
            <Switch>
                <Store.Provider value={{ state, dispatch }}
                    <Index />
                </Store.Provider>
            </Switch>
        </Router>
    )
}

step2. 子孫コンポーネントの中で先ほどのstep1で設定した定数をimportしてuseContextを使って定数として設定します。

step3. useContextで設定した定数にアクセスすることで参照することができます

Index.tsx
import { useContext } from
//親でcreateContextしてexportした値。この値をuseContextすることで参照する定数が作れる。
import { Store } from './App' 

const Index = () => {
    const { state } = useContext(Store)
    const isShow = state.isShow

//もしisShowがtrueならローディングインジケータを表示させfalseの場合何も表示しないようにする
    if (isShow) {
        return <CircularProgress />
    } else {
        return (
            <></>
        )
    }
}

またisShowをtrueやfalseにするスイッチとして今回のアプリではuseContextとuseReducerを組み合わせて、どこからでもdispatchをすることができるようにしています。

useReducer is 何

useContext単体だと子孫コンポーネントで状態を参照することはできますが、状態を「更新」することができません。

ここでめちゃめちゃ役に立つのがuseReducerです。

useReducerとは
useStateと似たような機能であり、dispatch(配達員のようなイメージ)した後にreducerに値を渡してそこから値を取り出すhooks

値だけでなく、値を変えるdispatchというものを持っています。
useStateと比べると

useState useReducer
扱えるstateのtype 数値、文字列、論理値 オブジェクト、配列
関連するstateの取り扱い 複数を同時に取り扱うことができる
ローカルorグローバル ローカル グローバルuseContextと一緒に取り扱う

使い方は下記の通りで
必要なものは初期値(ここではinitialState)とreducer(ここではreducer)

物語的な流れとしては

reducerは値を変える門番のようなもので、dispatchは配達員です。配達員はacitons.typeという届け物を門番に渡します。
門番は届け物が正しいものか確認してあっていたら値を変えることができます。

今回のアプリではSHOW_INDICATORとHIDE_INDICATORという届け物があります。
配達員がどちらかの届け物を門番に渡すのですが上記の届け物のどちらにも該当しなかった時に
門番「この届け物は合ってない!!!」
として「throw new TypeError(Illegal type of action: ${action.type})」というエラーを出します

  1. dispatch({type:'SHOW_INDITATOR'})
  2. reducerにdispatchが渡ってきてcase文で判断
  3. case文に該当したら値が変更される
app.tsx
import { useReducer } from 'react'

const initialState = {
    isShow: false,
}

const reducer = (state: stateType, action: actionType) => {
    const obj = { ...state }
    const SHOW_INDICATOR = 'SHOW_INDICATOR'
    const HIDE_INDICATOR = 'HIDE_INDICATOR'

    switch (action.type) {
        case SHOW_INDICATOR:
// SHOW_INDICATORが届いたらisShowをtrueにする
            console.log('SHOW_INDICATORが作動')
            obj.isShow = true
            console.log(obj)
            return obj
        case HIDE_INDICATOR:
// HIDE_INDICATORが届いたらisShowをfalseにする
            console.log('HIDE_INDICATORが作動')
            obj.isShow = false
            return obj
// どちらでもなければエラーを出す
        default: throw new TypeError(`Illegal type of action: ${action.type}`);
    }
}

const [state,dispatch] = useReducer(reducer,initialState)

useReducerの説明がめちゃめちゃわかりやすいサイト①
useReducerの説明がめちゃめちゃわかりやすいサイト②
useReducerの説明がめちゃめちゃわかりやすいサイト③
上記のサイトを参考にしました。

またこのreducerは前述の通りuseContextと相性が良く、子に渡すことができるため、子からdispatchを親での渡し方と同じように渡すことができます。

UserInfo.tsx
import * as React from 'react'
import { useState, useEffect, useContext } from 'react'
import axios from 'axios'
import { Store } from './App'

const UserInfo = () => {
    const [users, setUsers] = useState([])
// useContextすることにより子孫コンポーネントからdispatchが使える
    const { dispatch } = useContext(Store)

    // データの取得
    const getFormData = async () => {
        try {
//ここのdispatchで先祖のstateを変更させている
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await axios.get('/d/users?f').then((res) => {

                if (res && res.data && res.data.length) {
                    setUsers(res.data)
                }
            }).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
            })
        } catch (e) {
            alert('error:' + e)
        }
    }
    )
}

export default UserInfo
これにより、Index.tsxではisShowの参照を、UserInfo.tsxではisShowの更新を

propsを一切使うことなく実現することができています。

SUGOI!!!

Formコンポーネント(情報の新規登録)

Form.tsx
import * as React from 'react'
import { useState } from 'react'
import TextInput from './TextInput'
import RadioInput from './RadioInput'
import FormButton from './FormButton'
import TextareaInput from './TextareaInput'
import CheckboxInput from './CheckboxInput'
import SelectboxInput from './SelectboxInput'

const Form = (props: any) => {

    const [state, setState] = useState({
        name: '',
        gender: '',
        age: 0,
        address: '',
        password: '',
        email: '',
        postNumber: '',
        likeResidenceType: '',
        position: '',
        language: [],
    })

    // 登録ボタンを謳歌するとalertで出力する名前・性別・年齢をそれぞれ下記の定数に代入している

    // 名前
    const formName = state.name
    // 性別
    const formGender = state.gender
    // 年齢
    const formAge = Number(state.age)
    // 住所
    const formAddress = state.address
    // パスワード
    const formPassword = state.password
    // メールアドレス
    const formEmail = state.email
    // 郵便番号
    const formPostNumber = state.postNumber
    // 好きな住居形態
    const formLikeResidenceType = state.likeResidenceType
    // 役職
    const formPosition = state.position
    // 使用言語
    const formLanguage = state.language.toString()

    // prevStateにはe.target.valueが入ってくる
    const changeState = (prevState: string | number, stateType: string) => {

        const obj: any = { ...state }
        // 変数objにstateの内容をそのままコピーする
        const key = stateType
        // objのkeyをここで格納する。setしたいkeyはstateTypeで、子コンポーネントのprops.onChage関数の第二引数から渡される
        obj[key] = prevState
        // objのkeyプロパティのvalueをprops.onChange関数の第一引数に渡される値であるprevStateにする
        setState({ ...obj })
    }

    const addArrayState = (prevState: string | undefined) => {
        const obj: any = { ...state }
        const array: any = [...obj.language]
        console.log(array)

        // arrayの中にすでに値があった時の処理
        // 値を非破壊的メソッドで削除したい
        if (array.some((value: string) => value === prevState)) {
            const newArray = array.filter((array: string) => array !== prevState)
            obj.language = [...newArray]
            console.log(obj.language)
        } else {
            obj.language = [...array, prevState]
            console.log(obj.language)
        }
        setState({ ...obj })
        console.log(state)
    }

    const clickButton = (event: React.ChangeEvent<HTMLInputElement>) => {
        //親への通知
        event.preventDefault();
        props.click(formName, formGender, formAge, formAddress, formPassword, formEmail, formPostNumber, formLikeResidenceType, formPosition, formLanguage,)
    }

    return (
        <form>
            <TextInput label={'名前'} onChange={changeState} name="name" />
            <label>性別:</label>
            <RadioInput label={'男性'} gender={''} name="gender" onChange={changeState} checked={state.gender === ''} />
            <RadioInput label={'女性'} gender={''} name="gender" onChange={changeState} checked={state.gender === ''} />
            <TextInput label={'年齢'} name="age" onChange={changeState} />
            <TextareaInput label={'住所'} name="address" onChange={changeState} />
            <TextInput label={'パスワード'} name="password" onChange={changeState} />
            <TextInput label={'メールアドレス'} name="email" onChange={changeState} />
            <TextInput label={'郵便番号'} name="postNumber" onChange={changeState} />
            <SelectboxInput label={'好きな住居形態'} choice={[{ name: '一軒家' }, { name: 'マンション' }, { name: '' }]} name={'likeResidenceType'} onChange={changeState} />
            <TextInput label={'役職'} name="position" onChange={changeState} />
            <CheckboxInput label={'日本語'} name={'日本語'} onChange={addArrayState} checked={state.language.some(value => value === '日本語')} />
            <CheckboxInput label={'英語'} name={'英語'} onChange={addArrayState} checked={state.language.some(value => value === '英語')} />
            <CheckboxInput label={'中国語'} name={'中国語'} onChange={addArrayState} checked={state.language.some(value => value === '中国語')} />
            <FormButton onClick={clickButton} submitName={'登録する'} />
        </form>
    )
}

export default Form

Formコンポーネントは特に特筆すべき点はありません。
親コンポーネントであるappコンポーネントに入力した値をコールバック関数で渡しています

Form.tsx
const clickButton = (event: React.ChangeEvent<HTMLInputElement>) => {
        //親への通知
        event.preventDefault();
        props.click(formName, formGender, formAge, formAddress, formPassword, formEmail, formPostNumber, formLikeResidenceType, formPosition, formLanguage,)
    }
app.tsx
const app = () => {
    const [state, dispatch] = useReducer(reducer, initialState)
    const history = useHistory()

    const postFormData = async (name: string, gender: string, age: number, address: string, password: string, email: string, postNumber: string, likeResidenceType: string, position: string, language: string) => {
        const req: VtecxApp.Entry[] = [
            {
                // ここにはFormで入力した値が入ってくる
                "users": {
                    "name": name,
                    "gender": gender,
                    "age": age,
                    "address": address,
                    "password": password,
                    "email": email,
                    "post_number": postNumber,
                    "like_residence_type": likeResidenceType,
                    "position": position,
                    "language": language,
                },
            }
        ]
        try {
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await axios.post('/d/users', req).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
            })
            history.push('/')
        } catch (e) {
            dispatch({type:'HIDE_INDICATOR'})
            alert('error:' + e)
        }
    }

        return (
        <Router history={history}>
            <Switch>
                <Store.Provider value={{ state, dispatch }}>
                    <Index />
                    <Link to="/"><button>登録情報一覧へ</button></Link>
                    <Link to="/form"><button>新規登録画面へ</button></Link>
                    <Route exact path='/form' render={() => <Form click={postFormData} />} />
// この部分でpostFormDataが発火する
// componentでコンポーネントを指定するとpropsが渡せないため、renderを使ってコンポーネントを指定している。こうすることでporpsで情報を渡すことができる。
// このpropsの渡す方法がなかなか見つからずハマりました
                    <Route exact path='/' component={UserInfo} />
                    <Route exact path='/user-info-edit' component={UserInfoEdit} />
                </Store.Provider>
            </Switch>
        </Router>
    )


app.tsxでtry、catch構文でajax通信をしています。tryの始まりにdispatch({type:SHOW_INDICATOR})を送り、ajax通信が終わり次第dispatch({type:HIDE_INDICATOR})を送っています

またajax通信をする際にaxiosを使っています。
データを登録する際にはPOSTメソッドを使います。

まずtryメソッドの中で
おまじないである

axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'

を記述した後に

axios.post('/d/users',req)

とすることでデータを登録することができます。
reqにはFormから値をとってくるのですがその時の型としてVtecxApp.Entry[]を宣言しています。
この宣言によってvtecxのサービスの中で設定した型以外の型が入る可能性がある場合、コンパイルエラーで知らせてくれます。
型ファイルはtypingsフォルダの中にあるindex.d.tsです。

const req: VtecxApp.Entry[] = [
            {
                // ここにはFormで入力した値が入ってくる
                "users": {
                    "name": name,
                    "gender": gender,
                    "age": age,
                    "address": address,
                    "password": password,
                    "email": email,
                    "post_number": postNumber,
                    "like_residence_type": likeResidenceType,
                    "position": position,
                    "language": language,
                },
            }
        ]

UserInfo.tsx(情報一覧)

UserInfoはvte.cxに登録されているデータを一覧表示するコンポーネントです。

流れとしては

  1. useEffectを使って、初期レンダリング時と登録されているデータが変わった際にajax通信してデータを引っ張ってくる
  2. データをusersというstateに保存する
  3. 保存したデータをUserListという子コンポーネントにpropsで渡す
  4. UserList内でテーブルを用意してその中に親(UserInfo)からデータが渡ってきていれば表示する

以上が一連の流れとなっています。

UserInfo.tsx
import * as React from 'react'
import { useState, useEffect, useContext } from 'react'
import axios from 'axios'
import UserList from './UserList'
import { Store } from './App'

const UserInfo = () => {
    const [users, setUsers] = useState([])
    const { dispatch } = useContext(Store)
    // getしたデータは「?f」で取得していることから「配列」として設定する
    // stateが変わったときにgetFormDataが作動する
    useEffect(() => {
        getFormData()
        console.log('useEffectが作動')
    }, [users.length])
    //消去した後にusers.lengthが変化していないのが作動しない原因
    // grouptdatadeleteをした後にsetUsersをしなければいけない

    // データの取得
    const getFormData = async () => {
        try {
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'

            // resのデータの中身については型チェックしなくても良いがlengthチェックはしないといけない
            // 204がデータが入っていないHTTPステータス
            // ajax通信がされている時はisShowをtrueにして、完了したときにfalseにする
            await axios.get('/d/users?f').then((res) => {

                if (res && res.data && res.data.length) {
                    setUsers(res.data)
                }
            }).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
            })
        } catch (e) {
            alert('error:' + e)
        }
    }
    return (
        <>
            <UserList info={users} getFormData={getFormData} />
        </>
    )
}

export default UserInfo

useEffect is 何

いつ、どんな条件だったら〇〇な処理を実行をしてくれるものです。

useEffectの説明がめちゃめちゃわかりやすいサイト
useEffectの使い分けがめちゃめちゃわかりやすいサイト

上記のサイトを参考にしました。

基本的な使い方として

useEffect(() => {
//処理
})

これが最小セットです。
第二引数が結構重要で、ここの設定でいつ実行をするかを決めます。

useEffect(() => {
//処理
},[])

上記のように空の配列を渡すと、初回登場時のみ処理を実行します。

他にも、何かの値が変わった瞬間に処理を実行したいという時もあると思います。
そういう時は

useEffect(() => {
//処理
},[])

とします。

今回の使い方としてはデータが格納されているusersというstateの配列の中の数が変化した場合にgetFormDataメソッドを実行するようにしています。

const [ users, setUsers ] = useState([])

useEffect(() => {
  getFormData()
},[users.length])

ちなみにusersはデータが入ってくると

[
    {
    "users": {
        "name": "ゴリラ",
        "gender": "男",
        "age": 8,
        "address": "上野動物園",
        "password": "uhouho",
        "email": "uho@gorira.com",
        "post_number": "111",
        "like_residence_type": "森",
        "position": "部長",
        "language": ""
    },
    "author": [{ "uri": "urn:vte.cx:created:20152" }], "id": "/users/345,1",
    "link": [{ "___href": "/users/345", "___rel": "self" }],
    "published": "2020-09-17T09:24:39.004+09:00", "updated": "2020-09-17T09:24:39.004+09:00"
    },
]

こんな感じのデータ構造になります。

getFormメソッドの中身を見てみましょう。

// データの取得
    const getFormData = async () => {
        try {
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'

            // resのデータの中身については型チェックしなくても良いがlengthチェックはしないといけない
            // ajax通信がされている時はisShowをtrueにして、完了したときにfalseにする
            await axios.get('/d/users?f').then((res) => {

                if (res && res.data && res.data.length) {
                    setUsers(res.data)
                    console.log(res.data)
                    console.log('hoge')
                }
            }).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
                console.log(users)
            })
        } catch (e) {
            alert('error:' + e)
        }

データを取得するには下記のように値が格納されているエンドポイントの後に?fをつけることで取得できます。

await axios.get('/d/users?f')

またgetをすると値が帰ってくるのでthenメソッドでresとして渡しています。

res.dataには先ほどのusersに入ったデータが渡されます。

ここで受け取るデータは配列なのでがuseStateでの初期も必ず配列に設定しましょう。

const [users,setUsers] = useState([])

resとして受け取った後はresにちゃんと値が入っているかif文でチェックする必要があります。

if (res && res.data && res.data.length) {
    setUsers(res.data)
}

としてusersにデータを格納します。

usersはUserListという子コンポーネントにpropsで渡されます。

    return (
        <>
            <UserList info={users} getFormData={getFormData} />
        </>
    )

テーブルを作成するUserListコンポーネント

UserList.tsx
import * as React from 'react'
import { useState, useContext } from 'react'
import axios from 'axios'
import { Link } from 'react-router-dom'
import CheckboxInput from './CheckboxInput'
import FormButton from './FormButton'
import { Store } from './App'

const UserList = (props: any) => {
    const [state, setState] = useState({
        list: [],
    })
    const { dispatch } = useContext(Store)

    const addDeleteList = (prevState: string | undefined) => {
        const obj: any = { ...state }
        const array: any = [...obj.list]
        console.log(array)

        // arrayの中にすでに値があった時の処理
        // 値を非破壊的メソッドで削除したい
        if (array.some((value: string) => value === prevState)) {
            const newArray = array.filter((array: string) => array !== prevState)
            obj.list = [...newArray]
            console.log(obj.list)
        } else {
            obj.list = [...array, prevState]
            console.log(obj.list)
        }
        setState({ ...obj })
        console.log(state)
    }

    const deleteUsersGroupData = async (event: React.ChangeEvent<HTMLInputElement>) => {
        try {
            event.preventDefault()
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await state.list.map(async (item: any) => {
                await axios.delete('/d' + item).then(() => {
                    dispatch({ type: 'HIDE_INDICATOR' })
                })
            })
            await props.getFormData()
            // ここで画面を一新したい
            console.log('deleteGroup')
        } catch (e) {
            alert('error:' + e)
        }
    }

    const info = props.info
    const infoItems = info.map((item: any, key: string) => (
        <tr>
            <Link to={{ pathname: '/user-info-edit', state: { href: item.link[0].___href, rel: item.link[0].___rel, userInfo: item.users, } }}><td key={key}><button style={{ width: '100%' }}>編集</button></td></Link>
            <td key={key}><CheckboxInput onChange={addDeleteList} name={item.link[0].___href} key={key} /></td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.name ? item.users.name : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.gender ? item.users.gender : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.age ? item.users.age : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.address ? item.users.address : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.password ? item.users.password : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.email ? item.users.email : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.post_number ? item.users.post_number : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.like_residence_type ? item.users.like_residence_type : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.position ? item.users.position : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.language ? item.users.language : '未登録'}</td>
        </tr>
    ))

    const tableHeads = ['編集', '削除', '名前', '性別', '年齢', '住所', 'パスワード', 'メールアドレス', '郵便番号', '好きな住居形態', '役職', '使用言語']

    const tableHeadItems = tableHeads.map((item, index) => {
        return (
            <th style={{ border: '1px solid black' }} key={index}>{item}</th>
        )
    })

    return (
        <>
            <table style={{ border: '1px solid black', borderCollapse: 'collapse', width: '100%', textAlign: 'center' }}>
                <tr style={{ border: '1px solid black' }}>
                    {tableHeadItems}
                </tr>
                {infoItems}
            </table>
            <FormButton onClick={deleteUsersGroupData} submitName={'消去する'} />
        </>
    )
}

export default UserList


props.infoをmap関数を使ってデータがあればデータを表示、なければ未登録として設定しています。

const infoItems = info.map((item: any, key: string) => (
        <tr>
            <Link to={{ pathname: '/user-info-edit', state: { href: item.link[0].___href, rel: item.link[0].___rel, userInfo: item.users, } }}><td key={key}><button style={{ width: '100%' }}>編集</button></td></Link>
            <td key={key}><CheckboxInput onChange={addDeleteList} name={item.link[0].___href} key={key} /></td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.name ? item.users.name : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.gender ? item.users.gender : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.age ? item.users.age : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.address ? item.users.address : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.password ? item.users.password : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.email ? item.users.email : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.post_number ? item.users.post_number : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.like_residence_type ? item.users.like_residence_type : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.position ? item.users.position : '未登録'}</td>
            <td style={{ border: '1px solid black' }} key={key}>{item.users.language ? item.users.language : '未登録'}</td>
        </tr>
    ))

ここでは三項演算子を使って

{item.users.name ? item.users.name : '未登録'}

としているのですが
最近論理演算子というものを知りまして、どんなものかというと
a || b
この場合aがtrueならaをfalseならbを表示します。
なので上のコードだと以下のようにしても問題ありません

{item.users.name || '未登録'}

むしろこっちの方がいい!

論理演算子がめちゃめちゃわかりやすいサイト

そしてUserListの中での目玉がこちらです!

<Link to={{ pathname: '/user-info-edit', state: { href: item.link[0].___href, rel: item.link[0].___rel, userInfo: item.users, } }}>
 <td key={key}>
  <button style={{ width: '100%' }}>
編集
  </button>
 </td>
</Link>

こちら編集画面に遷移するためのタグなのですが、遷移先ではデータの編集をするためにデータを渡さなければいけません。
ググってもreact-router-domでの値の渡す方法を解説しているサイトがなく詰まっていたのですが、序盤に紹介したreact-router-domの説明がめちゃめちゃわかりやすいサイトで解説されていました。

Linkタグの解説は

<Link to="遷移したいリンクパス" />

だいたいこれしか書いてないのですが

<Link to={{pathname:"遷移したいリンクパス",state:{遷移先に渡したい情報}}}

とすることで渡すことができます。

今回はデータを編集するにあたってhref情報とrel情報とユーザの情報が必要なのでそれらの情報が格納されている

href: item.link[0].___href
rel: item.link[0].___rel
userInfo: item.users

を渡します

これらには

[
    {
    "users": {
        "name": "ゴリラ",
        "gender": "男",
        "age": 8,
        "address": "上野動物園",
        "password": "uhouho",
        "email": "uho@gorira.com",
        "post_number": "111",
        "like_residence_type": "森",
        "position": "部長",
        "language": ""
    },
    "link": [{ "___href": "/users/345", "___rel": "self" }],
]

こういった情報が格納されています。
遷移先の編集画面で
「どのような情報のデータか」
「どのようなhref,rel属性を持っているのか」
を知るために必要になります。

userInfoEdit.tsx(情報編集)

userInfoEditは登録されている情報を編集するためのコンポーネントです。

userInfoEdit.tsx
import * as React from 'react'
import { useState, useContext } from 'react'
import TextInput from './TextInput'
import RadioInput from './RadioInput'
import FormButton from './FormButton'
import TextareaInput from './TextareaInput'
import CheckboxInput from './CheckboxInput'
import SelectboxInput from './SelectboxInput'
import { useLocation, useHistory } from 'react-router-dom'
import axios from 'axios'
import { Store } from './App'

type userInfoType = {
    name: '',
    gender: '',
    age: 0,
    address: '',
    password: '',
    email: '',
    postNumber: '',
    likeResidenceType: '',
    position: '',
    language: '',
}


const UserInfoEdit = () => {
    const [state, setState] = useState({
        name: '',
        gender: '',
        age: 0,
        address: '',
        password: '',
        email: '',
        postNumber: '',
        likeResidenceType: '',
        position: '',
        language: [],
    })
    const { dispatch } = useContext(Store)

    const history = useHistory()
    const locationValue: {
        state: {
            href: string,
            rel: string,
            userInfo: userInfoType
        }
    } = useLocation()
    let linkValue: string = ''
    let relValue: string = ''
    let userInfoValue: userInfoType = {
        name: '',
        gender: '',
        age: 0,
        address: '',
        password: '',
        email: '',
        postNumber: '',
        likeResidenceType: '',
        position: '',
        language: ''
    }
    if (locationValue.state === null || locationValue.state === undefined) {
        alert('不正なアクセスです')
        history.push('/')
    } else {
        linkValue = locationValue.state.href
        relValue = locationValue.state.rel
        userInfoValue = locationValue.state.userInfo
    }

    // 名前
    const formName = state.name
    // 性別
    const formGender = state.gender
    // 年齢
    const formAge = Number(state.age)
    // 住所
    const formAddress = state.address
    // パスワード
    const formPassword = state.password
    // メールアドレス
    const formEmail = state.email
    // 郵便番号
    const formPostNumber = state.postNumber
    // 好きな住居形態
    const formLikeResidenceType = state.likeResidenceType
    // 役職
    const formPosition = state.position
    // 使用言語
    const formLanguage = state.language.toString()

    const req: VtecxApp.Entry[] = [
        {
            users: {
                name: formName,
                gender: formGender,
                age: formAge,
                address: formAddress,
                password: formPassword,
                email: formEmail,
                post_number: formPostNumber,
                like_residence_type: formLikeResidenceType,
                position: formPosition,
                language: formLanguage
            },
            link: [
                {
                    ___href: linkValue,
                    ___rel: relValue
                }
            ]
        }
    ]

    const putFormData = async (event: React.ChangeEvent<HTMLInputElement>) => {
        try {
            event.preventDefault()
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await axios.put('/d/users', req).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
            })
            history.push('/')
        } catch (e) {
            alert('error:' + e)
        }
    }

    const deleteFormData = async (event: React.ChangeEvent<HTMLInputElement>) => {
        try {
            event.preventDefault()
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await axios.delete('/d' + linkValue)
            history.push('/')
        } catch (e) {
            alert('error')
            console.log(e)
        }
    }

    // prevStateにはe.target.valueが入ってくる
    const changeState = (prevState: string | number, stateType: string) => {

        const obj: any = { ...state }
        // 変数objにstateの内容をそのままコピーする
        const key = stateType
        // objのkeyをここで格納する。setしたいkeyはstateTypeで、子コンポーネントのprops.onChage関数の第二引数から渡される
        obj[key] = prevState
        // objのkeyプロパティのvalueをprops.onChange関数の第一引数に渡される値であるprevStateにする
        setState({ ...obj })
    }

    const addArrayState = (prevState: string | undefined) => {
        const obj: any = { ...state }
        const array: any = [...obj.language]

        // arrayの中にすでに値があった時の処理
        // 値を非破壊的メソッドで削除したい
        if (array.some((value: string) => value === prevState)) {
            const newArray = array.filter((array: string) => array !== prevState)
            obj.language = [...newArray]
            console.log(obj.language)
        } else {
            obj.language = [...array, prevState]
            console.log(obj.language)
        }
        setState({ ...obj })
    }

    return (
        <>
            <table style={{ border: '1px solid black', borderCollapse: 'collapse', textAlign: 'center' }}>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>名前</th><td style={{ border: '1px solid black' }}>{userInfoValue.name ? userInfoValue.name : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>性別</th><td style={{ border: '1px solid black' }}>{userInfoValue.gender ? userInfoValue.gender : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>年齢</th><td style={{ border: '1px solid black' }}>{userInfoValue.age ? userInfoValue.age : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>住所</th><td style={{ border: '1px solid black' }}>{userInfoValue.address ? userInfoValue.address : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>パスワード</th><td style={{ border: '1px solid black' }}>{userInfoValue.password ? userInfoValue.password : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>メールアドレス</th><td style={{ border: '1px solid black' }}>{userInfoValue.email ? userInfoValue.email : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>郵便番号</th><td style={{ border: '1px solid black' }}>{userInfoValue.postNumber ? userInfoValue.postNumber : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>好きな住居形態</th><td style={{ border: '1px solid black' }}>{userInfoValue.likeResidenceType ? userInfoValue.likeResidenceType : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>役職</th><td style={{ border: '1px solid black' }}>{userInfoValue.position ? userInfoValue.position : '未登録'}</td></tr>
                <tr style={{ border: '1px solid black' }}><th style={{ border: '1px solid black' }}>使用言語</th><td style={{ border: '1px solid black' }}>{userInfoValue.language ? userInfoValue.language : '未登録'}</td></tr>
            </table>
            <form>
                <TextInput label={'名前'} onChange={changeState} name="name" />
                <label>性別:</label>
                <RadioInput label={'男性'} gender={''} name="gender" onChange={changeState} checked={state.gender === ''} />
                <RadioInput label={'女性'} gender={''} name="gender" onChange={changeState} checked={state.gender === ''} />
                <TextInput label={'年齢'} name="age" onChange={changeState} />
                <TextareaInput label={'住所'} name="address" onChange={changeState} />
                <TextInput label={'パスワード'} name="password" onChange={changeState} />
                <TextInput label={'メールアドレス'} name="email" onChange={changeState} />
                <TextInput label={'郵便番号'} name="postNumber" onChange={changeState} />
                <SelectboxInput label={'好きな住居形態'} choice={[{ name: '一軒家' }, { name: 'マンション' }, { name: '' }]} name={'likeResidenceType'} onChange={changeState} />
                <TextInput label={'役職'} name="position" onChange={changeState} />
                <CheckboxInput label={'日本語'} name={'日本語'} onChange={addArrayState} checked={state.language.some(value => value === '日本語')} />
                <CheckboxInput label={'英語'} name={'英語'} onChange={addArrayState} checked={state.language.some(value => value === '英語')} />
                <CheckboxInput label={'中国語'} name={'中国語'} onChange={addArrayState} checked={state.language.some(value => value === '中国語')} />
                <br />
                <FormButton onClick={putFormData} submitName={'更新する'} />
                <FormButton onClick={deleteFormData} submitName={'このデータを削除する'} />
            </form>
        </>
    )
}
export default UserInfoEdit

先ほどのUserListコンポーネントからデータを受け取って情報を画面に表示しています。

情報の参照方法は

import { useLocation } from 'react-router-dom'

const locationValue: {
        state: {
            href: string,
            rel: string,
            userInfo: userInfoType
        }
    } = useLocation()

としてlocationValueのなかにオブジェクトとして設定しています

またurlを直打ちした時に
編集画面に入れないように

if (locationValue.state === null || locationValue.state === undefined) {
        alert('不正なアクセスです')
        history.push('/')
    } else {
        linkValue = locationValue.state.href
        relValue = locationValue.state.rel
        userInfoValue = locationValue.state.userInfo
    }

locationValueに値が格納されていない場合、alertが出てトップページに強制送還されます。

次にこのコンポーネントではデータの削除とデータの更新ができるのですが
削除は

axios.delete(リンクパス + 個別のhref)

ここではdeleteFormDataというメソッドを使って下記のように記述しています

const deleteFormData = async (event: React.ChangeEvent<HTMLInputElement>) => {
        try {
            event.preventDefault()
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await axios.delete('/d' + linkValue)
            history.push('/')
        } catch (e) {
            alert('error')
            console.log(e)
        }
    }

更新は

axios.put(リンクパス , 更新したいデータ)

ここではputFormDataというメソッドを使って下記のように記述しています

const putFormData = async (event: React.ChangeEvent<HTMLInputElement>) => {
        try {
            event.preventDefault()
            dispatch({ type: 'SHOW_INDICATOR' })
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            await axios.put('/d/users', req).then(() => {
                dispatch({ type: 'HIDE_INDICATOR' })
            })
            history.push('/')
        } catch (e) {
            alert('error:' + e)
        }
    }

これでVte.cxを使った簡単なCRUDアプリを作ることができました。

感想

vte.cxはもちろん、React hooksやreact-router-dom,axiosを使ったAjax通信など初めて学んだのでかなり時間がかかりましたがSPAでBaaSを使ったCRUDができることになったのでかなり成長することができたと我ながら思っています。

コンポーネント設計やコードが洗練されていないのでそこも勉強していけたらなと思っています。

8
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8