React by Examples 第3回: ルーティング, 入力フォーム

ソースコードはこちら; netsphere / react-by-examples · GitLab

今回は Blog もどきを作ってみる。まだ、永続化はしない。また, Redux も使っていない。


スクリーンショット

ルーティング。

パッケージ追加

package.json ファイルに react-router-dom パッケージを追加する。

アプリケーションの雛形を作る。

$ npx create-react-app router-test

ディレクトリを降りて、npm コマンドでパッケージを追加。

$ npm install react-router-dom

npm install コマンドのオプション:

[--save-prod|--save-dev|--save-optional] [--save-exact] [--no-save]

今では --save オプションは不要。もし書いているような記事があったら、それは古いか、古いものを確認もせずコピペしている。

package.json ファイル:

JavaScript
[RAW]
  1. {
  2. "name": "03_router-routing",
  3. "version": "0.1.0",
  4. "author": "Hisashi Horikawa",
  5. "license": "MIT",
  6. "private": true,
  7. "description": "react-router-dom パッケージがルーティングを担当.",
  8. "dependencies": {
  9. "@testing-library/jest-dom": "^4.2.4",
  10. "@testing-library/react": "^9.5.0",
  11. "@testing-library/user-event": "^7.2.1",
  12. "react": "^16.13.0",
  13. "react-dom": "^16.13.0",
  14. "react-router-dom": "^5.1.2",
  15. "react-scripts": "3.4.0"
  16. },
  17. "scripts": {
  18. "start": "react-scripts start",
  19. "build": "react-scripts build",
  20. "test": "react-scripts test",
  21. "eject": "react-scripts eject"
  22. },
  23. "eslintConfig": {
  24. "extends": "react-app"
  25. },
  26. "browserslist": {
  27. "production": [
  28. ">0.2%",
  29. "not dead",
  30. "not op_mini all"
  31. ],
  32. "development": [
  33. "last 1 chrome version",
  34. "last 1 firefox version",
  35. "last 1 safari version"
  36. ]
  37. }
  38. }

今回は "react-router-dom v5.1.2以上" を使っている。react-router v3 から react-router-dom v4.0 (2017年4月) で非互換な変更があり、v3時代の記事は古くて注意が必要。一応, v3.2.6 が2020年3月にリリースされており、v3系列もメンテナンスされているようだ。

さらに v5.0 (2019年3月) が出ているが, v5 は v4 と互換性がある。

Routing

src/App.js ファイルに routing を書く。

JavaScript
[RAW]
  1. import { BrowserRouter, Route,
  2. Switch, // react-router v4: <Switch> が追加された.
  3. NavLink } from 'react-router-dom';
  4. function App() {
  5. // ルーティングは, <BrowserRouter> で囲む.
  6. // <Link> or <NavLink> も <Router> の中に入れなければならない.
  7. // exact を付けないと, 部分一致になる.
  8. return (<BrowserRouter>
  9. <nav>
  10. Posts
  11. <NavLink exact to="/">Home</NavLink>
  12. <NavLink to="/articles/">Articles</NavLink>
  13. <NavLink to="/about">About</NavLink>
  14. </nav>
  15. <Switch>
  16. <Route path="/about" component={About} />
  17. <Route exact path="/articles" component={ArticleList} />
  18. <Route path="/articles/new" component={ArticleEdit} />
  19. <Route exact path="/articles/:id" component={ArticleShow} />
  20. <Route path="/articles/:id/edit" component={ArticleEdit} />
  21. <Route exact path="/" component={Home} />
  22. <Route path="*" render={routeProps => (`Page not found`)} />
  23. </Switch>
  24. </BrowserRouter> );
  25. }

ルーティングは <BrowserRouter> で囲む。通常のテキストや HTMLタグを含めてもよい。

<Switch> 内の <Route> は排他になる。<Switch> で囲まなければ、マッチしたものがすべて描画される。

マッチ条件は path で指定する。"*" は何にでもマッチする。exact を付けると厳密マッチで, 付けなければ部分一致になる。path 末尾の "/" の有無は、よきにしてくれる。

<Link><NavLink> は、あたかも HTML aタグのように, ページ遷移してくれる。履歴 history への追加もよきにしてくれる。 <NavLink> は, to 値がWebブラウザの現在アドレス (現に表示されている) の場合, activeClassName, activeStyle で HTML クラス, HTMLスタイルを変更できる。

入力フォーム

いくつかポイントがある。

  • 値が変更されたときの取扱い
  • 保存時のエラー
  • リダイレクト

先にコードを示す;

JavaScript
[RAW]
  1. handleSubmit = (event) => {
  2. event.preventDefault();
  3. const title = this.state.title.trim();
  4. if (!title)
  5. return;
  6. try {
  7. const article = ArticleModel.create({
  8. id:this.state.id, title:title, body:this.state.body});
  9. this.setState({id: article.id,
  10. redirectToShow: true});
  11. }
  12. catch (err) {
  13. this.setState( {error:err} );
  14. }
  15. }
  16. onFieldChanged = (event) => {
  17. const name = event.target.name;
  18. // checkbox の場合は, 場合分け必要.
  19. const value = event.target.value;
  20. this.setState({[name]:value});
  21. if (name === 'title')
  22. this.setState( {submitEnabled: value.trim() !== ''} );
  23. }
  24. render() {
  25. if ( this.state.redirectToShow ) {
  26. return <Redirect to={`/articles/${this.state.id}`} />; /*/*/
  27. }
  28. return (<div>
  29. {Number(this.state.id)}
  30. {this.state.error ? <div>Error: {this.state.error.message}</div> : ''}
  31. <form onSubmit={this.handleSubmit} >
  32. Title: <input name="title" type="text" value={this.state.title}
  33. onChange={this.onFieldChanged} />
  34. <textarea name="body" value={this.state.body}
  35. onChange={this.onFieldChanged} />
  36. <button type="submit" disabled={this.state.submitEnabled ? '' : 'disabled'}>
  37. {Number(this.state.id) > 0 ? 'Update' : 'Create'}</button>
  38. </form>
  39. </div> );
  40. }
  41. }

フォームを作るとき, 値に変更があるたびに state を更新していくが、フィールドごとにメソッドを作っていては大変。

そこで, render() で, それぞれのHTMLタグに name属性を付けてやる。onFieldChanged では, イベントの target.name プロパティでどのタグか区別できる。このイベントは, DOM Level 2 Events で定義されている。

保存時のエラーは、やはり直接エラーメッセージを表示するのではなく、state にエラー状態を設定してやり、実際の表示は render() 内で行う。

保存が成功したときのリダイレクトも同様。JSX として <Redirect> を返すと、リダイレクトのように動作する。現在アドレスの更新もOK.