Make it to make it

いろいろ作ってアウトプットするブログ

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"
          >
            &#43;
          </button>
          <button
            onClick={onDecrement}
            className={`btn btn-secondary btn-sm m-2${
              disabled ? ' disabled' : ''
            }`}
          >
            &#8722;
          </button>
          <button onClick={onDelete} className="btn btn-danger btn-sm m-2">
            &times;
          </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と呼ぶ。