20210802のReactに関する記事は6件です。

正しいuseCallback()の使い方

今回、useCallbackを理解する上で、参考にさせていただいた記事がこちらです。 Your Guide to React.useCallback() こちらを翻訳してまとめたものになります。掲載許可済みです。 Dmitri Pavlutinさん、ご協力ありがとうございます? 「Good luck in your journey to mastering Frontend development!」 と、とても優しい方で、すっかりファンになってしまった。 その前に、関数の等価性チェックを理解する。 function factory() { return (a, b) => a + b; } const sum1 = factory(); const sum2 = factory(); sum1(1, 2); // => 3 sum2(1, 2); // => 3 sum1 === sum2; // => false sum1 === sum1; // => true 例えばfactory()から生成されたsum1とsum2は異なる関数オブジェクトであることがわかる。 sum1 === sum2 // => false sum1 === sum1 // => true 全てのオブジェクトは、それ自身としか等しくない。 useCallbackの目的 const MyComponent = () => { // handleClick is re-created on each render const handleClick = () => { console.log('Clicked!'); }; // ... } このhandleClick関数は、コンポーネントが再レンダリングされるたびに再生成されます。 そのため、レンダリングごとに異なるオブジェクトになります。 インライン機能は安価な(軽い?)なので、レンダリングごとに機能を作り直すことは問題になりません。 コンポーネントごとに数個のインライン関数があれば問題ありません。 ※インライン関数とは、名前のついた無名関数のこと。たとえば以下のような関数のこと。 const handleClick = () => { console.log('Clicked!'); }; しかし、場合によってはレンダリング間で1つの関数インスタンスを維持しておく必要があります。 React.memo()でラップされた機能コンポーネントが、関数オブジェクトpropを受けとっている場合。 useEffect(..., [callback])のように、関数オブジェクトが他のフックに依存している場合。 関数が何らかの内部状態を持っているとき、例えば関数がデバウンスやスロットルされているとき。 useCallback(callbackFun, deps)が役に立つのは以上3つのとき。 同じ依存関係の値(deps)が与えられると、hookはレンダリングの間に関数インスタンスを返す。 import { useCallback } from 'react'; const MyComponent = () => { // handleClick is the same function object const handleClick = useCallback(() => { console.log('Clicked!'); }, []); // ... } handleClickは、MyComponentがレンダリングされる間、常に同じコールバック関数オブジェクトを保持するようになります。 良い使い方 例えば、とても大量のitemリストをレンダリングするコンポーネントがあったとします。 import useSearch from './fetch-items'; const MyBigList = ({ term, onItemClick }) => { const items = useSearch(term); const map = item => <div onClick={onItemClick}>{item}</div>; return <div>{items.map(map)}</div>; } export default React.memo(MyBigList); リストはとても大きく、数百のアイテムがあるかもしれません。無駄なリストの再レンダリングを防ぐために、React.memo()でラップしています。 import { useCallback } from 'react'; export default const MyParent = ({ term }) => { const onItemClick = useCallback(event => { console.log('You clicked ', event.currentTarget); }, [term]); return ( <MyBigList term={term} onItemClick={onItemClick} /> ); } onItemClickをuseCallbackにてラップしています。 useCallback(callbackFun, term)のtermが同じであれば、useCallbackは同じ関数オブジェクトを返します。 このような使い方をすれば、MyParentコンポーネントが再レンダリングされても、onItemClick関数オブジェクトは同じままで、MyBigListのメモ化が壊れることはありません。 悪い使い方 import { useCallback } from 'react'; const MyComponent = () => { // Contrived use of `useCallback()` const handleClick = useCallback(() => { // handle the click event }, []); return <MyChild onClick={handleClick} />; } const MyChild = ({ onClick }) => { return <button onClick={onClick}>I am a child</button>; } このコンポーネントでは、useCallbackを使う意味があるのでしょうか? MyChildコンポーネントは軽く、その再レンダリングはパフォーマンスの問題を引き起こさないのでほとんどの場合には意味がないと思ってよいです。 useCallbackフックは、MyComponentがレンダリングされるたびに呼びされます。 useCallbackが同じ関数オブジェクトを返したとしても、再レンダリングのたびにインライン関数が再作成されます。(useCallbackはそのインライン関数を返すことをスキップするだけです。) また、useCallbackを使うことで、コードの複雑性も増します。useCallback(..., deps)のdepsをメモライズされたコールバックの内部で使用しているものと同期させておく必要があります。 結論として、最適化は、最適化を行わないよりもコストがかかることになります。 このような場合には、新しい関数が作成されることを許容するべきです。 まとめ 最適化を行うと、複雑さが増します。 最適化をするのが早すぎると、最適化されたコードが何度も変更される可能性があるため、リスクを伴います。 useCallbackを適切に使用するタイミングは、メモライズされた重たい子コンポーネントに供給されるコールバック関数をメモ化することです。 また、パフォーマンスの向上を数値化し、そのパフォーマンスの向上は複雑さの増加と比較して、useCallbackを使う価値があるかを判断しましょう。 紹介 私が所属している「もりけん塾」は、もりけん先生が運営するJavaScriptに特化したコミュニティーです。(とは言ってもいろんな方がいらっしゃいます。) フロントエンドエンジニアになりたい方にむけて、先生が道標となって初学者が迷わないように導いてくれます。コードの書き方、自走力を身に着けるにはとても良い環境です。 毎月1日に募集をかけていたのですが、ここ最近は募集かけなくても入塾したいとDMがくるそうです...!!(大人気) 先生のブログ 先生のTwitter
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascript オブジェクトの参照とコピーについて

