Help us understand the problem. What is going on with this article?

React + TypeScript + vte.cxで簡単なWebアプリを作ってみる

はじめに

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

今回作るアプリケーション

スクリーンショット 2019-09-18 10.17.47.png

ほとんどCSSを使ってないのでかなり殺風景な仕上がりになりました。笑
ただ機能としては動くので今回は良し。笑

編集の選択項目にチェックを入れて削除ボタン(位置おかしい)を押すとデータは削除されます。
削除後の一覧は都度、useEffectで描画される。

スクリーンショット 2019-09-18 10.35.56.png

新規登録はこんな感じ。各項目を入力後に登録ボタンを押すと、保存されさっきのトップページに遷移される。

スクリーンショット 2019-09-18 10.36.19.png

各項目の編集ボタンを押すと、登録画面に初期値をセットした状態で遷移できる。

以上が完成形です。では手順を見ていきましょう。

vte.cxとは

vte.cx
公式サイト

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

すばらしい。Webアプリケーションを作ろうと思ったら、Railsなどのバックエンド側の
フレームワークを使いこなさないとできないイメージだが、vte.cxを使うことによって
JavaScriptだけで開発できる。フロントエンドな人達や、HTML、CSS、JavaScriptから
学び始めた初学者にとってバックエンド技術を覚える学習コストに比べたらとても効率的なことなんじゃないかなー。

  • 今回使ったvte.cxの機能
    • ホスティング
    • ドキュメント型データベース機能

vte.cxをインストール

vte.cxのインストールなど環境構築はこちらの記事が参考になります。
vte.cxによるバックエンドを不要にする開発(その1)

アプリケーション機能

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

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

このあたりは
vte.cxによるバックエンドを不要にする開発(その2)
vte.cxによるバックエンドを不要にする開発(その3)
の記事が参考になります。

最終的にはエントリ項目一覧はこんな感じになりました。
スクリーンショット 2019-09-17 13.23.34.png

User(親)に対し各項目が子として存在する形です。基本的に型は文字列で登録するようにしています。
最後の特技(skil)だけArray型にしたのは、データを登録する際、チェックボックスを利用したくて言語(language)項目(例えばHTMLやCSS)にチェックが入っていればcheck(Boolean)型がtrueになり、複数の言語を登録できる仕組みです。

とりあえずこのエントリー項目の型を開発環境に反映させましょう。
1. エントリー項目の下にある右側のマークをクリック。
2. TS型定義ファイル出力をクリック
スクリーンショット 2019-09-17 15.34.30.png

npm run download:typings

これで自分のローカルのプロジェクトに独自に登録したエントリー項目の型定義ファイルが作れました。

Reactを使って開発していこう

スキーマにエントリー項目も追加できたのでさっそくコードをゴリゴリしていきます。

index.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { BrowserRouter, Route, Link } from 'react-router-dom'

import Top from './Top'
import Add from './Add'
import Edit from './Edit'

import './style.css'

const App: any = () => {
  return (
    <BrowserRouter>
      <ul>
        <li>
          <Link to="/">一覧</Link>
        </li>
        <li>
          <Link to="/add">
            <button>新規登録</button>
          </Link>
        </li>
      </ul>
      <Route exact path="/" component={Top} />
      <Route path="/add" component={Add} />
      <Route path="/edit/:entry_key" component={Edit} /> {/*id */}
    </BrowserRouter>
  )
}

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


今回画面遷移を行いたいため、react-router-domを使用します。
コンポーネントの役割は以下の通り

  • Topコンポーネントは一覧表示、削除機能
  • Addコンポーネントは新規登録画面
  • Editコンポーネントは編集画面

※注意!
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で追加されるのですが、reactreact-router-domaxiosといった共通コンポーネントについてはHTMLの方に追加する必要があります。

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

Topコンポーネント(一覧画面、削除)

Top.tsx
import * as React from 'react'
import { useState, useEffect } from 'react'
import axios from 'axios'
import { Link } from 'react-router-dom'

