やったこととか思ったこと

やったことを忘れないように気が向いたら書きます

Frontend Bootcampを読む Step1-07

Microsoftが公開していたFrontend Bootcampを読み進めていくメモ

自分なりの適当な翻訳なので間違いがあるかもしれません。
また公開されたばかりで現在進行形で更新されているので僕が読んだ時とは内容が少し変わっているかもしれません。

Step1-07: Types and creating a UI-driven state

これまでで、アプリのstateによって変わるUIを作りました。ですが、まだUIからstateを変更することができないので、変更できるようにする機能を実装する必要があります。これは、TodoHeaderで見たように、setStateを呼び出す関数によって行われます。その後、stateの値は子Componentにpropsとして渡されます。

このワークショップのパート2では、propsを使って明示的に渡すことなくそれらの関数を使う方法を学びます。

これがReactの中心的な"ビジネスロジック"であり、基本的な"CRUD"操作を処理します。Create,Read,Update,Deleteのすべての関数を書くことについてはまだ説明していませんが、デモのTodoAppにすでに実装してあり、Componentに渡されていることがわかります。

Intro to TypeScript

TodoApp Componentを見てみると、propsが、長くなっているのではなく、とても複雑になっていることがわかります。様々な関数、todosオブジェクト、フィルタの文字列を送っています。

アプリケーションが大きくなるにつれて、関数や、todosの内容を覚えておくことが難しくなります。また、JavaScriptは動的型付け言語なので、todosの値を、TodoList内の配列に変えたい場合、JavaScriptは大丈夫でしょう。ですが、TodoListItemsが、オブジェクトを期待していたなら、アプリケーションは壊れてしまうでしょう。

これら2つの理由で、業界は強く型付けされたアプリケーションを書くように移行しています。そしてたくさんの人が移行するためにTypeScriptを使っています。

TypeScript is a superset of JavaScript that compiles to plain JavaScript.

TypeScriptのウェブサイトに書いてあるように、TypeScriptはJavaScriptコンパイルされるJavaScirptのスーパーセットです。

もしSassを使ったことがあるなら、あなたはこの概念に精通しているでしょう。すべての有効なCSSが有効なSassであるように、すべての有効なJavaScriptは有効なTypeScriptです。これが、練習問題がjsとjsxの代わりにtsとtsxで書かれていた理由です。

TypeScriptを使ってComponentのpropsを明確にし、将来の後戻りを防ぎましょう。

Demo

最も多くのデータフローを持っているTodoListから始めましょう。TodoListItemにcompletedを渡しているだけなので、TodoListはインタラクティブなUIは持っていません。ですが、すべて正しく渡されていることを確認するためにpropsインターフェイスを書くことができます。

Writing TodoListProps

TodoAppを見ると、TodoListが3つのprops(filter,todos,complete)を持っていることがわかります。TodoListPropsというtodoListのporopsを表すインターフェイスを作ります。

// TodoList.tsx
interface TodoListProps {
    filter: any;
    todos: any;
    complete: any;
}

今はanyを使用しています。これによって安全性が得られるわけではありませんが、このComponentに渡す有効なpropsの名前を指定できます。

インターフェースを書いたら、それをComponentに追加してください。

export class TodoList extends React.Component<TodoListProps, any>

<>の最初の値はpropsのインターフェイス用で、2つ目の値はstate用です。

これで、型付きのComponentが作成できました。TodoAppに戻り、propsの名前を変更するとどうなるのかを見てみましょう。

Adding type safety

今のところ、propsの名前を指定しただけで、それらの型は指定していません。まずは、filterを、型安全にしましょう。

Filter Type

filterがオブジェクト、配列、関数ではないことがわかっているので、filterが常に文字列であるべきだと指定することができます。

interface TodoListProps {
    filter: string;
    todos: any;
    complete: any;
}

さらに、filterは3種類の値にしかならないことがわかっているので、filter共用型(union type)で明示的にすることができます。

interface TodoListProps {
    filter: 'all' | 'active' | 'completed';
    todos: any;
    complete: any;
}

TodoAppに戻り、TodoListのfilterを別のものにしてみてください。もし、VS Codeを使っているならエディタから、またファイルを保存したらコマンドラインからエラーが出るでしょう。

Complete Type

omplete propはデータではなく関数です。ですがTypeScriptではデータだけでなく関数型も扱うことができます。

interface TodoListProps {
    filter: 'all' | 'active' | 'completed';
    todos: any;
    complete: (id: string) => void;
}

関数の場合は渡されるパラメータと返される値の型を書きます。上の例では、stringのidを引数にとり、voidを返します。voidが意味するのは、返り値がないということです。

戻り値が指定していない場合、JavaScriptの全ての関数はundefinedを返しますが、戻り値の型としてvoidを宣言すると、関数から値を返そうとするとTypeScriptがエラーを出します。

Todos Type

todos propは、オブジェクトです。インターフェイスは次のようになります。

interface TodoListProps {
    filter: 'all' | 'active' | 'completed';
    todos: {
        [id: string]: {
            label: string;
            completed: boolean;
        };
    };
    complete: (id: string) => void;
};

[id: string]は配列を示すわけでわなく、オブジェクトのインデックスシグネチャです。

これで、インターフェイスが完成しました。filter === 'all'のallを変更してみてください、そしたらVS Codeがそれは常にfalseになると表示するでしょう。JacaScriptだった場合、その行にタイプミスがあったら、なぜフィルターが機能しなかったかの理由がわかりません。