挨拶 @hakeと申します。初投稿です! オブジェクトの参照とコピーについて勉強し直したので記事にしました! 知っている方も復習する気持ちで読んでいただけると嬉しいです! 以下のコードには問題があります! Index.tsx const [team, setTeam] = useState({ engineer: { reader: "kazu", member: ["eita", "yasu"] }, designer: { reader: "satoshi", member: ["keita", "tadashi"] }, }); // bad const setEngeneerReader = () => { // スプレッド構文でteamをコピーする const newTeam = { ...team }; // 深くネストされた値を変更する newTeam.engineer.reader = "koki"; console.log( "prevReader", team.engineer.reader, "newReader", newTeam.engineer.reader ); // →出力:prevReader koki newReader koki // この時点でコピー元であるteam.engineer.readerが変更されている // →意図していない挙動 setTeam(newTeam); }; stateを直接変更しては行けないというルールがあるにもかかわらず、変更しちゃってますね。 https://ja.reactjs.org/docs/state-and-lifecycle.html#using-state-correctly こういったミスをしないためにも参照やコピーについて理解しておきましょう!!! 参照とは 他の場所にあるデータを指している情報を含む小さなオブジェクトであり、それ自身の中に(指している)データ自体を含まない。 名前の例 名前を識別子とすると、実際の「人」が値で、値に情報がたくさん詰まっている。 →(例)「のび太」という名前はのび太くん本体(値)を参照していると言える。 のび太くん本体 ← のび太 のび太くん本体 ← のびくん のび太くん本体 ← のびちゃん といったように複数の名前(あだ名)がのび太くん本体を参照している 参照によるメリット 複数のコードが参照によってひとつのデータを共有することができ、メモリの節約になる コピーの種類 シャローコピー(浅いコピー) オブジェクトの参照をコピーする コピー元とコピー先で同じ値を参照する ディープコピー(深いコピー) オブジェクトの値をコピー コピー元とコピー先で別の値を参照する プリミティブ値のコピー Index.tsx let a = "a"; let b = "b"; a = b; b = "c"; console.log(a); // →出力:"b" console.log(b); // →出力:"c" プリミティブ値の場合は値がコピーされる →Another worksの値を変更してもbの値は変更されない オブジェクトのコピー Index.tsx let obj = {p1: "dora", p2: "nobi", p3: "shizu"} let obj2 = obj obj2.p1 = "sune" console.log(obj.p1) // →出力:"sune" console.log(obj2.p1) // →出力:"sune" オブジェクトの場合は値への参照がコピーされる →obj2.p1を変更すると元の値が変更されてしまうので注意が必要! とはいっても、コピー元を変更せずにコピー先を変更したいとき、ありますよね? コピー元を変更せずにコピー先を変更する方法 パターン1:Object.assign Index.tsx let obj = {p1: "dora", p2: "nobi", p3: "shizu"} let obj2 = Object.assign({}, obj) obj2.p1 = "sune" console.log(obj.p1); // →出力:"dora" console.log(obj2.p1); // →出力:"sune" パターン2:スプレッド演算子 Index.tsx let obj = {p1: "dora", p2: "nobi", p3: "shizu"} let obj2 = {...obj} obj2.p1 = "sune" console.log(obj.p1); // →出力:"dora" console.log(obj2.p1); // →出力:"sune" 問題点 ①②の問題点:プロパティにオブジェクトがネストされている場合、そのオブジェクトは参照がコピーされる。 Index.tsx let obj = { p1: "nobi", p2: "shizu", robot: { r1: "doraemon", r2: "dorami" } }; let obj2 = { ...obj }; obj2.robot.r1 = "roomba"; console.log(obj.robot.r1); // →出力:"roomba" // 同じ値を参照しているので元の値も変更されてしまう console.log(obj2.robot.r1); // →出力:"roomba" つまり、上記のパターンはシャローコピー(浅いコピー)!!! →深いネストのオブジェクトをコピーした場合はコビー元も変更されてしまう!! ディープコピーしたい場合 パターン1:一度json文字列に変換する Index.tsx let obj = {p1: "nobita", p2: "shizu", robot: {r1: "doraemon", r2: "dorami"}} let obj2 = Object.assign({}, JSON.parse(JSON.stringify(obj))) obj2.robot.r1 = "roomba" console.log(obj.robot.r1); // →出力:"doraemon" console.log(obj2.robot.r1); // →出力:"roomba" 以下のオブジェクトはdeepCopyできないのであまり推奨されていない ・Dateオブジェクト ・function ・undefined パターン2:lodashのcloneDeep Index.tsx let obj = { p1: "nobi", p2: "shizu", robot: { r1: "doraemon", r2: "dorami" } }; let obj2 = _.cloneDeep(obj); obj2.robot.r1 = "roomba"; console.log(obj.robot.r1); // →出力:"doraemon" console.log(obj2.robot.r1); // →出力:"roomba" 問題のコードをもう一度見てみよう! Index.tsx const [team, setTeam] = useState({ engineer: { reader: "kazu", member: ["eita", "yasu"] }, designer: { reader: "satoshi", member: ["keita", "tadashi"] }, }); // bad const setEngeneerReader = () => { // teamをディープコピーする const newTeam = _.cloneDeep(team); // 深くネストされた値を変更する newTeam.engineer.reader = "koki"; console.log( "prevReader", team.engineer.reader, "newReader", newTeam.engineer.reader ); // →出力:prevReader koki newReader koki // この時点でコピー元であるteam.engineer.readerが変更されている // →意図していない挙動 setTeam(newTeam); }; とは言っても、なるべくネストしないように定義するべきですね!!! おまけ 分割代入 Index.tsx const obj = { p: "dora", }; let { p } = obj; p = "nobi"; console.log(obj.p, p); // →出力:"dora" "nobi" const obj2 = { p1: { p2: "dora", }, }; let { p1 } = obj2; p1.p2 = "nobi"; console.log(obj2.p1.p2, p1.p2); // →出力:"nobi" "nobi" 分割代入の場合オブジェクトがネストされている場合、参照がコピーされるので注意!! 新しくインスタンス化 Index.tsx class Team { public id: number = 0; public engineer: { reader: string; front: string[]; back: string[] } = { reader: "", front: [], back: [], }; public designer: { reader: string; member: string[] } = { reader: "", member: [], }; constructor(init?: Partial<Team>) { // assign Object.assign(this, init); } } const team1 = new Team({ id: 1, engineer: { reader: "dora", front: ["atom", "haro"], back: ["r2", "c3"], }, designer: { reader: "roomba", member: ["braaba", "eufy"], }, }); const team2 = new Team(team1); team2.engineer.reader = "ashimo"; console.log(team1.engineer.reader, team2.engineer.reader); // →出力:"ashimo" "ashimo" newでのインスタンス化はシャローコピーになる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React(typescript) x Firebaseで心霊スポットを共有できるサービスを作ってみた