const Top: React.FC = () => {
  const [user_feed, setUserFeed] = useState<VtecxApp.Entry[]>([])
  const [delete_entry, setDeleteEntry] = useState('')

  const getFeed = async () => {
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d/foo?f')
      if (res.data) {
        setUserFeed(res.data)
      } else {
        setUserFeed([])
      }
    } catch (e) {
      alert('error')
      console.log(e)
    }
  }

  const getDeleteCheck = (entry_key: string) => {
    if (delete_entry.indexOf(entry_key) >= 0) {
      return true
    } else {
      return false
    }
  }

  useEffect(() => {
    getFeed()
  }, [user_feed])

  const showFeed = () => {
    return (
      <>
        <table>
          <tr>
            <th>名前</th>
            <th>メールアドレス</th>
            <th>説明</th>
            <th>性別</th>
            <th>誕生日</th>
            <th>血液型</th>
            <th>住所</th>
            <th>出身地</th>
            <th>趣味</th>
            <th>特技</th>
            <th colSpan={2}>編集</th>
          </tr>
          {user_feed.map((entry, index) => {
            const no_message = '未登録'
            if (entry.user && entry.id) {
              const entry_key = entry.id && entry.id.split(',')[0] //fooのentry_key取得
              return (
                <>
                  <tr key={index}>
                    <td>{entry.user.name ? entry.user.name : no_message}</td>
                    <td>{entry.user.email ? entry.user.email : no_message}</td>
                    <td>{entry.user.description ? entry.user.description : no_message}</td>
                    <td>{entry.user.sex ? entry.user.sex : no_message}</td>
                    <td>{entry.user.birthday ? entry.user.birthday : no_message}</td>
                    <td>{entry.user.bloodtype ? entry.user.bloodtype : no_message}</td>
                    <td>{entry.user.address ? entry.user.address : no_message}</td>
                    <td>{entry.user.birthplace ? entry.user.birthplace : no_message}</td>
                    <td>{entry.user.hobby ? entry.user.hobby : no_message}</td>
                    <td>
                      {entry.user.skil &&
                        entry.user.skil.map(skil => {
                          return skil.check ? skil.language + ' ' : ''
                        })}
                    </td>
                    <td>
                      <Link to={'/edit' + entry_key}>
                        <button>編集</button>
                      </Link>
                    </td>
                    <td>
                      <TableCheck
                        delete_entry={delete_entry}
                        setDeleteEntry={setDeleteEntry}
                        entry_key={entry_key}
                        getDeleteCheck={getDeleteCheck}
                      />
                    </td>
                  </tr>
                </>
              )
            }
          })}
        </table>
        <DeleteBtn delete_entry={delete_entry} />
      </>
    )
  }

  return <div>{showFeed()}</div>
}

type ChildProps = {
  delete_entry?: any
  setDeleteEntry?: any
  entry_key?: any
  getDeleteCheck?: any
}

const TableCheck: React.FC<ChildProps> = ({
  delete_entry,
  setDeleteEntry,
  entry_key,
  getDeleteCheck
}) => {
  return (
    <>
      <input
        type="checkbox"
        onClick={() => {
          let temp = delete_entry
          if (temp.indexOf(String(entry_key)) >= 0) {
            temp = temp.replace(String(entry_key + ','), '')
          } else {
            temp += entry_key + ','
          }
          setDeleteEntry(temp)
        }}
        checked={getDeleteCheck(entry_key)}
      />
      選択
    </>
  )
}

const DeleteBtn: React.FC<ChildProps> = ({ delete_entry }) => {
  return (
    <>
      <button
        onClick={() => {
          let delete_feed = delete_entry.split(',')
          if (delete_feed.length > 2) {
            delete_feed.forEach((entry: string) => {
              if (delete_entry) {
                axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
                axios.delete('/d' + entry + '?e')
              }
            })
          } else if (delete_feed.length === 2) {
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            axios.delete('/d' + delete_feed[0] + '?e')
          } else {
            alert('削除するデータが選択されていません')
          }
        }}
        className="delete-button"
      >
        削除
      </button>
    </>
  )
}

export default Top

全体のコードとしてはこんな感じ。

順番に解説していきます。

Top.tsx
  const [user_feed, setUserFeed] = useState<VtecxApp.Entry[]>([])
  const [delete_entry, setDeleteEntry] = useState('')

React HooksのuseStateを使ってコンポーネントに状態を持たせます。
user_feedはvte.cx側に登録されているentryの一覧データ。
delete_entryは削除したいentryデータです。
VtecxApp.Entry[]はスキーマで登録したエントリー項目の型です

index.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[]
    user?: User
  }
  interface Content {
    ______text: string
  }
  interface Link {
    ___href: string
    ___rel: string
  }
  interface Contributor {
    uri?: string
    email?: string
  }
  interface User {
    name?: string
    email?: string
    description?: string
    sex?: string
    birthday?: string
    bloodtype?: string
    address?: string
    birthplace?: string
    hobby?: string
    skil?: UserSkil[]
  }
  interface UserSkil {
    language?: string
    check?: boolean
  }
}

typingsフォルダにいます。(npm run download:typingsでダウンロードしたやつ)

