8
7

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 1 year has passed since last update.

はじめてのReactで作るカンバン方式TODOリスト

Posted at

なぜつくるのか

SPAに可能性を感じてしまったから

どんな可能性?

(ソース:自分で調べた限りの浅い知識)
SPA化されたWebサイトは、スマホから見た時にまるで純正アプリみたいな使いやすさがあるらしい
しかも、SPA化されたWebサイトにはアフィも付けれる為、広告収入も入ってくる
そして、純正アプリとは違い、プラットフォームにアプリ登録料を支払う必用がなくなる
(iOSアプリ作ってみたかったけど、このハードルで萎えた時があるから、個人的にはポイント大)

何より、実質Webサイトをブラウザから開いているだけだから、Flutterみたいな感じでクロスプラットフォーム化されていると言っても少しだけ過言なくらいだと思う

つまり、WebサイトをSPA化するとユーザーにとって使いやすくなり、なおかつ収益化も見込めると思った

作業開始前の自分の状態

  • マイナー言語を扱うPG(実務経験1年)

    • 余談:どのくらいマイナー?
      • A.公式が出版している言語規格がネットで調べると英語版しかなく、日本語版は有料(高い)
      • 基本的にはJSと同じだが、JS+独自宣言された関数がある(記憶が正しければ、2000年代前半のJS仕様)
      • 参入障壁が高く、需要が限られている業界で使われているプログラム言語
  • 文系(?)大学出身 知識はITパスポート程度

    • 基本情報技術者試験の為に絶賛勉強中

今回の目標:一旦、Reactに慣れる

今の状態ではSPAどころか、そもそもWebサイト(Webアプリ)を作れるかも怪しいレベル
SPA化すらされていないシンプルなWebサイトを作ってみてる。

作業の流れ

  1. 企画(作るものを決める)
  2. 要件定義(どんな機能を実装するか決める)
  3. 設計(コンポーネント設計)
  4. 実装(Reactで実際に実装を進めてみる)

※諸事情により、テストは省略

1.企画

作るものを決める
どの言語でも共通だと思うが、最初はTodoリストを作るのが王道になっている。
例に漏れずにTodoリストを作る

但し、(タイトル通り)カンバン方式TODOリストを作成する。

なぜカンバン方式なのか

  1. 多くが想像するTodoリストが個人的には使いにくい(タスクの状態が分かると進捗を把握しやすい)
  2. ドラッグ&ドロップのイベントハンドラを使ったことがなかったから
  3. ロマン

2.要件定義

  • キーボード入力でタスクを入力でき、「追加」ボタン押下でタスクが未着手リストに追加される
  • 未着手に追加されたタスクは「作業中」「完了」にドラッグ&ドロップで移動させることができる
  • リロードしても、タスクのステータスを残っているようにする
  • 細かい部分はケースバイケース

最終的な完成画面はこんな感じ

Animation.gif

3.設計

ここからは、逐一調べながら実装を進めていく
コンポーネント設計は画像のような感じ

image.png

コンポーネント設計はもう感覚で進めました
多分、セオリー的な設計があると思われるが、まずは思い通り作ってみることにした

4.実装

ここが一番苦労した。。。
ここまで、作ろうとしてから2週間が経過している

HTML,CSS,Javascriptの最低限の知識はあると思っていたが、そんなこともなかったので、Reactの仕様等を調べながら、Javascriptの仕様を時々調べてたりして進めた。

1. App.js内でレイアウト実装する

こんな感じ
使っていない変数等があるが、先に見た目を作った

App.js
import logo from './logo.svg';
import './App.css';
import { useForm } from 'react-hook-form';

