今回は残りのアクションに関して実装していく。残りのアクションは以下の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 側も起動する。動作が少し遅いので、そのあたりは改善の余地がある。
成果物
以下に今回の成果をアップしている。
GitHub - bamchoh/react-study at 64c4cdccb4590d8aa906955d48b1320068da4ba6