React + Typescriptで簡単なtodoアプリを作ってみる

React + Typescriptを初学者向けに書いてみました。

## 始める前に
・基本的なTypescriptの知識
・React Hooks
が分かっていることが望ましいです。

## React + Typescriptの環境構築

https://create-react-app.dev/docs/adding-typescript/

> 以前に npm install -g create-react-app を使ってグローバルに create-react-app をインストールしたことがある場合、npxが常に最新バージョンを使用するようにするために、 npm uninstall -g create-react-app を使ってパッケージを**アンインストール**することをお勧めします。
create-react-app のグローバルインストールはサポートされなくなりました。



```
npx create-react-app my-app --template typescript
// my-appの部分は任意の名前でOK
```

```
yarn start
```

で、ローカル環境で動くか確認。
公式が提供している為、ダウンロード後すぐに動かせられるのが嬉しい。

動き始めたら、
App.css
App.test.tsx
logo.svg
serviceWorkers.tsx
を**削除**

index.tsx
App.tsx
を以下のように変更。

```index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';


ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

```

```App.tsx
import React from 'react';

function App() {
  return (
    <div className="App">
    </div>
  );
}

export default App;
```
これで真っ白な画面になったかと思いますので、ここから実際に実装してみたいと思います。

## Todo.tsxの導入

まずは、src配下に/components/Todoディレクトリを作成。

```/components/Todo.tsx
import React from 'react';

const Todo = () => {
    return <ul>これはTodo.tsxから来ています。</ul>
}

export default Todo;
```
これでTodoコンポーネントをエクスポートできるので、App.tsxに取り込みます。

```App.tsx
import React from 'react';
import Todo from './components/Todo';

//React.FC = React.FunctionalComponentの略。
const App: React.FC = () => {
  return (
    <div className="App">
      <Todo />
    </div>
  );
}

export default App;
```
画面はこんな感じ

<img width="898" alt="スクリーンショット 2020-04-18 23.59.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439329/99ce0559-5428-d12f-77b1-cf350537f32c.png">
とりあえず、Todo.tsxをApp.tsxに読み込ませる事ができました。

## Todo.tsxで型を定義する
Todo.tsxに型を実装していきます。
string値を持つidとstring値を持つtextを、TodoPropsの中に定義します。

```Todo.tsx
import React from 'react';
// interfaceは、新しい型を定義する為に使います。
interface TodoProps {
  items: {id: string, text: string}[]
};

const Todo: React.FC<TodoProps> = props => {
  return (
    <ul>これはTodo.tsxから来ています。</ul>
  ) 
}

export default Todo;
```
App.tsxはまだいじっていないので、コンソール上だとこんなエラー。

```console.
//App.tsx
Property 'items' is missing in type '{}' but required in type 'TodoProps'.
```
TodoPropsでitemsという名前のプロパティを使うなら、App.tsx内で定義して欲しい！と言っています。

なので、App.tsxを次のように変更。

```App.tsx
import React from 'react';
import Todo from './components/Todo';

const App: React.FC = () => {
  const todo = [{id: 'タスク１', text: 'ご飯を作る'}]
  return (
    <div className="App">
      <Todo items={todo} />
      //itemsは、{todo}
    </div>
  );
}

export default App;
```

これで、エラーが消えましたが、描画されるのは

```Todo.tsx
<ul>これはTodo.tsxから来ています。</ul>
```
部分なので、こちらをmap関数を使って、変更します。

```Todo.tsx
import React from 'react';

interface TodoProps {
  items: {id: string, text: string}[]
};

const Todo: React.FC<TodoProps> = props => {
  return (
    <ul>
      {props.items.map(list => (
      <li key={list.id}>{list.text}</li>
      ))}
    </ul>
  ) 
}

export default Todo;
```

とすれば、textで書いた

> ご飯を作る

という文字列が描画されるかと思います。また、

```Todo.tsx
items: {id: string, text: string}
```
とitemsにはnumberではなくstring値を指定しているので、

```App.tsx
const todo = [{id: 11, text: 'ご飯を作る'}]
```
と、idに数字を渡すと、以下の画像のように

<img width="844" alt="スクリーンショット 2020-04-19 0.25.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439329/a55ec0b4-9d32-f610-c726-63067be1f4a5.png">

エラーを吐いてくれるので、安全なReactでの実装が出来ます。
ここからは、Hooksを組み合わせたコードを書いてみます。

## useRefコンポーネントを使ってコンソール上にテキストを出力させる
React HooksのuseRefを使って、Todoリストを作ってみます。
まずは、以下のように新しいファイルを定義。

```App.tsx
import React from 'react';
import Todo from './components/Todo';
import AddTodo from './components/AddTodo';

const App: React.FC = () => {
  const todo = [{id: 'タスク１', text: 'ご飯を作る'}]
  return (
    <div className="App">
      <AddTodo />
      <Todo items={todo} />
    </div>
  );
}

export default App;
```

components配下にはAddTodo.tsxを作ります。中身はこんな感じにします。

