🚶

Reactでシンプルな気象情報付きTo Doリストを作る

2022/07/24に公開

今回はReactのプロジェクトを作成し、UIもめちゃくちゃシンプルなTo Doリストを作っていきたいと思います。しかしそれだけだと味気ないので無料で天気情報を取得できるOpenWeatherMapのAPIを使用して、軽く添えたいと思います。

以下が完成形になります。SPビューだと1カラムで縦並びですが、PCビューだと2カラムで横並びになるようにCSSを書いていきます。

使用する主な技術

  • React(create-react-appによる開発)
  • ReactHooks(useState、useEffect、カスタムHook)
  • Local Storage(To Doのタスクをブラウザに保存する為)
  • axios(OpenWeatherMapのAPI呼び出しの際に使用)
  • tailwind.css(スタイル調整用)

create-react-appでReactプロジェクトを作る

まずはReactの開発環境を構築していきましょう。ターミナルで以下のコマンドを打つだけです。(Node.jsのインストールが済んでることを想定しています。)
todo_reactの部分は自分の好きなプロジェクト名に変えていただいて問題ありません。

npx create-react-app todo_react

最初に必要モジュールをインストールしておく

本格的に開発を始める前に今回使用するパッケージをnpmからインストールしておきましょう。ターミナルで以下を実行してください。

npm install axios tailwindcss

CSSを反映させるためにtailwind.cssの設定をする

srcフォルダ内、index.cssに以下を記述してください。

@tailwind base;
@tailwind components;
@tailwind utilities;

次に以下のコマンドでtailwind.cssの設定ファイルであるtailwind.config.cssを生成します。

npx tailwindcss init

tailwind.config.cssに以下を記述してください。
この記述によって、これから開発中に作られるjsやjsxの拡張子がついたファイル内でtailwind.cssを反映させることができます。

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,jsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

まずは親コンポーネントとなるTodo.jsxを作成する

開発に入ります。npm startのコマンドによって、ブラウザでlocalhostのサーバーが立ち上がります。ホットリロード機能がついているため、コードの変更が即座にブラウザに反映されます。

ますはTo Doリストや天気チェックのコンポーネントを子に持つことを想定したTodoコンポーネントを作成します。src配下にcomponentsフォルダを作成し、その中にTodo.jsxファイルを作成しましょう。以下がTodo.jsxの現時点での中身です。

const Todo = () => {
  return (
    <div>
      <h1 className='text-4xl mb-16'>Simple ToDo</h1>
    </div>
  );
};

export default Todo;

とてもシンプルなコンポーネントですね。classNameに設定された値はtailwind.cssで定義されているクラスになります。まだこの段階ではただh1タグでタイトルが存在するだけです。このコンポーネントに後から作成した子のコンポーネントを追記していきます。

肝心なTo Doリスト部分のコンポーネントを作る

次に子のコンポーネントであるTo DoリストのUI部分を作成していきます。今回もcomponents配下にTodoList.jsxを作成します。コードは以下になります。

const TodoList = () => {
  return (
    <div className="mb-16">
      <form className="text-xl">
        Add Task :
        <input className="ml-5 outline-none" placeholder="Add with enter" />
      </form>
      <button className="text-xl">Clear</button>
    </div>
  );
};

export default TodoList;

現状ではこちらもかなりシンプルな構造かと思います。formで囲まれたinputのタスク入力欄と、タスクの一括削除を想定したclearボタンがあるのみとなります。
ではこちらのコンポーネントをTodo.jsx側で読み込みましょう。追加後のコードは以下になります。

import TodoList from './TodoList';

const Todo = () => {
  return (
    <div>
      <h1 className='text-4xl mb-16'>Simple ToDo</h1>
      <div className='md:flex'>
        <TodoList />
      </div>
    </div>
  );
};

export default Todo;

TodoListコンポーネントを囲うdivにflexを指定しています。後に天気情報取得結果のUI部分を追加するため、横並びの指定を先に書いておきます。

カスタムHookでTo Doリストの機能を実装する

現状だとお気づきかと思いますが、UIのレンダリングだけで、肝心の機能が定義されていません。方法としてはTodoList.jsxにHooksの機能と組み合わせて関数を作成していっても問題はありません。しかし学習も兼ねてという観点もありますが、カスタムHookを使うことによってロジックを分離できるという理由もあります。
コンポーネントにはUIの構築だけに責務を持たせて、機能はカスタムHookで外に取り出してあげるといった考え方になります。
また、一度作ったHooksは様々なコンポーネントで使いまわせるという利点があります。しかし今回のような簡易的なアプリではあまり恩恵は得られなそうです。

では実際に作っていきましょう。src配下にhooksフォルダを作成し、その中にuseHandleTodoTask.jsを置きます。コードの中身は以下になります。

import { useState,useEffect } from 'react';