function App() {
  const { register, handleSubmit } = useForm();

  function onDropEvent(event){
    console.log("dropped");
    console.log(event);
  }

  function onDragOver(event){
    event.preventDefault(); // これがないとdropイベントが発火しない
  }

  function onSubmitEventHander(event){
    console.log(event.task);
  }

  const noStartTasks = ["nostart","taskA","taskB"];
  const list = noStartTasks.map((noStartTask) => <ol className="list-centering list" draggable="true" key={noStartTask}>{noStartTask}</ol>);

  const workingTasks = ["working","taskA","taskB"];
  const workingList = workingTasks.map((workingTask) => <ol className="list-centering list" draggable="true" key={workingTask}>{workingTask}</ol>);

  const doneTasks = ["done","taskA","taskB"];
  const doneList = doneTasks.map((doneTask) => <ol className="list-centering list" draggable="true" key={doneTask}>{doneTask}</ol>);


  return (
    <div className="App">
      <h1>カンバン方式TODO</h1>
      <form name="newTask" onSubmit={handleSubmit(onSubmitEventHander)} >
        <input id="task" type="text" {...register('task')}/>
        <input type="submit" value="追加"/>
      </form>

      <div id="statusArea" className="width100 centering">
        <div id="notStart" className="left statusBox width30" onDragOver={onDragOver} onDrop={onDropEvent}>
          <h2>未着手</h2>
          <ul dropzone="move">
            {list}
          </ul>
        </div>
        <div id="working" className="left statusBox width30" onDragOver={onDragOver} onDrop={onDropEvent}>
          <h2>作業中</h2>
          <ul dropzone="move">
            {workingList}
          </ul>
        </div>
        <div id="done" className="left statusBox width30" onDragOver={onDragOver} onDrop={onDropEvent}>
          <h2>完了</h2>
          <ul dropzone="move">
            {doneList}
          </ul>
        </div>
      </div>
    </div>
  );
}

export default App;

2. コンポーネント化

ソースコードや、どのように処理させたいかによって、どのようにコンポーネント化するか検討する
(設計時点でコンポーネント設計をしていたが、実際はこの段階で設計を確定させていた。)
フォルダ構成は最終的に以下のようになった

Kanban-todo
├─public
│      index.html
└─src
    │  App.css
    │  App.js
    │  firebase.js
    │  index.css
    │  index.js
    │  reportWebVitals.js
    │  setupTests.js
    │
    ├─component
    │      Form.jsx
    │      TaskArea.jsx
    │
    └─store
            data-context.jsx

3. DB準備

今回は、リロードしても情報が残っているようにするため、DBにタスク情報を保存する必要があった
多分、AWSが提供しているDBだったり、他のイケてるDBがあるかもしれませんが、今回はFirebase
選んだ理由は、

  • セットアップが簡単
  • すぐ組み込むことができる
  • Googleがサポートしていること

以上。あまり深くは考えてない

4. Form.jsx実装

まず、最終的なソースコードは以下(ログ出力は全てデバッグの為)

Form.jsx
import { useForm } from 'react-hook-form';
import {Firebase} from "../firebase.js";
import { getFirestore, collection, doc, setDoc, query, getDocs} from "firebase/firestore";

function Form({onTaskAdd}){
    console.log("==========Form rendering==========");
    const { register, handleSubmit,reset } = useForm();
    const db = getFirestore(Firebase);
    const Ref = collection(db, "kanban-todo");

    function onSubmitEventHander(event){
        let newTask = {status: "notStart",name: event.task};
      
        const document = doc(Ref, event.task);
        setDoc(document, {
            status: "notStart",
            name: event.task
        });
        onTaskAdd(newTask);
        reset();
      }

    return(
    <div>
        <form name="newTask" onSubmit={handleSubmit(onSubmitEventHander)} >
            <input id="task" type="text" {...register('task')}/>
            <input type="submit" value="追加"/>
        </form>
    </div>

    )

}
export default Form;

フォームの実装ってどうやってやるんだっけ。。。となりながら、同時並行でFormフックを調べて何とか実装
(正直、 {...register('task')}の部分はなぜこうしているのか未だに理解できていない。良くない)

reset()の部分が個人的に感動してしまった
最初、inputタグのIDをとってきて、"(空)"をセットして初期化としようとしたが、うまく行かなくて困っていたが、この1行ですべて解決した。すごい