ReactとFirebaseのみで心霊スポットをシェアとレビューできるサイトを作成しました 作った経緯 私がホラー映像をはじめ心霊系が好きであったことと いざ、心霊スポットにいきたいなあと思って検索するんですが、どのブログも同じような場所を書いていてマンネリ化している状況でした。 あと”マジで出る”ところはネット上には無い場合が多い気がしています それなら共有できるサイトを僕が作っちゃえ!と思い作ったのがこのサービスです リリースした後の影響を考えた このサービスは本当にリリースしていいのかというのを考えました 不法侵入が不本意に起きてしまわないかや近隣住民の迷惑になりえないかその他諸々 そのあたりの注意事項は投稿時や利用規約に明記するようにしました 技術スタック React(Typescript) 使ったライブラリー react-hook-form react-router chakra ui Vueも少し触ってみたのですが、個人的にjsxがhtmlに似ていて、コンポーネントがわかりやすいというのがありました。あくまで僕が感じたことなので個人差はあるかと思います あとは今後サービスを大きくしたい時やReact Nativeが使える点で将来的にスマホにも移植したい場合に移行しやすさも考えました まだunit testはできていませんが、これから挑戦していこうと思っています Firebase 使った機能 Authentication Firestore Storage Hosting Analytics Firebaseを採用した理由はReactに学習時間を割きたかったからというのが大きいです 結局同じぐらい勉強したんですけどね・・・・・ 最終的にはrulesのテストまで実装することができました テストなどを勉強するにあたって公式ドキュメントやYouTubeを参考にしました Firebaseではログイン認証からデプロイまですべてコンソールでできる点が魅力的でした あとGoogle Domainsを使えば独自ドメインも簡単に設定できます これからやることとしては、現在は無料枠なのでBlazeプランに移行しCloud Functionsを使いたいと思っています 苦労・工夫したこと モック作成とロゴ作成にFigmaとAffinity Designerを使用しました デザイン全般については大学時代から使用していた経験もあり、比較的苦労なく作成できました プログラミングについては完全に初心者だったのですべてにおいて苦労しました。笑 まずReactを始めるにあたってtypescriptの基礎とjavascriptの基礎を固める期間を設けました。 期間を決めて勉強してみて、何か作って、わからないことを解決するみたいなことを数回繰り返しました しばらく勉強してからprogateなどをやってみましたが、結局udemyに落ち着きました 個人差があるとおもいます 無限ループ問題 React初心者がまず躓くポイントであろうuseEffectの無限ループ問題です useEffectの無限ループが起きるてしまうと、Firebaseの場合その分だけデータの読み取りが行われてしまうのでお金の問題に直結してしまいます なのでこの辺は慎重に行いました ページごとにデータを読み取るようにして、同じような処理が繰り返し行われる場合にカスタムフックを作成し、ページ単体でドキュメント参照のみをするようにしました render回数 極力render回数を減らしたくコンポーネント分割も慎重に考えました。 関数にはuseCallbackなどを使いました 次に取り組もうと思っているのはuseMemoやReact.memoを使って投稿一覧などをメモ化していきたいと思っています 直観的に操作できるように デザインについては多少幼稚かなと思えるぐらいに簡潔にしあげたつもりです 心霊スポットに行くときは夜が多いと思うので長時間スマホをいじらなくて済むように知りたい情報をすぐに手に入れられるUI/UXを目指しました。 探せばもうちょっと出てくるかと思いますが、ぱっと出てくるところはこのくらいです 以上が工夫・苦労したところです 反省と今後の課題 完全初心者が完全独学でWebサービスを作ってみましたが、総じて言うと楽しかったと思います エラーで挫けそうになったことや、できないからこの機能を削除しようと思ったことが何度かありますが諦めずに実装できたときの快感は言葉にできないものでしたし、成長してる感覚がリアルに感じれてひさしぶりの感覚でした あとは独学だったこともあり、ネット上に聞くしかなかったので検索力も向上した気がしますし、検索していく中でReact以外のさまざまな面白そうなものを見つけれたり目的以外の収穫があったように感じます 反省点としては、まず第一に要件定義をするべきだったと感じました はじめは思いつきで実装をしていて、途中これだったら必要以上に時間がかかると気づき要件定義をするようにしました 要件定義といってもページで必要なコンポーネントをAtom単位から検討しpropsの受け渡しをどうするかを考えただけなので要件定義を言っていいものかわかりませんが・・・ この作業はパズルを作ってるようで楽しかったです 今後の課題としてはもっとjavascriptについて理解することだと思います 今はなんだか雰囲気で書いてるような気がしていて、無駄なコードが多いんじゃないかと感じてます なのでjavascriptを機能をしっかりと理解し、なんとなくを脱却することを今後の課題としたいと思います 簡単な自己紹介 最後に簡単な自己紹介をさせていただきます。 私は現在27歳で以前は行政系の事務員として働いていましたが、現在は転職活動中です。 この記事を読んで少しでも気になる方がいればDMまでお待ちしております(お願いします。。) プログラミング学習は全て独学で学びました スクールなどを使った方が効率的だとは思いますがちょっと高かったので・・ ですがUdemyやYouTubeなどの動画媒体やQiitaなどの記事媒体を余すことなく活用しました 読んだ書籍はweb技術の基本やオライリーのTypeScriptです オライリーの方は少し早かった気がしています。笑 web技術の基本はすごくわかりやすくレビュー通りいいものだと思いました あとVueの勉強も少ししていたのでVue.js入門も一通り目を通しました あと最後に動画教材で大変お世話になった方々にこの場を借りてお礼を言わせていただきます はむさん @diveintohacking 基礎から応用まで網羅しており、TypescriptとJavascriptの違いがわかりやすく説明されていました。 おまけとして基本的なGitコマンドとlinuxコマンドを学習できたのがすごく良かったです おすすめの講座はこちらです ハンズオンで学ぶTypeScript - JavaScript エンジニアのためのTypeScript徹底入門 他にも講座は購入したのですが、この講座が僕の中で一番でした じゃけぇさん @bb_ja_k Reactの基礎、customHooksの作り方などなどを網羅的に教えてくださいました。 この講座ではReactのより実践的な使いかたやAtomic Designについて学べたのが良かったです Reactの講座ではこれが僕の中で一番です Reactに入門した人のためのもっとReactが楽しくなるステップアップコース完全版 mogaさん @_mogaming Firebaseを勉強するならmogaさんのYouTubeと公式ドキュメントがかなり有用でした 公式ドキュメントを一緒に読んだり、rulesの書き方、便利な関数など、実際に作っていく中でのDB設計方法などすごくわかりやすくためになりました 以上の御三方本当にありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

