1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CDNでReact入門

Reactは、JavaScriptWebフレームワークの中で一番人気である。しかし、こちらを扱うには、Nodejsの環境を作る必要があり、ハードルが高い。
ところが、CDNを使えば、環境構築をしなくても、お試しでReactが使える。
今回で、Reactの良さを色んな人に味わって欲しい。

前提条件

  • HTML/CSSがわかる
  • JavaScriptの最新バージョンのコードがわかる
  • 分割代入がわかる
  • Visual Studio Codeが使える
  • LiveServerをインストールしている
  • Prettierをインストールし、セットアップも完了している。(推奨)

プロジェクト作成

例によってTodoListを作る。
CLI使用

mkdir react-todolist
cd react-todolist

Linux系

touch index.html
touch style.css

PowerShell

New-Item index.html
New-Item style.css

初期化

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="style.css" />
    <title>React Todo List</title>
  </head>
  <body>
    <div id="root"></div>
    <script
      src="https://unpkg.com/react@18/umd/react.production.min.js"
      crossorigin
    ></script>
    <script
      src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
      crossorigin
    ></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
    <script type="text/babel">
      const App = () => {
        return (
          <div>
            <h1>Todo List</h1>
          </div>
        );
      };

      const container = document.getElementById("root");
      const root = ReactDOM.createRoot(container);
      root.render(
        <React.StrictMode>
          <App />
        </React.StrictMode>
      );
    </script>
  </body>
</html>

style.css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  text-decoration: none;
}

body {
  width: 100%;
  height: 100vh;
}

#root {
  display: flex;
  justify-content: center;
  align-items: center;
}

h1 {
  font-size: 32px;
}

補完機能が使えるように、htmlの中に直接JavaScriptを書いている。

この状態で、LiveServerを起動すると、
init-image.png
このようになっているはず。

index.html
// Appコンポーネントという。
const App = () => {
    return (
        <div>
            <h1>Todo List</h1>
        </div>
    );
};

この書き方は、htmlではなく、JSX記法と呼ばれており、
htmlレイアウトを返す関数をコンポーネントという。
このコンポーネントの中にはJavaScriptの式を埋め込むことができる。
例えば、今のコードを次のように変えても、同じ結果となる。

index.html
const App = () => {
    const title = "Todo List";
    return (
        <div>
            <h1>{title}</h1>
        </div>
    );
};

「{JavaScriptの式}」で、画面に変数や式を埋め込むことができる。

実際にタスクを入力するフォームを作る。

Appコンポーネントを以下のように変えてみてほしい。

index.html
const App = () => {
    const title = "Todo List";
    let task = "";
    return (
        <div>
            <h1>{title}</h1>
            <input type="text" value={task} />
            <div id="displayTask">{task}</div>
        </div>
    );
};

一見うまくいきそうに見えるが、実際は、inputボックスに何も入力できない。
その上、コンソールでみても、何も変わっていない。
このような、ユーザーの行動によって変わる値を管理することを状態管理という。

Reactでは、そういう難しいことをhooksと呼ばれるものに任せる。
Hooksについてもっと知りたい方はこちら
状態管理をするには、useState()と呼ばれるhooksを使う。
let task = "";を、以下のように変えよう。

index.html
const [task, setTask] = React.useState("");

taskのことを、状態変数と呼び、この値は、setTask関数により変更できる。
例えば、setTask("あああ")とすると、taskの値が「あああ」となる。
set関数の引数の値に変更されるのだ。
これを踏まえて、inputボックスの値を取得しよう。

index.html
<input type="text" onChange={(e) => setTask(e.target.value)} value={task} />

ここで、「e」という引数が出てきたが、これは、addEventListenerを使うときの「e」だと思ってもらいたい。e.target.valueには、input要素のvalue属性が格納されてる。

e.targetの補足 コンソールで、e.targetを見ると、inputタグが格納されている。
hoverすると、自分が入力したinputボックスがハイライトされるはずだ。

hooksの呼び出しを簡単にする。

ここで、hooksは、今後何度も登場し、いちいちReact.としてアクセスするのは面倒くさいので分割代入で取り出すことにする。
scriptタグの真下に以下のコードを足そう。

index.html
const { useState } = React;

これによって、taskを定義する部分は、これでも正常に動く。

index.html
const [task, setTask] = useState("");