Top.tsx
  const getFeed = async () => {
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d/foo?f')
      if (res.data) {
        setUserFeed(res.data)
      } else {
        setUserFeed([])
      }
    } catch (e) {
      alert('error')
      console.log(e)
    }
  }

次にこのgetFeedは実際にvte.cx側に登録されているデータを呼びsetUserFeedでセットします
今回はfooというエンドポイントを作ったので/d/foo?fで登録されているエントリー全件セットできる。このres.dataにデータが一件もなかった場合、setUserFeedで空をセットする。ちなみにここでハマりました。空をセットする理由としてあとで説明するuseEffectのためです。

Top.tsx
  const getDeleteCheck = (entry_key: string) => {
    if (delete_entry.indexOf(entry_key) >= 0) {
      return true
    } else {
      return false
    }
  }

getDeleteCheck関数はエントリー(entry_key)のkeyを引数にとり
delete_entry(state)で状態を保持してる削除したい値であればtrue、そうでなければfalse。
簡潔に言うとこの関数の役割は、checkboxのcheckedでチェックしたいときにtrue、そうでなければfalseを返せればいい

Top.tsx
const TableCheck: React.FC<ChildProps> = ({
  delete_entry,
  setDeleteEntry,
  entry_key,
  getDeleteCheck
}) => {
  return (
    <>
      <input
        type="checkbox"
        onClick={() => {
          let temp = delete_entry
          if (temp.indexOf(String(entry_key)) >= 0) {
            temp = temp.replace(String(entry_key + ','), '')
          } else {
            temp += entry_key + ','
          }
          setDeleteEntry(temp)
        }}
        checked={getDeleteCheck(entry_key)}
      />
      選択
    </>
  )
}

TableCheckコンポーネント(子)のcheckedで使ってる。

Top.tsx
  useEffect(() => {
    getFeed()
  }, [user_feed])

React Hooksの仲間であるuseEffect

🎉React 16.8: 正式版となったReact Hooksを今さら総ざらいする

このあたりを参考に勉強しました。

これはレンダリング後に行う処理を指定できるフックです。クラスコンポーネントのライフサイクルでいえば、componentDidMount及びcomponentDidUpdateにおおよそ相当するものです

なるほど。
Reactのライフサイクルやコールバック関数が苦手で、曖昧なまま進んでいたら詰まりました。

まずuseEffectは引数を二つ取ります(二つ目は省略可能)
一つ目の引数はコールバック関数でコンポーネントのレンタリング時に呼ばれます。
今回はgetFeed()これを第二引数で制御していないとめちゃめちゃ呼ばれます...汗

なのでgetFeed()を呼びたいタイミングとしてはTopの削除ボタンでデータが消えたとき、
つまりuser_feedの値に変化があったときなので第二引数に指定します。
こうすることで削除後のデータ一覧を非同期で描画できます。

ここで問題だったのが、getFeed()の処理の中で

Top.tsx
        .....
        省略

      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d/foo?f')
      if (res.data) {
        setUserFeed(res.data)
      }

user_feedの値がゼロ件に達するとき(一件しかデータがないとき、そのデータを削除し)getFeed()
通るとuser_feedに変化が起きないので、削除後のデータを非同期で描画できないという沼にはまりました。
useEffectの動きがわかっていれば簡単なことなのですが、elseでres.dataがなくてもsetUserFeedを通してあげることでコンポーネントの再レンタリングが実行され解決しました。

Top.tsx
  const showFeed = () => {
    return (
      <>
        <table>
          <tr>
            <th>名前</th>
            <th>メールアドレス</th>
            <th>説明</th>
            <th>性別</th>
            <th>誕生日</th>
            <th>血液型</th>
            <th>住所</th>
            <th>出身地</th>
            <th>趣味</th>
            <th>特技</th>
            <th colSpan={2}>編集</th>
          </tr>
          {user_feed.map((entry, index) => {
            const no_message = '未登録'
            if (entry.user && entry.id) {
              const entry_key = entry.id && entry.id.split(',')[0] //fooのentry_key取得
              return (
                <>
                  <tr key={index}>
                    <td>{entry.user.name ? entry.user.name : no_message}</td>
                    <td>{entry.user.email ? entry.user.email : no_message}</td>
                    <td>{entry.user.description ? entry.user.description : no_message}</td>
                    <td>{entry.user.sex ? entry.user.sex : no_message}</td>
                    <td>{entry.user.birthday ? entry.user.birthday : no_message}</td>
                    <td>{entry.user.bloodtype ? entry.user.bloodtype : no_message}</td>
                    <td>{entry.user.address ? entry.user.address : no_message}</td>
                    <td>{entry.user.birthplace ? entry.user.birthplace : no_message}</td>
                    <td>{entry.user.hobby ? entry.user.hobby : no_message}</td>
                    <td>
                      {entry.user.skil &&
                        entry.user.skil.map(skil => {
                          return skil.check ? skil.language + ' ' : ''
                        })}
                    </td>
                    <td>
                      <Link to={'/edit' + entry_key}>
                        <button>編集</button>
                      </Link>
                    </td>
                    <td>
                      <TableCheck
                        delete_entry={delete_entry}
                        setDeleteEntry={setDeleteEntry}
                        entry_key={entry_key}
                        getDeleteCheck={getDeleteCheck}
                      />
                    </td>
                  </tr>
                </>
              )
            }
          })}
        </table>
        <DeleteBtn delete_entry={delete_entry} />
      </>
    )
  }

  return <div>{showFeed()}</div>
}

