16
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-18

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

始める前に

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

React + 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;

画面はこんな感じ

スクリーンショット 2020-04-18 23.59.23.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: 'タスク1', 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に数字を渡すと、以下の画像のように

スクリーンショット 2020-04-19 0.25.17.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: 'タスク1', 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に表示
スクリーンショット 2020-04-19 1.13.31.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のモデル生成速度比較

次に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拡張を使用。)
スクリーンショット 2020-04-19 2.45.51.png

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

まとめ

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

参考

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

16
14
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?