こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
今回は、GraphQLの導入を行います。GraphQLはスキーマーを定義して実装することから、スキーマー駆動開発手法を取り入れることができます。
現場ではGraphQLを触ったことはありませんが後々使っていきたいと考えているので、そのメリット・デメリットを探っていきたいですね。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
<目次>
前回
事前準備
フォルダ/ファイル
1 2 |
touch backend-app/cmd/api/graphql.go touch go-movies/src/components/GraphQL.tsx |
モジュール
1 2 |
cd backend-app/ go get github.com/graphql-go/graphql |
GraphQLの実装
GraphQLは、Facebookが開発しているWeb APIのための規格で、 クエリ言語とスキーマ言語からなります。
REST APIだと、欲しい情報の数だけエンドポイント(URL)が必要になりますが、GraphQLではエンドポイントは一つだけで済みます。
例えば、以下のqueryをGraphQLサーバーに送信したとします。
1 2 3 4 5 6 |
例) query { allPets { animal } } |
データ送信後、次のような形式で、データを取得できます。
1 2 3 4 5 6 7 8 9 |
{ "data": { "allPets": [ { "animal": "Cat" } ] } } |
REST APIに比べると以下のメリットがあるようです。
・ エンドポイントを増やさずに欲しい情報を取得できる
・ 不必要な情報は取ってこない
・ フロントエンド・サーバーサイドの作業を完全に分業化できる
・ アプリケーションに縛られない自由なデータ取得サーバー(GraphQLサーバー)を立てられる
早速実装していきましょう!
ルートの追加
API側で、GraphQLへのルートを追加します。
1 2 3 4 5 6 7 |
// cmd/api/routes.go ... func (app *application) routes() http.Handler { ... router.HandlerFunc(http.MethodPost, "/v1/graphql/list", app.moviesGraphQL) ... } |
GraphQL SchemaとField設定
GraphQLのSchemaとFieldを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
// cmd/api/graphql.go package main import ( "backend/models" "io" "log" "net/http" "github.com/graphql-go/graphql" ) var ( movies []*models.Movie // graphql schema definition fields = graphql.Fields{ "movie": &graphql.Field{ Type: movieType, Description: "Get movie by id", Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.Int, }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(int) if ok { for _, movie := range movies { if movie.ID == id { return movie, nil } } } return nil, nil }, }, "list": &graphql.Field{ Type: graphql.NewList(movieType), Description: "Get all movies", Resolve: func(params graphql.ResolveParams) (interface{}, error) { return movies, nil }, }, } movieType = graphql.NewObject( graphql.ObjectConfig{ Name: "Movie", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.Int, }, "title": &graphql.Field{ Type: graphql.String, }, "description": &graphql.Field{ Type: graphql.String, }, "year": &graphql.Field{ Type: graphql.Int, }, "release_date": &graphql.Field{ Type: graphql.DateTime, }, "runtime": &graphql.Field{ Type: graphql.Int, }, "rating": &graphql.Field{ Type: graphql.Int, }, "mpaa_rating": &graphql.Field{ Type: graphql.String, }, "created_at": &graphql.Field{ Type: graphql.DateTime, }, "updated_at": &graphql.Field{ Type: graphql.DateTime, }, }, }, ) ) |
ちょっと複雑ですよね。詳しくは、GraphQLのLearnを見てください。
ハンドラーの実装
次にハンドラーを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<meta charset="utf-8">// cmd/api/graphql.go func (app *application) moviesGraphQL(w http.ResponseWriter, r *http.Request) { movies, _ = app.models.DB.All() q, _ := io.ReadAll(r.Body) query := string(q) log.Println(query) rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields} schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} schema, err := graphql.NewSchema(schemaConfig) if err != nil { app.errorJSON(w, errors.New("failed to create schema")) log.Println(err) return } params := graphql.Params{Schema: schema, RequestString: query} resp := graphql.Do(params) if len(resp.Errors) > 0 { app.errorJSON(w, errors.New(fmt.Sprintf("failed: %+v", resp.Errors))) } j, _ := json.MarshalIndent(resp, "", " ") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(j) } |
GrphQLAPIへリクエストを送る
今度は、React側でGraphQL APIへリクエストを送信する処理を実装します。
GraphQLコンポーネント
リクエストを送信するために、GraphQLコンポーネントを実装します。内容はこれまで実装してきたものとほぼ同じなので、サクッといきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// components/GraphQL.tsx import { useEffect, useState } from 'react' import { Movie } from '../models/movie' import { AlertType } from '../models/ui-components' import axios from 'axios' const GraphQL = (props: any) => { const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") const [alert, setAlert] = useState<AlertType>({type: "d-none", message: ""}) useEffect(() => { ( async () => { const payload = ` { list { id title runtime year } } ` const config = { headers: { 'Content-Type': 'application/json' } } await axios.post('graphql/list', payload, config) .then((response) => { setMovies(response.data.data.list) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) return ( <h2>GraphQL</h2> ) } export default GraphQL |
ルートの追加
GraphQLコンポーネントへのルートを追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// src/App.tsx ... import GraphQL from './components/GraphQL' const App = (props: any) => { ... return ( <Router> ... <div className="row"> <div className="col-md-2"> <nav> <ul className="list-group"> ... <li className="list-group-item"> <Link to="/graphql">GraphQL</Link> </li> </ul> </nav> </div> <div className="col-md-10"> <Switch> ... <Route exact path="/graphql"> <GraphQL /> </Route> ... </Switch> </div> </div> </div> </Router> ) } ... |
検証
「docker-compose up」でコンテナを立ち上げ、「http://localhost:3000」にアクセスしましょう。