showFeed関数はuseEffectでセットしたデータをテーブルで表示させます
user_feedをmap関数で回しユーザーを描画します。その際にエントリーのIDを編集画面のとき
に必要となるのでkeyだけを変数に格納します。 /foo/101,1このidのカンマより左(key)。
そのkeyを元に編集ボタンを押したとき一意のedit画面に飛ばせる。

Top.tsx
const TableCheck: React.FC<ChildProps> = ({
  delete_entry,
  setDeleteEntry,
  entry_key,
  getDeleteCheck
}) => {
  return (
    <>
      <input
        type="checkbox"
        onClick={() => {
          let temp = delete_entry
          if (temp.indexOf(String(entry_key)) >= 0) {
            temp = temp.replace(String(entry_key + ','), '')
          } else {
            temp += entry_key + ','
          }
          setDeleteEntry(temp)
        }}
        checked={getDeleteCheck(entry_key)}
      />
      選択
    </>
  )
}

const DeleteBtn: React.FC<ChildProps> = ({ delete_entry }) => {
  return (
    <>
      <button
        onClick={() => {
          let delete_feed = delete_entry.split(',')
          if (delete_feed.length > 2) {
            delete_feed.forEach((entry: string) => {
              if (delete_entry) {
                axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
                axios.delete('/d' + entry + '?e')
              }
            })
          } else if (delete_feed.length === 2) {
            axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
            axios.delete('/d' + delete_feed[0] + '?e')
          } else {
            alert('削除するデータが選択されていません')
          }
        }}
        className="delete-button"
      >
        削除
      </button>
    </>
  )
}

TableCheckコンポーネントはクリック時にdelete_entry(state)を変数tempに入れ
entry_key + ","という形で管理し、新しい値が追加されるとreplaceでカンマ区切りで追加。そしてDeleteBtnコンポーネントを押したときに、delete_entryで管理していた値をsplitでカンマ区切りでオブジェクトにし、一括で削除する。

以上の処理がTopコンポーネントで行う、一覧表示、削除の処理です。

Addコンポーネント(新規登録)

Add.tsx
import * as React from 'react'
import Form, { FormProps } from './Form'

const Add: React.FC<FormProps> = () => {
  return <Form type={'add'} button_name={'登録'} />
}

export default Add

Formコンポーネントに新規登録として入れる。

Editコンポーネント(編集)

Edit.tsx
import * as React from 'react'
import Form from './Form'

//編集
const Edit: any = () => {
  const entry_key = location.pathname.split('edit')[1] //fooのURL
  return <Form type={'edit'} entry_key={entry_key} button_name={'更新'} />
}

export default Edit

Formコンポーネントに編集として入れる。
propsでfooのURL(一意のデータ)として入れるのがAddコンポーネントとの違い。

Formコンポーネント

Form.tsx
import * as React from 'react'
import { useState, useEffect } from 'react'
import axios from 'axios'
import { withRouter, RouteComponentProps } from 'react-router-dom'

export type FormProps = {
  type?: string
  entry_key?: string
  button_name?: string
  history?: any
} & RouteComponentProps

