- 投稿日:2019-08-21T22:32:34+09:00
Go Signinしたときにtokenを作成する
token作成やsessionの部分でつまずいたのでメモです。
SigninしたときにGo側でtokenを作成し、Javascriptのlocalstorageでtokenの情報を保持します。
データベースにaccountsテーブルがあり、email,password,nameなどその他諸々が入ってます。サインイン画面
Go
フレームワークとしてechoを使用しています。
トークンの中身はとりあえずaccount_idとトークンの有効期間を設定するやつです。handler.gotype token struct { AccountID uint jwt.StandardClaims }signin.go// 画面からメールアドレスとパスワードが送られてくるため、リクエストを受け取る構造体を用意してあげる type loginParam struct { Email string `json:"email"` Password string `json:"password"` } func (h *handler) loginUser(c echo.Context) error { var params loginParam err := c.Bind(¶ms) if err != nil { return err } // リクエストとデータベースのemailが一致するレコードをセットする var account models.Account err = h.DB.Where("email = ?", params.Email).First(&account).Error if err != nil { return err } // リクエストされたemailをもつレコードがデータベースに存在しない場合のエラー処理 if account.ID == 0 { return echo.ErrUnauthorized } // リクエストされたpasswordとデータベースのpasswordが一致しない時のエラー処理 if params.Password != account.Password { return echo.ErrUnauthorized } // トークン作成 claims := &token{ AccountID: account.ID, StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(time.Hour * 72).Unix(), }, } // 暗号化してtokenにしまう token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // 生成された暗号を文字列としてsecretというキーをつけてtにしまう t, err := token.SignedString([]byte("secret")) if err != nil { return err } return c.JSON(http.StatusOK, map[string]string{"token": t}) }React
作成したtokenはlocalstorageに保持します。
emailとpasswordだけを送るようにして、リクエストが通ったらlocalstorageにトークンをセットして、トップページに飛ばします。
通らなかった場合は、emailかpasswordが間違っているというエラー文を画面に表示します。signin.jshandleSubmit() { return http .post('http://localhost:5000/login', { email: this.state.email, password: this.state.password }) .then(response => { // 暗号化されたトークンをlocalStorageにabc_tokenというキーをつけてセット localStorage.setItem('abc_token', response.data.token); this.props.history.push(`/cust/toppage`) return response.data }) .catch(error => { console.log(error) this.setState({ errors: "メールアドレスもしくはパスワードに誤りがあります" }) }) }リクエストを送るときに毎回headersにトークンを詰めなければいけないため、axiosを用いて、http.jsに切り出して呼び出す形にしています。
http.jsimport axios from 'axios'; const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:5000'; // localStorageからabc_tokenというキーでトークンをgetする const http = axios.create({ baseURL: API_HOST, headers: { Authorization: localStorage.getItem('abc_token') || '' } }); export const defaultHttp = axios.create({ headers: { Authorization: localStorage.getItem('abc_token') || '' } }); export default http;検証
abc_tokenというキーがついた、暗号化されたtokenが保持されているのが確認できます
以上です。
今のところ、トークンを作成しただけなので、トークンに紐づいたアカウントの情報をGETする方法などをのちのち追加します。
理解が足りていなくて抜け落ちているところがあるかもしれないので、コメントで教えていただけると幸いです。
- 投稿日:2019-08-21T21:15:05+09:00
expressとreactで作るSPA(1) - reactでhello world!
概要
サーバサイドをexpress、フロントエンドをreactで構成するSPAのWebアプリケーションを作る流れを書いていきます。
Ubuntu 18.04 LTSで必要なものを一からinstallしながら進めていきます。作業ディレクトリを作る
アプリケーションを作るディレクトリを作ります。ディレクトリ名をappとします。
mkdir ~/app cd ~/app git init以下、作業はすべてappディレクトリで行うものとします。
appディレクトリの中はこうなりました。
. └── .git/
.git にはGitの状態を管理するファイルが入っているので触らないように。
reactをインストール
Yarnを使ってreactをインストールします。
yarn add react react-domそうすると何やら動いてappが以下のようになります。
. ├── .git/ ├── node_modules/ ├── package.json └── yarn.lock
package.json と yarn.lock はYarnがパッケージの状態を管理するためのテキスト情報が格納されていて、node_modulesは実際にダウンロードしたパッケージが格納されています。
node_modulesをGit管理対象外にする
作成されたファイル・ディレクトリはGitの管理対象になっています。
$ git status ブランチ master No commits yet 追跡されていないファイル: (use "git add <file>..." to include in what will be committed) node_modules/ package.json yarn.locknode_modulesは、今後もダウンロードしたパッケージがどんどん増えていくのでGitの管理対象になっているとリポジトリのサイズがどんどん大きくなって非常にウザいので、Gitの管理対象から外します。
Gitの管理対象外にするには、.gitignore というファイルを作ってそこに対象外にするファイルのパスを書きます。.gitignorenode_modulesこうするとGitの管理対象から外れます。
$ git status ブランチ master No commits yet 追跡されていないファイル: (use "git add <file>..." to include in what will be committed) .gitignore package.json yarn.lockここまでで、一旦Gitにcommitします。
git add . git commit
コーディング
ReactでHello, world!を出力するコードをコーディングしていきます。
脱create-react-app ~ 真面目に express × react 環境を構築する~ を参考にさせてもらいました。src/client ディレクトリを作成して、その下に以下3つのファイルを作成します。
src/client/index.html<html> <head> <meta charset="UTF-8"> <title>Hello world!</title> </head> <body> <section id="index"></section> </body> </html>src/client/index.jsximport React from 'react'; import ReactDOM from 'react-dom'; import Page from './page'; ReactDOM.render(<Page />, document.getElementById("index"));src/client/page.jsximport React from 'react'; import ReactDOM from 'react-dom'; const Page = () => <div>Hello, World!</div>; export default Page;reactのコードを動かす
webでプログラムを動かすにはwebサーバが必要です。
最終的にはnode.jsで動かすわけなんですが、いきなりそこまでやると大変なので、webpack-dev-serverという開発・デバッグ用のserverを立てて動かします。関連パッケージのinstall
yarn add -D webpack-cli webpack webpack-dev-server html-webpack-plugin yarn add -D @babel/core @babel/cli @babel/preset-env @babel/preset-react yarn add -D babel-loader babel-plugin-module-resolverwebpackの設定ファイル
config/webpack.config.jsconst HtmlWebPackPlugin = require("html-webpack-plugin"); const path = require('path'); const htmlWebpackPlugin = new HtmlWebPackPlugin({ template: "./src/client/index.html", filename: "./index.html" }); module.exports = module.exports = { entry: "./src/client/index.jsx", output: { path: path.resolve('dist'), filename: '[name].js' }, module: { rules: [ { test: /(\.js$|\.jsx$)/, exclude: /node_modules/, use: { loader: "babel-loader" } } ] }, plugins: [htmlWebpackPlugin], resolve: { extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx'], modules: [path.resolve(__dirname, 'src'), 'node_modules'] } };babelの設定ファイル
.babelrc{ "presets": [ "@babel/preset-env", "@babel/preset-react", ], "plugins": [ ["module-resolver", { "root": ["./src/client"] }] ] }以下のコマンドでwebpack-dev-serverを起動
yarn webpack-dev-server --config config/webpack.config.js
ブラウザで http://localhost:8080 にアクセス -> 「Hello, World!」 表示
お疲れ様でした!
参考
Webpack | Configration
Configure Babel
脱create-react-app ~ 真面目に express × react 環境を構築する~
- 投稿日:2019-08-21T18:42:43+09:00
vte.cxによるバックエンドを不要にする開発(その3)
前回=> vte.cxによるバックエンドを不要にする開発(その2)
今回はエントリにユーザ定義項目を追加してデータを表示するところまでをやってみましょう。
エントリに独自項目を追加する
前回の記事では、entryのtitleに
Hello
という文字列を入れるだけでした。
実はtitleはデフォルトで定義されている項目なのでスキーマ定義は必要ありませんでした。
デフォルト項目は基本的にATOMのメタ情報の項目です。それは以下のものがあります。
- author、id、published、updatedなどは自動で値がセットされます
- title、subtitle、summary等はユーザによって自由にセット可能です。
- contentはコンテンツ(リソース本体)の格納場所として使われます。
- linkは別名(alias)やキー(self)として使われます。
- contributorはアクセス権限(ACL)を管理するために使われます。
- rightsは設定情報などで使われます。この項目は暗号化されます。
詳しくは、ドキュメントの方を参照してください。
デフォルト項目以外でユーザが独自の項目を追加したい場合はスキーマ定義が必要になります。
スキーマ定義は、vte.cx管理画面のエントリスキーマ管理タブから行えます。
以下の画面は、userというユーザ定義項目を追加した様子です。項目名(英語)にuser、日本語名にユーザ、親項目選択に最上位を選択して追加ボタンを押すとuser項目が追加されます。
また、userの子項目である、nameとemailを追加することもできます。
例えば以下のような構造のJSONを登録するためのエントリスキーマを作成してみましょう。{ user: { name: "bar", email: "bar@vte.cx" } }項目名にname、日本語名に名前、親項目選択にuserを選択して追加ボタンを押します。
また、項目名にemail、日本語名にメールアドレス、親項目選択にuserを選択して追加ボタンを押します。
すると、エントリ項目一覧では以下のようになります。日本語名やコメントはいつでも更新可能です。
次に、以下のコマンドをターミナルから実行してください。
npm run download:templateこれにより、スキーマ情報がダウンロードされ、ローカルファイル(
setup/settings/template.xml
)が更新されます。中身を覗いてみましょう。template.xml<?xml version="1.0" encoding="UTF-8" ?> <feed> <entry> <content>user name email</content> <link href="/_settings/template" rel="self"/> </entry> <entry> <link href="/_settings/template_property" rel="self"/> </entry> <entry> <link href="/_settings/template_property/user" rel="self"/> <title>ユーザ</title> </entry> <entry> <link href="/_settings/template_property/user.email" rel="self"/> <title>メールアドレス</title> </entry> <entry> <link href="/_settings/template_property/user.name" rel="self"/> <title>名前</title> </entry> </feed>最初のentryのcontentの中で以下のようなスキーマ情報が格納されています。userの下の行に一つスペースを空けてname、その下にemailがあります。一つスペースを空けることで子要素であることを意味します。
user name emailこれを手修正して更新することもできます。修正したら必ず、
npm run upload:template
を実行してサーバを更新してください。ちなみに、サービスを止めることなくスキーマ更新は可能であり登録済のデータが壊れることはありません。ただし、項目名の変更や追加は可能ですが、削除はできません。データをアップロードする
以下のようなJSONデータを/dataフォルダ上に作成し、
npm run upload:data
を実行してください。キーであるlink.___href
が/foo/2
になっていますので、/d/foo/2
に登録されるはずです。sample2.json[{ "user": { "name": "bar", "email": "bar@vte.cx" }, "link": [ { "___href": "/foo/2", "___rel": "self" } ] }]登録されたかブラウザで確認してみましょう。
http://{サービス名}.vte.cx/d/foo?x&f
をブラウザで開いてみてください。以下のように表示されたらアップロード成功です。もし、表示されない場合は、
/d/foo
フォルダが正しく作成されているか確認してください。
管理画面のエンドポイント管理タブのエンドポイント一覧で以下が表示されていれば作成されています。作成されていなければ新規エンドポイント作成を行ってください。(詳しくは、前回の記事を参照)プログラムからデータを取得して表示する
前回のコードを修正して、登録したデータをプログラムから表示させてみましょう。
index.tsximport * as React from 'react' import * as ReactDOM from 'react-dom' import { useState,useEffect } from 'react' import axios from 'axios' const App = () => { const [x, f] = useState(0) const getdata = async () => { try { axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest' const res = await axios.get('/d/foo/2?e') alert(`res= ${res.data.user.name} `); } catch (e) { alert('error') console.log(e) } } useEffect(() => { getdata() }) return ( <div> <button onClick={() => { f(x+1) }}> {x} times </button> </div> ) } ReactDOM.render(<App/>, document.getElementById('container'))
npm run serve:index
を実行するとブラウザが起動し、res.data.user.name
の中身が表示されますので、以下のようにres=bar
が表示されれば成功です。(errorが表示される場合は、npm run serve:login
を実行してログインしてください)型を定義する
最後に、TypeScriptの型を利用した安全なコードの作成について説明します。
まず、
npm run download:typings
で型定義ファイルをダウンロードしてください。vte.cxではダウンロードの際、エントリスキーマの情報を元にTypeScriptの型定義ファイルを自動生成します。
ダウンロードすると、/typings
フォルダの下に、index.d.ts
ファイルが作成されますので、それを開いてみてください。以下のように、ATOM項目とユーザ定義項目が定義されているのがわかります。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 } }次に、先程のソースを編集して型を追加してください。
const entry: VtecxApp.Entry = res.data
が該当の箇所です。
これにより、entry.user.name
がエラーになるので、空チェックを行うif文を追加してください。if (entry.user&&entry.user.name)
これで、より堅牢なコードになりました。index.tsximport * as React from 'react' import * as ReactDOM from 'react-dom' import { useState,useEffect } from 'react' import axios from 'axios' const App = () => { const [x, f] = useState(0) const getdata = async () => { try { axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest' const res = await axios.get('/d/foo/2?e') const entry: VtecxApp.Entry = res.data if (entry.user&&entry.user.name) { alert(`res= ${entry.user.name} `); } } catch (e) { alert('error') console.log(e) } } useEffect(() => { getdata() }) return ( <div> <button onClick={() => { f(x+1) }}> {x} times </button> </div> ) } ReactDOM.render(<App/>, document.getElementById('container'))今回はこれで以上です。お疲れ様でした。
- 投稿日:2019-08-21T09:44:44+09:00
React Hooks でページネーションを実装する
やりたいこと
Hooks がリリースされてから、コンポーネントのシンプルな状態管理に関しては
useState
などで済ませられるようになったが、やや複雑なコンポーネントの状態管理+アルファを Hooks でやってみたかった。つくったもの
今回は
useReducer
を使って市町村名をリストで表示するページネーションを実装してみた。
サンプルコード:https://codesandbox.io/s/paginator-demo-mnxvc
UIフレームワークは Material UI でサクッと用意。useReducer を使った例としては公式のリファレンスでもカウンターなどがありますが、もう少し実用的で、複雑な状態管理と副作用をうまく利用することが要求されるコンポーネントを作って試してみたいと思っていたので今回それをやりました。
ページネーションを題材とした理由は、主に
- コンポーネントの操作で非同期にデータをGETする必要性がある
- ページの始点・終点・中間点等でコンポーネント(「進む」「戻る」などのボタン、ページ位置の表示)の制御を切り替える必要がある
- 実際のWebアプリケーションでも使用される機会が多い
という点。その他に1ページあたりの表示件数とか、データの表示スタイルの切り替えといったアレンジも加えやすいので、思いつく限り今回は機能を盛り込んでみました。
※今回はサンプルコードを全て公開している都合上、公開APIを叩いて非同期でデータをGETするかわりに静的なデータ(市町村名とIDの一覧)を用意し擬似的に何件かずつarray.slice
で切り出して表示するという実装に変えてあります。ソースコードとコンポーネントについて
詳しくは上記の CodeSandbox の中身をみていただくとして、今回はページネーションのコンポーネントとReducerの動きについて軽く説明します。
<Paginator />
コンポーネント初期Stateは以下のように設定しています。
index.jsfunction App() { return ( <div className="App"> <Container fixed> <Paginator sum={cities.length} //データの総件数 per={10} //1ページあたりの表示件数 initialData={cities.slice(0, 10)} //読み込み時に表示するデータ component={ListComponent} //データを渡すPresentaionコンポーネント /> </Container> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement);
<Button>
前へ・次へページの操作を行います。
「前へ」ボタンがクリックされたら前n件のデータとともにviewPreview
という関数を、「次へ」ボタンがクリックされたらviewNext
という関数を発行します。Paginator.jsconst reducer = (state, action) => { switch (action.type) { //[前へ]ボタンをクリック時に発行。 //ページ数のデクリメント case "viewPreview": return { ...state, currentPage: state.currentPage - 1, resourceData: action.data }; //[次へ]ボタンをクリック時に発行。 //ページ数のデクリメント case "viewNext": return { ...state, currentPage: state.currentPage + 1, resourceData: action.data }; . . . } };reducer は発行される直前の
state
とdispatch()
の中身(≒変更するstate)をaction
として引数にとります。変更を加えないstateは...state
とスプレッド演算子を使って return しないと上手くコンポーネントが機能しないため忘れずに。
<Select>
1ページあたりの表示件数の切り替えSelectボックスを変更した場合、
setPerAmount
という関数を発行します。Paginator.js//reducer //表示件数の切り替え時に発行。 //現在のページを1ページ目にリセット。 case "setPerAmount": return { ...state, currentPage: 1, per: action.per, pageAmount: Math.ceil(state.sum / action.per), resourceData: action.data };表示件数を切り替えた場合は一番先頭のデータからまた表示し直すことが良い?気がするので
action
に入っているデータは先頭からn件の内容です。
<Switch>
表示形式(コンポーネントのスタイル)を切り替え初期状態では市町村名とIDが2段組になったリスト形式で表示していますが、スイッチを切り替えることでシンプルな1行のコンポーネントで現在のページの内容を表示し直します。
スイッチの切り替え時にデータを1行ずつ表示するための子コンポーネントとswitchComponent
という関数を発行します。Paginator.js//reducer //表示形式の切り替え時に発行。 case "switchComponent": return { ...state, component: action.component };最後に
公式リファレンスを読むと使い方がなんとなくわかった気になりますが、実務ではカウンターやTodoリスト以上に複雑な機能を扱うことの方が多いためどういう場面で Hooks の力を最大限利用できるのかはまだ手探り中です。今回は
useContext
抜きで実装しましたが、扱うコンポーネントの数が増えてきたら避けては通れなくなるかも。
- 投稿日:2019-08-21T04:53:24+09:00
redux-thunk入門!thunkとは?なぜ必要?新人が書く簡単まとめ
はじめに
(ちょっと分かりやすくように再編しました)
昔ReactとReduxを触ったこと全然ないので、インターンはじめて以来、ただ「こういう風に書くもの」、「このように書くのは正しい」としか認識していない状況でした。そもそもどの部分がredux-thunk
なのかも全然分かってません。
なので、今回はredux-thunk
公式に推奨されてる文章を読んで、自分なりに理解した後、記録してみます。
全文を翻訳するわけではないので、詳細はそちらに参照していただければと思います。
やはり、ライブラリーは何故生まれたのか、どういう問題を解決したのか、それを理解するのは大事なことだと思います。Thunkとは
一般的にいうと、functional programmingのテックニックの一つで、そのまま関数Aを利用するではなく、まず函数Bに変数を提供して、関数Bはそれを使って関数Aの中身を完成させる。最後は完成した関数Aを返す、必要の時でそれを呼び出すみたいな感じ。
コードだとこういう感じ。
function yell (text) { console.log(text + '!') } yell('bonjour') // 'bonjour!' function thunkedYell (text) { return function thunk () { console.log(text + '!') } } const thunk = thunkedYell('bonjour') // まだ実行されてない thunk() // 'bonjour!'で、React / Redux はどういう風にThunkの仕組みを利用しているのかというと、主には
actions
、action creators
、components
が「直接的に」side effectsを起こさせないようにしている。
それらの処理は全部Thunkに包んで、そのあとmiddlewareがThunk呼ぶ時に実行される。
このようの仕組みだと、少なくともMiddlewareレベル以外のところは比較的にpureになるので、メンテナンス、テスト、読みやすさでは役に立つ。基礎&作られた原因
原生のreduxだけ利用すると遭う問題
Reduxのdispatchはaction objectを引数にしてる。
const LOGIN = 'LOGIN' store.dispatch({ type: LOGIN, user: {name: 'Lady GaGa'} })だが、Asyncのリクエストが組み込まれた場合はそれをできなくなる。(Axiosを例としている)
const asyncLogin = () => axios.get('/api/auth/me') .then(res => res.data) .then(user => { // このuserはどうやって利用する? }) // componentのどこかで: store.dispatch(asyncLogin()) // こういう風にはできない; `asyncLogin()` は promise、actionではない解決法
一見では、async handlerの中で
store.dispatch
呼ぶことで解決できるが:import store from '../store' const simpleLogin = user => ({ type: LOGIN, user }) const asyncLogin = () => axios.get('/api/auth/me') .then(res => res.data) .then(user => { //直接Storeに送るか! store.dispatch(simpleLogin(user)) }) // componentのどこかで: asyncLogin()さて、これだといくつかの問題に遭う。
問題1:Inconsistent API
このように書くと、
components
の中の処理は二種類に別れられる。
一つはstore.dispatch(syncActionCreator())
を呼ぶ処理と、
doSomeAsyncThing()
の処理。
それでは一致性を失うことになる。後者の処理ではdispatch関連の部分がどう処理されてるのかのを理解しにくい。
それに、もし処理をsyncからasyncに変更する場合(逆も然り)は、component
の中の関連記述も修正しなければならないので、メンテナンスでは非常に面倒い。問題2:Impurity
前述した
asyncLogin
は明らかにpure function
ではない、それ自体はまあ仕方がないが、component
が直接にside effectsを起こすと、一見では悟りにくいので、メンテナンス上でも、unit testing
上(例えばaxiosをmockする)でもよしとされない。問題3:Tight Coupling
asyncLogin
(action creator)の利用は一つ特定のstore
に限定されている。リユースできない。Thunkでの解決法を試みる
Thunkを使って、network関連の処理は「直ちに」実行させない、
その代わりにThunkの中に包んで、Thunkを返す。import store from '../store' // 今はバインドしているのでTight Coupling const simpleLogin = user => ({ type: LOGIN, user }) const thunkedLogin = () => () => axios.get('/api/auth/me') .then(res => res.data) .then(user => { store.dispatch(simpleLogin(user)) }) // componentのどこかで: store.dispatch(thunkedLogin()) // thunk自体をStoreに送…れるのか?これでは、問題1の
doSomeAsyncThing()
は存在しなくなる、thunkedLogin
も比較的にpureになる(実行される時にThunkを返すだけなので)。でも待って、
action creator
が返すのはaction object
ではないので、Reduxは理解できないはず。それと問題3も放置されたままでは?確かに、なので、これからは
redux-thunk
の出番だ。Redux-Thunk Middleware
redux-thunkがインストールされると、Dispatchはこのように変えられる:
actionOrThunk => typeof actionOrThunk === 'function' ? actionOrThunk(dispatch, getState) : passAlong(actionOrThunk);ようするに:
- Dispatchされたのは一般のaction object
だと、そのまま通す。
- Dispatchされたのは関数(すなわちthunk)になると、その関数にstore
のdispatch
とgetState
を渡して実行させる。これだと、Reduxがもらったものは確か
action object
になる!あとは、Tight Couplingの問題が残っているが、
それもredux-thunk
がdependency injectionを通じて解決できた。どういうことかというと、
前の例では、storeがcomponent
で指定されて、変えられない状態になっている。
で、redux-thunk
はThunkにdispatch
(とあるstoreのdispatch関数)、getState
渡しているので、違うdispatch
を渡すともちろん違うstoreに行く。ちなみに普通のThunkは引数を取らないが、
redux-thunk
ではその型を破った。これで、今まであった問題はすべて解決しました!
終わりに
何回も読みながら情報整理した後、ようやく
redux-thunk
の基礎を理解した…気がしなくてもない。
本来の文章では他の内容ものってるので、よければそちらも参照してみてはいかがでしょうか。
あとは日本語で書くのが時間かかるので疲れてました…
- 投稿日:2019-08-21T00:35:53+09:00
Reactでメモアプリを作る(React基礎講座9)
はじめに
今回は、Reactを使って、メモを追加・削除ができる簡単なメモアプリを作成していきます。
挙動は以下のような感じです。
シリーズ
本記事はReact基礎講座のための連載になっています。気になる方のために、前の章は以下です。
React開発で見かける配列処理系のメソッド map , filter について(React基礎講座8) - Qiita
最初の記事は、以下です。
Reactを使ってJSXの内容をレンダリングする(create-react-app)(React基礎講座1) - Qiita
メモアプリとは
仕様は以下のようなシンプルなReactアプリケーションです。
- テキストエリアにテキストを入力して、入力ボタンを押したらその内容が一覧で表示される
- メモの一覧にはそれぞれ、削除ボタンがあり、それを押したらそのアプリが消える
- メモはDBには格納されない
ファイル構成
root/ ├ public/ └ src/ └ components/ | └ Form.js | └ List.js └ memoApp.js └ index.js
- index.js
React.Component
であるmemoApp
をimportする- MemoApp.js
- stateとして以下の情報を初期設定します
memos
: 具体的なメモの情報nextId
: 次に追加するメモのidの情報
- stateを持たせるので、クラスコンポーネントで実装します
- メモを保存する機能
addMemo
と、削除する機能deleteMemo
を作る
- これら2つの機能が
state
を変更させます- コンポーネント
Form.js
とList.js
をimportする
- これらも今回はクラスコンポーネントで実装しましょう
- /components/Form.js
- メモのフォーム部分の見た目を作る
- /components/List.js
- メモ一覧の見た目を作る
コンポーネントの配置
まずは、ファイル構成どおりにコンポーネントを配置していきましょう。
まずは、
index.js
index.jsimport React from "react"; import { render } from "react-dom"; import MemoApp from "./MemoApp"; render(<MemoApp />, document.getElementById("root"));次に、
MemoApp.js
このコンポーネントから、
<Form />
と<List />
をimportして呼び出していることが分かります。MemoApp.jsimport React from "react"; import { render } from "react-dom"; import Form from "./components/Form"; import List from "./components/List"; class MemoApp extends React.Component { constructor(props) { super(props); } render() { return ( <div> <h2>MemoApp</h2> <Form /> <List /> </div> ); } } export default MemoApp;続いて、
Form
とList
コンポーネントを作成していきます。Form.jsimport React from "react"; import { render } from "react-dom"; class Form extends React.Component { constructor(props) { super(props); } render() { return <h3>Form</h3>; } } export default Form;List.jsimport React from "react"; import { render } from "react-dom"; class List extends React.Component { constructor(props) { super(props); } render() { return <h3>List</h3>; } } export default List;すると、こんな感じで作成できましたかね?これで、下準備というか、コンポーネントの配置は完了です。
MemoAppにStateを定義してListで表示
MemoAppにStateを定義していきます。
前出しましたが、定義するStateは2種類です。
memos
: 具体的なメモの情報
- 配列の中に連想配列を持たせましょう(keyは、
id
とcontent
にしましょう)- これ、最終的にはテキストボックスからcontentを生成しますが、今回はコンポーネントの見た目確認のため、Stateに定義します
nextId
: 次に追加するメモのidの情報
MemoApp
では、<List />
コンポーネント部分にstateのmemos
を渡して、List.js
で表示させるようにしましょう。
List.js
では、props
で、MemoAppにStateとして定義したmemos
を要素全て表示させましょう。
その時に、使用するメソッドは....mapでしたね。それでは、実装していきましょう。まずは、
MemoApp.js
から。MemoApp.jsimport React from "react"; import { render } from "react-dom"; import Form from "./components/Form"; import List from "./components/List"; class MemoApp extends React.Component { constructor(props) { super(props); this.state = { memos: [ { id: 1, content: "one" }, { id: 2, content: "two" }, { id: 3, content: "three" }, { id: 4, content: "four" }, { id: 5, content: "five" } ], nextId: 0 }; } render() { return ( <div> <h2>MemoApp</h2> <Form /> <List memos={this.state.memos} /> </div> ); } } export default MemoApp;こちらは、
State
を定義して、それをState
として<List />
コンポーネントにmemos
として渡します。
List.js
では、MemoApp.js
からState
に乗って渡ってきたmemos
をmap
で要素ごとに処理した結果を変数list
コンポーネントに入れて、その下のrenderメソッドで{list}
として受け取ります。List.jsimport React from "react"; import { render } from "react-dom"; class List extends React.Component { constructor(props) { super(props); } render() { const list = this.props.memos.map(memo => { return ( <li> #{memo.id} - {memo.content} </li> ); }); return ( <div> <h2>List</h2> {list} </div> ); } } export default List;こんな感じですね。見た目は、こんな感じになります。
Form.jsを作成する
今度は、メモを作成する
Form
のコンポーネントを作成します。Form.jsimport React from "react"; import { render } from "react-dom"; class Form extends React.Component { constructor(props) { super(props); this.state = { content: "content" }; } render() { return ( <div> <h2>Form</h2> <input value={this.state.content} /> <input type="submit" value="Add Memo" /> </div> ); } } export default Form;ただ、このままだと、テキストエリアの値がいつまでも変わらないので、テキストエリア内の値が更新された場合、その変更後の値をテキストエリアに表示させます。テキストエリア内の値のstateが変更されたことを検知するイベントハンドラといえば、
onChange
ですね。これは、フォーム内の要素の内容が変更された時に起こるイベントハンドラです。これをハンドラにして、メソッド
handleChange
を呼び出すような処理を書きましょう。handleChange
関数の引数にevent
をようして、関数内でevent.target.value
と記述すると、イベントの結果更新された値を取得することができます。それを変数content
として格納する。その変数(
content
)を使って、setState
してフォーム内の値(state)を更新します。つまり、handleChange
関数を使って自分自身の値を更新させていることがわかります。それでは、実装していきましょう。
Form.jsimport React from "react"; import { render } from "react-dom"; class Form extends React.Component { constructor(props) { super(props); this.state = { content: "content" }; } render() { return ( <div> <h2>Form</h2> <input value={this.state.content} onChange={this.handleChange} /> <input type="submit" value="Add Memo" /> </div> ); } handleChange = event => { const content = event.target.value; this.setState({ content: content }); }; } export default Form;次に、
submit
した際の挙動を書いていきましょう。input
タグをform
タグで囲って、<input type="submit" value="Add Memo" />
をクリックした際の処理(関数を)form
の開始タグに記述します。その際のハンドラは、onSubmit
ですね。onSubmit
の詳細は、以下の記事を見て見てください。参考
https://www.sejuku.net/blog/28720
onSubmit
で検知した際に、処理させる関数名はhandleSubmit
とでもしましょう。今回も引数event
を受け取るようにしましょう。処理内容は、一旦以下のようにします。
- まずは、submitをクリックした際のデフォルトの挙動をしないようにする
- 次に、アラートにて、現状の
this.state.content
の値を表示させるようにする- アラートで、現状のcontentを表示させたら、現状のテキストエリアの値は何も無いことにします
こんな感じです。では、実装していきましょう。
Form.jsimport React from "react"; import { render } from "react-dom"; class Form extends React.Component { constructor(props) { super(props); this.state = { content: "content" }; } render() { return ( <div> <h2>Form</h2> <form onSubmit={this.hamdleSubmit}> <input value={this.state.content} onChange={this.handleChange} /> <input type="submit" value="Add Memo" /> </form> </div> ); } handleChange = event => { const content = event.target.value; this.setState({ content: content }); }; hamdleSubmit = event => { event.preventDefault(); alert(this.state.content); this.setState({ content: "" }); }; } export default Form;こんな感じですね。挙動は、こんな感じになります。
ここまでできれば一旦OKです。
なので、
Form.js
のコンストラク内のstateのcontentで定義している文字列は空白でもいいかもですね。次は、いよいよ、アラートを出すのではなく、実際に、stateを変更して、
List
コンポーネントを変更するような機能を作成していきます。stateを変更する機能を作成する
今度は、テキストフォームにメモの内容を記述したら、
List
コンポーネントにその値が反映されるようにしましょう。手順は以下のような感じです。
- (色々なやり方がありますが今回は)
MemoApp.js
でテキストを入力してボタンを押したら
- 関数(
addMemo
)が走るようにしましょう
- 関数(
addMemo
)自体は、
setState
が走るようにして- その中でstate(memosの配列に
Spread operator
で既存のものを残しつつ、新たな要素を追加します)- contentの内容は
Form.js
内のhamdleSubmit
関数の中で引数として渡します- 宣言元の
addMemo
は引数としてhamdleSubmit
関数から渡ってきたメモ内容(this.state.content
)を引数(content
)として受け取って、Spread operator
(3点)の後にカンマして、あらたなオブジェクトに渡す- そして、idも
this.state.nextId
もセットして、その後のsetState
内で 1たす項目を設けます- 既存のstate(
memos
)にsubmit
した際、要素が加われば、コンストラクタで定義した既存のstate(memos
)は消しましょうJSでは、
Spread operator
(スプレッド構文)は頻出シンタックスなのでぜひ覚えてください。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax
また、今回実装した
state
を変更するメソッド(addMemo
)を親(MemoApp.js
)に持たせて、それを子(Form.js
)に渡して実行する方法はよくあるので難しいですが手に馴染ませてください。子に渡っているのは
hamdleSubmit
関数の中のthis.props.addMemo(this.state.content);
← この部分ですね。それでは、実装していきましょう。
MemoApp.jsimport React from "react"; import { render } from "react-dom"; import Form from "./components/Form"; import List from "./components/List"; class MemoApp extends React.Component { constructor(props) { super(props); this.state = { memos: [], nextId: 0 }; } addMemo = content => { this.setState({ memos: [...this.state.memos, { id: this.state.nextId, content: content }], nextId: this.state.nextId + 1 }); }; render() { return ( <div> <h2>MemoApp</h2> <Form addMemo={this.addMemo} /> <List memos={this.state.memos} /> </div> ); } } export default MemoApp;Form.jsimport React from "react"; import { render } from "react-dom"; class Form extends React.Component { constructor(props) { super(props); this.state = { content: "" }; } render() { return ( <div> <h2>Form</h2> <form onSubmit={this.hamdleSubmit}> <input value={this.state.content} onChange={this.handleChange} /> <input type="submit" value="Add Memo" /> </form> </div> ); } handleChange = event => { const content = event.target.value; this.setState({ content: content }); }; hamdleSubmit = event => { event.preventDefault(); this.props.addMemo(this.state.content); this.setState({ content: "" }); }; } export default Form;こんな感じですね。挙動は、こんな感じになります。
stateを削除する機能を作成する
次は、個々のメモを削除するような機能を作成していきます。
手順は以下のような感じです。
List.js
のコンポーネントにタグを設置- 機能自体は
MemoApp.js
のaddMemo
の下にdeleteMemo
メソッドを作成していきましょう
deleteMemo
メソッドは引数にmemosのid
を取れるようにしてください- では、上記の呼び出しを
List.js
に定義したbuttonタグにハンドラはonClick
として、アロー関数をdeleteMemo
メソッドのコールバックとして実行してください- コール先の関数の中の処理ですが
- 配列(
memos
)をfilter
で処理して、引数として受け取ったid以外をTRUE
として返すような条件でにして、結果を変数filteredArray
に格納し、それをmemosのsetState
として渡すそれでは、実装していきましょう。
MemoApp.jsimport React from "react"; import { render } from "react-dom"; import Form from "./components/Form"; import List from "./components/List"; class MemoApp extends React.Component { constructor(props) { super(props); this.state = { memos: [], nextId: 0 }; } addMemo = content => { this.setState({ memos: [...this.state.memos, { id: this.state.nextId, content: content }], nextId: this.state.nextId + 1 }); }; deleteMemo = id => { const filteredArray = this.state.memos.filter(memo => { return memo.id !== id; }); this.setState({ memos: filteredArray }); }; render() { return ( <div> <h2>MemoApp</h2> <Form addMemo={this.addMemo} /> <List memos={this.state.memos} deleteMemo={this.deleteMemo} /> </div> ); } } export default MemoApp;react.List.jsimport React from "react"; import { render } from "react-dom"; class List extends React.Component { render() { const list = this.props.memos.map(memo => { return ( <li> #{memo.id} - {memo.content}{" "} <button onClick={() => this.props.deleteMemo(memo.id)}>delete</button> </li> ); }); return ( <div> <h2>List</h2> <ul>{list}</ul> </div> ); } } export default List;こんな感じですね。挙動は、こんな感じになります。
冗長な部分をリファクタ
冗長な書き方になってしまっている部分を
const { ... } = this.state;
(props
でも同様)と定義してリファクタしてあげましょう。また、この記述
import { render } from "react-dom";
が不要なファイルはあるので、不要であれば消します。そして、
Warning: Each child in a list should have a unique "key" prop.というエラー対応をします。具体的には、
タグにList
コンポーネント’にあるid
を追加します。<li key={memo.id}>こんな感じですね。
それでは、実装していきましょう。
MemoApp.jsimport React from "react"; import Form from "./components/Form"; import List from "./components/List"; class MemoApp extends React.Component { constructor(props) { super(props); this.state = { memos: [], nextId: 0 }; } addMemo = content => { const { memos, nextId } = this.state; this.setState({ memos: [...memos, { id: nextId, content: content }], nextId: this.state.nextId + 1 }); }; deleteMemo = id => { const { memos } = this.state; const filteredArray = memos.filter(memo => { return memo.id !== id; }); this.setState({ memos: filteredArray }); }; render() { const { memos } = this.state; return ( <div> <h2>MemoApp</h2> <Form addMemo={this.addMemo} /> <List memos={memos} deleteMemo={this.deleteMemo} /> </div> ); } } export default MemoApp;List.jsimport React from "react"; class List extends React.Component { render() { const { memos, deleteMemo } = this.props; const list = memos.map(memo => { return ( <li key={memo.id}> #{memo.id} - {memo.content}{" "} <button onClick={() => deleteMemo(memo.id)}>delete</button> </li> ); }); return ( <div> <h2>List</h2> <ul>{list}</ul> </div> ); } } export default List;長くなりましたが、Reactを使ったメモアプリ作成は以上です。参考にしてみてください。
参考
- 改訂新版JavaScript本格入門 ~モダンスタイルによる基礎から現場での応用まで | 山田 祥寛