リスト内に「GraphQL」が表示されるはずなので、クリックします。

ChromeのデベロッパーツールからConsoleを確認するとデータが取得できているので、成功ですね!
Movieリストの表示
Movieリストを表示しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// components/GraphQL.tsx ... const GraphQL = (props: any) => { ... if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>GraphQL</h2> <div className="list-group"> {movies.map((m) => { return ( <Link key={m.id} to={`/movies/${m.id}`} className="list-group-item list-group-item-action" > <strong>{m.title}</strong> </Link> ) })} </div> </Fragment> ) } export default GraphQL |

OKですね。
GraphQLのメリット
現在、Reactに設定しているクエリは以下です。
1 2 3 4 5 6 |
<meta charset="utf-8">list { id title runtime year } |
そして、取得データは下記のようになっています。

次に、このクエリにdescriptionを足してみましょう。
1 2 3 4 5 6 7 |
<meta charset="utf-8">list { id title runtime year description } |

descriptionが追加されました!
欲しいデータをクエリに指定するだけで取得できるのでとても便利です。
また、APIの作り(エンドポイント)に依存しないのでフレキシブルですし、必要最低限のデータを取得するようにリクエストを遅れるので、フロントエンド <—>サーバ間の通信量を節約できますね。
GraphQLのデメリット
データ取得に関してはとても便利でしたが、API側できちっとしたスキーマーを定義しないといけないところがちょっとめんどくさそうですね。
今回はリクエスト先のテーブルが一つだけだったのでそこまで複雑なスキーマーにならなかったのですが、データベース間のリレーションとかあった場合はどんな感じになるんだろうと思いました。
慣れればとっつきやすくなるんですかね?
次回
記事まとめ
参考書籍
ソースコード
graphql.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
// cmd/api/graphql.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "io" "log" "net/http" "github.com/graphql-go/graphql" ) var ( movies []*models.Movie // graphql schema definition fields = graphql.Fields{ "movie": &graphql.Field{ Type: movieType, Description: "Get movie by id", Args: graphql.FieldConfigArgument{ "id": &graphql.ArgumentConfig{ Type: graphql.Int, }, }, Resolve: func(p graphql.ResolveParams) (interface{}, error) { id, ok := p.Args["id"].(int) if ok { for _, movie := range movies { if movie.ID == id { return movie, nil } } } return nil, nil }, }, "list": &graphql.Field{ Type: graphql.NewList(movieType), Description: "Get all movies", Resolve: func(params graphql.ResolveParams) (interface{}, error) { return movies, nil }, }, } movieType = graphql.NewObject( graphql.ObjectConfig{ Name: "Movie", Fields: graphql.Fields{ "id": &graphql.Field{ Type: graphql.Int, }, "title": &graphql.Field{ Type: graphql.String, }, "description": &graphql.Field{ Type: graphql.String, }, "year": &graphql.Field{ Type: graphql.Int, }, "release_date": &graphql.Field{ Type: graphql.DateTime, }, "runtime": &graphql.Field{ Type: graphql.Int, }, "rating": &graphql.Field{ Type: graphql.Int, }, "mpaa_rating": &graphql.Field{ Type: graphql.String, }, "created_at": &graphql.Field{ Type: graphql.DateTime, }, "updated_at": &graphql.Field{ Type: graphql.DateTime, }, }, }, ) ) func (app *application) moviesGraphQL(w http.ResponseWriter, r *http.Request) { movies, _ = app.models.DB.All() q, _ := io.ReadAll(r.Body) query := string(q) log.Println(query) rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields} schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)} schema, err := graphql.NewSchema(schemaConfig) if err != nil { app.errorJSON(w, errors.New("failed to create schema")) log.Println(err) return } params := graphql.Params{Schema: schema, RequestString: query} resp := graphql.Do(params) if len(resp.Errors) > 0 { app.errorJSON(w, errors.New(fmt.Sprintf("failed: %+v", resp.Errors))) } j, _ := json.MarshalIndent(resp, "", " ") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write(j) } |
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
// cmd/api/routes.go package main import ( "context" "net/http" "github.com/julienschmidt/httprouter" "github.com/justinas/alice" ) func (app *application) wrap(next http.Handler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ctx := context.WithValue(r.Context(), "params", ps) next.ServeHTTP(w, r.WithContext(ctx)) } } func (app *application) routes() http.Handler { router := httprouter.New() secure := alice.New(app.checkToken) router.HandlerFunc(http.MethodGet, "/status", app.statusHandler) router.HandlerFunc(http.MethodPost, "/v1/graphql/list", app.moviesGraphQL) router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin) router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) router.HandlerFunc(http.MethodGet, "/v1/movies", app.getAllMovie) router.HandlerFunc(http.MethodGet, "/v1/movies/:genre_id", app.getAllMoviesByGenres) router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres) router.POST("/v1/admin/editmovie", app.wrap(secure.ThenFunc(app.editMovie))) router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie) return app.enableCORS(router) } |
Admin.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// components/Admin.tsx import { Dispatch, Fragment, useEffect, useState } from "react" import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' import { Movie } from '../models/movie' import { Link } from 'react-router-dom' import axios from 'axios' const Admin = (props: any) => { const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { if (props.jwt === "") { props.history.push({ pathname: "/login" }) return } ( async () => { await axios.get('movies') .then((response) => { setMovies(response.data.movies) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Manage Catalogue</h2> <hr /> <div className="list-group"> {movies.map((m) => { return ( <Link key={m.id} to={`/admin/movie/${m.id}`} className="list-group-item list-group-item-action" > {m.title} </Link> ) })} </div> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(Admin) |
EditMovie.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
// components/EditMovie.tsx import { Dispatch, Fragment, useEffect, useState, SyntheticEvent } from "react" import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' import { AlertType } from '../models/ui-components' import { Movie } from '../models/movie' import { Link } from 'react-router-dom' import { confirmAlert } from 'react-confirm-alert' import 'react-confirm-alert/src/react-confirm-alert.css' import Input from './form-components/Input' import TextArea from './form-components/TextArea' import Select from './form-components/Select' import Alert from './ui-components/Alert' import './EditMovie.css' import axios from 'axios' const EditMovie = (props: any) => { const [movie, setMovie] = useState<Movie>() const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") const [errors, setErrors] = useState<string[]>([]) const [alert, setAlert] = useState<AlertType>({type: "d-none", message: ""}) // form data const [id, setID] = useState(0) const [title, setTitle] = useState("") const [description, setDescription] = useState("") const [releaseDate, setReleaseDate] = useState("") const [runtime, setRuntime] = useState(0) const [rating, setRating] = useState(0) const [mpaaRating, setMpaaRating] = useState("") const [mpaaOptions, setMpaaOptions] = useState([{}]) useEffect(() => { if (props.jwt === "") { props.history.push({ pathname: "/login" }) return } ( async () => { setMpaaOptions([ {id: "G", value: "G"}, {id: "PG", value: "PG"}, {id: "PG13", value: "PG13"}, {id: "R", value: "R"}, {id: "NC17", value: "NC17"}, ]) // URLからIDを取得 setID(props.match.params.id) if (id > 0) { // Edit await axios.get(`movie/${id}`) .then((response) => { let movie = response.data.movie const releaseDate = new Date(movie.release_date) movie.release_date = releaseDate.toISOString().split("T")[0] setMovie(movie) setTitle(movie.title) setDescription(movie.description) setReleaseDate(movie.release_date) setRuntime(movie.runtime) setRating(movie.rating) setMpaaRating(movie.mpaa_rating) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } else { // New setIsLoaded(true) } } )() }, [id]) const submit = async (e: any) => { e.preventDefault() const movie: Movie = { id: id, title: title, description: description, year: "2021", release_date: releaseDate, runtime: Number(runtime), rating: Number(rating), mpaa_rating: mpaaRating, genres: [] } // validation let localErrors: string[] = [] if (movie?.title === "") { localErrors.push("title") } if (movie?.release_date === "") { localErrors.push("release_date") } if (movie?.runtime === 0) { localErrors.push("runtime") } if (movie?.rating === 0) { localErrors.push("rating") } setErrors(localErrors) if (errors.length > 0) { return false } const config = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${props.jwt}` } } await axios.post('admin/editmovie', JSON.stringify(movie), config) .then((response) => { setAlert({ type: 'alert-success', message: 'Changes saved!' }) props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.message) setAlert({ type: 'alert-danger', message: err.response.data.error.message }) }) } const hasError = (key: string) => { return errors.indexOf(key) !== -1 } const confirmDelete = (e: SyntheticEvent) => { e.preventDefault() confirmAlert({ title: 'Delete Movie?', message: 'Are you sure?', buttons: [ { label: 'Yes', onClick: async () => { await axios.get(`admin/deletemovie/${id}`) .then((response) => { props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.message) setAlert({ type: 'alert-danger', message: err.response.data.error.message }) }) } }, { label: 'No', onClick: () => {} } ] }); } if (error) { // return ( // <div>Error: {error}</div> // ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Add/Edit Movie</h2> <Alert alertType={alert.type} alertMessage={alert.message} /> <hr /> <form method="post" onSubmit={submit}> <Input title={"Title"} className={hasError("title") ? "is-invalid" : ""} type={'text'} name={'title'} value={title} handleChange={setTitle} errorDiv={hasError("title") ? "text-danger" : "d-none"} errorMsg={"Please enter a title"} /> <Input title={"Release Date"} className={hasError("release_date") ? "is-invalid" : ""} type={'date'} name={'release_date'} value={releaseDate} handleChange={setReleaseDate} errorDiv={hasError("release_date") ? "text-danger" : "d-none"} errorMsg={"Please enter a release_date"} /> <Input title={"Runtime"} className={hasError("runtime") ? "is-invalid" : ""} type={'text'} name={'runtime'} value={runtime} handleChange={setRuntime} errorDiv={hasError("runtime") ? "text-danger" : "d-none"} errorMsg={"Please enter a runtime"} /> <Select title={'MPAA Rating'} name={'mpaa_rating'} options={mpaaOptions} value={mpaaRating} handleChange={setMpaaRating} placeholder={'Choose...'} /> <Input title={"Rating"} className={hasError("rating") ? "is-invalid" : ""} type={'text'} name={'rating'} value={rating} handleChange={setRating} errorDiv={hasError("rating") ? "text-danger" : "d-none"} errorMsg={"Please enter a rating"} /> <TextArea title={"Description"} name={'description'} value={description} handleChange={setDescription} rows={3} /> <hr /> <button className="btn btn-primary" type="submit">Save</button> <Link to="/admin" className="btn btn-warning ms-1">Cancel</Link> {id > 0 && ( <a href="#!" onClick={confirmDelete} className="btn btn-danger ms-1"> Delete </a> )} </form> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(EditMovie) |
Home.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// components/Home.tsx import { Fragment } from "react" import Ticket from "./../images/movie_tickets.jpg" import "./Home.css" const Home = () => { return ( <Fragment> <div className="text-center"> <h2>This is the home page</h2> <hr /> <img src={Ticket} alt="movie ticket" /> <hr /> <div className="tickets"></div> </div> </Fragment> ) } export default Home |
Home.css
1 2 3 4 5 6 7 8 9 |
/* components/Home.css */ .tickets { background-image: url("./../images/movie_tickets.jpg"); width: 200px; height: 143px; margin-left: auto; margin-right: auto; } |
Login.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
// components/Login.tsx import { Dispatch, useState, Fragment, SyntheticEvent } from 'react' import { connect } from 'react-redux' import { AlertType } from '../models/ui-components' import { Credentials } from '../models/tokens' import { setJWTAction } from '../redux/actions/setJWTAction' import Alert from './ui-components/Alert' import Input from './form-components/Input' import axios from 'axios' const Login = (props: any) => { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [error, setError] = useState("") const [errors, setErrors] = useState<string[]>([]) const [alert, setAlert] = useState<AlertType>({type: "d-none", message: ""}) const submit = async (e: SyntheticEvent) => { e.preventDefault() // エラー初期化 setErrors([]) let submitErrors: string[] = [] if (email === "") { submitErrors.push("email") } if (password === "") { submitErrors.push("password") } if (submitErrors.length > 0) { setErrors(submitErrors) return } const payload: Credentials = { username: email, password: password } await axios.post("signin", JSON.stringify(payload)) .then((response) => { const jwtToken = response.data.response props.setJWT(jwtToken) window.localStorage.setItem( "jwt", JSON.stringify(jwtToken)) props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.response.data.error.message) setAlert({ type: "alert-danger", message: err.response.data.error.message}) }) } const hasError = (key: string) => { return errors.indexOf(key) !== -1 } return ( <Fragment> <h2>Login</h2> <hr /> <Alert alertType={alert.type} alertMessage={alert.message} /> <form className="pt-3" onSubmit={submit}> <Input title={"Email"} type={'email'} name={'email'} handleChange={setEmail} className={hasError("email") ? "is-invalid" : ""} errorDiv={hasError("email") ? "text-danger" : "d-none"} errorMsg={"Please enter a valid email address"} /> <Input title={"Password"} type={'password'} name={'password'} handleChange={setPassword} className={hasError("password") ? "is-invalid" : ""} errorDiv={hasError("password") ? "text-danger" : "d-none"} errorMsg={"Please enter a password"} /> <hr /> <button className="btn btn-primary">Login</button> </form> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(Login) |
tokens.ts
1 2 3 4 5 6 |
// models/tokens.ts export interface Credentials { username: string password: string } |
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
// src/App.tsx import { Dispatch, Fragment, useEffect } from 'react' import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { connect } from 'react-redux' import { setJWTAction } from './redux/actions/setJWTAction' import Home from './components/Home' import Movies from './components/Movies' import Admin from './components/Admin' import Genres from './components/Genres' import OneMovie from './components/OneMovie' import OneGenre from './components/OneGenre' import EditMovie from './components/EditMovie' import Login from './components/Login' import GraphQL from './components/GraphQL' const App = (props: any) => { useEffect(() => { const jwtToken = window.localStorage.getItem("jwt") if (jwtToken) { if (props.jwt === "") { props.setJWT(JSON.parse(jwtToken)) } } }) const logout = () => { props.setJWT("") window.localStorage.removeItem("jwt") } let loginLink if (props.jwt === "") { loginLink = <Link to="/login">Login</Link> } else { loginLink = <Link to="/logout" onClick={logout}>Logout</Link> } return ( <Router> <div className="container"> <div className="row"> <div className="col mt-3"> <h1 className="mt-3"> Go Watch a Movie! </h1> </div> <div className="col mt-3 text-end"> {loginLink} </div> <hr className="mb-3" /> </div> <div className="row"> <div className="col-md-2"> <nav> <ul className="list-group"> <li className="list-group-item"> <Link to="/">Home</Link> </li> <li className="list-group-item"> <Link to="/movies">Movies</Link> </li> <li className="list-group-item"> <Link to="/genres">Genres</Link> </li> { props.jwt !== "" && <Fragment> <li className="list-group-item"> <Link to="/admin/movie/0">Add Movie</Link> </li> <li className="list-group-item"> <Link to="/admin">Manage Catalog</Link> </li> </Fragment> } <li className="list-group-item"> <Link to="/graphql">GraphQL</Link> </li> </ul> </nav> </div> <div className="col-md-10"> <Switch> <Route path="/movies/:id" component={OneMovie} /> <Route path="/movies"> <Movies /> </Route> <Route path="/genre/:id" component={OneGenre} /> <Route exact path="/login" component={(props: any) => <Login {...props} />} /> <Route exact path="/genres"> <Genres /> </Route> <Route exact path="/graphql"> <GraphQL /> </Route> <Route path="/admin/movie/:id" component={EditMovie} /> <Route path="/admin" component={Admin} /> <Route path="/"> <Home /> </Route> </Switch> </div> </div> </div> </Router> ) } // State -> Props const mapStateToProps = (state: {jwt: string}) => ({ jwt: state.jwt }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) // AppコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(App) |
GraphQL.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
// components/GraphQL.tsx import { Fragment, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Movie } from '../models/movie' import axios from 'axios' const GraphQL = (props: any) => { const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { ( async () => { const payload = ` { list { id title runtime year description } } ` const config = { headers: { 'Content-Type': 'application/json' } } await axios.post('graphql/list', payload, config) .then((response) => { setMovies(response.data.data.list) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) console.log({movies}) if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>GraphQL</h2> <div className="list-group"> {movies.map((m) => { return ( <Link key={m.id} to={`/movies/${m.id}`} className="list-group-item list-group-item-action" > <strong>{m.title}</strong> </Link> ) })} </div> </Fragment> ) } export default GraphQL |
コメントを残す
コメントを投稿するにはログインしてください。