export const useHandleTodoTask = () => {

  const [todos, setTodos] = useState(JSON.parse(localStorage.getItem('todos')) || []);
  const [task, setTask] = useState('');

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos))
  }, [todos]);

  const handleNewTask = (event) => {
    setTask(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    if (task === '') return;
    setTodos((todos) => [...todos, { task, isCompleted: false }]);
    setTask('');
  };

  const handleRemoveTask = (index) => {
    const newTodos = [...todos];
    newTodos.splice(index, 1);
    setTodos(newTodos);
  };

  const handleUpdateTask = (index) => {
    const newTodos = todos.map((todo, todoIndex) => {
      if (todoIndex === index) {
        todo.isCompleted = !todo.isCompleted;
      }
      return todo;
    });
    setTodos(newTodos);
  };

  const handleRemoveAllTask = (index) => {
    const newTodos = [...todos];
    newTodos.splice(0);
    setTodos(newTodos);
  };

  return {todos,task,handleNewTask,handleSubmit,handleRemoveTask,handleUpdateTask,handleRemoveAllTask}
}

いきなりめちゃくちゃ長いコードですね。少しずつ分けて解説していきます。

まずは以下の部分から

const [todos, setTodos] = useState(JSON.parse(localStorage.getItem('todos')) || []);
const [task, setTask] = useState('');

useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos))
  }, [todos]);

useStateでtodosを定義しています。これは簡単に言えばTo Doリストのタスクをまとめたものになります。今回はブラウザを更新してもタスクが保持されるように初期値にはLocal Storageを使用しています。JSON.parse(localStorage.getItem('todos'))によって取得したjson文字列をオブジェクトに変更しています。
ではどこでLocal Storageに値をセットしているかというと、その下に続くuseEffectとなります。
localStorage.setItem('todos', JSON.stringify(todos))によって、json文字列に変更したtodosをLocal Storageに保存しています。そしてその実行タイミングは、useEffectの第二引数である配列で定義しています。
useStateで定義したtodosに変更が加えられたら(今回だとタスクの追加や削除)、useEffectが動いて、Local Storageにその時のタスクの状態が保持されるという仕組みになります。

次に以下の部分

  const handleNewTask = (event) => {
    setTask(event.target.value);
  };

  const handleSubmit = (event) => {
    event.preventDefault();
    if (task === '') return;
    setTodos((todos) => [...todos, { task, isCompleted: false }]);
    setTask('');
  };

  const handleRemoveTask = (index) => {
    const newTodos = [...todos];
    newTodos.splice(index, 1);
    setTodos(newTodos);
  };

  const handleUpdateTask = (index) => {
    const newTodos = todos.map((todo, todoIndex) => {
      if (todoIndex === index) {
        todo.isCompleted = !todo.isCompleted;
      }
      return todo;
    });
    setTodos(newTodos);
  };

  const handleRemoveAllTask = (index) => {
    const newTodos = [...todos];
    newTodos.splice(0);
    setTodos(newTodos);
  };

記述量は多いですが、全て関数となっていてその機能はそれぞれTo Doリストで必要なものとなっています。上からタスク記入、タスク追加、タスク削除、タスク更新(今回は上に完了の線を追加)、タスク全削除となっています。全ての機能をこのようにカスタムHookでまとめて、コンポーネントの外に定義しています。

そして最後は以下のように全てのstateや関数をreturnしてあげます。

return {todos,task,handleNewTask,handleSubmit,handleRemoveTask,handleUpdateTask,handleRemoveAllTask}

TodoList.jsxでカスタムHookを読み込み、関数のトリガーをjsxに追記する

次にカスタムHookで作った機能群をコンポーネント側で使用する為に、 TodoList.jsxを編集します。コードは以下になります。

import { useHandleTodoTask } from "../hooks/useHandleTodoTask";

const TodoList = () => {

  const {todos, task, handleNewTask, handleSubmit, handleRemoveTask, handleUpdateTask,handleRemoveAllTask} = useHandleTodoTask();

  return (
    <div className="mb-16">
      <form onSubmit={handleSubmit} className="text-xl">
        Add Task :
        <input className="ml-5 outline-none" value={task} placeholder="Add with enter" onChange={handleNewTask} />
      </form>
      <ul className="mb-16">
      {todos.map((todo, index) => (
        <li className="text-lg m-8"
          key={index}
          style={{
            textDecoration: todo.isCompleted ? 'line-through' : 'none',
          }}
        >
          <input className="mr-5"
            type="checkbox"
            checked={todo.isCompleted}
            onChange={() => handleUpdateTask(index)}
          />
          {todo.task}
          <button className="ml-5"
            onClick={() => handleRemoveTask(index)}
            style={{ cursor: 'pointer' }}
          >
            Delete
          </button>
        </li>
      ))}
      </ul>
      <button className="text-xl" onClick={handleRemoveAllTask}>Clear</button>
    </div>
  );
};

export default TodoList;

先ほどはUIのレンダリングだけのシンプルなコンポーネントでしたが、一気に記述量が増えました。まず一番上でカスタムHookを読み込みます。

import { useHandleTodoTask } from "../hooks/useHandleTodoTask";

そして先ほどreturnしたstateや関数をuseHandleTodoTask.jsから以下のように分割代入で定義します。

