20210410のReactに関する記事は4件です。

【Railsサーバー】Address already in use - bind(2) for "127.0.0.1" port 8080 (Errno::EADDRINUSE)の対処方法

症状 RailsAPIモードとReactを使用中に、npmstartとRailsサーバーを起動しようとすると、Railsサーバーを起動したターミナルに下記のエラーメッセージが表示されてしまいました。 ターミナル rails s => Booting Puma => Rails 6.0.3.6 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.7 (ruby 2.6.3-p62), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development Exiting Traceback (most recent call last): 40: from bin/rails:3:in `<main>' 39: from bin/rails:3:in `load' 38: from /home/ubuntu/environment/プロジェクト名/bin/spring:15:in `<top (required)>' 37: from /home/ubuntu/environment/プロジェクト名/bin/spring:15:in `require' 36: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.1/lib/spring/binstub.rb:11:in `<top (required)>' 35: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.1/lib/spring/binstub.rb:11:in `load' 34: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.1/bin/spring:49:in `<top (required)>' 33: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.1/lib/spring/client.rb:30:in `run' 32: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.1/lib/spring/client/command.rb:7:in `call' 31: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.1/lib/spring/client/rails.rb:28:in `call' 30: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/spring-2.1.1/lib/spring/client/rails.rb:28:in `load' 29: from /home/ubuntu/environment/プロジェクト名/bin/rails:9:in `<top (required)>' 28: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:31:in `require' 27: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require_with_bootsnap_lfi' 26: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register' 25: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `block in require_with_bootsnap_lfi' 24: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/bootsnap-1.7.3/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:23:in `require' 23: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/railties-6.0.3.6/lib/rails/commands.rb:18:in `<main>' 22: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/railties-6.0.3.6/lib/rails/command.rb:46:in `invoke' 21: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/railties-6.0.3.6/lib/rails/command/base.rb:69:in `perform' 20: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/thor-1.1.0/lib/thor.rb:392:in `dispatch' 19: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/thor-1.1.0/lib/thor/invocation.rb:127:in `invoke_command' 18: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/thor-1.1.0/lib/thor/command.rb:27:in `run' 17: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/railties-6.0.3.6/lib/rails/commands/server/server_command.rb:138:in `perform' 16: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/railties-6.0.3.6/lib/rails/commands/server/server_command.rb:138:in `tap' 15: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/railties-6.0.3.6/lib/rails/commands/server/server_command.rb:147:in `block in perform' 14: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/railties-6.0.3.6/lib/rails/commands/server/server_command.rb:39:in `start' 13: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/rack-2.2.3/lib/rack/server.rb:327:in `start' 12: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/rack/handler/puma.rb:73:in `run' 11: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/launcher.rb:172:in `run' 10: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/single.rb:98:in `run' 9: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/runner.rb:161:in `load_and_bind' 8: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:90:in `parse' 7: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:90:in `each' 6: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:106:in `block in parse' 5: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:222:in `add_tcp_listener' 4: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:222:in `each' 3: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:223:in `block in add_tcp_listener' 2: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:229:in `add_tcp_listener' 1: from /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:229:in `new' /home/ubuntu/.rvm/gems/ruby-2.6.3/gems/puma-4.3.7/lib/puma/binder.rb:229:in `initialize': Address already in use - bind(2) for "127.0.0.1" port 8080 (Errno::EADDRINUSE) 最後の行にあるエラーメッセージを翻訳すると「`initialize ':アドレスはすでに使用されています」でした。 解決方法 npmのほうでも、同様のポート番号を使っていたようです。 npmでは起動時にすでにポートを使用していると、違うポートを使うかを聞いてくるので、Railsサーバーを起動後にnpmstartを実施したら、エラーが表示されなくなりました。 rails側でポート番号を変更する方法についてはこちらに記載があったので、ご参考ください。 https://qiita.com/Tocyuki/items/de66987ead2183e4fcae 参考 rails sが立ち上がらない・・・ https://qiita.com/kamyu/items/1d39d4e57f04a3ad6b0c RailsでポートとIPアドレスを指定する方法 https://qiita.com/Tocyuki/items/de66987ead2183e4fcae
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Django REST Framework] [React] simpleJWTによるユーザー認証②

実行環境 MacOS BigSur -- 11.2.1 Python3 -- 3.8.2 Django -- 3.1.7 djangorestframework -- 3.12.2 djoser -- 2.1.0 djangorestframework-simplejwt -- 4.6.0 npm -- 6.14.4 react -- 17.0.1 react-dom -- 17.0.1 axios -- 0.21.1 react-cookie -- 4.0.3 react-hook-form -- 7.0.0 DRFとReactによるアプリケーションでJWT認証を実装したい 前回の記事⇨ https://qiita.com/kachuno9/items/1fa592093c0fd7074aa2 でDRFによるバックエンドにおけるJWT認証の実装は終わっているのでその続きからです。 今回はReactによるフロントエンドの部分を実装しました。 個人的にはReact側の実装の方がはるかに難しく、分からないことがいっぱいあり、Qiitaの質問で沢山の方々に助けられて実装できたので、その手順を残したいと思います。 認証方法 認証方法の選定については、前回の記事に詳しく書いています。 今回はJWTをCookieに保存する方法で実装しました。 必要なライブラリのインストール 今回必要になる各ライブラリを以下の様にインストールします。(React) $ npm install react-cookie $ npm install react-hook-form これらはCookieを扱うためのライブラリと、ReactHookのフォームのためのライブラリです。 React(フロントエンド) Default.js DefaultコンポーネントでルーティングやAPIURLの設定を行います。 Default.js import React from 'react'; import { Switch, Route } from 'react-router-dom'; import Header from '../Header'; import Footer from '../Footer'; import SignUp from './SignUp'; import Login from './Login'; import Trend from './Trend'; import MyPage from './MyPage'; import PostDetail from './PostDetail'; import Top from './Top'; import Logout from './Logout'; //APIURL export const apiURL = 'http://localhost:8000/api/v1/'; class Default extends React.Component { render() { return ( <div> <Header /> <div className="main"> <Switch> <Route exact path="/" component={Top} /> <Route exact path="/signup" component={SignUp} /> <Route exact path="/login" component={Login} /> <Route exact path="/logout" component={Logout} /> <Route exact path="/trend" component={Trend} /> <Route exact path="/mypage" component={MyPage} /> <Route exact path="/post/:id" component={PostDetail} /> <Route render={() => <p>not found!.</p>} /> </Switch> </div> <Footer /> </div> ); } } export default Default; Login.js Loginコンポーネントでログイン処理を行います。 Login.js import React, { useState, useEffect, useRef } from 'react'; import { useCookies } from 'react-cookie'; import axios from 'axios'; import { useForm } from "react-hook-form"; import { useHistory } from 'react-router-dom'; import { apiURL } from './Default'; const Login = (props) => { const history = useHistory(); const [cookies, setCookie] = useCookies(); const { register, handleSubmit, watch, errors } = useForm(); const getJwt = async (data) =>{ console.log(data) await axios.post(`${apiURL}auth/jwt/create/`, { email:data.email, password:data.password, }, ) .then(function (response) { console.log(response.data.access) setCookie('accesstoken', response.data.access, { path: '/' }, { httpOnly: true }); setCookie('refreshtoken', response.data.refresh, { path: '/' }, { httpOnly: true }); history.push('/'); }) .catch(err => { console.log("miss"); alert("EmailかPasswordが違います"); }); }; return ( <div className="top-wrapper"> <div class="login"> <h3>Login</h3> </div> <div class="login-block"> <form onSubmit={handleSubmit(getJwt)}> <label for="email">Email:</label> <input className='form-control' {...register('email')} /> <label for="password">PassWord:</label> <input className='form-control' type="password" {...register('password', { required: true })} /> <input className='btn btn-secondary' type="submit" value="ログイン" /> </form> </div> </div> ); } export default Login; ログインページはこの様になっています。 正しいEmail、Passwordを入力することでホーム画面に遷移し、開発者ツールでCookieを確認すると"accesstoken"と"refreshtoken"が追加されていることが確認できます。 useCookies const [cookies, setCookie] = useCookies(); setCookie('accesstoken', response.data.access, { path: '/' }, { httpOnly: true }); setCookie('refreshtoken', response.data.refresh, { path: '/' }, { httpOnly: true }); React HookのuseCookiesを用いて取得したトークンをCookieに保存する処理を行なっています。 *ちなみに、Cookieを削除したい場合、同様にremoveCookieを使えますが、setCookieが必要無い場合にも、3要素の配列で宣言する必要がある様です。 const [cookies, setCookie, removeCookie] = useCookies(); react-hook-form form の部分で取得したemail,passwordを用いてJWTを発行してもらう仕様になっていますが、このreact-hook-formのバージョンによって書き方が異なることで大分つまずきました。 Qiitaの質問で教えていただいたのですが、react-hook-formのV7から書き方が大幅に変わっており、以下の様な書き方はエラーを吐きます。 <input className='form-control' name="email" ref={register} /> <input className='form-control' name="password" type="password" ref={register({ required: true })} /> V7からの書き方は以下の様に変更しなければなりませんでした。 <input className='form-control' {...register('email')} /> <input className='form-control' type="password" {...register('password', { required: true })} /> 具体的にはこちらの通りです。 https://react-hook-form.com/migrate-v6-to-v7/ トークンをヘッダーに追加してAPI接続 ここまでで、 ① Email,Passwordを用いて、APIからJWTトークン(access,refresh)を取得 ② トークンをCookieに保存 まで完了しました。あとは、③だけです。 ③ ヘッダーにトークンを追加してAPI接続 これについては、axiosを用いてデータを取得する際に、axios.getメソッド内で以下の様にheadersを追加すると、DRF側でパーミッション設定がIsAuthenticatedのデータも取得できる様になります import Cookies from 'universal-cookie'; const cookies = new Cookies(); 省略 headers: { 'Content-Type': 'application/json', 'Authorization': `JWT ${cookies.get('accesstoken')}` } これでDRFとReactによるJWT認証の実装をある程度完了できました。まだまだセキュリティ面での対策や、UXについての検討など課題は沢山ありますが、どんどん進めていきたいと思います。^^ 参考 以下のページが非常に分かりやすく、参考にさせていただきました。 - https://qiita.com/yukiaprogramming/items/593b510d4bfb53c5091d - https://ichi.pro/django-rest-fure-mu-wa-ku-react-jwt-o-shiyoshita-ninsho-nikansuru-poppukarucha-gaido-38119564222107 - https://www.kthksgy.com/web/make-react-django-blog5/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

学習週次報告 #9

お疲れ様です。 この記事は、すごく個人的な学習週次報告です。 自分自身の「目標の明確化」「学んだこと・わからないことの整理」「成長記録」のために書いています。 今回も、”今週で主にやったこと”から、 ”今週で学んだこと”、”つまづいたけど7、8割くらい理解できたこと”、 ”つまづいて、今でもよくわかっていないこと”を整理して、 ”次週何をやるべきか”を書いていきます。 今週で主にやったこと 転職活動 Next.jsでISG・ISRの挙動確認 React、ReacrRauter、Redux、ReactRedux、ReduxThunkの復習 React:簡易的な掲示板の作成 ReacrRauter:APIを使ったクイズアプリの作成 Redux:TODOを管理できるツールの作成 ReactRedux:Reduxで作ったツールを元にTODOアプリの作成 ReduxThunk:ReacrRauterで作ったクイズアプリのデータをStore管理できるようにする 家族・友人から集めたフィートバックを中心にポートフォリオを改善 別記事にまとめた:https://qiita.com/Yopipo415/items/41fd9435cdd6cb7ad294 UdemyでMySQLの勉強 https://www.udemy.com/course/the-ultimate-mysql-bootcamp-go-from-sql-beginner-to-expert/ 今週で学んだこと Next.js ISG fallback: true リクエストに応じてCSRでHTML生成 fallback: ‘blocking’ リクエストに応じてSSRでHTML生成 ISR revalidate リクエスト時に指定する秒数に応じてHTML生成 つまづいたけど7、8割くらい理解できたこと 確認ダイアログの出し方(チャットアプリ修正) window.confirm() 参考:https://www.tagindex.com/javascript/window/confirm.html つまづいて、今でもよくわかっていないこと 今週は特に無し 次週何をやるべきか 基本は今週と同じように、就職活動と並行して勉強を続けていこうと思います。 内容は以上です。 コツコツ、コツコツ。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで子コンポーネントのフォームからCreateアクションを実行し、親コンポーネントのデータを更新する。[Rails使用]

これは何? Rails + React でSPAのアプリを作っています。途中、子コンポーネントから親コンポーネントにデータを渡す実装がありました。 復習もかね、やったことを記録にまとめます。なお、当方のReactの経験は3日前に初めて触った程度。また、実行環境は下記の通りです。 Rails 6.0.3 React 17.0.2 作ったもの 基本的なCRUD操作を行うアプリを作っています。Postsページ内にあるCreatePostFormコンポーネント内で、投稿を作成すると、Postsページに投稿が作成されます。 ▼画面イメージと親子関係はこんな感じ Railsの設定 まずはRails側でcreateアクションを実装し、必要なデータをJSON形式で出力できるようにします。 app/controllers/posts_controller.rb class PostsController < ApplicationController skip_before_action :verify_authenticity_token, only: %i[create] def index @posts = Post.all render json: @posts end def create post = Post.build(post_params) if post.save render json: post else lender json: post.errors end end private def post_params params.require(:label).permit(:name) end end 親コンポーネントを作成 次に、親コンポーネントであるPostsを記載していきます。今回はapp/javascript/pages/Posts.jsxというディレクトリ下に作成しました。 フォーム要素はPostCreateFormというコンポーネント内で作成して、まずは投稿の一覧表示の部分を作っていきます。 Posts.jsx import React, { useState } from 'react' import { PostCreateForm } from '~/components/PostCreateForm' import axios from 'axios' export const Posts = () => { // 解説します(★1) const [Posts, setPosts] = useState([]) // 解説します(★2) React.useEffect(async () => { const response = await axios.get('/posts'); // 解説します(★3) setPosts(response.data) }, []) return ( <div> <h1>投稿一覧</h1> <PostCreateForm /> // これが子コンポーネント <h3>作成した投稿</h3> <ul> {posts.map(post => ( <li key={post.id}> // 解説します(★4) {post.content} </li> ))} </ul> </div> ) } 関数コンポーネント まず★1 の部分の書き方を関数コンポーネントというようです。 公式のドキュメントのこちらのページで簡単に比較がしてありましたが、 コンポーネントと props Reactのコンポーネントがより簡潔に書ける書き方のようです。例えば、以下の2つのコードはReactでは同じものを表現しています。 // クラスコンポーネント class Content extends React.Component { constructor(props){ super(props) this.state = { content: '' } } render() { return <p>{this.props.content}</p>; } } // 関数コンポーネント export const Content = () => { const [content, setContent] = useState('') return ( return <p>{content}</p>; ) } ざっと調べた限りでは、これまで、stateやライフサイクルフックなどの主要機能がクラスコンポーネントでしか使えなかったため、クラスコンポーネントでの書き方が多かったようですが、 最近hookという機能が導入され、関数コンポーネントでも同様の書き方ができるようになったとのことでした。 関数コンポーネントでのステートの管理 そして、★2の部分が関数コンポーネントでステートを管理するときの書き方のようです。 const [Posts, setPosts] = useState([]) まず、useState([])の()内にあるのがstateの初期値です。なお、useState()メソッドは、同コンポーネントの情報に書いているように、 import React, { useState } from 'react' で関数を読み込んでこないと使えません。 そして、[Posts, setPosts]の部分ですが、これはステートの値(Posts)とそれを更新するための関数(setPosts)です。公式のAPIドキュメントが詳しかったです。 フック API リファレンス - 基本のフック このとき、Postsは最初に画面が読み込まれたとき画面に表示する初期値であり、画面が再描画されたときにはPostsの中身を表示します。 setPostsは以下のように新しい値を受け取って、新しい値を受け取って実行されることで、画面を再描画します。 setPosts(newPosts) 今回はここの部分で React.useEffect(async () => { const response = await axios.get('/posts'); setPosts(response.data) // ココ }, []) レスポンスとして帰ってきたデータを受け取って、画面を再描画していますね async と await ★3 をつけたこの部分。 React.useEffect(async () => { const response = await axios.get('/posts'); // 解説します(★3) setPosts(response.data) }, []) これはaxiosを使った新しい書き方のようです。 axios、async/awaitを使ったHTTPリクエスト(Web APIを実行) 残念ながら今の私にはJSの知識が不足していて、これ以上詳しくは書けないので、この記事のURLを貼付するに留めておきます。そのうち、知識がつけばしっかり復習したいです。 keyとmap 最後に、この★4をつけた部分。 {posts.map(post => ( <li key={post.id}> // ココ {post.content} </li> ))} Reactでも出てきましたね。これも公式のドキュメントの記載がわかりやすかったですが、要素を削除したときに、Reactがどの要素が削除されたのかわかりやすくするためにつける要素です。 keys 子コンポーネントにデータを渡す準備 さて、これで親コンポーネントで無事データを一覧表示することができました。このあと、親要素で定義したpostsを子要素に渡して、フォームで入力した値を一覧画面に表示していきますが、そのために親コンポーネントに少し工夫をします。 Posts.jsx <PostCreateForm posts={posts} setPosts={setPosts} /> ★2のところで、ステートを管理した[posts, setPosts]二つの関数を、それぞれpropsとして子コンポーネントに渡します。 子コンポーネントを作成 子コンポーネントPostCreateFormに書いたコードはこちらです。 PostCreateForm.jsx import React, { useState } from 'react' import axios from 'axios' export const PostCreateForm = (props) => { // 解説します★5 const [value, setValue] = useState('') const handleSubmit = (e) => { e.preventDefault(); if(value){ axios.post("/posts", { // 解説します★6 post: { content: value, } }) .then( response => { const newPosts = [...props.posts, response.data] // 解説します★7 props.setPosts(newPosts) setValue('') }) .catch( error => { console.log(error) }) } else { alert('内容を入力してください'); } } return ( <div> <form onSubmit={handleSubmit}> <input type="text" value={value} onChange={event => setValue(event.target.value)} /> <input type="submit" value="投稿" /> </form> </div> ) } 親のステートは子にpropsの形で渡す 公式チュートリアルを読む限り、Reactの使用上、子コンポーネントから親コンポーネントのstateを直接書き換えることはできないようです。 そのため、先程記載したように、親コンポーネントからpostsとsetPostsをそれぞれpropsとして子コンポーネントに渡し、 Posts.jsx <PostCreateForm posts={posts} setPosts={setPosts} /> 子コンポーネントでは、それをpropsとして受け取ります。★5 PostCreateForm.jsx export const PostCreateForm = (props) => { // ココ // 略 } axiosでpostデータを送る axiosでPOSTリクエストを送信するときは、第2引数で指定したデータがリクエストとして送信されるようです。 axios.post("/posts", { // ここから第二引数 post: { content: value, } }) axiosの使い方は、この記事がわかりやすかったです。 axiosの使い方まとめ (GET/POST/例外処理) そして、RailsではPOST以外のデータのやりとりにセキュリティートークンを付与して、それが一致しないとデータを送信できない仕組みになっています。 今回はRails外でPOSTリクエストを送信しようとしていますので、このままデータを送信すると、 Can't verify CSRF token authenticity. と出て、ステータスコード422が返ってきます。 本来は、ここでトークンを付与してエラーを解消すべきなのですが、今回は実装時間の関係上、セキュリティ認証を飛ばすというチートを行いました。。。(仕事で作るアプリでは、ちゃんと認証入れます。。。) /controllers/posts_controller.rb class PostsController < ApplicationController # 認証を飛ばすコード skip_before_action :verify_authenticity_token, only: %i[create] end 親要素のstateを更新する 最後に、リクエストが成功したときに、以下の方法で親要素のstateを更新します。 .then( response => { const newPosts = [...props.posts, response.data] // 解説します★7 props.setPosts(newPosts) setValue('') }) propsとして渡ってきた、親要素にあるstateを更新するための関数(setPosts)に新しい値newPostsをセットし、最後に、フォームを空にしています。 newPostsの中身である [...props.posts, response.data] の...の部分は、「スプレッド演算子」というらしく、配列や連想配列の要素をまるッと書きたいときに使う構文だそうです。 (以下のコードは、こちらの記事から引用させていただきました。とてもわかりやすかったです!) Reactにおけるスプレッド構文の使い所 // Array const odd = [1, 3] const even = [2, 4] const numbers = [...odd, ...even] console.log(numbers) // [1, 3, 2, 4] // Object const name = {first: "Tanaka", last: "Taro"} const age = {age: 27} const profile = {...name, ...age} console.log(profile) // {first: "Tanaka", last: "Taro", age: 27} 今はちょっとできる環境にないのですが、後ほど、...props.postsにどんなデータが入っているか確かめてみたいと思います。 完成! これで、Reactでデータを更新し、一覧表示するフォームができました! CRUDのCとRができたところになりますので、次回以降UとDも頑張ってみたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む