はじめに
このところ、更新が滞っていましたが、ようやく少し落ち着いてきたので、研鑽したいと思います。
ここ3回ほど、Vue
を離れて React
を勉強していました。前回、前々回で『スクローラブルリスト』を題材に Vue
と React
の違いを実感しました。
今回はそれをさらに進めて React + Typescript
で『スクローラブルリスト』に挑戦してみたいと思います。
React
で Typescript
を使うための環境構築とプロジェクトの作成からはじまり、前々回のコードをベースに Typescript
に対応するための修正を加えていく進め方で展開したいと思いますので、しばしお付き合いください。
出来上がりはこんな感じ!
作るのは『スクローラブルリスト』なので出来上がりも前々回のものと同じです。 一応、こんな感じです。
前々回の記事はこちら
techblo.shikataramuno.com
今回のコード(React + Typescript版)はここです。
GitHub - Shikataramuno/responsive-scrollable-grid-react-ts: スクローラブルリストをReact + Typescriptでやってみる
Reactの開発環境
まずは React
で Typescript
を使う環境の構築ですが、create-react-app
がインストールされていればOKです。インストール方法は以前の記事を参考にしてください。
Typescriptを使うプロジェクトの作成
create-react-app
がインストールされていれば、次のコマンド一発で Typescript
を使う React
のプロジェクトをつくることができます。とっても便利ですね!
create-react-app project_name --typescript
コマンドを実行するとこんな具合にメッセージが表示されプロジェクト一式が自動生成されます。
PS C:\Shikataramuno_GitHub> create-react-app my-app --typescript Creating a new React app in C:\Shikataramuno_GitHub\my-app. (node:7204) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead. Installing packages. This might take a couple of minutes. Installing react, react-dom, and react-scripts... yarn add v1.3.2 (node:8676) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead. [1/4] Resolving packages... warning react-scripts > fsevents@2.0.6: Please update: there are crash fixes [2/4] Fetching packages... info fsevents@2.0.6: The platform "win32" is incompatible with this module. info "fsevents@2.0.6" is an optional dependency and failed compatibility check. Excluding it from installation. info fsevents@1.2.7: The platform "win32" is incompatible with this module. info "fsevents@1.2.7" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... [4/4] Building fresh packages... success Saved lockfile. warning Your current version of Yarn is out of date. The latest version is "1.16.0" while you're on "1.3.2". info To upgrade, download the latest installer at "https://yarnpkg.com/latest.msi". success Saved 224 new dependencies. ├─ @babel/generator@7.4.4 ├─ @babel/helper-define-map@7.4.4 ├─ @babel/helper-replace-supers@7.4.4 ・・・・ <中略> ・・・・ ├─ yargs-parser@11.1.1 └─ yargs@12.0.5 Done in 158.87s. We detected TypeScript in your project (src\App.test.tsx) and created a tsconfig.json file for you. Your tsconfig.json has been populated with default values. Initialized a git repository. Success! Created my-app at C:\Shikataramuno_GitHub\my-app Inside that directory, you can run several commands: yarn start Starts the development server. yarn build Bundles the app into static files for production. yarn test Starts the test runner. yarn eject Removes this tool and copies build dependencies, configuration files and scripts into the app directory. If you do this, you can’t go back! We suggest that you begin by typing: cd my-app yarn start Happy hacking! PS C:\Shikataramuno_GitHub>
自動生成されるフォルダとファイルの構造はこんな感じです。
C:\SHIKATARAMUNO_GITHUB\MY-APP │ .gitignore │ package.json │ README.md │ tsconfig.json │ yarn.lock │ ├─node_modules │ ├─public │ favicon.ico │ index.html │ manifest.json │ └─src App.css App.test.tsx App.tsx index.css index.tsx logo.svg react-app-env.d.ts serviceWorker.ts
React版の移植
前々回のプロジェクトから src 以下の MemberList.js と MemberList.css を src にコピーします。
コピーするコードは以下の内容です。
import React, { Component } from 'react'; import {Row, Col, Form, ProgressBar} from 'react-bootstrap'; import 'bootstrap/dist/css/bootstrap.min.css'; import './MemberList.css'; export default class MemberList extends Component { columns = ['id', 'name', 'admin', 'address', 'progress']; memberList = []; constructor(props) { super(props); this.state = { members: [], sortOrders: {}, sortKey: "" } } filter = (e) => { const filter = e.target.value; let members = this.memberList; members = members.filter((member) => { return Object.keys(member).some((key) => { return String(member[key]).indexOf(filter) > -1; }); }); this.setState({members: members}) } sortBy = (name) => { let sortOrders = this.state.sortOrders sortOrders[name] = sortOrders[name] * -1; let members = this.state.members; members = members.slice().sort((a,b) => { a = a[name] b = b[name] return (a === b ? 0 : a > b ? 1 : -1) * sortOrders[name]; }); this.setState({sortKey: name}); this.setState({sortOrders: sortOrders}); this.setState({members: members}) } handleAdminChanged = (member) => { console.log(member); let list = this.state.members; const target = list.find(rec => { return rec.id === member.id }) target.admin = !target.admin; this.setState({members: list}) } dhead = () => { return ( <div className="pc table-row header"> ・・・中略・・・ </div> ); } dlist = () => { return ( <div className="data-field"> ・・・中略・・・ </div> ); } list = () => { return ( <div className="container-fluid"> <this.dhead /> <this.dlist /> </div> ); } render () { return <this.list/> } componentDidMount () { let orders = {} this.columns.forEach((key) => { orders[key] = 1; }) this.memberList = [ {id: 1, name: 'aaaa', admin: true, progress: 10, address: 'aaaa@shikataramuno.com'}, ・・・中略・・・ {id: 26, name: 'zzzz', admin: true, progress: 80, address: 'zzzz@shikataramuno.com'} ]; this.setState({sortOrders: orders}); this.setState({members: this.memberList}); } }
次に、src以下にmodelフォルダを作成し、Memer.js sorOrders.js をコピーして ~.ts に拡張子を変更します。 ビルドしてみましょう。
次のようなエラーが発生すると思いますが、いかがですか?
C:/Shikataramuno-GitHub/my-app/src/MenberList.tsx TypeScript error in C:/Shikataramuno-GitHub/my-app/src/MenberList.tsx(10,15): Parameter 'props' implicitly has an 'any' type. TS7006 8 | columns = ['id', 'name', 'admin', 'address', 'progress']; 9 | memberList = []; > 10 | constructor(props) { | ^ 11 | // super(...arguments); 12 | super(props); 13 | this.state = {
これは MemberList のコンストラクタ仮引数の型が暗黙的に any
(なんでもOK)になってるのが原因です。エラーが出て当然ですね!
Typescript
を使う意義の一つは Javascript
で静的型付け言語と同じ様な実装を可能にするとことにあります。すなわち、ビルド段階で静的に型チェックを行い、実行時エラーを極力なくするのが目的の一つです。
その意味では、このエラーが出ないとそれはそれで困る。っというわけです。
ですので、全ての変数宣言、仮引数宣言で型を明示する様にしてやればエラーは解消できます。
では、型を明示するとはどうすれば良いのでしょうか?
対象となる変数には2種類あります。一つは Typescript
で定義されているプリミティブな変数、もう一つはクラスや配列を格納する変数です。
プリミティブ変数の型宣言
プリミティブな変数の型を明示するには、変数宣言の後ろに : 型名
を付けます。
具体的には
20行目のconst filter = …
は const filter: string = …
30行目のlet sortOrders = …
は let sortOrder: number = …
といった具合です。
オブジェクト変数の型宣言
オブジェクト変数の型を明示するには、2段階の定義が必要になります。
まず、クラスベースのオブジェクト指向言語における interface
と同じ要領で interface
でオブジェクトの型を宣言し、プリミティブ変数と同じ要領でオブジェクト変数の宣言時に型を明示します。
Memberオブジェクトを例に、まず src/model/Member.ts にて
interface MemberProps { id: number; name: string; address: string; admin: boolean; progress: number; }
で 'interface' で型を宣言したうえで、src/MemerList.tsx で Member.tsx をインポートし
let member: Member = new Member(・・・
や
handleAdminChanged = (member: Member) => {
といった具合にオブジェクト変数の型を明示します。
Typescript対応版のコード
上記の要領で変数、仮引数宣言で型を明示したコードはこんな感じです。
import * as React from 'react'; import {Row, Col, Form, ProgressBar} from 'react-bootstrap'; import 'bootstrap/dist/css/bootstrap.min.css'; import Member from './model/Member'; import SortOrders from './model/SortOrders'; import './MemberList.css'; import { FormControl } from 'react-bootstrap'; interface State { members: Member[], sortOrders: SortOrders, sortKey: string } interface Props {} interface DHeadProps { sortKey: string; sortOrders: SortOrders; filter(event: React.FormEvent): void; sortBy(name: string): void; } interface DListProps { members: Member[]; handleAdminChanged(member: Member): void; } export default class MemberList extends React.Component<Props, State> { columns: string[] = ['id', 'name', 'admin', 'address', 'progress'] memberList: Member[] = [] constructor(props: Props) { super(props); this.state = { members: [], sortOrders: new SortOrders({id: 1, name: 1, admin: 1, progress: 1, address: 1}), sortKey: "" } } filter = (event: React.FormEvent) => { const filter: string = (event.target as HTMLInputElement).value; let members: Member[] = this.memberList; members = members.filter((member: Member) => { return member.isIncluded(filter) }); this.setState({members: members}) } sortBy = (name: string) => { let order: number = this.state.sortOrders.getOrder(name); console.log('order : ' + order); let members: Member[] = this.state.members; members = members.slice().sort((a: Member,b: Member) => { const aVal: string = a.getValue(this.state.sortKey); const bVal: string = b.getValue(this.state.sortKey); return (aVal === bVal ? 0 : aVal < bVal ? -1 : 1) * order; }); this.state.sortOrders.selectKey(name); this.setState({sortKey: name}); this.setState({members: members}) } handleAdminChanged = (member: Member) => { console.log(member); let list: Member[] = this.state.members; const target: Member = list.find((rec) => { return rec.id === member.id })! target!.admin = !target!.admin; this.setState({members: list}) } dhead = (props: DHeadProps) => { return ( <div className="pc table-row header"> <Row> <Form.Label className="title" >メンバ一覧 </Form.Label> </Row> <Row className='query-box'> <Col xs={2}> <Form.Control as="input" type="text" id="search" className="filter" placeholder="フィルタ文字列" onChange={(e: React.FormEvent) => props.filter(e)}/> </Col> <Col xs={10}></Col> </Row> <div className="wrapper attributes header"> { this.columns.map((name: string, col: number) => { const className: string = props.sortKey === name ? "active " + name : name const arrow = props.sortKey === name ? (props.sortOrders.getOrder(name) > 0? <span className={"arrow asc"}></span> : <span className="arrow dsc"></span>) : ""; return ( <div className={className} key={col} onClick={() => props.sortBy(name, e)}> {name} {arrow} </div> ) }) } </div> </div> ); } dlist = (props: DListProps) => { return ( <div className="data-field"> { props.members.map((member: Member, row: number) => { return ( <div className="table-row wrapper attributes data" key={row}>{ this.columns.map((name: string, idx: number) => { if(name === "admin") { return ( <div className={name} key={idx}> <Form.Check type="checkbox" checked={member.isAdmin()} onChange={() => props.handleAdminChanged(member)} /> </div> ) } else if(name === "progress") { return ( <div className={name} key={idx}> <ProgressBar variant="success" now={member.getProgress()} label={`${member.getValue(name)}%`}/> </div> ) } else { return ( <div className={name} key={idx}> {member.getValue(name)} </div> ) } }) }</div> ); }) } </div> ); } list = () => { return ( <div className="container-fluid"> <this.dhead sortKey={this.state.sortKey} sortOrders={this.state.sortOrders} filter= {this.filter} sortBy= {this.sortBy} /> <this.dlist members = {this.state.members} handleAdminChanged={this.handleAdminChanged} /> </div> ); } render () { return <this.list/> } componentDidMount () { let list: Member[] = [ new Member({id: 1, name: 'aaaa', admin: true, progress: 10, address: 'aaaa@shikataramuno.com'}), ・・・中略・・・ new Member({id: 26, name: 'zzzz', admin: true, progress: 80, address: 'zzzz@shikataramuno.com'}) ]; this.setState({members: list}); } }
変数の型宣言に加えて、2点ほどポイントがあります。
メソッドの型を指定
18,19,23行目で dhead
と dlist
に渡す引数プロパティの型宣言に MemberListクラスのメソッドの型としてシグニチャを宣言しています。
メソッドのシグニチャは メソッド名(仮引数名: 仮引数の型, ...): 戻り値の型
で指定します。
この様に関数に対しても静的な型付けをする必要があるので、インターフェースを変更する機会が多いとコードの修正良も増えてしまうので、初期段階の設計が肝になりそうですね。
イベントの型
37行目でフィルタ文字列の入力イベントハンドラーである filter
メソッドの引数にイベントオブジェクトを定義していて、79行目のdhead
メソッドの JSX
で呼び出しています。
ハンドラーで入力文字列を取得するには、イベントオブジェクトを経由し、event.target.value
にアクセスします。そのため、イベントオブジェクトに適切な型を指定しておかなければなりません。
React
ではブラウザ毎の違いをなくすため、ブラウザ間で共通のイベント型を定義しています(詳しくは以下を参照)。
ここでは 入力フォームのイベント型(React.FormEvent型
)でイベントオブジェクトの型を定義しています。キャプチャするイベントに応じて適切に型を指定する必要があります。
出来上がり!
src/model/Member.ts、sortOrder.ts にも型宣言のための同様の修正を加えます。
npm start
でビルドとローカルテストサイトを立ち上げます。
如何ですか?エラーなくスクローラブルリストが表示され、ソートやフィルタの操作ができましたでしょうか?
謝辞
今回、React
で Typescript
に挑戦してみました。
Vue
よりも、型の扱いが厳格な印象をうけました。特にイベントハンドラの props
など型を明示しないとビルドが通りません。
Typescript
のメリットを生かすことを考えると、厳格な型の指定は重要ですが、変更にともなう影響範囲(修正範囲)が広がる傾向があるように思いました。
次回は別の題材で更に React
を深堀していみたいと思います。
長々と書きましたが、最後までお付き合いいただきありがとうございました。
React
で Typescript
を始める際の参考になれば幸いです。ではでは…