TodoListの状態を管理する。

TodoListは、1個や2個の世界ではなく、無限に追加できるため、配列によって管理する。
そのため、Appコンポーネントの一番上に、todosを自分で定義しよう。

解答例
index.html
// 初期値は、状態変数の型によって変わる。
// 今回は配列を使いたいので、空の配列になっている。
const [todos, setTodos] = useState([]);

配列の中にTodoを入れたいが、Todoの情報は、タスクだけではない。
今回は、完了・未完了を与える。
そのため、Todoはオブジェクトで管理する。
todosを以下のように変更しよう。

index.html
const [todos, setTodos] = useState([
    { task: "散歩", checked: false },
    { task: "ゲーム", checked: false }
])

画面にTodoを表示する。

todosをすべて表示したいときはmap関数を使って値を取り出して、表示する。

index.html
{/* <div id="displayTask">{task}</div> */}
<ul>
    {todos.map((todo) => (
        <li>
            <input
                type="checkbox" 
                checked={todo.checked} 
            />
            <p>{todo.task}</p>
        </li>
    ))}
</ul>
index.html
{todos.map(({ task, checked }) => (
        <li>
            <input
                type="checkbox" 
                checked={checked} 
            />
            <p>{task}</p>
        </li>
))}

のように、分割代入でも取り出せる。

ただし、このままでは、タスクが被った場合、見分けがつかなくなってしまう
そこで、Todo一つひとつにidを付与する必要がある
idは今後タスクが増えることが予想されるため、
ランダムな文字列にしよう。
Appコンポーネントの外に以下の関数を定義してほしい。

index.html
const genId = () => {
    const chars = [..."abcdefghijklmnopqrstuvwxyz1234567890"];
    let id = "";
    for(let i = 0; i < 32; i++) {
        id += chars[Math.floor(Math.random() * chars.length)];
    }
    return id;
}

npmには、uuidというユニークなidを付与するライブラリが存在する。

そしたら、Todoにidを付与しよう。

index.html
const [todos, setTodos] = useState([
    { id: genId(), task: "散歩", checked: false },
    { id: genId(), task: "ゲーム", checked: false }
])

todosにidを付与したら、liタグに、key={todo.id}を足してほしい。

レイアウトをTodoListに近づける。

TodoListぽくするために、Appコンポーネントのレイアウトを大幅に変える。

index.html
return (
    <div>
        <h1>{title}</h1>
        <div id="task">
            <label>タスク:</label>
                <input
                    type="text"
                    value={task}
                    onChange={(e) => setTask(e.target.value)}
                />
            <button id="add">タスクの追加</button>
        </div>
        
        <ul>
            {todos.map(({ id, task, checked }) => (
                <li key={id}>
                    <input type="checkbox" checked={checked} />
                    <p>{task}</p>
                    <button>削除</button>
                </li>
            ))}
        </ul>
    </div>
);

これだと、liタグの中が縦長なのはアレなのでflexを当てたい。
そこで、liに「todo」というクラスを当てよう。

index.html
<li key={id} class="todo"></li>

としたいところだが、Javascriptでclassが予約語なので、JSXではclassNameという単語を使う。

index.html
<li key={id} className="todo"></li>
style.css
.todo {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 10px;
}

.todo * {
    padding: 2px;
    margin: 5px;
}

これで、CSSが当たるはずである。

タスクの追加

まずは、タスクの追加から実装していこう。
流れとしては、
inputの取得 → todosに追加 → 表示 → inputボックスをクリアー
となる。

inputの取得 → todosに追加 → 表示 → inputボックスをクリアー
ここを一気にやりましょう。

タスクを追加する関数を定義する。
setTodos()の引数に、todosが更新されるので、非破壊的に追加しなければならない。
そのため、スプレッド構文を用いる。

index.html
const addTodo = () => {
    setTodos([...todos, {
        id: genId(),
        task,
        checked: false
    }])
}

inputの取得 → todosに追加 → 表示 → inputボックスをクリアー
では、今の太字の部分を実装してみよう。

解答例
index.html
// taskの値を空の文字列にすればよい。
const addTodo = () => {
    setTodos([...todos, {
        id: genId(),
        task,
        checked: false
    }])
+  setTask("");
}