~part2~【FC版】React + Rails API + axios + react-router-domでCRUDを実装する

こんにちは!スージーです。 前回書いたこちらの続きです やりたい事 モデルを1対1で関連付け CRUDを実装 React × Rails APIで関連付けをしたモデルのCRUDを実装するにはこんな感じでやるんだなーって感じで見ていただけると幸いです 開発環境 Ruby 2.7.1 Rails 6.0.4 MySQL node.js 14.8.0 React 17.0.2 参考 【Rails】モデルの関連付けで用いられるbuildメソッドまとめ やらないこと rails トランザクションの説明 リレーションの説明 エラーハンドリング まずgem foremanを入れる 毎回ローカルサーバを立ち上げる時に、rails sとyarn startをするのが面倒くさいので backend $ gem install foreman touch Procfile backendディレクトリにProcfileを作成します // Procfile web: bundle exec rails s npm: cd ../frontend && yarn start group :development do gem 'listen', '~> 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' gem 'foreman' end gem foremanはデフォルトだとrails側が5000番ポート、client側が5100番ポートをlistenするのでcors.rbとclient.jsを修正します # cors.rb Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:5100' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end // client.js const client = applyCaseMiddleware( axios.create({ baseURL: 'http://localhost:5000/api/v1', }), options ); これで設定完了です backend $ foreman start 07:06:36 web.1 | started with pid 6341 07:06:36 npm.1 | started with pid 6342 07:06:37 npm.1 | yarn run v1.22.10 07:06:37 npm.1 | $ react-scripts start 07:06:38 web.1 | => Booting Puma 07:06:38 web.1 | => Rails 6.0.4 application starting in development 07:06:38 web.1 | => Run `rails server --help` for more startup options 07:06:39 web.1 | Puma starting in single mode... 07:06:39 web.1 | * Version 4.3.8 (ruby 2.7.1-p83), codename: Mysterious Traveller 07:06:39 web.1 | * Min threads: 5, max threads: 5 07:06:39 web.1 | * Environment: development 07:06:39 web.1 | * Listening on tcp://127.0.0.1:5000 07:06:39 web.1 | * Listening on tcp://[::1]:5000 07:06:39 web.1 | Use Ctrl-C to stop 07:06:45 npm.1 | ℹ 「wds」: Project is running at http://172.20.10.5/ 07:06:45 npm.1 | ℹ 「wds」: webpack output is served from 07:06:45 npm.1 | ℹ 「wds」: Content not from webpack is served from /Users/sugawarakouhei/project/local-project/react-form-sample/frontend/public 07:06:45 npm.1 | ℹ 「wds」: 404s will fallback to / 07:06:45 npm.1 | Starting the development server... web => rails npm => react foreman startコマンド1つで両方のプロセスが実行される事が確認できればOK modelを作成 postモデルと1対1(has_one)で関連付けるdetailInfoモデルを作成します backend $ rails g model detailInfo # 日付_create_detail_infos.rb class CreateDetailInfos < ActiveRecord::Migration[6.0] def change create_table :detail_infos do |t| t.references :post, null: false t.string :favorite_food, limit: 100 t.string :favorite_toy, limit: 100 t.timestamps end end end 外部キーと他に2つのカラムを持たせます。好きな食べ物と好きなおもちゃです。全くテーブルを分ける必要がないデータ構造ですが、練習なので悪しからず... # post.rb class Post < ApplicationRecord has_one :detail_info, dependent: :destroy end dependent: :destroyは親モデルが物理削除された時に一緒に子モデルも物理削除します。 # detail_iinfo.rb class DetailInfo < ApplicationRecord belongs_to :post end これで1対1の関係になりました コントローラーの修正 class Api::V1::PostsController < ApplicationController def index render json: Post.all.to_json(include: :detail_info) end def show render json: Post.find(params[:id]).to_json(include: :detail_info) end def create ActiveRecord::Base.transaction do post = Post.new(post_params) detail_info = post.build_detail_info(detail_info_params) if post.save && detail_info.save render json: post.to_json(include: :detail_info) else ender json: { post: post.errors, detail_info: detail_info.errors }, status: 422 end end end def update ActiveRecord::Base.transaction do post = Post.find(params[:id]) detail_info = DetailInfo.find_by(post_id: params[:id]) if post.update(post_params) && detail_info.update(detail_info_params) render json: post.to_json(include: :detail_info) else render json: { post: post.errors, detail_info: detail_info.errors }, status: 422 end end end def destroy post = Post.find(params[:id]) post.destroy render json: post.to_json(include: :detail_info) end private def post_params params.require(:post).permit(:name, :neko_type) end def detail_info_params params.require(:detail_info).permit(:favorite_food, :favorite_toy) end end render末尾に.to_json(include: :detail_info)を追加しました。リレーションを設定したデータを紐付けてjsonを返してくれるようになります。後ほどcurlコマンドを叩いて確認します createアクションはbuildメソッドを使って保存します。1対1のときはbuild_モデル名で保存します。 あとトランザクションを使い、失敗した時にはrollbackするように処理を囲みます。 複数テーブルへの保存処理にはaccepts_nested_attributes_for(非推奨)やFormオブジェクトを使う方法など存在します。ただこれらをうまく動作させる事ができなかった(筆者の知識が乏しく)のでtransactionとbuildメソッドを使い、2つのモデルに登録する処理をcontrollerに書きました(fatコントローラーになってしまいました) updateアクションもcreateアクション同様にtrunsactionを使い処理を囲みます。findメソッドでpostモデルから該当レコードを1件取得します。find_byメソッドを使い、親モデルと同じid(post_id)を持つ該当レコードをdetail_infoモデルから1件取得します。 seeds.rbを修正 post1 = Post.create!(name: 'ニャア', neko_type: 'アメリカンショートヘア') DetailInfo.create!(post: post1, favorite_food: '魚', favorite_toy: '猫じゃらし') post2 = Post.create!(name: 'まる', neko_type: 'スコッティシュフォールド') DetailInfo.create!(post: post2, favorite_food: '野菜', favorite_toy: 'まりたん') post3 = Post.create!(name: 'むぎ', neko_type: 'スコッティシュフォールド') DetailInfo.create!(post: post3, favorite_food: '肉', favorite_toy: 'ダンボール') 以前の記事で作成したseeds.rbを修正しました。post: post1のように記載して、外部キーを持っている子モデルに対してcreateしています curlコマンドを叩いて確認してみます backend $ rails db:migrate:reset rails db:seed // 一覧 curl http://localhost:5000/api/v1/posts [{"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-30T23:22:17.084Z","updated_at":"2021-07-30T23:22:17.084Z","detail_info":{"id":1,"post_id":1,"favorite_food":"魚","favorite_toy":"猫じゃらし","created_at":"2021-07-30T23:22:17.106Z","updated_at":"2021-07-30T23:22:17.106Z"}},{"id":2,"name":"まる","neko_type":"スコッティシュフォールド","created_at":"2021-07-30T23:22:17.111Z","updated_at":"2021-07-30T23:22:17.111Z","detail_info":{"id":2,"post_id":2,"favorite_food":"野菜","favorite_toy":"まりたん","created_at":"2021-07-30T23:22:17.116Z","updated_at":"2021-07-30T23:22:17.116Z"}},{"id":3,"name":"むぎ","neko_type":"スコッティシュフォールド","created_at":"2021-07-30T23:22:17.121Z","updated_at":"2021-07-30T23:22:17.121Z","detail_info":{"id":3,"post_id":3,"favorite_food":"肉","favorite_toy":"ダンボール","created_at":"2021-07-30T23:22:17.126Z","updated_at":"2021-07-30T23:22:17.126Z"}}] // 詳細 curl http://localhost:5000/api/v1/posts/1 {"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","created_at":"2021-07-30T23:22:17.084Z","updated_at":"2021-07-30T23:22:17.084Z","detail_info":{"id":1,"post_id":1,"favorite_food":"魚","favorite_toy":"猫じゃらし","created_at":"2021-07-30T23:22:17.106Z","updated_at":"2021-07-30T23:22:17.106Z"}} // 新規作成 curl -X POST "http://localhost:5000/api/v1/posts" -H "Accept: application/json" -H "Content-type: application/json" -d '{"post": {"name": "test","neko_type":"test"},"detail_info":{"favorite_food": "test","favorite_toy":"test"}}' {"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"test","favorite_toy":"test","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:33:18.496Z"}}% // 更新 curl -X PATCH "http://localhost:5000/api/v1/posts/4" -H "Accept: application/json" -H "Content-type: application/json" -d '{"post": {"name": "test","neko_type":"test"},"detail_info":{"favorite_food": "update3","favorite_toy":"update3"}}' {"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"update3","favorite_toy":"update3","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:34:19.279Z"}}% // 削除 curl -X DELETE http://localhost:5000/api/v1/posts/4 {"id":4,"name":"test","neko_type":"test","created_at":"2021-08-01T01:33:18.492Z","updated_at":"2021-08-01T01:33:18.492Z","detail_info":{"id":4,"post_id":4,"favorite_food":"update3","favorite_toy":"update3","created_at":"2021-08-01T01:33:18.496Z","updated_at":"2021-08-01T01:34:19.279Z"}} [{"id": 1 ~~ detailInfo: {"id": 1, "post_id": 1 ~~} }] オブジェクトの中にdetailInfoというキーでオブジェクトがネストされたデータ構造になっています。想定通りにデータが入っています またpost / patchのリクエストを受けてcreateアクション / updateアクションが正常に動いている事が確認できました deleteのリクエストを受けて親モデルのレコードが削除された時にはモデルに追記したdependent: :destroyによって子モデルのレコードも削除されている事が確認できました 一覧画面の修正 以前の記事でapiコールの実装は完了しているので、取得したデータの中にあるdetailInfo: {"id": 1, "post_id": 1 ~~}のオブジェクト内から必要なプロパティを展開し表示します // List.jsx import React, { useEffect, useState } from 'react'; // deletePostを追加 import { getList, deletePost } from '../lib/api/post'; import { useHistory, Link } from 'react-router-dom'; const List = () => { const [dataList, setDataList] = useState([]); useEffect(() => { handleGetList(); }, []); const handleGetList = async () => { try { const res = await getList(); console.log(res.data); setDataList(res.data); } catch (e) { console.log(e); } }; const history = useHistory(); const handleDelete = async (item) => { console.log('click', item.id); try { const res = await deletePost(item.id); console.log(res.data); handleGetList(); } catch (e) { console.log(e); } }; return ( <> <h1>HOME</h1> <button onClick={() => history.push('/new')}>新規作成</button> <table> <thead> <tr> <th>名前</th> <th>猫種</th> {/* 追加 */} <th>好きな食べ物</th> {/* 追加 */} <th>好きなおもちゃ</th> <th colSpan='1'></th> <th colSpan='1'></th> <th colSpan='1'></th> </tr> </thead> {dataList.map((item, index) => ( <tbody key={index}> <tr> <td>{item.name}</td> <td>{item.nekoType}</td> {/* 追加 */} <td>{item.detailInfo.favoriteFood}</td> {/* 追加 */} <td>{item.detailInfo.favoriteToy}</td> <td> <Link to={`/edit/${item.id}`}>更新</Link> </td> <td> <Link to={`/post/${item.id}`}>詳細へ</Link> </td> <td> <button onClick={() => handleDelete(item)}>削除</button> </td> </tr> </tbody> ))} </table> </> ); }; export default List; 一覧に好きな食べ物と好きなおもちゃが表示されていればOK 詳細画面の修正 同じ要領で詳細画面の修正をしていきます。詳細画面ではquery.idの取得タイミングがrenderingより遅くなってしまうので{data.detailInfo.favoriteFood}がTypeError: Cannot read property 'favoriteFood' of undefinedと怒られてしまいます。なのでstateに初期値を設定します // Detail.jsx import React, { useEffect, useState } from 'react'; import { getDetail } from '../lib/api/post'; import { useHistory, useParams } from 'react-router-dom'; const Detail = () => { // 修正 const [data, setData] = useState({ name: '', neko_type: '', detailInfo: { favorite_food: '', favorite_toy: '', }, }); const query = useParams(); console.log(query.id); const history = useHistory(); useEffect(() => { handleGetDetail(query); }, [query]); const handleGetDetail = async (query) => { try { const res = await getDetail(query.id); console.log(res.data); setData(res.data); } catch (e) { console.log(e); } }; return ( <> <h1>DETAIL</h1> <div>ID:{data.id}</div> <div>名前:{data.name}</div> <div>猫種:{data.nekoType}</div> {/* 追加 */} <div>好きな食べ物:{data.detailInfo.favoriteFood}</div> {/* 追加 */} <div>好きなおもちゃ:{data.detailInfo.favoriteToy}</div> <button onClick={() => history.push('/')}>戻る</button> </> ); }; export default Detail; 好きな食べ物と好きなおもちゃが表示されていればOK 削除機能(追加無し) 削除機能は変更なしで削除できることが確認できればOK 新規作成の修正 新規作成を修正します テキストフィールをForm.jsxに追加 Submitイベントでapiに送るパラメータのオブジェクト構造を加工する // Form.jsx import React from 'react'; const Form = (props) => { const { handleChange, handleSubmit, value, buttonType } = props; return ( <> <form> <div> <label htmlFor='name'>猫の名前:</label> <input type='text' name='name' id='name' onChange={(e) => handleChange(e)} value={value.name} /> </div> <div> <label htmlFor='nekoType'>猫種</label> <input type='text' name='nekoType' id='nekoType' onChange={(e) => handleChange(e)} value={value.nekoType} /> </div> {/* 追加 */} <div> <label htmlFor='nekoType'>好きな食べ物</label> <input type='text' name='favoriteFood' id='favoriteFood' onChange={(e) => handleChange(e)} value={value.favoriteFood} /> </div> <div> <label htmlFor='nekoType'>好きなおもちゃ</label> <input type='text' name='favoriteToy' id='favoriteToy' onChange={(e) => handleChange(e)} value={value.favoriteToy} /> </div> {/* ここまで */} <input type='submit' value={buttonType} onClick={(e) => handleSubmit(e)} /> </form> </> ); }; export default Form; // New.jsx import React, { useState } from 'react'; import FormBody from './Form'; import { createPost } from '../lib/api/post'; import { useHistory } from 'react-router-dom'; const New = () => { const [value, setValue] = useState({}); const history = useHistory(); const handleChange = (e) => { setValue({ ...value, [e.target.name]: e.target.value, }); }; const handleSubmit = async (e) => { e.preventDefault(); console.log(value); // 追加 const params = generateParams(); try { const res = await createPost(params); console.log(res); history.push('/'); } catch (e) { console.log(e); } }; // パラメータのオブジェクト構造を加工 const generateParams = () => { const params = { name: value.name, nekoType: value.nekoType, // detailInfoというキーでオブジェクトをネストする detailInfo: { favoriteFood: value.favoriteFood, favoriteToy: value.favoriteToy, }, }; return params; }; return ( <> <h1>NEW</h1> <FormBody handleChange={handleChange} handleSubmit={handleSubmit} value={value} buttonType='登録' /> </> ); }; export default New; paramsの中身を見てみるとこのようなオブジェクトになっています 登録ボタンを押下してデータが登録できればOK railsのログを見てみると、パラメータを受け取って2つのinsert処理が走っている事がわかります。BEGINとCOMMITに挟まれ処理が実行されているのでtrunsactionが効いている事も確認できます 10:11:26 web.1 | Started POST "/api/v1/posts" for ::1 at 2021-08-02 10:11:26 +0900 10:11:26 web.1 | Processing by Api::V1::PostsController#create as HTML 10:11:26 web.1 | Parameters: {"name"=>"test", "neko_type"=>"test", "detail_info"=>{"favorite_food"=>"test", "favorite_toy"=>"test"}, "post"=>{"name"=>"test", "neko_type"=>"test"}} 10:11:26 web.1 | (54.6ms) BEGIN 10:11:26 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create' 10:11:26 web.1 | Post Create (14.8ms) INSERT INTO `posts` (`name`, `neko_type`, `created_at`, `updated_at`) VALUES ('test', 'test', '2021-08-02 01:11:26.894098', '2021-08-02 01:11:26.894098') 10:11:26 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create' 10:11:27 web.1 | DetailInfo Create (26.6ms) INSERT INTO `detail_infos` (`post_id`, `favorite_food`, `favorite_toy`, `created_at`, `updated_at`) VALUES (4, 'test', 'test', '2021-08-02 01:11:26.977189', '2021-08-02 01:11:26.977189') 10:11:27 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:15:in `block in create' 10:11:27 web.1 | (6.9ms) COMMIT 10:11:27 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:12:in `create' 10:11:27 web.1 | Completed 200 OK in 142ms (Views: 0.4ms | ActiveRecord: 102.9ms | Allocations: 4071) 更新画面を修正 最後に更新処理を修正します stateの初期値を修正する Submitイベントでapiに送るパラメータのオブジェクト構造を加工する // Edit.jsx import React, { useEffect, useState } from 'react'; import { useHistory, useParams } from 'react-router-dom'; import { updatePost, getDetail } from '../lib/api/post'; import FormBody from './Form'; const Edit = () => { const [value, setValue] = useState({ name: '', nekoType: '', // 追加 favoriteFood: '', // 追加 favoriteToy: '', }); const query = useParams(); const history = useHistory(); useEffect(() => { handleGetData(query); }, [query]); const handleGetData = async (query) => { try { const res = await getDetail(query.id); console.log(res.data); setValue({ name: res.data.name, nekoType: res.data.nekoType, // 追加 favoriteFood: res.data.detailInfo.favoriteFood, // 追加 favoriteToy: res.data.detailInfo.favoriteToy, }); } catch (e) { console.log(e); } }; const handleChange = (e) => { setValue({ ...value, [e.target.name]: e.target.value, }); }; const handleSubmit = async (e) => { e.preventDefault(); // 追加 const params = generateParams(); try { const res = await updatePost(query.id, params); console.log(res); history.push('/'); } catch (e) { console.log(e); } }; // パラメータのオブジェクト構造を加工 const generateParams = () => { const params = { name: value.name, nekoType: value.nekoType, detailInfo: { favoriteFood: value.favoriteFood, favoriteToy: value.favoriteToy, }, }; return params; }; return ( <> <h1>Edit</h1> <FormBody handleChange={handleChange} handleSubmit={handleSubmit} value={value} buttonType='更新' /> </> ); }; export default Edit; 修正箇所はNew.jsxとほとんど同じです 更新ボタンを押下してデータが更新できればOK railsのログを見てみると、パラメータを受け取って2つのselectと2つのupdate処理が走っている事がわかります。id=4のレコードをpostテーブルから取得して更新処理をしています。post_id=4のレコードをdetailInfoテーブルから取得して更新処理をしています。BEGINとCOMMITに挟まれ処理が実行されているのでtrunsactionが効いている事も確認できます 10:16:04 web.1 | Started PATCH "/api/v1/posts/4" for ::1 at 2021-08-02 10:16:04 +0900 10:16:04 web.1 | Processing by Api::V1::PostsController#update as HTML 10:16:04 web.1 | Parameters: {"name"=>"update", "neko_type"=>"update", "detail_info"=>{"favorite_food"=>"udpate", "favorite_toy"=>"update"}, "id"=>"4", "post"=>{"name"=>"update", "neko_type"=>"update"}} 10:16:04 web.1 | (0.2ms) BEGIN 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:25:in `block in update' 10:16:04 web.1 | Post Load (0.6ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:25:in `block in update' 10:16:04 web.1 | DetailInfo Load (21.6ms) SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:26:in `block in update' 10:16:04 web.1 | Post Update (12.4ms) UPDATE `posts` SET `posts`.`name` = 'update', `posts`.`neko_type` = 'update', `posts`.`updated_at` = '2021-08-02 01:16:04.763790' WHERE `posts`.`id` = 4 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update' 10:16:04 web.1 | Post Load (14.5ms) SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update' 10:16:04 web.1 | DetailInfo Update (1.0ms) UPDATE `detail_infos` SET `detail_infos`.`favorite_food` = 'udpate', `detail_infos`.`favorite_toy` = 'update', `detail_infos`.`updated_at` = '2021-08-02 01:16:04.821784' WHERE `detail_infos`.`id` = 4 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:27:in `block in update' 10:16:04 web.1 | DetailInfo Load (21.8ms) SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 4 LIMIT 1 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:28:in `block in update' 10:16:04 web.1 | (7.4ms) COMMIT 10:16:04 web.1 | ↳ app/controllers/api/v1/posts_controller.rb:24:in `update' 10:16:04 web.1 | Completed 200 OK in 184ms (Views: 0.2ms | ActiveRecord: 79.4ms | Allocations: 6798) 最後に コントローラがfatになっているので、もっと良い書き方があればご教示ください! おわり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのReduxについての備忘録

