TypeScript学習2(ファンクション周りでの扱い)
ファンクション周りでのannotations/inference使い分け
Type annotations for functions
Code we add to tell TS what type of arguments a function will receive and what type of values it will return.
Type inference for functions
TS tries to figure out what type of value a function will return.
Type inferenceは返却値はファンクションにおいて型付けはしてくれるが、引数に関しては型宣言もしくは初期値の割当が必要
つまり、ファンクションを宣言する際は、このしきたりに従うようにする。
const add = (a: number, b: number) => { return a + b; };
即時関数での記述方法
特に大きな違いはない。
function divide(a: number, b: number) { return a / b; } const multiply = function(a: number, b: number) { return a * b; };
返却値が存在しないファンクション
voidを指定してあげることで、返却をさせないことができる。
const logger = (message: string): void => { console.log(message); };
Argument destructuring
const forecast = { date: new Date(), weather: 'sunny' }; const logWeather = (forecast: { date: Date; weather: string }): void => { console.log(forecast); }; // ES2015 const logWeatherDestructured = ({ date, weather }: { date: Date; weather: string; }): void => { console.log(date); console.log(weather); };
Object destructuring
const profile = { personName: 'alex', age: 20, coords: { lat: 0, lng: 15 }, setAge(age: number): void { this.age = age; } }; const { age, personName }: { age: number; personName: string } = profile; const { coords: { lat, lng } }: { coords: { lat: number; lng: number } } = profile;
TypeScript学習1(Type annotations, Type inference)
重要な概念を2つほど。
Type annotations / Type inference
Type annotations
Code we add to tell TypeScript what type of value a variable will refer to
Type inference
TypeScript tries to figure out what type of value a variable refers to
前者はコードでTSに明示的に型付けをさせているのに対して、 後者はTS側で型付けを自動的に行っているというものである。
Type annotations
型アノテーションの一例を以下に示す。
// Basic primitives const apples: number = 5; const speed: string = 'fast'; const hasName: boolean = true; const nothingMuch: null = null; const nothing: undefined = undefined; // Built-in objects const now: Date = new Date(); // Array const colors: string[] = ['red', 'green', 'blue']; const myNumbers: number[] = [1, 2, 3]; const truths: boolean[] = [true, true]; // Classes class Car {} const car: Car = new Car(); // Object literal const point: { x: number; y: number } = { x: 10, y: 20 }; // Function const logNumber: (i: number) => void = (i: number) => { console.log(i); }; // : (i: number) => void Description of a function
ここまで例を述べてきたが、実は上記の型アノテーションの記述は全て除却しても全く同様にはたらく。 なぜかというと、TS側で全ての型について当ててくれるから。
Type inference
宣言と初期化が同じ行に存在すれば、TS側で勝手に型を割り当ててくれるというもの。
const color = 'red'; // variable declaration = variable initialization
なので、必ず同じ行に宣言と初期化を行うこと。
使い分け
Type annotations
- 変数宣言を行い、違う行で初期化を行う場合
- TSの方で自動的に型の割当を行えないという場合
- ファンクションが
any
型を返却し、値を明確にしないといけない場合 any
になってしまう場合(TSでのチェックを通過してしまう場合)
anyになってしまうことをtype annotationsを記述することで避ける!
string | JSON.parse() |
---|---|
'false' | boolean |
'4' | number |
'{ "value": 5 }' | {value: number} |
'{ "name": "alex" }' | {name: string} |
// When to use annotations // 1) Function that returns the 'any' type const json = '{"x": 10, "y": 20}'; const coords: { x: number; y: number } = JSON.parse(json); console.log(coords); // 2) When we declare a variable on one line and initialize it later let words = ['red', 'green', 'blue']; let foundWord: boolean; // or just use `let foundWord = false` for (let i = 0; i < words.length; i++) { if (words[i] === 'green') { foundWord = true; } } // 3) Variables whose type cannot be inferred correctly let numbers = [-10, -1, 12]; let numberAboveZero: boolean | number = false; for (let i = 0; i < numbers.length; i++) { if (numbers[i] > 0) { numberAboveZero = numbers[i]; console.log(numberAboveZero); } }
Type inference
上記以外は、ほとんどこちらを使うことになる。
まとめ
Type inferenceはconst
的に常に使い、それ以外の場合はlet
を使うような要領でType annotationsを使う。
React学習7(機能追加)
外部データから取得して一覧表示して、Like付けたり削除できたりできるところまで。
ファイル構成
. ├── App.js ├── Vidly.jsx ├── components │ ├── atoms │ │ └── Like.jsx │ └── molecules │ └── Movies.jsx ├── data │ └── services │ ├── fakeGenreService.js │ └── fakeMovieService.js └── index.js
ファイル中身
Vidly.jsx
import React, { Component } from 'react' import { getMovies } from './data/services/fakeMovieService' import Movies from './components/molecules/Movies' class Vidly extends Component { state = { movies: getMovies(), } constructor() { super() const { movies } = this.state this.state.movies = movies.map(movie => { movie.liked = false return movie }) } render() { const { movies } = this.state return ( <div className="container"> <main className="container" role="main"> <h1>Vidly</h1> <Movies movies={movies} onLike={this.handleLike} onDelete={this.handleDelete} /> </main> </div> ) } handleLike = movie => { const likedMovies = [...this.state.movies] const movieIndex = likedMovies.findIndex(m => m === movie) likedMovies[movieIndex].liked = !likedMovies[movieIndex].liked this.setState({ movies: likedMovies, }) } handleDelete = movie => { const filteredMovies = [...this.state.movies].filter(m => { return m !== movie }) this.setState({ movies: filteredMovies, }) } } export default Vidly
Movies.jsx
import React from 'react' import Like from '../atoms/Like' const Movies = ({ movies, onLike, onDelete }) => { if (movies.length === 0) return <p>There are no movies!</p> return ( <> <p>There are {movies.length} movies!</p> <table className="table"> <thead> <tr> <th scope="col">id</th> <th scope="col">Title</th> <th scope="col">Genre</th> <th>Like</th> <th></th> </tr> </thead> <tbody> {movies.map((movie, index) => ( <tr key={index}> <th scope="row">{movie._id}</th> <td>{movie.title}</td> <td>{movie.genre.name}</td> <td> <span onClick={() => onLike(movie)} style={{ display: 'inline-block', cursor: 'pointer' }} > <Like movie={movie} /> </span> </td> <td> <button onClick={() => onDelete(movie)} type="button" className="btn btn-danger" > Delete </button> </td> </tr> ))} </tbody> </table> </> ) } export default Movies
Like.jsx
import React from 'react' const Like = ({ movie }) => { return <i className={movie.liked ? 'fas fa-heart' : 'far fa-heart'} /> } export default Like
fakeMovieService.js
import * as genresAPI from './fakeGenreService' const movies = [ { _id: '5b21ca3eeb7f6fbccd471815', title: 'Terminator', genre: { _id: '5b21ca3eeb7f6fbccd471818', name: 'Action' }, numberInStock: 6, dailyRentalRate: 2.5, publishDate: '2018-01-03T19:04:28.809Z', }, { _id: '5b21ca3eeb7f6fbccd471816', title: 'Die Hard', genre: { _id: '5b21ca3eeb7f6fbccd471818', name: 'Action' }, numberInStock: 5, dailyRentalRate: 2.5, }, { _id: '5b21ca3eeb7f6fbccd471817', title: 'Get Out', genre: { _id: '5b21ca3eeb7f6fbccd471820', name: 'Thriller' }, numberInStock: 8, dailyRentalRate: 3.5, }, { _id: '5b21ca3eeb7f6fbccd471819', title: 'Trip to Italy', genre: { _id: '5b21ca3eeb7f6fbccd471814', name: 'Comedy' }, numberInStock: 7, dailyRentalRate: 3.5, }, { _id: '5b21ca3eeb7f6fbccd47181a', title: 'Airplane', genre: { _id: '5b21ca3eeb7f6fbccd471814', name: 'Comedy' }, numberInStock: 7, dailyRentalRate: 3.5, }, { _id: '5b21ca3eeb7f6fbccd47181b', title: 'Wedding Crashers', genre: { _id: '5b21ca3eeb7f6fbccd471814', name: 'Comedy' }, numberInStock: 7, dailyRentalRate: 3.5, }, { _id: '5b21ca3eeb7f6fbccd47181e', title: 'Gone Girl', genre: { _id: '5b21ca3eeb7f6fbccd471820', name: 'Thriller' }, numberInStock: 7, dailyRentalRate: 4.5, }, { _id: '5b21ca3eeb7f6fbccd47181f', title: 'The Sixth Sense', genre: { _id: '5b21ca3eeb7f6fbccd471820', name: 'Thriller' }, numberInStock: 4, dailyRentalRate: 3.5, }, { _id: '5b21ca3eeb7f6fbccd471821', title: 'The Avengers', genre: { _id: '5b21ca3eeb7f6fbccd471818', name: 'Action' }, numberInStock: 7, dailyRentalRate: 3.5, }, ] export function getMovies() { return movies } export function getMovie(id) { return movies.find(m => m._id === id) } export function saveMovie(movie) { let movieInDb = movies.find(m => m._id === movie._id) || {} movieInDb.name = movie.name movieInDb.genre = genresAPI.genres.find(g => g._id === movie.genreId) movieInDb.numberInStock = movie.numberInStock movieInDb.dailyRentalRate = movie.dailyRentalRate if (!movieInDb._id) { movieInDb._id = Date.now() movies.push(movieInDb) } return movieInDb } export function deleteMovie(id) { let movieInDb = movies.find(m => m._id === id) movies.splice(movies.indexOf(movieInDb), 1) return movieInDb }
fakeGenreService.js
export const genres = [ { _id: '5b21ca3eeb7f6fbccd471818', name: 'Action' }, { _id: '5b21ca3eeb7f6fbccd471814', name: 'Comedy' }, { _id: '5b21ca3eeb7f6fbccd471820', name: 'Thriller' }, ] export function getGenres() { return genres.filter(g => g) }
React学習6(リファクタリング)
カウンターコンポーネントを最終的にリファクタリングしたもの。
ファイル構成
. ├── App.jsx ├── components │ ├── Counter.jsx │ ├── Counters.jsx │ └── Navbar.jsx └── index.js
ファイルの中身
App.jsx
import React, { Component } from 'react' import _ from 'lodash' import Navbar from './components/Navbar' import Counters from './components/Counters' class App extends Component { state = { counters: [ { id: 1, value: 2 }, { id: 2, value: 0 }, { id: 3, value: 3 }, { id: 4, value: 4 }, ], } render() { const { counters } = this.state return ( <div className="App"> <Navbar totalCounters={counters.length > 0 && counters.length} /> <div className="container"> <Counters counters={counters} onReset={this.handleReset} onIncrement={this.handleIncrement} onDecrement={this.handleDecrement} onDelete={this.handleDelete} /> </div> </div> ) } handleIncrement = counter => { this.handleValueChange(counter, 'inc') } handleDecrement = counter => { this.handleValueChange(counter, 'dec') } handleValueChange = (counter, flag) => { const { counters } = this.state const clickedCounterIndex = counters.findIndex(c => c.id === counter.id) const countersChanged = _.cloneDeep(counters) if (flag === 'inc') { countersChanged[clickedCounterIndex].value++ } if (flag === 'dec') { if (countersChanged[clickedCounterIndex].value === 0) return countersChanged[clickedCounterIndex].value-- } if (flag === 'inc' || flag === 'dec') { this.setState({ counters: countersChanged, }) } } handleReset = () => { const counters = this.state.counters.map(c => { c.value = 0 return c }) this.setState({ counters, }) } handleDelete = counter => { const { counters } = this.state const filteredCounters = counters.filter(c => { return c !== counter }) this.setState({ counters: filteredCounters, }) } } export default App
Counters.jsx
import React, { Component } from 'react' import Counter from './Counter' class Counters extends Component { render() { const { counters, onReset, onIncrement, onDecrement, onDelete } = this.props if (counters.length === 0) return <p>Nothing!</p> return ( <> <button onClick={onReset} className="btn btn-primary btn-sm m-2"> Reset </button> {counters.map(counter => ( <Counter key={counter.id} value={counter.value} selected={true} onIncrement={() => onIncrement(counter)} onDecrement={() => onDecrement(counter)} onDelete={() => onDelete(counter)} disabled={counter.value === 0} /> ))} </> ) } } export default Counters
Counter.jsx
import React, { Component } from 'react' class Counter extends Component { render() { return <>{this.renderCounter()}</> } renderCounter() { const { onIncrement, onDecrement, onDelete, disabled } = this.props return ( <div className="row"> <div className="col-1" style={{ minWidth: '70px' }}> <span className={this.getBadgeClasses()}>{this.formatCount()}</span> </div> <div className="col"> <button onClick={onIncrement} className="btn btn-secondary btn-sm m-2" > + </button> <button onClick={onDecrement} className={`btn btn-secondary btn-sm m-2${ disabled ? ' disabled' : '' }`} > − </button> <button onClick={onDelete} className="btn btn-danger btn-sm m-2"> × </button> </div> </div> ) } getBadgeClasses() { let classes = 'badge m-2 badge-' classes += this.props.value === 0 ? 'warning' : 'primary' return classes } formatCount() { const { value } = this.props return value === 0 ? 'Zero' : value } } export default Counter
Navbar.jsx
import React from 'react' const Navbar = ({ totalCounters }) => { return ( <> <nav className="navbar navbar-light bg-light"> {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} <a className="navbar-brand" href="#"> Navbar <span className="badge badge-pill badge-info">{totalCounters}</span> </a> </nav> </> ) } export default Navbar
React学習5(lifecycle hooks)
Lifecycle Hooks
ReactのLifecycle hooksでよく用いられるものはだいたい次の通り。
- Mount
constructor
render
componentDidMount
- Update
render
componentDidUpdate
- Unmount
componentDidUnmount
Mounting phase
constructor
, render
, componentDidMount
の順番で呼ばれる。
render
内で読み込まれる子孫コンポーネントは、親から子へと下るように呼ばれる。
Lifecycle.jsx
import React, { Component } from 'react' class Lifecycle extends Component { state = { something: '', } constructor(props) { super(props) console.log('Lifecycle - Constructor') // `this.setState()` cannot be used here since it can be used after rendering this.state = this.props.something } render() { // Every children components are rendered recursively console.log('Lifecycle - Rendered') // return <>Something</> } componentDidMount() { console.log('Lifecycle - Mounted') // Make ajax call and set states this.setState({ something: 'something' }) } } export default Lifecycle
Updating phase
あるコンポーネントのstateやpropsに変更が生じたときに呼ばれるフェーズ。 つまり再度renderがされ、Virtual DOMで変更された部分だけがアップデートされる。
componentDidMount
内で直前のstateやpropsの変化を検知して、ajaxリクエストなどを投げる
などというような使い方ができる。
Lifecycle.jsx
import React, { Component } from 'react' class Lifecycle extends Component { state = { something: { value: 0, }, } render() { console.log('Lifecycle - Rendered') } componentDidUpdate(prevProps, prevState) { console.log('prevProps', prevProps) console.log('prevState', prevState) if (prevProps.something.value !== this.props.something.value) { // Make ajax call and get new data from the server } } } export default Lifecycle
Unmounting phase
タイマー処理やイベントリスナーの解除などを行うときによく使用されるフェーズ。
import React, { Component } from 'react' class Lifecycle extends Component { state = { something: { value: 0, }, } render() { console.log('Lifecycle - Rendered') } componentDidUpdate(prevProps, prevState) { console.log('prevProps', prevProps) console.log('prevState', prevState) if (prevProps.something.value !== this.props.something.value) { // Make ajax call and get new data from the server } } componentWillUnmount() { console.log('Lifecycle - Unmount') // Clean up times or listeners } } export default Lifecycle
React学習4(functional component)
React復習がてら要点をまとめていく。
React学習3で学んだところから、さらに階層深くコンポーネント構成をする、次のようなケースの場合。
. ├── App.jsx ├── components │ ├── Counter.jsx │ ├── Counters.jsx │ └── Navbar.jsx └── index.js
Objectやargument destructuringもふんだんに利用しながら記述していく。
App.jsx
import React, { Component } from 'react' import _ from 'lodash' import Navbar from './components/Navbar' import Counters from './components/Counters' class App extends Component { state = { counters: [ { id: 1, value: 2 }, { id: 2, value: 0 }, { id: 3, value: 3 }, { id: 4, value: 4 }, ], } render() { const { counters } = this.state return ( <div className="App"> <Navbar totalCounters={counters.length > 0 && counters.length} /> <div className="container"> <Counters counters={counters} onReset={this.handleReset} onIncrement={this.handleIncrement} onDelete={this.handleDelete} /> </div> </div> ) } handleIncrement = counter => { const { counters } = this.state const clickedCounterIndex = counters.findIndex(c => c.id === counter.id) const countersIncremented = _.cloneDeep(counters) countersIncremented[clickedCounterIndex].value++ this.setState({ counters: countersIncremented, }) } handleReset = () => { const counters = this.state.counters.map(c => { c.value = 0 return c }) this.setState({ counters, }) } handleDelete = counter => { const { counters } = this.state const filteredCounters = counters.filter(c => { return c !== counter }) this.setState({ counters: filteredCounters, }) } } export default App
Counters.jsx
import React, { Component } from 'react' import Counter from './Counter' class Counters extends Component { render() { const { counters, onReset, onIncrement, onDelete } = this.props if (counters.length === 0) return <p>Nothing!</p> return ( <> <button onClick={onReset} className="btn btn-primary btn-sm m-2"> Reset </button> {counters.map(counter => ( <Counter key={counter.id} value={counter.value} selected={true} onIncrement={() => onIncrement(counter)} onDelete={() => onDelete(counter)} /> ))} </> ) } } export default Counters
Counter.jsx
import React, { Component } from 'react' class Counter extends Component { render() { return <>{this.renderCounter()}</> } renderCounter() { const { onIncrement, onDelete } = this.props return ( <div> <span className={this.getBadgeClasses()}>{this.formatCount()}</span> <button onClick={onIncrement} className="btn btn-secondary btn-sm"> Increment </button> <button onClick={onDelete} className="btn btn-danger btn-sm m-2"> Delete </button> </div> ) } getBadgeClasses() { let classes = 'badge m-2 badge-' classes += this.props.value === 0 ? 'warning' : 'primary' return classes } formatCount() { const { value } = this.props return value === 0 ? 'Zero' : value } } export default Counter
Navbar.jsx
stateを持たないコンポーネントに関しては、Stateless Functional Component (SFC)
を用いて、次のような記述をする。
Lifecycle hooksに関しても、SFC内では使用することができない。
import React from 'react' const Navbar = ({ totalCounters }) => { return ( <> <nav className="navbar navbar-light bg-light"> {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} <a className="navbar-brand" href="#"> Navbar <span className="badge badge-pill badge-info">{totalCounters}</span> </a> </nav> </> ) } export default Navbar
ちなみに、class componentについてもstateを持たないことも当然できるので、単に
Functional Component (FC)
とのみ区別した方がより適しているという議論がある。
React学習3(props, single source of truth, controlled component)
React復習がてら要点をまとめていく。
Single source of truth
Reactでの状態(state)管理は、一つのソースで行わないといけない。これがSingle source of truthと言われるものである。 あるlocal stateを持ったコンポーネントを複数読み込んでその親もstateを持つとすると、stateは親のコンポーネント管理下でなければいけない。
具体的な例として、counterコンポーネントが複数読み込まれているというケースを考える。
Counters.jsx
(親コンポーネント)
import React, { Component } from 'react' import Counter from './Counter' class Counters extends Component { state = { counters: [ { id: 1, value: 0 }, { id: 2, value: 0 }, { id: 3, value: 3 }, { id: 4, value: 0 }, ], } render() { const { counters } = this.state if (counters.length === 0) return <p>Nothing!</p> return ( <> <button onClick={this.handleReset} className="btn btn-primary btn-sm m-2" > Reset </button> {counters.map(counter => ( <Counter key={counter.id} value={counter.value} selected={true} onDelete={() => this.handleDelete(counter)} /> ))} </> ) } handleReset = () => { const counters = this.state.counters.map(c => { c.value = 0 return c }) this.setState({ counters, }) } handleDelete = counter => { const { counters } = this.state const filteredCounters = counters.filter(c => { return c !== counter }) this.setState({ counters: filteredCounters, }) } } export default Counters
Counter.jsx
(子コンポーネント)
import React, { Component } from 'react' class Counter extends Component { state = { count: this.props.value, selected: this.props.selected, } render() { return <>{this.renderCounter()}</> } handleIncrement = product => { this.setState({ count: this.state.count + 1, }) } renderCounter() { return ( <div> <span className={this.getBadgeClasses()}>{this.formatCount()}</span> <button onClick={this.handleIncrement} className="btn btn-secondary btn-sm" > Increment </button> <button onClick={this.props.onDelete} className="btn btn-danger btn-sm m-2" > Delete </button> </div> ) } getBadgeClasses() { let classes = 'badge m-2 badge-' classes += this.state.count === 0 ? 'warning' : 'primary' return classes } formatCount() { const { count } = this.state return count === 0 ? 'Zero' : count } } export default Counter
この例だとResetボタンが効かない。子コンポーネントと親コンポーネントの両方で重複したstateを持っているからだ。
そこでリファクタリングを行い、親コンポーネントに全てのstateを集約させる。
ちなみにこういう場合、子コンポーネントのことをControlled component
と呼ぶ。