const Form: React.FC<FormProps> = ({ type, entry_key, button_name, history }) => {
  //データ登録
  let req: VtecxApp.Entry[] = [
    {
      user: {
        name: '',
        email: '',
        description: '',
        sex: '',
        birthday: '',
        bloodtype: '',
        address: '',
        birthplace: '',
        hobby: '',
        skil: [
          { language: 'HTML', check: false },
          { language: 'CSS', check: false },
          { language: 'JavaScript', check: false }
        ]
      }
    }
  ]

  //追加
  const setEntryKey = async () => {
    let entry_length: Number = 0
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d/foo?f')
      const feed: VtecxApp.Entry[] = res.data
      entry_length = feed.length
    } catch (e) {
      alert('error')
      console.log(e)
    }

    //データ0件のとき
    if (entry_length === 0) {
      try {
        // 採番の初期値を設定
        // eslint-disable-next-line require-atomic-updates
        axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
        await axios.put('/d/foo?_setids=100')
      } catch (e) {
        alert('error:' + e)
      }
    }

    try {
      // カウントアップしてエントリにセット
      // eslint-disable-next-line require-atomic-updates
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.put('/d/foo?_addids=1')
      req = [
        {
          link: [
            {
              ___href: '/foo/' + res.data.feed.title,
              ___rel: 'self'
            }
          ]
        }
      ]
      setEntry([{ user: entry[0].user, id: entry[0].id, link: req[0].link }])
    } catch (e) {
      alert('error:' + e)
    }
  }

  const [entry, setEntry] = useState(req)

  const handleValueChange = (e: any) => {
    e.preventDefault()
    let user = entry[0].user
    //checkbox用 (number 0 html 1 css 2 javascript)
    const isChecked = (type: number) => {
      if (user && user.skil) {
        if (user.skil[type].check) {
          return (user.skil[type].check = false)
        } else {
          return (user.skil[type].check = true)
        }
      }
    }

    if (user) {
      switch (e.target.name) {
        case 'name':
          user.name = e.target.value
          break
        case 'email':
          user.email = e.target.value
          break
        case 'description':
          user.description = e.target.value
          break
        case 'sex':
          user.sex = e.target.value
          break
        case 'birthday':
          user.birthday = e.target.value
          break
        case 'bloodtype':
          user.bloodtype = e.target.value
          break
        case 'address':
          user.address = e.target.value
          break
        case 'birthplace':
          user.birthplace = e.target.value
          break
        case 'hobby':
          user.hobby = e.target.value
          break
        case 'skil':
          if (user.skil) {
            switch (e.target.value) {
              case 'HTML':
                user.skil[0].check = isChecked(0)
                break
              case 'CSS':
                user.skil[1].check = isChecked(1)
                break
              case 'JavaScript':
                user.skil[2].check = isChecked(2)
                break
            }
          }
          break
      }
    }
    setEntry([{ user: user, id: entry[0].id, link: entry[0].link }]) //ここでidとlinkセットしないと消える?
  }

  //保存
  const putEntry = async (e: any) => {
    e.preventDefault()

    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      await axios.put('/d/foo', entry) //保存
    } catch (e) {
      alert('登録に失敗しました')
      console.log(e)
    }
    history.push('/')
  }

  //更新
  const updateEntry = async (e: any) => {
    e.preventDefault()
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      await axios.put('/d/foo?', entry)
    } catch (e) {
      alert('更新に失敗しました')
      console.log(e)
    }

    history.push('/')
  }

  //表示
  const getEntry = async () => {
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d' + entry_key + '?e') // /foo/id
      setEntry([res.data])
    } catch (e) {
      alert('error')
      console.log(e)
    }
  }

  useEffect(() => {
    if (type === 'add') {
      setEntryKey()
    }

    if (type === 'edit') {
      getEntry()
    }
  }, [])

  return (
    <>
      {entry[0].user && (
        <form>
          <TextForm
            name={'name'}
            title_name={'名前'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.name}
          />
          <TextForm
            name={'email'}
            title_name={'メールアドレス'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.email}
          />
          <TextForm
            name={'description'}
            title_name={'説明'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.description}
          />
          <RadioForm
            name={'sex'}
            title_name={'性別'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.sex}
          />
          <TextForm
            name={'birthday'}
            title_name={'誕生日'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.birthday}
          />
          <RadioForm
            name={'bloodtype'}
            title_name={'血液型'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.bloodtype}
          />
          <TextForm
            name={'address'}
            title_name={'住所'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.address}
          />
          <SelectForm
            name={'birthplace'}
            title_name={'出身地'}
            onChange={handleValueChange}
            select_value={entry[0].user.birthplace}
          />
          <TextForm
            name={'hobby'}
            title_name={'趣味'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.hobby}
          />
          <CheckBoxForm
            name={'skil'}
            title_name={'特技'}
            onChange={handleValueChange}
            entry_user_value={entry[0].user.skil}
          />
          <FormButton
            form_click={type === 'edit' ? updateEntry : putEntry}
            button_name={button_name}
          />
        </form>
      )}
    </>
  )
}

type FormChildProps = {
  name?: string
  title_name?: string
  onChange?: (e: any) => void
  entry_user_value?: any
  select_value?: any
  form_click?: any
  button_name?: string
}