このままでもいいのだが、inputテキストの値をuseStateで管理すると、入力するたびに、画面が書き換えられてしまうためパフォーマンスが悪い。
なので、useRefという、別のHooksを使用する。
Hooksを取り出す部分にuseRefを追加しよう。

index.html
const { useState, useRef } = React;

このuseRefは、取得したい要素にref属性を付与して使う。
inputタグを以下のように変えてほしい

index.html
<input
    type="text"
-   value={task}
-   onChange={(e) => setTask(e.target.value)}
+   ref={taskRef}
/>

taskRefは、taskの状態変数を定義している部分を置き換える。

index.html
- const [task, setTask] = useState("");
+ const taskRef = useRef();

addTodo関数では、refからタスクを取得する処理に変える。
ついでに空のタスクを追加できないようする。

index.html
const addTodo = () => {
+   const task = taskRef.current.value;
+   const msg = "タスクを入力してください。";
+   if (task === "") {
+       taskRef.current.placeholder = msg;
+       throw new Error(msg);
+   }
   
    setTodos([...todos, {
        id: genId(),
        task,
        checked: false
    }]);
    
-  setTask("");
+  taskRef.current.value = "";
}

currentの中に、querySelectorとかで取れるオブジェクトが入っている。

チェックボックスの実装

チェックボックスの切り替えは、toggle関数を作って、実装していく。
流れは、
todosをコピーする。→ idから、変えたいチェックボックスを取得する。 → checkedを反転させる。 → 状態を更新して表示する。

idから、変えたいチェックボックスを取得する。 には、配列のfindメソッドを利用する。

addTodoの下に定義しよう。

index.html
const toggle = (id) => {
    // todosをコピーする
    const newTodos = [...todos];
    // idから変えたいチェックボックスを取得する。
    const todo = newTodos.find(
        todo => todo.id === id // 条件がtrueになる一番最初の要素を取得。
    );
    // checkedを反転させる。
    todo.checked = !todo.checked;
    // 状態を更新して表示する。
    setTodos(newTodos);
};

そしたら、自力で、チェックボックスにtoggle関数を与えてみよう。

解答例
index.html
todos.map(({ id, task, checked }) => (
    // 省略...
    <input
        type="checkbox"
        checked={checked}
        onChange={() => toggle(id)}
    />
));

checkedがtrueの時は、タスクに取り消し線を入れ、テキストをグレーに変えよう。
三項演算子がわかれば、自力で実装できると思うので、挑戦してみてほしい。

解答例
index.html
<li key={id} className={`todo ${checked ? "delline" : ""}`}>
    {/* 省略 */}
</li>
style.css
.delline *:not(button) {
    text-decoration: line-through;
    color: grey;
}

削除の実装

remove関数を作って実装する。
流れ
todosをコピー。 → フィルタリングする → 状態を更新して表示。
フィルタリングは、filterメソッドを使うとよい。

index.html
const remove = (id) => {
    // todosをコピー。→ フィルタリングする
    const filteredTodos = [...todos].filter(
        // 引数todoには、todosの要素一つ一つが入っている。
        todo => todo.id !== id // 条件がtrueになるすべて要素の配列が返される。
    );
    // 状態を更新して表示。
    setTodos(filteredTodos);
    
}

軽く修正

今のままでは、このサイトを開いた瞬間、散歩とゲームをすることが確定してしまうので
todosの初期値を空の配列にしておこう。

index.html
const [todos, setTodos] = useState([]);

やってみよう1: 残りのタスクを表示する。

配列のfilterメソッドを使えば実装できる。

コードの一例
index.html
<div>残りのタスク: {todos.filter(({ checked }) => !checked).length}</div>

やってみよう2: 完了したタスクをまとめて削除する。

こちらも、filterメソッドで実装しよう。
一行で書けるので、buttonタグの中に直接、無名関数を入れてしまおう。

解答
index.html
<button
    onClick={
        () => setTodos([...todos].filter(({ checked }) => !checked))
    }
>
    完了したタスクを削除
</button>

これで、機能は十分だと思う。

一つ上の段階

このままで完成でもいいが、コードが見ずらいかもしれない。
現Appコンポーネントをここに示す。