最近、開発を勉強しようと思って始めたのがReactでその中でReduxの概念を†完全に理解†したので備忘録としてメモを書きます。 あくまでも自分用 として書くのでご了承ください。また、間違え等ありましたらご指摘くださると幸いです。 そもそもReduxとは?? Reduxとは、Reactのライブラリの1つで(実はReact専用ではないが)、Stateをグローバルな状態に保持するのに役に立つ。 ReactHooksのuseStateは、各々のコンポーネントの状態を保持できるがそれを他のコンポーネントに伝えるためには Props を使い、順番に伝達していかなければならない。それだと、同じ状態を複数のコンポーネントで使うためには、その使用するコンポーネント全てにPropsを通して伝える必要があり、管理やメンテナンスがとても大変だった。 しかし、 その状態を同じ場所で一括に管理できるようにしたのがReduxである。 各々のコンポーネントである状態を知りたい時、同じ参照先、Reduxの言葉だとStoreを見ればよくなる。 Reduxで使う用語 大まかな概念は以上の通りなのだが、Reduxを難しく感じさせる理由はその手順、手続きにある。 例えば、あるボタンをクリックした時 count というStateを+1したいとする。 大雑把にやりたいことを実現しようとすると、以下の様な流れが直感的だ。 (クリックしたという情報をStoreに伝える)=>(Storeにあるcountという状態を+1する) しかし、現実には以下のような手続きを踏むことになる。 (クリックする)=>("+1したい"というActionが発行される)=> ("+1したい"というActionがStoreに伝えられる(これをdispatchという) )=> (Storeのcountを変更するためにReducerという関数を通す)=> (Reducerを通してできた新しいcountを値をStoreのcountに設定する) 以上のような、わけのわからない手続きを踏むことで、ようやく状態が変更されるわけだ。 しかし、偉い人が考えたのだから、めんどくさいのにもちゃんと理由がある。ここで、用語を1つづつ解説する。 ・Store 上でも説明したが、ここで全体の状態を管理する。どこのコンポーネントからでもその中身に一発でアクセスできる。 ただし、読み込みは一発でアクセスできるが、書き込みは上の手続きを通さないとしてはいけない。 また、Storeは1つだけしか持ってはいけない。 ・Action これはボタンを押したり、特定のイベントが発生した時に発行される(これを実現したい!)というだけのただの命令が書かれた紙である。 ・Dispatch dispatch(action)の様なふうに使われる、Action(指令)をStoreに伝えるための伝達役だ。 ・Reducer こいつが一番えらい。 Storeの中の情報を書き換える権限を与えられた唯一のやつ。 なお、古い(現時点での)Stateとdispatchによって伝えられたやりたいActionの2つを引数にとり、新しいStateを戻り値として返す。 超簡単に書くと (OldState,Action)=>NewState を行うだけの関数。 ただし、これは純粋関数(外の情報を変更しない)である必要がある。 Reactの根本にある、関数型プログラミング的な思想を知らないと理解しにくいかもしれないが、そうすることでバグを減らせる(らしい) また、直接Stateを変更するのではなく、コピーをとり新しい値を返すことで、 Chrome拡張のRedux Devtoolsを使いいつでもその状態を再現することができる. まとめ Reduxを使うためには、Reactの根本にある思想、データフローは単方向であるべしというものを理解した上でないと、理解するのが難しいかもしれない。 ちなみに初心者なので、useReducerとかとの違いはまだ知りません。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Reactやるなら知っておきたい】関数コンポーネントとクラスコンポーネントの違い