/* フォームコンポーネント */
const TextForm: React.FC<FormChildProps> = ({ name, title_name, onChange, entry_user_value }) => {
  return (
    <>
      <p>{title_name}</p>
      <input type="text" name={name} onChange={onChange} value={entry_user_value} />
    </>
  )
}

const RadioForm: React.FC<FormChildProps> = ({ name, title_name, onChange, entry_user_value }) => {
  let form

  //性別
  if (name === 'sex') {
    form = (
      <>
        <p>{title_name}</p>
        男性
        <input
          type="radio"
          name={name}
          value="男性"
          onChange={onChange}
          checked={entry_user_value === '男性' && true}
        />
        女性
        <input
          type="radio"
          name={name}
          value="女性"
          onChange={onChange}
          checked={entry_user_value === '女性' && true}
        />
      </>
    )
  }

  //血液型
  if (name === 'bloodtype') {
    form = (
      <>
        <p>{title_name}</p>
        A
        <input
          type="radio"
          name="bloodtype"
          value="A"
          onChange={onChange}
          checked={entry_user_value === 'A' && true}
        />
        B
        <input
          type="radio"
          name="bloodtype"
          value="B"
          onChange={onChange}
          checked={entry_user_value === 'B' && true}
        />
        O
        <input
          type="radio"
          name="bloodtype"
          value="O"
          onChange={onChange}
          checked={entry_user_value === 'O' && true}
        />
        AB
        <input
          type="radio"
          name="bloodtype"
          value="AB"
          onChange={onChange}
          checked={entry_user_value === 'AB' && true}
        />
      </>
    )
  }

  return <>{form}</>
}

const SelectForm: React.FC<FormChildProps> = ({ name, title_name, onChange, select_value }) => {
  return (
    <>
      <p>{title_name}</p>
      <select name={name} onChange={onChange}>
        <option value="" selected>
          未選択
        </option>
        <option value="北海道" selected={select_value === '北海道' && true}>
          北海道
        </option>
        <option value="東北" selected={select_value === '東北' && true}>
          東北
        </option>
        <option value="関東" selected={select_value === '関東' && true}>
          関東
        </option>
        <option value="中部" selected={select_value === '中部' && true}>
          中部
        </option>
        <option value="近畿" selected={select_value === '近畿' && true}>
          近畿
        </option>
        <option value="中国" selected={select_value === '中国' && true}>
          中国
        </option>
        <option value="四国" selected={select_value === '四国' && true}>
          四国
        </option>
        <option value="九州" selected={select_value === '九州' && true}>
          九州
        </option>
      </select>
    </>
  )
}

const CheckBoxForm: React.FC<FormChildProps> = ({
  name,
  title_name,
  onChange,
  entry_user_value
}) => {
  return (
    <>
      <p>{title_name}</p>
      {entry_user_value.map((skil: any) => {
        return (
          <>
            <input
              type="checkbox"
              name={name}
              onChange={onChange}
              value={skil.language}
              checked={skil.check && true}
            />
            {skil.language}
          </>
        )
      })}
    </>
  )
}

const FormButton: React.FC<FormChildProps> = ({ form_click, button_name }) => {
  return (
    <>
      <p>
        <button onClick={form_click}>{button_name}</button>
      </p>
    </>
  )
}
/* ここまでフォームコンポーネント */

export default withRouter(Form)
// export default Form

Formコンポーネントの全体のコードです。これも順を追って説明していきます。

Form.tsx
import { withRouter, RouteComponentProps } from 'react-router-dom'

今回はreact-router-domでwithRouterを使用します。
いままで画面遷移をしたかったら、<Link to=""></Link>で囲ってあげれば
できていましたが、onClickで処理がある場合はそれでは遷移できなくて
処理を追加してあげる必要があります。

Form.tsx
export default withRouter from "react-router-dom"

exportするときにFormコンポーネントをwithRouterで囲ってあげて

Form.tsx
const Form: React.FC<FormProps> = ({type,entry_key,buttonname,history}) => {

・・・・・
省略
}

Formコンポーネントにhistoryというpropsを渡してあげます。
ちなみにその他のpropsの役割を軽く説明すると

Form.tsx
<FormButton
 form_click={type === 'edit' ? updateEntry : putEntry}
 button_name={button_name}
/>

type button_name
Add(新規登録)かEdit(編集)を区別するために使う

Form.tsx
const res = await axios.get('/d' + entry_key + '?e')

entry_key
Edit時、どのデータを編集するかkeyを用いて判別し初期値をgetするため