index.html
const App = () => {
    const title = "Todo List";
    const taskRef = useRef();
    const [todos, setTodos] = useState([]);

    // タスクを追加する。
    const addTodo = () => {
        // inputテキストの取得。
        const task = taskRef.current.value;
        const msg = "タスクを入力してください。";
        
        if (task === "") {
            taskRef.current.placeholder = msg;
            throw new Error(msg);
        }
        
        taskRef.current.placeholder = "";
        setTodos([
            ...todos,
            {
                id: genId(),
                task,
                checked: false,
            },
        ]);
        taskRef.current.value = "";
    };
    
    const toggle = (id) => {
        // todosをコピーする
        const newTodos = [...todos];
        // idから変えたいチェックボックスを取得する。
        const todo = newTodos.find((todo) => todo.id === id);
        // checkedを反転させる。
        todo.checked = !todo.checked;
        // 状態を更新して表示する。
        setTodos(newTodos);
    };
    
    const remove = (id) => {
        const newTodos = [...todos].filter((todo) => todo.id !== id);
        setTodos(newTodos);
    };
    
    return (
        <div>
            <h1>{title}</h1>
            <div id='task'>
                <label>タスク:</label>
                <input type='text' ref={taskRef} />
                <button id='add' onClick={addTodo}>
                タスクの追加
                </button>
            </div>
            
            <div>
                残りのタスク: {[...todos].filter(({ checked }) => !checked).length}
            </div>
            <button
                onClick={() =>
                    setTodos([...todos].filter(({ checked }) => !checked))
                }>
                完了したタスクを削除
            </button>
        
            <ul>
                {todos.map(({ id, task, checked }) => (
                    <li key={id} className={`todo ${checked ? "delline" : ""}`}>
                        <input
                            type='checkbox'
                            checked={checked}
                            onChange={() => toggle(id)}
                        />
                        <p>{task}</p>
                        <button onClick={() => remove(id)}>削除</button>
                    </li>
                ))}
            </ul>
        </div>
    );
};

Reactには、コードを見やすくしたり、チームで開発しやすくしたりするために、
子コンポーネントという概念がある。
コンポーネントは、本来別ファイルで管理するが、CDNの環境のため、importなどの処理が面倒になることから、同じファイル内に二つのコンポーネントを定義する。
Appのしたに、TodoListコンポーネントを定義する。

index.html
const TodoList = () => {
    return;
}

TodoListのレイアウトは、ulタグを切り取って、貼り付けよう
toggleとremoveは、TodoListでしか使わないので、これも切り取って貼り付けよう。

index.html
const TodoList = () => {
   const toggle = (id) => {
      // todosをコピーする
      const newTodos = [...todos];
      // idから変えたいチェックボックスを取得する。
      const todo = newTodos.find((todo) => todo.id === id);
      // checkedを反転させる。
      todo.checked = !todo.checked;
      // 状態を更新して表示する。
      setTodos(newTodos);
    };

    const remove = (id) => {
      const newTodos = [...todos].filter((todo) => todo.id !== id);
      setTodos(newTodos);
    };
    
    return (
        <ul>
            {todos.map(({ id, task, checked }) => (
              <li key={id} className={`todo ${checked ? "delline" : ""}`}>
                <input
                  type='checkbox'
                  checked={checked}
                  onChange={() => toggle(id)}
                />
                <p>{task}</p>
                <button onClick={() => remove(id)}>削除</button>
              </li>
            ))}
        </ul>
    );
}

さて、問題は、[todos, setTodos]である。

AppにTodoListを表示する。

AppにTodoListを表示するには、TodoListタグのような形をとる。

index.html
const App = () => {
    return (
        // 割愛
-       <ul>{/* 省略 */}</ul>
        <TodoList />
    );
};

propsで値を渡す

コンポーネントの引数に、{ some }を入れると、HTMLの属性のような形で値を受け取れる。
これは、見たほうが早い。

index.html
const TodoList = ({ todos, setTodos }) => { // 割愛

const App = () => {
    // 割愛
    <TodoList todos={todos} setTodos={setTodos} />
}

勘づいた方もいると思うが、propsは、分割代入で取り出さている。
なので、const TodoList = props => props.todos.map // 省略
のように取り出すこともできる。これは、好みである。

最後に

これでTodoリストは99%完成した!

1%の課題は、リロードしたら、タスクが消えること。
この課題の解答例は、別の記事に書こうかと思う。
ヒントは、ローカルストレージだ。


最後まで読んでいただきありがとうございました。
公式ドキュメントはこちらから。

1
2
0

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
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?