React 入門 (10) ~ TODOアプリを作る(5) ~ データの永続化(2)

今回は残りのアクションに関して実装していく。残りのアクションは以下の3つ

  • タスク完了時のアクション
  • 削除時のアクション
  • 初期描画時のアクション

初めの2つについては既存のアクションだが、最後の1つは今回新たに追加するアクションだ。

タスク完了時のアクション

フロント側

前回の追加時のアクションと同様、フロント側はアクションクリエイターとディスパッチャーの間に通信用のタスクを噛ます形で作成する。sendToApiServer にタスク完了時のアクション用のコードを追加する。

    case 'COMPLETE_TODO':
      {
        const method = 'POST'
        const obj = action;
        const body = JSON.stringify(obj);
        const res: Response = await fetch('/api/complete_todo', {method, headers, body})

        if(res.ok) {
          const json = await res.json()
          dispatch(json)
        }
        break
      }

バック側

エンドポイント /api/complete_todo に対してハンドラを追加する。

// /api/complete_todo に対するハンドラ
func postCompleteTodoHandler(c *gin.Context) {
    var action AddTodoAction
    if err := c.ShouldBindJSON(&action); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    var err error
    if _, err = completeTodo(action.ID); err != nil {
        log.Printf("[COMPLETE] %v", err.Error())
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    // completeTodo でエラーが発生しなかったということは正常に値の変更ができたと仮定して
    // ここでは送られてきたデータを使ってレスポンスを作成
    c.JSON(http.StatusOK, gin.H{"type": action.Type, "id": action.ID})
}

// Complete アクションをDBに伝える
func completeTodo(id int64) (sql.Result, error) {
    act, err := fetchAddTodoAction(id)
    if err != nil {
        return nil, err
    }

    format := "UPDATE todo SET completed = %d WHERE id = %d"
    stmt := ""

    // sqlite3 は boolean タイプがないため、boolean を数値にして格納する必要がある
    if act.Completed {
        stmt = fmt.Sprintf(format, 0, id)
    } else {
        stmt = fmt.Sprintf(format, 1, id)
    }
    return db.Exec(stmt)
}

削除時のアクション

フロント側

完了時のアクション同様に sendToApiServer 関数にアクションを追加する。完了時のアクションと異なるのはエンドポイント名のみ(`/api/delete_todo') なので、コードは省略する。

バック側

こちらも特筆すべきことはないので省略する。

初期描画時のアクション

フロント側

初回のアクセスの際にすでにDBにあるTODOリストを取得して表示させる方法を考える。Reactを使わない場合は window.on_load 時にやることだが、React では コンポーネントがマウントされた時点で呼ばれる関数 componentWillMount() がある。そこで APIと通信することで state を更新する。

componentWillMount() {
  sendToApiServer(this.props.dispatch, fetchTodo());
}

アクションクリエイターは単純にtypeを指定しているだけ

export const fetchTodo = () => ({
  type: 'FETCH_TODO',
})

sendToApiServer は GET リクエストで指定している (POST でもいいと思うが、なんとなく)

    case 'FETCH_TODO':
      {
        const method = 'GET'
        const ep = '/api/fetch_todo'
        const res: Response = await fetch(ep, {method, headers})

        if(res.ok) {
          const json = await res.json()
          dispatch(json)
        }
        break
      }

レデューサーは複数のデータが返ってくることを期待してステートを更新している。(アクションクリエイターで作成したアクションの構造と異なってもいいのか? という疑問はある) また、DBが空の場合はnullチェックで現在のstateを返すようにしている。

      if(action.data === null) {
        return state
      }
      return action.data.map((todo:any) => {
        return ({
          id: todo.id,
          text: todo.text,
          completed: todo.completed,
        })
      })

サーバー側

/api/fetch_todo のハンドラを作成する

// /api/fetch_todo のハンドラ
func postFetchTodoHandler(c *gin.Context) {
    var acts []AddTodoAction
    var err error
    if acts, err = fetchAllTodo(); err != nil {
        log.Printf("[SELECT] %v", err.Error())
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "type": "FETCH_TODO",
        "data": acts,
    })
}

// データベースから全データ取得
func fetchAllTodo() ([]AddTodoAction, error) {
    var stmt = ""
    stmt += fmt.Sprintf("SELECT * FROM todo")
    rows, err := db.Query(stmt)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var acts []AddTodoAction
    for rows.Next() {
        var act AddTodoAction
        var complete int64
        if err := rows.Scan(&act.ID, &act.Text, &complete); err != nil {
            return nil, err
        }
        if complete == 0 {
            act.Completed = false
        } else {
            act.Completed = true
        }
        acts = append(acts, act)
    }
    return acts, nil
}

実行結果

APIサーバー側を go run main.go で起動し、そのあと npm start で React 側も起動する。動作が少し遅いので、そのあたりは改善の余地がある。

f:id:bamch0h:20190203134403g:plain
TODO DB通信

成果物

以下に今回の成果をアップしている。

GitHub - bamchoh/react-study at 64c4cdccb4590d8aa906955d48b1320068da4ba6