Form.tsx
    let req: VtecxApp.Entry[] = [
        {
            user: {
                name: "",
                email: "",
                description: "",
                sex: "",
                birthday: "",
                bloodtype: "",
                address: "",
                birthplace: "",
                hobby: "",
                skil: [{language: "HTML",check: false},{language: "CSS",check: false},{language: "JavaScript",check: false}]
            }
        }
    ]

//・・・・・
//省略

   const [entry,setEntry] = useState(req)



続いて、このreqという変数はentry(state)の初期値をセットするときに使う。
基本的に空なんですが、skilだけarray型のチェックボックスで値を登録したいので
languageに値を入れ、初期値をfalseにしています。(これ合ってるか微妙...)

Form.tsx
  const setEntryKey = async () => {
    let entry_length: Number = 0
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.get('/d/foo?f')
      const feed: VtecxApp.Entry[] = res.data
      entry_length = feed.length
    } catch (e) {
      alert('error')
      console.log(e)
    }

    //データ0件のとき
    if (entry_length === 0) {
      try {
        // 採番の初期値を設定
        // eslint-disable-next-line require-atomic-updates
        axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
        await axios.put('/d/foo?_setids=100')
      } catch (e) {
        alert('error:' + e)
      }
    }

    try {
      // カウントアップしてエントリにセット
      // eslint-disable-next-line require-atomic-updates
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      const res = await axios.put('/d/foo?_addids=1')
      req = [
        {
          link: [
            {
              ___href: '/foo/' + res.data.feed.title,
              ___rel: 'self'
            }
          ]
        }
      ]
      setEntry([{ user: entry[0].user, id: entry[0].id, link: req[0].link }])
    } catch (e) {
      alert('error:' + e)
    }
  }

次にこのsetEntryKey関数の役割は新規登録をするときに採番(keyの値)を設定するものです。
データが1件も登録されていない場合だけ、setidsで初期値は100と設定します。
そしてaddidsでカウントアップし、setEntrylinkに値を追加します。
こうすることで登録されるデータのkeyは+1で登録されていきます。

Form.tsx
  const handleValueChange = (e: any) => {
    e.preventDefault()
    let user = entry[0].user
    //checkbox用 (number 0 html 1 css 2 javascript)
    const isChecked = (type: number) => {
      if (user && user.skil) {
        if (user.skil[type].check) {
          return (user.skil[type].check = false)
        } else {
          return (user.skil[type].check = true)
        }
      }
    }

    if (user) {
      switch (e.target.name) {
        case 'name':
          user.name = e.target.value
          break
        case 'email':
          user.email = e.target.value
          break
        case 'description':
          user.description = e.target.value
          break
        case 'sex':
          user.sex = e.target.value
          break
        case 'birthday':
          user.birthday = e.target.value
          break
        case 'bloodtype':
          user.bloodtype = e.target.value
          break
        case 'address':
          user.address = e.target.value
          break
        case 'birthplace':
          user.birthplace = e.target.value
          break
        case 'hobby':
          user.hobby = e.target.value
          break
        case 'skil':
          if (user.skil) {
            switch (e.target.value) {
              case 'HTML':
                user.skil[0].check = isChecked(0)
                break
              case 'CSS':
                user.skil[1].check = isChecked(1)
                break
              case 'JavaScript':
                user.skil[2].check = isChecked(2)
                break
            }
          }
          break
      }
    }
    setEntry([{ user: user, id: entry[0].id, link: entry[0].link }]) //ここでidとlinkセットしないと消える?
  }

このhandleValueChangeはフォームに値を入力時、switch文でe.target.nameでどの項目か
判別し、e.target.valueで入力された値をセットしていき、最終的にsetEntryで更新する。

Form.tsx
  const putEntry = async (e: any) => {
    e.preventDefault()

    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      await axios.put('/d/foo', entry) //保存
    } catch (e) {
      alert('登録に失敗しました')
      console.log(e)
    }
    history.push('/')
  }

  //更新
  const updateEntry = async (e: any) => {
    e.preventDefault()
    try {
      axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
      await axios.put('/d/foo?', entry)
    } catch (e) {
      alert('更新に失敗しました')
      console.log(e)
    }

    history.push('/')
  }

このputEntryとupdateEntry関数はボタンを押したときにvte.cx側にデータを登録します。
ここで事前に渡していたpropsのhistoryに対してpushとすることで画面遷移したい場所に飛ばせる。
なので登録時データ一覧にいきたいので、history.push('/')と書く。

Form.tsx
const TextForm: React.FC<FormChildProps> = ({ name, title_name, onChange, entry_user_value }) => {
  return (
    <>
      <p>{title_name}</p>
      <input type="text" name={name} onChange={onChange} value={entry_user_value} />
    </>
  )
}