const {todos, task, handleNewTask, handleSubmit, handleRemoveTask, handleUpdateTask,handleRemoveAllTask} = useHandleTodoTask();

テンプレート部分を見てみましょう。

<form onSubmit={handleSubmit} className="text-xl">
  Add Task :
  <input className="ml-5 outline-none" value={task} placeholder="Add with enter" onChange={handleNewTask} />
</form>

formタグでonSubmit={handleSubmit}とすることでタスク追加関数を呼び出しています。inputタグではonChange={handleNewTask}とすることでタスク記入関数を呼び出し、value={task}に値を反映しています。

次にulタグを見ていきます。

<ul className="mb-16">
   {todos.map((todo, index) => (
      <li className="text-lg m-8"
          key={index}
          style={{
            textDecoration: todo.isCompleted ? 'line-through' : 'none',
          }}
        >
         <input className="mr-5"
            type="checkbox"
            checked={todo.isCompleted}
            onChange={() => handleUpdateTask(index)}
         />
         {todo.task}
          <button className="ml-5"
            onClick={() => handleRemoveTask(index)}
            style={{ cursor: 'pointer' }}
          >
          Delete
         </button>
     </li>
   ))}
 </ul>

ul内でmap関数を使って配列の中にあるtodoオブジェクトを一つずつ処理して、liタグとしてレンダリングしています。onChange={() => handleUpdateTask(index)}ではtodo.isCompletedの値を反転させて、タスクの上に線を引くスタイルを反映させる為のタスク更新関数が呼び出されています。
buttonタグのonClick={() => handleRemoveTask(index)}はタスク削除関数が呼び出されいます。

そしてタスクを一括で削除する関数の実行はclearボタンにトリガーを持たせています。

<button className="text-xl" onClick={handleRemoveAllTask}>Clear</button>

To Doリストは完成

これでTo Doリストは完成しました。
次に気象情報の取得ができるOpenWeatherMapのAPIを使って、その結果をUIにレンダリングさせていきます。

APIキーを取得しよう

以下OpenWeatherMapのwebサイトからAPIキーを取得します。取得方法については省略いたします。ここで取得したAPIキーを使って情報を取得していきます。
https://openweathermap.org/api

気象情報の結果を表示するコンポーネントを作る

components配下にWeatherCheck.jsxというファイルを作成します。コードは以下になります。

import axios from "axios";
import { useState,useEffect } from 'react';

const WeatherCheck = () => {

  const baseURL = "https://api.openweathermap.org/data/2.5/weather?q=Tokyo,JP&appid={APIキー}&lang=ja&units=metric";

  const [post, setPost] = useState(null);

  useEffect(() => {
    axios.get(baseURL).then((response) => {
      setPost(response.data);
    });
  }, []);

  if (!post) return null;

  const { weather, main } = post;
  
  return (
    <div className="mb-16">
      <h2 className="text-xl mb-6">東京の気象情報</h2>
      <p className="mb-4">空模様 : {weather[0].description}</p>
      <p>気温 : {main.temp}</p>
    </div>
  );

}
export default WeatherCheck;

以下が着目すべき箇所になります。

 const [post, setPost] = useState(null);

  useEffect(() => {
    axios.get(baseURL).then((response) => {
      setPost(response.data);
    });
  }, []);

  if (!post) return null;

 const { weather, main } = post;

useStateで今回のAPIから取得したデータの状態を管理します。初期値はnullにしておきます。
useEffectでaxiosを使用して、baseURLで定義したAPIにアクセスしています。それをthenで繋いで、返ってきた結果をsetPostで包むことによってpostの値を更新しています。

後は返ってきたjsonの構造からどのようにアクセスすれば、表示できるのかを確認します。
今回だと一度分割代入をしてからアクセスしやすくしています。
APIの取得結果を調べるときにおすすめなのが以下のChromeの拡張機能を使う方法です。APIのURLを叩けば結果を表示してくれます。
https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo?hl=ja

仕上げ

最後にTodo.jsxとApp.jsを修正しましょう。

まずTodo.jsxはWeatherCheck.jsxをimportしてコンポーネントを追加します。

import TodoList from './TodoList';
import WeatherCheck from './WeatherCheck';

const Todo = () => {
  return (
    <div>
      <h1 className='text-4xl mb-16'>Simple ToDo</h1>
      <div className='md:flex'>
        <TodoList />
        <WeatherCheck />
      </div>
    </div>
  );
};

export default Todo;

最後にApp.jsに今回の大元のTodo.jsxをコンポーネントとして追加します。

import Todo from './components/Todo';

function App() {
  return (
    <div className='m-10'>
      <Todo />
    </div>
  );
}

export default App;

以上で全ての工程が完了となります。

最後に

無事To Doリストが完成しましたでしょうか。クライアントのみの静的なサイトなので、デプロイはnetlifyなどでも可能となります。
最後までお読みいただきありがとうございました。

参考文献

https://reffect.co.jp/react/reack-usestate-to-do-application#inputuseState

Discussion