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のほとんどが、todos
とfilter
の型を指定する必要があります。なので、TypeScriptを使用してファイル間で型を共有しましょう。既にTodoApp.types.ts
ファイルにこれらの型を書き出し、エクスポートしているので、それらをインポートして使いましょう。
import { FilterTypes, Todos, CompleteTodo } from "../TodoApp.types"; interface TodoListProps { complete: CompleteTodo; todos: Todos; filter: FilterTypes; }
Writing TodoListItemProps
TodoListItemを見ると、label
とcompleted
が既に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
- TodoFooterを開き、
TodoFooterProps
を書いてください。それはclear
とtodos
の2つの値を持ちます。このインターフェイスは関数のComponentでは次のように使用します。(props: TodoFooterProps
) props.clear
を呼び出す_onClick
関数を作ってください。- TodoFooterはクラスではないので、
_onClick
は戻り値の前のconst格納する必要があります。 - このクリックハンドラを定義するのにアロー関数を使ってください。
- TodoFooterはクラスではないので、
_onClick
をボタンのonClick
に割り当ててください。TodoFooterはクラスではないのでthis
を使う必要はありません。- この機能を試してみてください。いくつかのtodosを完了したら、
Clear Completed
ボタンをクリックします。
TodoHeader
TodoHeaderを開き、
addTodo
,setFilter
,filter
を含むTodoHeaderProps
を書いてください。そしてクラス宣言の最初のanyと置き換えてください。このComponentはstateを持っています。
TodoHeaderState
を書き、2つ目のanyと置き換えてください。各filterボタンに
_onFilter
を割り当ててください- onClickに新しいパラメータを追加することはできませんが、イベントターゲットから情報を取得することはできます。
- これもアロー関数にするのを忘れないでください
送信ボタンから
_onAdd
を呼び出せるようにしてください。この機能を試してみてください。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