参考にしたチュートリアルは下記のYoutube。これをTypeScript版でやってみたので備忘録。
Learn React - React Crash Course [2019] - React Tutorial with Examples | Mosh
TypeScriptは前に入門書を読んだことあるけどほとんど覚えてなくて、型を付けないといけないという認識がある程度。
Reactは触るの初めて(Vueは触ったことある)
##導入
まずは、プロジェクトを作る場所でcreate react appを導入
$ npx create-react-app my-app --typescript
しばし待つと、諸々インストール完了するので、プロジェクトフォルダに移動
$ cd my-app
中身を確認してみると、基本的なファイルが、一式がTypeScriptで生成されている。
とりあえず、プロジェクトディレクトリでnpm startでローカルホストが立ち上がる。
$ npm start
Compiled successfully!
You can now view little-list in the browser.
Local: http://localhost:3000/
On Your Network: http://192.168.0.12:3000/
Note that the development build is not optimized.
To create a production build, use npm run build.
問題なく動けば、書いてあるように、ブラウザでlocalhost:3000にアクセスすると、プロジェクト初期化状態がレンダリングされる。
ひとまず初期セットアップは完了なので、ここからコードを書いていく。
コーディング
基本触るのはsrcディレクトリなので中身を確認すると、
-App.css
-App.test.tsx
-App.tsx
-index.css
-index.tsx
-logo.svg
-react-app-env.d.ts
-serviceWorker.ts
こんな感じのファイル構成
index.tsxが、起点となるファイルで、初期状態ではメインの記述は、app.tsxに書いてあって、それをimportしている。
コンポーネント作成
ReactDOM.render(<App />, document.getElementById('root'));
チュートリアルでは、App.tsxは触らず、コンポーネントを作って、それをindex.tsxで読み込むようにして進めている。
チュートリアルは、VSCodeを使っててオススメの拡張も教えてくれているのでそのまま使うと良さそう。
Simple React Snippetsを言われるがまま使ってみたけど、TypeScriptにも対応してて、Reactの構文を勝手に補完してくれるので助かる。
基本的な書き方は、コンポーネントのクラスを作って、renderメソッドのreturn文の中に書き出したいJSXを書いていく感じ。
その中で動的な要素が必要なら、Reactの特別プロパティのProps, Stateと、クラスメソッドを書いていって実装する感じ。
import * as React from 'react';
import * as ReactDOM from 'react-dom';
export interface Props {
}
export interface State {
value: number
}
class Counter extends React.Component<Props, State> {
state = {
value: 0,
}
getBadgeClasses = () => {
let badgeClasses = 'badge badge-'
this.state.value === 0 ? badgeClasses += 'warning' : badgeClasses += 'primary'
return badgeClasses;
}
incrementCount = () => {
// console.log('Increment Clicked', this);
this.setState({ value: this.state.value + 1 });
}
formatCount = () => this.state.value === 0 ? 'Zero' : this.state.value;
render() {
return (
<div>
<span className={ this.getBadgeClasses() }>{ this.formatCount() }</span>
<button onClick={ this.incrementCount } className="btn btn-primary btn-sm m-2">increment</button>
</div>
);
}
}
export default Counter;
return文に書くJSXは、一つのタグで囲まれていないとだめなので、外側を,<div>
で囲んでいる。
それが嫌な場合は、<React.Fragment>
で囲むと、DOM上は囲むためのタグが発生せずに済む。
propsとstateの使い分けについて
これが難しい。
・stateはコンポーネント内部からしかアクセスできない。内部から変更はできる。変更するときはsetStateで変更後のstateをセットする。
・propsはコンポーネント間の受け渡しができる。propsの変更はできない。親コンポーネントで、子コンポーネントを呼び出すときにpropsを設定して、渡す。
・親コンポーネントで設定したstateを子コンポーネントでpropsとして受け取るイメージ。子コンポーネントはpropsを直接変更できない。受け取ったpropsをstateにコピーしてそれを変更することは可能。
・その場合、親コンポーネントとは独立したローカルな値になる。
・stateは基本的にトップレベルのコンポーネントで一括管理して、子コンポーネントは、親から受け取ったpropsを元にレンダリングと、親へのイベント受け渡しのみにする方が良いらしい。
コンポーネント連携
作成したコンポーネントは、別ファイルからimportすれば読み込めるようになる。
別のコンポーネントの中で、<Component />
みたいな感じで呼び出せる。
Counterコンポーネントをまとめるコンポーネント
import * as React from 'react';
import { Component } from 'react';
import Counter from './counter';
export interface Props {
}
export interface State {
counters: counterType[]
}
interface counterType {
id: number,
value: number
}
class Counters extends React.Component<Props, State> {
state = {
counters: [
{ id: 1, value: 0 },
{ id: 2, value: 0 },
{ id: 3, value: 0 },
{ id: 4, value: 0 },
]
}
render() {
return (
<div>
{ this.state.counters.map(counter =>
<Counter key={ counter.id } value={ counter.value } id={ counter.id } />) }
</div>
);
}
}
export default Counters;
型については、ちょこちょこググりながらやってるけかなりふわっとした感じ。
interfaceは、型のプレースホルダーみたいなもの。
Arrayの型を指定するときは、Arrayの中身を指定して後ろに[]を付ける。
オブジェクトをArrayに入れるときは、オブジェクトの中身の型を定義した型を作って、それを型にする。
この記事が参考になった。
困ったときは、VSCodeでハイライトするとヒントを教えてくれる。わからなければ、一旦anyを入れておいてやり過ごす。慣れてきたらanyに頼らないようにする。
チュートリアルを進めていき最終的に作ったファイルは、
・couter.tsx (カウンターのコンポーネント)
・couters.tsx (カウンターをまとめて保持するコンポーネント)
・navbar.tsx (全体のカウント数を把握するナビゲーション)
・App.tsx (アプリ全体の収納、管理しているコンポーネント)
・index.tsx (エントリーポイント)
になった。
import * as React from 'react';
interface counterProps {
id: number,
value: number,
onIncrement: any,
onDelete: any,
selected: boolean
}
export interface counterState {
value: number,
selected: boolean,
}
class Counter extends React.Component<counterProps, counterState> {
constructor(props: counterProps) {
super(props);
this.state = {
value: this.props.value,
selected: this.props.selected,
}
}
getBadgeClasses = () => {
let badgeClasses = 'badge badge-'
this.props.value === 0 ? badgeClasses += 'warning' : badgeClasses += 'primary'
return badgeClasses;
}
formatCount = () => this.props.value === 0 ? 'Zero' : this.props.value;
render() {
return (
<div>
<h4>Counter: #{ this.props.id }</h4>
<span className={ this.getBadgeClasses() }>{ this.formatCount() }</span>
<button onClick={ () => this.props.onIncrement(this.props.id) } className="btn btn-secondary btn-sm m-2">increment</button>
<button onClick={ () => this.props.onDelete(this.props.id) } className="btn btn-danger btn-sm m-2">Delete</button>
</div>
);
}
}
export default Counter;
import * as React from 'react';
import Counter from './counter';
export interface countersProps {
counters: counterType[],
onReset: any,
onIncrement: any,
onDelete: any,
}
interface counterType {
id: number,
value: number,
selected: boolean
}
export interface countersState {
}
class Counters extends React.Component<countersProps, countersState> {
render() {
return (
<div>
<button
onClick={ this.props.onReset }
className="btn btn-primary btn-sm m-2"
>Reset</button>
{ this.props.counters.map(counter =>
<Counter
key={ counter.id }
value={ counter.value }
id={ counter.id }
selected={ counter.selected }
onIncrement={ this.props.onIncrement }
onDelete={ this.props.onDelete }
/>
)}
</div>
);
}
}
export default Counters;
import * as React from 'react';
export interface navbarProps {
totalCount: number
}
export interface navbarState {
}
class NavBar extends React.Component<navbarProps, navbarState> {
render() {
return (
<nav className="navbar navbar-light bg-light">
<a href="#" className="navbar-branc">
Navbar
<span className="badge badge-pill badge-secondary">
{ this.props.totalCount }
</span>
</a>
</nav>
);
}
}
export default NavBar;
import * as React from 'react';
import NavBar from './components/navbar';
import Counters from './components/counters';
export interface appProps {
}
export interface appState {
counters: counterType[],
}
interface counterType {
id: number,
value: number,
selected: boolean,
}
class App extends React.Component<appProps, appState> {
state = {
counters: [
{ id: 1, value: 0, selected: true },
{ id: 2, value: 0, selected: false },
{ id: 3, value: 0, selected: false },
{ id: 4, value: 0, selected: false },
]
}
handleIncrement = (counterId: number) => {
const counters = this.state.counters.map(c => {
if (c.id === counterId) {
c.value++;
}
return c;
});
this.setState({ counters: counters });
}
handleDelete = (counterId: number) => {
const counters = this.state.counters.filter(c => c.id !== counterId);
this.setState({ counters: counters });
}
handleReset = () => {
const counters = this.state.counters.map(c => {
c.value = 0;
return c;
});
this.setState({ counters: counters });
}
calculationTotalCount = ():number => {
let result: number = 0;
this.state.counters.forEach(c => {
result += c.value;
});
return result;
}
componentWillUpdate = () => {
}
render() {
return (
<React.Fragment>
<NavBar
totalCount={ this.calculationTotalCount() }
/>
<main className="container">
<Counters
counters={ this.state.counters }
onIncrement={ this.handleIncrement }
onReset={ this.handleReset }
onDelete={ this.handleDelete }
/>
</main>
</React.Fragment>
);
}
}
export default App;
やっぱり英語の動画やドキュメントは良いものが多い。もっと英語の資料を疲れず読めるようになりたい。