```AddTodo.tsx
import React, { useRef } from 'react';

const AddTodo: React.FC = () => {
  // ()でundefined指定することはuseRefは出来ないので、初期値はnull。
  const textInputRef = useRef<HTMLInputElement>(null);
  // フォーム内で何かがsubmitされたら、useRefが発火する
  const todoSubmitHandler = (event: React.FormEvent) => {
    // preventDefaultでDOMをレンダーする事を防ぐ。
    // これが無いと、inputを押した瞬間に再レンダーされ、入力した内容が消えてしまいます。
    event.preventDefault();
    // 後述
    const enteredText = textInputRef.current!.value;
    console.log(enteredText);
  };
  return (
    <form onSubmit={todoSubmitHandler}>
      <div>
        <label htmlFor="todo-text">Todo Text</label>
        {/* ref {textInputRef}を追加して、レンダーされる場所を明記 */}
        <input type="text" id="todo-text" ref={textInputRef} />
      </div>
      <button type="submit">ADD todo</button>
    </form>
  ); 
};

export default AddTodo;
```

### AddTodo.tsxの解説

```
const enteredText = textInputRef.current!.value;
console.log(enteredText);
```
の部分ですが、textInputRef の部分にカーソルを当てると
> const textInputRef: React.RefObject\<HTMLInputElement>

と、VSCode上で表示してくれます。これは、return内のinput要素(HTMLInputElement)と接続していることが分かります。しかし、textInputRefを定義した際に、

```AddTodo.tsx
const textInputRef = useRef<HTMLInputElement>(null);
```
と、初期値をnullの状態にしたので、例えば

```AddTodo.tsx
const enteredText = textInputRef.current.value;
// error オブジェクトは 'null' である可能性があります。
```
**currentの!マークを外すとTypeScript側から怒られてしまいます。**
そのため、nullでも大丈夫である事をTypeScriptに伝えるために、!マークをつける必要があります。

実行するとこんな感じになります。
inputElement内に"宿題をする"入力
↓
ADD todoボタン押下
↓
consoleに表示
<img width="861" alt="スクリーンショット 2020-04-19 1.13.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439329/dc8b3b8d-e61e-da22-0fd9-33a201bf9039.png">

## useStateを使って、コンソールから画面に反映させる
コメントアウトした部分に解説を書きました。

```App.tsx
import React, { useState } from 'react';
import Todo from './components/Todo';
import AddTodo from './components/AddTodo';
// modelの導入は、可読性を上げるため（後述)
import { ToDo } from './todo.model';


const App: React.FC = () => {
  // ToDoの中身がidとtextなので、ここでは[]を代入。
  const [todos, setTodos] = useState<ToDo[]>([]);
  
  const todoAdd = (text: string) => {
    setTodos(prevTodos => [
      ...prevTodos, 
      { id: Math.random().toString(), text: text }
      // これにより、{id: "0.8759291112629384", text: "宿題をする"}のようなデータをもつテキストが生成される。
    ]);
  };

  return (
    <div className="App">
      // AddTodo.tsxから渡されたテキストデータがここに入る
      <AddTodo todoAdded={ todoAdd } />
      <Todo items={todos} />
    </div>
  );
}

export default App;
```

**次にsrc配下にtodo.model.tsファイルを作成する。**
(作らなくても良いのですが、可読性を上げるために、モデルファイルを作ります。)

```/src/todo.model.ts
export interface ToDo {
  id: string;
  text: string;
}
```

todo.model.tsを作らない場合、

```App.tsx
  const [todos, setTodos] = useState<{id: string; text: string}[]>([]);
```
と、useStateの後ろの部分が冗長化してしまいます。モデル化をしておく事で、読みやすくなるので覚えておくと良いかと思います！ちなみに読み込み速度は、このような影響範囲になるそうです。
[TypeScriptのモデル生成速度比較](https://blog.mamansoft.net/2019/03/17/typescript-model-created-comparison/#%E7%B5%90%E6%9E%9C%E3%81%A8%E8%80%83%E5%AF%9F)

次にAddTodo.tsxを編集します。

```AddTodo.tsx
import React, { useRef } from 'react';

// typeで型付けと継承をしています。
type AddTodoProps = {
  todoAdded: (todoText: string) => void;
}

const AddTodo: React.FC<AddTodoProps> = props => {
  const textInputRef = useRef<HTMLInputElement>(null);
  const todoSubmitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = textInputRef.current!.value;
    // AddTodoProps関数のtodoAddedに入力されたテキストを渡している。
    props.todoAdded(enteredText);
  };
  return (
    <form onSubmit={todoSubmitHandler}>
      <div>
        <label htmlFor="todo-text">Todo Text</label>
        <input type="text" id="todo-text" ref={textInputRef} />
      </div>
      <button type="submit">ADD todo</button>
    </form>
  ); 
};

export default AddTodo;
```

## 完成
上記のような実装にすると、以下の画像のように出力されます。
(ChromeのReactDevloperTools拡張を使用。)
<img width="1267" alt="スクリーンショット 2020-04-19 2.45.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/439329/6365c68f-6031-3085-fbcf-d4dedaefec5c.png">

削除機能は、filterを使って組み込めるので、よければ挑戦してみてください。

## まとめ
HooksとTSを使って、Todoアプリを作ってみました。
小規模なアプリケーションであれば、TSが無くとも開発速度に影響は出にくいと思いますが、これが大規模開発になればなるほど、重要性が高まるんだなと。
んー、TS偉大。


## 参考
英語ですが。
Understanding TypeScript - 2020 Edition
https://www.udemy.com/course/understanding-typescript/