4. TextArea.jsx実装

ここが一番時間かかった
最終的なソースコードは以下

TextArea.jsx
import { useContext,} from "react";
import {DnDContext} from '../store/data-context.jsx';

function TaskArea({title,status,taskList,onTaskMove}){
console.log(title + " TaskArea rendering");
    const dndContext = useContext(DnDContext);


    let tempList = [];

    function onDropEvent(event){
        console.log("=======" + title + " onDropEvent=======");
        let newTaskStatus = dndContext.draggedId;
        newTaskStatus.status = status;
        onTaskMove(newTaskStatus);

    }

    let dragInfo = dndContext.draggedId;
    function onDragStartHandler(event){
        console.log("==========" + title + " onDragStartHandler==========");
        dragInfo.name = event.target.innerText;
        dragInfo.status = status;

        dndContext.setDraggedId(dragInfo);
        for(let i=0; i<taskList.length; i++){
            if(taskList[i].name == dragInfo.name){
                taskList.splice(i,1);
                break;
            }
        }

    }

    function onDragEndHandler(event){
        console.log("==========" + title + " onDragEndHandler==========");
    }

    function getTaskList(){
        tempList = [];
        for(let i=0; i<taskList.length; i++){
            if(taskList[i].status == status){
                tempList[i] = <ol className="list-centering list" draggable="true" onDragStart={onDragStartHandler} onDragEnd={onDragEndHandler} key={"id" + i}>{taskList[i].name}</ol>;
            }
        }
        return tempList;
    }


    function onDragOver(event){
        event.preventDefault(); // これがないとdropイベントが発火しない
    }

    return(
        <div id={status} className="left statusBox width30" onDragOver={onDragOver} onDrop={onDropEvent}>
            <h2>{title}</h2>
            <ul dropzone="move">
                {
                    getTaskList()
                }
            </ul>
        </div>
    )
}

export default TaskArea;

TaskAreaコンポーネントだけで、2週間以上かけていた。。。
覚えている限りのつまずきポイントは以下

  • ドラッグ&ドロップの実装方法
  • useEffectの扱い
  • レイアウトに反映させる方法
  • state更新したのになぜか再レンダリングされない
  • 再レンダリングの最適化がうまく行かない
  • stateを他のコンポーネントで参照させたい
  • 子コンポーネントに変数を引き渡したい
  • 子コンポーネントから親コンポーネントの関数を実行したい

...全て挙げるとキリがない
その中で、なるほど!となったのは以下

子コンポーネントからから親コンポーネントの関数を実行する方法

Parent.jsx
function Parent(){
    function ParentFunc(param){
        console.log("子コンポーネントから呼ばれた");
        console.log(param); // Child.jsx内でonSomething()がコールされた時の引数が出力される
    }
    return(
        <Child onSomething={ParentFunc}/>
    )
}
Child.jsx
function Child({onSomething}){

    onSomething("test");// Parent.jsxのParentFunc()がコールされる

    return(
        <p>なんか</p>
    )
}

大体1か月くらいかけて、やっと完成

最終的なソースコードは以下に格納
https://github.com/TakayukiOshiro/kanban-todo
firebase.jsのAPIキーなどは、自分でDB作ってからコピペして頂ければ、、、

作ってみた感想

Reactは情報が豊富なので、調べること自体は苦ではなかったが、情報が古かったり、思い通りに動かなかったりしたのが、逆に新鮮だった

今後、このTodoリストをベースにして機能追加できれば良いかなと思った
この機能追加してみてとかあったらコメントください。

最後に

初めて?Qiitaで記事を書いてみたけど、Markdown構文はレイアウトに反映されると見やすいけど、普通のメモとして使うには見づらいからメモのフォーマットには向いていないと思った
(以前、マークダウン構文でメモ書いている人いたけど、逆にすごいと感じた)

8
7
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
8
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?