const RadioForm: React.FC<FormChildProps> = ({ name, title_name, onChange, entry_user_value }) => {
  let form

  //性別
  if (name === 'sex') {
    form = (
      <>
        <p>{title_name}</p>
        男性
        <input
          type="radio"
          name={name}
          value="男性"
          onChange={onChange}
          checked={entry_user_value === '男性' && true}
        />
        女性
        <input
          type="radio"
          name={name}
          value="女性"
          onChange={onChange}
          checked={entry_user_value === '女性' && true}
        />
      </>
    )
  }

  //血液型
  if (name === 'bloodtype') {
    form = (
      <>
        <p>{title_name}</p>
        A
        <input
          type="radio"
          name="bloodtype"
          value="A"
          onChange={onChange}
          checked={entry_user_value === 'A' && true}
        />
        B
        <input
          type="radio"
          name="bloodtype"
          value="B"
          onChange={onChange}
          checked={entry_user_value === 'B' && true}
        />
        O
        <input
          type="radio"
          name="bloodtype"
          value="O"
          onChange={onChange}
          checked={entry_user_value === 'O' && true}
        />
        AB
        <input
          type="radio"
          name="bloodtype"
          value="AB"
          onChange={onChange}
          checked={entry_user_value === 'AB' && true}
        />
      </>
    )
  }

  return <>{form}</>
}

const SelectForm: React.FC<FormChildProps> = ({ name, title_name, onChange, select_value }) => {
  return (
    <>
      <p>{title_name}</p>
      <select name={name} onChange={onChange}>
        <option value="" selected>
          未選択
        </option>
        <option value="北海道" selected={select_value === '北海道' && true}>
          北海道
        </option>
        <option value="東北" selected={select_value === '東北' && true}>
          東北
        </option>
        <option value="関東" selected={select_value === '関東' && true}>
          関東
        </option>
        <option value="中部" selected={select_value === '中部' && true}>
          中部
        </option>
        <option value="近畿" selected={select_value === '近畿' && true}>
          近畿
        </option>
        <option value="中国" selected={select_value === '中国' && true}>
          中国
        </option>
        <option value="四国" selected={select_value === '四国' && true}>
          四国
        </option>
        <option value="九州" selected={select_value === '九州' && true}>
          九州
        </option>
      </select>
    </>
  )
}

const CheckBoxForm: React.FC<FormChildProps> = ({
  name,
  title_name,
  onChange,
  entry_user_value
}) => {
  return (
    <>
      <p>{title_name}</p>
      {entry_user_value.map((skil: any) => {
        return (
          <>
            <input
              type="checkbox"
              name={name}
              onChange={onChange}
              value={skil.language}
              checked={skil.check && true}
            />
            {skil.language}
          </>
        )
      })}
    </>
  )
}

const FormButton: React.FC<FormChildProps> = ({ form_click, button_name }) => {
  return (
    <>
      <p>
        <button onClick={form_click}>{button_name}</button>
      </p>
    </>
  )
}

return内の子コンポーネントたちの処理はこんな感じ。
基本的にどの処理もentryのそれぞれの値、onChangeでhandleValueChangeで値を更新していく。
ただこの書き方はバグが生まれやすいのでやめたほうがよい。ここはまだまだ勉強。

以上でFormの処理が終了なのでwebアプリケーション完成である。

vte.cxとFirebaseの違いについて

今回vte.cxを利用して開発したんですが、BaaSは他にも最近噂のFirebaseがあります。
僕もFirebaseはちょっとだけ触ったことがあるんですが(ほとんど初心者レベル、間違っていたらすみません)今回はvte.cxの違いを自分なりの感想を書いていきたいと思います。

まず導入するときに大きな違いがあると思いました。
vte.cxでは

npm install create-vtecx-app

このcreate-vtecx-appだけでReactで開発できるアプリケーションを構築できます。

Firebaseだと既存のアプリケーションにFirebaseを組み込む作業が必要となるので、
なかなか複雑だった気が・・・(勉強不足ですみません)

とにかくこの手軽さは初心者にとってはとても大事だと思います。

他にもデプロイにも違いがあります。
Firebaseではローカルのファイルを変更したら都度デプロイしなければなりませんが、
vte.cxではローカルに変更があった場合、即座にサーバー環境にも反映できます。
都度デプロイの必要がないのはありがたいです。

あと最後に一番ここが良いと思ったのはvte.cxReactに特化しているという点です。
やはりReactは革新的な技術でこれからもどんどん伸びていくと思うので、そのReactを使って
効率的に開発できるのは素晴らしいですよねー

以上、初心者エンジニアがWebアプリケーションを作ってみるでした。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away