Sharing types

Componentのほとんどが、todosfilterの型を指定する必要があります。なので、TypeScriptを使用してファイル間で型を共有しましょう。既にTodoApp.types.tsファイルにこれらの型を書き出し、エクスポートしているので、それらをインポートして使いましょう。

import { FilterTypes, Todos, CompleteTodo } from "../TodoApp.types";

interface TodoListProps {
    complete: CompleteTodo;
    todos: Todos;
    filter: FilterTypes;
}

Writing TodoListItemProps

TodoListItemを見ると、labelcompletedが既にTodoItemインターフェイスで定義されていることがわかります。TodoListPropsにTodoItemインターフェイスを継承して再利用することができます。

import { CompleteTodo } from "./TodoApp.types";

interface TodoListItemProps extends TodoItem {
    id: string;
    complete: CompleteTodo;
}

TodoListItemPropsは、4つのプロパティ(id, complete, completed, label)を持つインターフェイスです。

次に、propsをrenderに取り込むことができます。

const { label, completed, complete, id } = this.props;

そしたら、inputのonChangeイベントを使ってコールバックを書きます。シグネチャの中でidがstringだと宣言しているので、idを渡します。

コールバックはpropsとしてComponentに渡される関数です。

<input type="checkbox" chacked={completed} onChange{() => complete(id)} />

propの名前と関数名が偶然同じですが、これは必須ではありません。

todoがonChangeを起動しているので、クリックしてアプリがどのような動作をするのかみてください。Footerのテキストは未完了のTodoの数に基づいているので、Footerは新しいstateを反映するために自動的に更新されます。

Exercise

まだアプリを実行していない場合はfrontend-bootcampフォルダのルートからnpm startを実行して起動してください。動作を確認するためには、1日目のstep7の"exersice"をクリックしてください。

TodoFooter

  1. TodoFooterを開き、TodoFooterPropsを書いてください。それはcleartodosの2つの値を持ちます。このインターフェイスは関数のComponentでは次のように使用します。(props: TodoFooterProps)
  2. props.clearを呼び出す_onClick関数を作ってください。
    • TodoFooterはクラスではないので、_onClickは戻り値の前のconst格納する必要があります。
    • このクリックハンドラを定義するのにアロー関数を使ってください。
  3. _onClickをボタンのonClickに割り当ててください。TodoFooterはクラスではないのでthisを使う必要はありません。
  4. この機能を試してみてください。いくつかのtodosを完了したら、Clear Completedボタンをクリックします。

TodoHeader

  1. TodoHeaderを開き、addTodo, setFilter, filterを含むTodoHeaderPropsを書いてください。そしてクラス宣言の最初のanyと置き換えてください。

  2. このComponentはstateを持っています。TodoHeaderStateを書き、2つ目のanyと置き換えてください。

  3. 各filterボタンに_onFilterを割り当ててください

    • onClickに新しいパラメータを追加することはできませんが、イベントターゲットから情報を取得することはできます。
    • これもアロー関数にするのを忘れないでください
  4. 送信ボタンから_onAddを呼び出せるようにしてください。

  5. この機能を試してみてください。todoを追加、フィルターすることができるでしょう。

   // TodoFooter
   import React from 'react';
   import { Todos } from '../TodoApp.types';
   interface TodoFooterProps {
     clear: () => void;
     todos: Todos;
   }
   
   export const TodoFooter = (props: TodoFooterProps) => {
     const itemCount = Object.keys(props.todos).filter(id => !props.todos[id].completed).length;
     const _onClick = () => {
       props.clear();
     };
   
     return (
       <footer>
         <span>
           {itemCount} item{itemCount === 1 ? '' : 's'} left
         </span>
         <button onClick={_onClick} className="submit">
           Clear Completed
         </button>
       </footer>
     );
   };
   
   // TodoHeader
   import React from 'react';
   import { FilterTypes } from '../TodoApp.types';
   
   interface TodoHeaderProps {
     addTodo: (label: string) => void;
     setFilter: (filter: FilterTypes) => void;
     filter: FilterTypes;
   }
   
   interface TodoHeaderState {
     labelInput: string;
   }
   
   export class TodoHeader extends React.Component<TodoHeaderProps, TodoHeaderState> {
     constructor(props) {
       super(props);
       this.state = { labelInput: '' };
     }
   
     render() {
       const { filter } = this.props;
       return (
         <header>
           <h1>todos - step1-07 exercise</h1>
           <div className="addTodo">
             <input value={this.state.labelInput} onChange={this._onChange} className="textfield" placeholder="add todo" />
             <button className="submit" onClick={this._onAdd}>
               Add
             </button>
           </div>
           <nav className="filter">
             <button className={filter === 'all' ? 'selected' : ''} onClick={this._onFilter}>
               all
             </button>
             <button className={filter === 'active' ? 'selected' : ''} onClick={this._onFilter}>
               active
             </button>
             <button className={filter === 'completed' ? 'selected' : ''} onClick={this._onFilter}>
               completed
             </button>
           </nav>
         </header>
       );
     }
   
     _onFilter = evt => {
       this.props.setFilter(evt.target.innerText);
     };
   
     _onChange = evt => {
       this.setState({ labelInput: evt.target.value });
     };
   
     _onAdd = () => {
       this.props.addTodo(this.state.labelInput);
       this.setState({ labelInput: '' });
     };
   } 

参考

frontend-bootcamp/step1-07/exercise at master · Microsoft/frontend-bootcamp · GitHub