はじめに 僕自身、現在React勉強中なのですが、 クラスコンポーネントと関数コンポーネントの二種類あることを理解していなかったため、 備忘録として残しておきます。 なにから勉強したらいいかわからない人はサラッと読んでみてください! Reactとは facebook社が開発したJSのライブラリ 用途としてはUIのコンポーネントを作成するのに使われる 【本題】関数コンポーネントとクラスコンポーネントの違い 前提 ★現在は主に関数コンポーネントを使用する 〈理由〉 以前は下記の機能がクラスコンポーネントでしか使用出来なかった  ■State(状態管理)  ■Lifecycle Hooks   - Lifecycle とは、Mounting、Updating、Unmountingの一連の流れの事 ・Mounting(マウント):コンポーネントをレンダーする ・Updating(更新):コンポーネントのStateを更新する ・Unmounting(アンマウント):コンポーネントのレンダーが切れる ▶現在はHooks(React 16.8 で追加された新機能)により、どちらも使えるようになった。 違いについて 【関数コンポーネント】 シンプルで、状態(state)を持たない。 渡された値に従って特定の固定要素を描画するだけ。 要はただの「関数」である。 【クラスコンポーネント】 classを用いてコンポーネントを定義する。 クラスコンポーネントは状態(state)を持つことができ、内容を柔軟に変化させることが可能 また、Reactに関するメソッドをクラスに実装することで、より便利な制御が可能となる。 関数コンポーネント使うメリットなくない????? ★そこで考案されたのがHooksである。 状態管理などが関数コンポーネントでも出来るようになった! よってクラスコンポーネントと同等の機能を実現可能になった。 最低限、【useState】 【useEffect】 【useContext】 は習得しよう! 例えばuseStateというフックは関数コンポーネントに状態を持たせることができる。 (※HooksはすべてuseXXXという命名規則) 例 // お約束。フックを使うならそのフックを必ずimportする import React, { useState } from 'react' // set〇〇とキャメルケースで書こう(ちなみにset〇〇は関数です) // useState(0)の(0)は初期値です const App = () => { const [count, setCount] = useState(0) // {count}には、stateで保存されているcountの値が表示されます // {count}の値が変わるたびにレンダリングされます // setCountを使わないと、countの値を変更することはできません // また、setCountの引数に関数を渡すこともできます return ( <> <p>現在、{count} 回押しました</p>             // このボタンがクリックされると、countの値が+1されて、{count}の表示が変わります <button onClick={() => setCount(count + 1)}> 押すとイチ増えるよ </button> </> ) } export default App 同等の機能ならどっちでもよくない????? ★関数コンポーネントの方が、、、 1. thisやbindを多用しなくて良い 2. コードが短くシンプル 3. ロジックを共通化出来る 最後に 今からReactを勉強するなら迷わず関数コンポーネントを使用して勉強しよう。 さらに、Hooksはカスタムフックといってロジックを再利用出来るようになっているのです。 下記のようなカスタムフックを提供するライブラリも出ているので調べてみては。 ・react-use ・react-table ・Formik 引き続き、React関連の記事を書いていきますので宜しくお願いいたします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む