20211130のReactに関する記事は12件です。

[ReactTransitionGroup]ハンズオン!モーダルアニメーションを実装する!

ReactでCSSアニメーションを扱うためのライブラリを紹介していきます。 React-transition-groupとは アニメーションそのものを提供しているのではなく、CSSをDOMに反映するタイミングを管理してアニメーションを実現するライブラリとなっています。そして4つのコンポーネントが用意されています。 Transition CSSTransition SwitchTransition TransitionGroup 作りたいアニメーションによって使い分ける必要があります。 公式 インストール # npm npm install react-transition-group # yarn yarn add react-transition-group モーダルを実装 今回はCSSTransitionとStyledComponentを用いてモーダルを実装します。 実際の挙動がこちらです。 このモーダルのアニメーションはオーバーレイとモーダル本体の2つで構成されています。オーバーレイはフェードインとフェードアウト、一方モーダル本体はフェードインとフェードアウトに加え拡大縮小が含まれています。 それではコードを見ていきましょう。 import './App.css'; import {CSSTransition} from "react-transition-group"; import {useState} from "react"; import styled from "styled-components"; export const App = () => { const [isOpen, switchIsOpen] = useState(false) return ( <> <TransitionStyle> <div className="open" onClick={() => switchIsOpen(true)}>開く</div> <div className="modal-wrapper"> <CSSTransition classNames="modal" in={isOpen} timeout={700} unmountOnExit> <ModalStyle> <div className="content"> モーダルです。 </div> <div className="close" onClick={() => switchIsOpen(false)}>閉じる</div> </ModalStyle> </CSSTransition> </div> <CSSTransition classNames="overlay" in={isOpen} timeout={700} unmountOnExit> <OverlayStyle/> </CSSTransition> </TransitionStyle> </> ); } export default App // transitionのスタイル const TransitionStyle = styled.div` .open{ cursor: pointer; font-size: 40px; font-weight: bold; } .modal-wrapper{ position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); .modal-enter { opacity: 0; transform: scale(0.9); } .modal-enter-active { opacity: 1; transform: translateX(0); transition: opacity 0.3s, transform 0.3s; } .modal-exit { opacity: 1; } .modal-exit-active { opacity: 0; transition: opacity 0.3s, transform 0.3s; transform: scale(0.9); } } .overlay-enter { opacity: 0; } .overlay-enter-active { opacity: 1; transform: translateX(0); transition: opacity 0.3s, transform 0.3s; } .overlay-exit { opacity: 1; } .overlay-exit-active { opacity: 0; transition: opacity 0.3s, transform 0.3s; } `; // モーダルのスタイル const ModalStyle = styled.div` padding: 100px; background-color: #ffffff; display: flex; flex-direction: column; justify-content: center; align-items: center; .content{ font-size: 40px; font-weight: bold; } .close{ cursor: pointer; margin: 50px 0 0; } ` // オーバーレイのスタイル const OverlayStyle = styled.div` display: flex; align-items: center; justify-content: center; position: fixed; z-index: -1; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); `; オーバーレイとモーダル本体で使用したCSSTransitionのプロパティは同じなので、モーダル本体を例に挙げ解説します。 inは表示非表示を切り替えるためのステートを渡します。 timeoutはアニメーションの開始から終了までの時間です。 最後のunmountOnExitはアニメーションが終了した時にCSSTransitionをアンマウントさせることができます。 そしてCSSTransitionにchildrenで渡しているのがモーダル本体です。オーバーレイも同様です。 <CSSTransition classNames="modal" in={isOpen} timeout={700} unmountOnExit> <ModalStyle> <div className="content"> モーダルです。 </div> <div className="close" onClick={() => switchIsOpen(false)}>閉じる</div> </ModalStyle> </CSSTransition> classNamesに渡した文字列が、アニメーションのクラス名のプレフィックスになります。 例 classNames="modal" アニメーション開始時のクラス名="modal-enter" アニメーション中のクラス名="modal-enter-active" アニメーション終了時のクラス名="modal-exit" アニメーション中のクラス名="modal-exit-active" 上記のクラス名を使用したコードです。 .modal-enter { opacity: 0; transform: scale(0.9); } .modal-enter-active { opacity: 1; transform: translateX(0); transition: opacity 0.3s, transform 0.3s; } .modal-exit { opacity: 1; } .modal-exit-active { opacity: 0; transition: opacity 0.3s, transform 0.3s; transform: scale(0.9); } モーダルの切り替えステートをtrueにするとmodal-enterのスタイルでマウントされ、modal-enter-activeのスタイルに切り替わります。 falseにするとmodal-exitのスタイルが適応され、modal-exit-activeに切り替わりアンマウントされるという流れになります。 まとめ モーダル以外にも他のコンポーネントを用いて様々な動きのアニメーションを実装することができます。自分でcssを書いて実装するため柔軟性の高いアニメーションを作ることができます。ぜひ使ってみてはいかがでしょうか。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsとReact Hooksを使って簡易タスクアプリ作ってみた

ReactのフレームワークのNext.jsを使って定番のタスクアプリを作っていきます。  作るもの こんな感じの簡単なタスクアプリを作ってみます。 テキストに入力した値を「追加」ボタンでタスクとして追加できます。 「削除」ボタンでタスクを削除できます。 「すべてのタスク」、「ゴミ箱」のフィルターでタスクを削除済みかそうでないかに分けています。 「ゴミ箱を空にする」ボタンで削除したタスクを完全に消去できます。 こちらの記事を参考にさせていただきました。かなり詳しく書いてありました。  環境構築 Next.jsの環境構築をします。 terminal npx create-next-app --ts --use-npm your-project create-next-appでNext.jsのプロジェクトの作成です。 --ts でTypeScriptを使用します。 --use--npm でnpmを優先して使用します。 your-projectはプロジェクトの名前です。 プロジェクトファイルに移動します。 terminal cd your-project サーバーを立ちあげてみます。 terminal npm run dev ターミナル上のurl: http://localhost:3000をcommandを押しながらクリックすることで以下のページに飛ぶことができます。 Tailwind CSS の導入 CSSはTailWindCSSが使いやすいと思います。以下のサイトを参考に導入しました。 基本的な入力フォームの作成 タスクアプリを実装していきます。 まず、pages/index.tsxのデフォルトの内容を全て削除して以下の内容に書き換えます。 index.tsx const App = () => { return ( <div className="w-full mx-auto max-w-2xl my-6 px-3"> <form onSubmit={(e) => e.preventDefault()}> {/* テキスト入力フォーム */} <input className="border border-black" type="text" value="" onChange={(e) => e.preventDefault()} /> {/* 追加ボタン */} <input type="submit" value="追加" onSubmit={(e) => e.preventDefault()} /> </form> </div> ); }; export default App; 以下のように入力フォームと追加ボタンが表示されます。 e.preventDefault()によりonSubmitやonChangeのデフォルトの挙動がキャンセルされています。 フォームに文字を入力できるようにする フォームに入力された文字列をステイトとして保持するため、useStateを導入します。 index.tsx //フォームに入力された値をtodoに登録するまで保持しておくためのstate const [text, setText] = useState(''); useState: 引数となるのはステイトの初期値 現在のステイトtextとそれを更新するための関数setTextを返す。 text: 現在のステイトの値 setText: ステイトを更新するメソッド 'set' + ステイト(ロワーキャメルケース)で書きます。 useStateの引数:ステイトの初期値(=空の文字列) index.tsx // React から useState をインポート + import { useState } from 'react'; const App = () => { + const [text, setText] = useState(''); return ( <div className="w-full mx-auto max-w-2xl my-6 px-3"> <form onSubmit={(e) => e.preventDefault()}> {/* 入力中テキストの値を text ステイトが 持っているのでそれを value として表示 onChange イベント(=入力テキストの変化)を text ステイトに反映する */} <input className="border border-black" type="text" - value="" + value={text} onChange={(e) => setText(e.target.value)} /> <input type="submit" value="追加" onSubmit={(e) => e.preventDefault()} /> </form> </div> ); }; export default App; 入力フォームに文字が打てるようになります。 todoをリストとして追加できるようにする Todoオブジェクトの型の定義 1つのtodoをオブジェクトとすると、そのオブジェクトにはタスクの内容を保持するプロパティが必要となり、valueプロパティとして持ちます。 入力フォームに入力されたテキスト文字列が代入されるため、value プロパティはstring型となります。 これから作成される複数のtodoの雛形としてTodo 型オブジェクトの型エイリアスを定義します。 index.tsx + type Todo = { + value: string; + }; const App = () => { ステイトとして保持するタスクたち(todos)はTodo 型オブジェクトの配列となります。 index.tsx const App = () => { const [text, setText] = useState(''); + const [todos, setTodos] = useState<Todo[]>([]); return ( todosステイトを更新する todosステイトを更新(=新しいタスクの追加)していきます。 ステイトを更新するコールバック関数を作成します。 index.tsx const [todos, setTodos] = useState<Todo[]>([]); // todos ステイトを更新する関数 + const handleOnSubmit = () => { + // formの内容が空白の場合はalertを出す + if (text === "") { + alert("文字を入力してください"); + return; + } // 新しい Todo を作成 + const newTodo: Todo = { + value: text, + }; + // スプレッド構文を用いて todos ステイトのコピーへ newTodo を追加する + setTodos([newTodo, ...todos]); + // フォームへの入力をクリアする + setText(''); + }; onSubmitイベントに紐付ける コールバックとして() => handleOnSubmit()もしくはhandleOnSubmit関数そのものを渡します。handleOnSubmit()のみだと即時に実行されるためです。 index.tsx return ( <div className="w-full mx-auto max-w-2xl my-6 px-3"> {/*コールバックとして () => handleOnSubmit()を渡す */} <form onSubmit={(e) => { e.preventDefault(); + handleOnSubmit(); + }} > <input className="border border-black" type="text" value={text} onChange={(e) => setText(e.target.value)} /> <input type="submit" value="追加" + onSubmit={handleOnsubmit} /> </form> </div> ); onSubmitイベントが発火するとhandleOnSubmit関数が実行され、todosステイトを更新(=新しいタスクを追加)します。 todosステイトを展開してページに表示する todosステイトを展開して、タスク一覧としてページに表示します。 todos (=配列)を非破壊メソッドのArray.prototype.map()を使って<li></li>タグへ展開します。 Reactではリストをレンダリングする際、どのアイテムが変更になったのか特定する必要があるため、リストの各項目に一意な識別子となるkeyが必要です。 Todo型にidプロパティとして一意な数字(number 型)をもたせることにします。 また、書き換え不可能であるreadonly(読み取り専用)のプロパティとします。 index.tsx type Todo = { value: string; + readonly id: number; }; Todo型オブジェクトにはidプロパティの指定が必須となったため、handleOnSubmit()メソッドを更新します。 index.tsx const handleOnSubmit = () => { if (text === "") { alert("文字を入力してください"); return; } const newTodo: Todo = { value: text, + id: new Date().getTime(), }; setTodos([newTodo, ...todos]); setText(""); }; <li></li>タグにkey (=id)を付加します。 index.tsx <ul> {todos.map((todo) => { return <li key={todo.id}>{todo.value}</li>; })} </ul> keyにindex番号を割り当てる時(非推奨) 以下のようにしてkeyにindex番号を割り当てることでも動きますが推奨されていないみたいです。以下のサイトに書いてあります。 index.tsx <ul> + {todos.map((todo, index) => { + return <li key={index}>{todo.value}</li>; })} </ul> タスクを追加することができました。 削除機能を追加する 追加したタスクを削除できるようにします。 Booleanのtrue、falseで削除/未削除を管理します。Todo 型に追加します。 index.tsx type Todo = { value: string; readonly id: number; + removed: boolean; }; handleOnSubmit()メソッドを更新します。 index.tsx const newTodo: Todo = { value: text, id: new Date().getTime(), + removed: false, }; それぞれの入力フォームの後ろへ削除ボタンを追加します。 また、すでに削除済みかどうか可視化するため、todo.removedの値によってラベルを入れ替えます。 index.tsx return ( <li key={todo.id}> {todo.value} + <button onClick={() => handleOnRemove(todo.id, todo.removed)}> + {todo.removed ? "復元" : "削除"} + </button> </li> ); 削除ボタンがクリックされたときのコールバック関数を作成します。 index.tsx + const handleOnRemove = (id: number, removed: boolean) => { + const deepCopy: Todo[] = JSON.parse(JSON.stringify(todos)); + const newTodos = deepCopy.map((todo, todoIndex) => { + if (todo.id === id) { + todo.removed = !removed; + } + return todo; + }); + setTodos(newTodos); + }; タスクをフィルタリングする機能を追加する 削除済みアイテムも一緒に表示されてしまうので、タスクをフィルタリングする機能を追加します。 index.tsx return ( <div className="w-full mx-auto max-w-2xl my-6 px-3"> + <select + defaultValue="all" + onChange={(e) => setFilter(e.target.value as Filter)} + > + <option value="all">すべてのタスク</option> + <option value="removed">ゴミ箱</option> + </select> 現在のフィルターを格納するfilterステイトを追加します。 フィルターの状態を表すFilter 型を作ります。 index.tsx + type Filter = "all" | "removed"; <option />タグの値をFilter 型のステイトとして保持します。 index.tsx const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); + const [filter, setFilter] = useState<Filter>("all"); onChangeイベントが発火すると、filterステイトを更新するようにしています。 index.tsx // e.target.value: string を Filter 型にキャストする <select defaultValue="all" onChange={(e) => setFilter(e.target.value as Filter)} > <option value="all">すべてのタスク</option> <option value="removed">ゴミ箱</option> </select> フィルタリング後のTodo 型配列をリスト表示します。 todos ステイト配列の表示方法を変化させる関数を作成します。 <ul></ul>タグの中で展開されているtodos ステイトをタグへ渡す前に加工します。 現在のfilter ステイトに応じてTodo 型配列の要素をフィルタリングします。 index.tsx + const filteredTodos = todos.filter((todo) => { + // filterステイトの値に応じて異なる内容の配列を返す + switch (filter) { + case "all": + // 削除されていないもの全て + return !todo.removed; + case "removed": + // 削除済みのもの + return todo.removed; + default: + return todo; + } + }); todoステイトを展開する<ul></ul>タグにフィルタリング済みのリストを渡すように書き換えます。 index.tsx <ul> - {todos.map((todo) => { + {filteredTodos.map((todo) => { 「ゴミ箱」が表示されている時は新しいタスクを追加できないように入力フォームを無効化します。 index.tsx <input className="border border-black" type="text" value={text} + disabled={filter === "removed"} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" + disabled={filter === "removed"} onSubmit={handleOnSubmit} /> 「ゴミ箱を空にする」機能を追加する フィルターで「ゴミ箱」のタスクリストを表示している時は、削除済みタスクを完全に消去できるように機能を追加します。 フィルターが「ゴミ箱」の場合は「ゴミ箱を空にする」ボタンを表示し、それ以外の時は従来の入力フォームを表示するようにします。 index.tsx <option value="removed">ゴミ箱</option> </select> + {filter === "removed" ? ( + <button + onClick={handleOnEmpty} + disabled={todos.filter((todo) => todo.removed).length === 0} + > + ゴミ箱を空にする + </button> + ) : ( <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input className="border border-black" type="text" value={text} disabled={filter === "removed"} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" disabled={filter === "removed"} onSubmit={handleOnSubmit} /> </form> + )} 三項演算子? 'trueの時' : 'falseの時' で評価しています。 入力フォームが描写される時はfilter === 'removed'という状態が発生しないので入力フォームからこれを削除します。 index.tsx <input className="border border-black" type="text" value={text} - disabled={filter === "removed"} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" - disabled={filter === "removed"} onSubmit={handleOnSubmit} /> 「ゴミ箱を空にする」コールバック関数の作成します。 todos ステイト配列から、removedフラグが立っている要素を取り除きます。 index.tsx + const handleOnEmpty = () => { + const newTodos = todos.filter((todo) => !todo.removed); + setTodos(newTodos); + }; 以上です。ありがとうございました。コード全文はこちらです。 コード全文 index.tsx import { useState } from 'react'; type Todo = { value: string; readonly id: number; removed: boolean; }; type Filter = "all" | "removed"; const App = () => { const [text, setText] = useState(''); const [todos, setTodos] = useState<Todo[]>([]); const [filter, setFilter] = useState<Filter>("all"); // todosステイトを更新する関数 const handleOnSubmit = () => { // 何も入力されていない時 if (text === "") { alert("文字を入力してください"); return; } //新しい Todo を作成 const newTodo: Todo = { value: text, id: new Date().getTime(), removed: false, }; setTodos([newTodo, ...todos]); //フォームへの入力をクリアする setText(''); }; // formに入力更新された時更新する関数 const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { setText(e.target.value); }; // 削除ボタンがクリックされた時更新する関数 const handleOnRemove = (id: number, removed: boolean) => { const deepCopy: Todo[] = JSON.parse(JSON.stringify(todos)); const newTodos = deepCopy.map((todo) => { if (todo.id === id) { todo.removed = !removed; } return todo; }); setTodos(newTodos); }; // ゴミ箱を空にする時更新する関数 const handleOnEmpty = () => { const newTodos = todos.filter((todo) => !todo.removed); setTodos(newTodos); }; // フィルター const filteredTodos = todos.filter((todo) => { switch (filter) { case "all": return !todo.removed; case "removed": return todo.removed; default: return todo; } }); return ( <div className="w-full mx-auto max-w-2xl my-6 px-3"> <select defaultValue="all" onChange={(e) => setFilter(e.target.value as Filter)} > <option value="all">すべてのタスク</option> <option value="removed">ゴミ箱</option> </select> {filter === "removed" ? ( <button onClick={handleOnEmpty} disabled={todos.filter((todo) => todo.removed).length === 0} > ゴミ箱を空にする </button> ) : ( <form onSubmit={(e) => { e.preventDefault(); handleOnSubmit(); }} > <input className="border border-black" type="text" value={text} onChange={(e) => handleOnChange(e)} /> <input type="submit" value="追加" onSubmit={handleOnSubmit} /> </form> )} <ul> {filteredTodos.map((todo) => { return ( <li key={todo.id}> {todo.value} <button onClick={() => handleOnRemove(todo.id, todo.removed)}> {todo.removed ? "復元" : "削除"} </button> </li> ); })} </ul> </div> ); }; export default App;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React から Vue への出戻りを検討する 3 つの理由

はじめに 「結局 Vue.js と React.js、結局どっちがいいの?」 2年半ほど前に、流行っている JS フレームワークのどちらかを勉強してみようと思った時、そんなことを延々と考えていました。 「Vue は Easy、React は Simple」「Vue は独自構文が多いけど、普段マークアップをメインに仕事してる人は触りやすいと思う」「React は生 JavaScript を触ってる感じ、JS 得意なら新しく覚えないといけないことは少なさそう」 こんな話を周りから聞き、まず自分の普段の業務内容から考えてとっつきやすそうな Vue から「なんとなく」触り始めたのを覚えています。 最近になってようやく React を触り始めたのですが、両方実際に触ってみることで「なんとなく」ではない、2 つの JS フレームワークそれぞれの具体的な良さが見えつつあります。 「出戻りを検討する理由」としていますが、当然 React のよい点も多々感じています。 自分の普段の JS フレームワークの使用方法としては、Web サイト制作業務から(主に趣味程度の)簡単な Web アプリケーション制作がほとんどであり、使用歴も決して長くありません。ベストな方法を知り尽くしているわけではありませんし、使用機会として Vue.js の方が多いためそちらに知識やスタンスがそちらに偏っているということもあるかと思います。 本記事のスタンスとしては 「最近 Vue.js が v3 へのアップデートで React に近づいたらしいし、じゃあもう React やったほうがいいんじゃない?」 という(主に過去の自分の)考えに対し 「でもどっちも触った上で改めて考えてみると、Vue もやっぱりいいよねー」と伝えたい、という気持ちで書いています。 どちらが確実に優位であるという内容を書くつもりはなく、これから少しでも JS フレームワークを触ってみようかな?と選定に迷っている方の参考となれば幸いです。 理由 1. CSS の書き方で悩まない Vue.js の SFC(Single-file component、いわゆる .vue ファイル)では、CSS の記述を style タグの中に記述することがほとんどかと思います。 style タグの中でどう CSS を記述するかの選択はあれど、基本的にはプレーンな CSS と同様の扱いで記述することができます。 当たり前かと思うかもしれませんが、普段 CSS を書いている方であれば、特に迷うことなくスタイリングを行うことができますし、この当たり前さが Vue を Easy たらしめる良さの 1 つであると思っています。 <template> <p class="hoge">サンプルテキスト<p> </template> <style> .hoge { color: red; } </style> React だとどうでしょうか? React プロジェクトを生成するコマンドである create-react-app のデフォルトでは、CSS ファイルが JS ファイル内で import されているかと思いますが、実際のプロジェクトでそのまま使用されている方は少ないのではないでしょうか。 どの方法で CSS を書くのがより良いか、手法及びライブラリの選定をまず行うことが多いかと思います。 「そのままプレーンな CSS を書くか、CSS Modules で記述するか、CSS in JS にするか、どのプラグインに何を使用するか。。」などを考え出すと結構労力がかかりますし、実際に使ってみてベストプラクティスを検討するとなるとなおさらです。 自分が React を使用したタイミングでは styled-components を採用しましたが、正直なところ今までの CSS 設計で解決できなかった問題が楽になったという印象はありませんでした。いつもの CSS 設計を styled-components にどう反映すべきなのか?を考えている時間の方が長かったかもしれません。 その点、Vue であれば、多少の設計の違いはあれど少なくとも記法の違いで悩むことはありません。チームで開発する際にも、記法に学習コストをかけずに、設計にフォーカスしたレビューに時間を割くことができます。 (React に CSS modules を採用する場合であれば大差なかったかも知れませんが、maintenance only になっているとのことと、CSS を別ファイルで管理したくなかった等の理由で採用を見送りました。。) コンポーネントの挙動とスタイルが密接に関連づけられている UI が多い場合であれば、もっと CSS in JS が生きるのかもしれませんが、自分の業務内容の場合は CSS 設計で解決できることが多かったため、styled-components の旨味を生かしきれなかった、ということでもあるかも知れません。 理由 2. 設計方針やライブラリと選定での悩みが比較的少ない Vue は「The Progressive JavaScript Framework」 と銘打たれており、ルーティングの機能が必要であれば vue-router, 状態管理を整理したければ Vuex というように、機能の必要性に応じて拡張されることを前提として周辺のエコシステムが提供されています。 必要とする機能が少し特殊である時などは不十分な場合もあるかもしれませんが、それでも選定基準の一つとして第一候補に上がる公式のライブラリがあるというのは心強いです。 対して React は「UI 構築のための JavaScript ライブラリ」であり、本体が持っていない機能の実装時には別ライブラリを選定する必要があります。Vue のように公式のエコシステムがリリースされているわけではないため、適切なライブラリを自身で選定ことになるかと思いますが、その際「そもそもどういったライブラリがあり、今回自分が実装するにあたって何が適切か」「メンテナンスされており幅広く使用されているライブラリか、古くなっていないか」など、機能拡張の際に実装や学習以外の部分で一苦労あります。 React は本体の機能としてはシンプルに整っているかもしれませんが、ほとんどの場合 React 本体だけでプロジェクトの用件を満たすことはおそらくできないでしょう。 「React の実装自身はシンプルだが、ライブラリを含めた React プロジェクトがシンプルに保たれるかどうかは別問題」かと思います。 (それぞれのフレームワークに対する慣れや知識量の問題、と言われてしまえば返す言葉もないのですが。。) 「1. CSS の書き方で悩まない」の styled-components の内容と重なるかもしれませんが、React は技術選定や学習コスト、ベストプラクティスを探るのに時間がかかる印象があります。少なくとも追加機能のライブラリ選定時にあまり悩む必要がないのは Vue ではないだろうか、と感じています。 理由 3.Composition API の導入による TypeScript との食い合わせ問題の緩和 Vue.js と React.js の比較の際、 Vue.js と TypeScript との食い合わせの悪さはよく挙げられていました。 TypeScript で容易に型の恩恵を受けることができるというのは React.js の大きなアドバンテージであったかと思いますが、昨年リリースされた Vue 3 では TypeScript との食い合わせ問題が大きく改善されたように思えます。 ・Vue 2 系からの Options API(型なし) <template> <div> <p>{{ computedNumber }}</p> <button @click="addCount" type="button">add</button> </div> </template> <script> // 型を付けたい場合は defineComponent + as 節での型アサーションとなる // https://v3.vuejs.org/guide/typescript-support.html#using-with-options-api export default { data() { return { countNumber: 0 } }, computed: { doubleNumber() { return this.countNumber * 2 } }, methods: { addCount() { this.countNumber++ } }, } </script> ・Vue 3 で導入された Composition API(型は推論が効きますが、ドキュメント代わりに書くようにしています) <template> <div> <p>{{ doubleNumber }}</p> <button @click="addCount" type="button">add</button> </div> </template> <script lang="ts"> import { defineComponent, ref, computed } from 'vue' export default defineComponent({ setup () { const countNumber = ref<number>(0) const doubleNumber = computed((): number => { // ref オブジェクトの値は .value に格納されるため countNumber.value を使用する // よく忘れそうになるが、TypeScript だと .value 忘れのエラーを吐いてくれるので助かる return countNumber.value * 2 }) const addCount = (): void => { countNumber.value++ } return { doubleNumber, addCount } } }) </script> Composition API の導入によって TypeScript のサポートだけではなく、機能ごとにロジックの記述をまとめることができたり、カスタムフックのように import / export を使用してロジックを見通しよく記述できるようになりましたが、import / export の数がかなり多くなってしまうことや、ほぼ一緒になるはずの props の type プロパティと setup 関数の引数に渡る props の TS 型定義を別に記述する必要があったり、それなりに面倒くささもありました。 しかし script setup の導入によって、CompositionAPI で個人的に気になっていた記述の面倒くささは、現状かなりシンプルに解決されています。 ・Composition API (script setup) <template> <div> <p>{{ doubleNumber }}</p> <button @click="addCount" type="button">add</button> </div> </template> <script lang="ts" setup> import { ref, computed } from 'vue' const countNumber = ref<number>(0) const doubleNumber = computed((): number => { return countNumber.value * 2 }) const addCount = (): void => { countNumber.value++ } </script> トップレベルの記述はそのまま template で使用することができるようになり、煩わしい import や return などが整理されました。 また props の型は defineProps を用いて TypeScript の型定義をそのまま使用することができるようになったため、Vue, TypeScript の型宣言も不要となりました。 https://v3.vuejs.org/api/sfc-script-setup.html#typescript-only-features 以前【Vue.js 3.0】Composition API なんかと同じ空間にいられるか!俺は 2.x 系に戻るぞ! を書いた際、は「記述の冗長さも気になる節があるので、コンポーネントの粒度が大きくなってきたら Composition API を導入、くらいで良さそう」程度に考えていましたが、Composition API と TypeScript での開発や、setup script を使用した記述は思いの外良く、Simple かつ Easy にまとまってきていると感じます。 (あとは .vue ファイルに型定義の import が問題なくできるようになってくれると個人的には嬉しい限りです。どうやら今はまだ調整中のようで。。) https://github.com/vuejs/vue-next/issues/4294 まとめ 再度簡単にまとめておくと そのまま CSS を書くことができる ライブラリ選定で悩まない CompositionAPI + script setup による開発体験の改善 という点が、自分が最近改めて感じた Vue の良さになります。 自分も引き続き両方触っていき、よりコアな機能も含めて末長く付き合っていければよいなと思っています。 少しでも興味を持っていただけたようであれば、この機会にお手元で是非一度触っていただけると、多少なりとも師走を楽しく過ごすことができるかもしれません。 それでは皆様、良い年末をお過ごしください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「毎日誰かのプルリクを脳死でマージするアドベントカレンダー」を始めました

概要 以下のアドベントカレンダーを開始しました。 簡単に言うと、僕が初日に空っぽのNext.jsプロジェクトを作って公開しておくので、2日目以降毎日どなたかが機能実装してPull Requestを出してください、というものです。 前日時点で23人参加で、最終2日だけ空いている状況というまあまあの盛況をいただいております。 きっかけ 例によって例のごとく、ツイートしたら反応があったのでやることにしました。 期待しているもの ネタ丸出しのマージから、ESLint等の設定調整、新しいライブラリの導入など、どんなものが飛んでくるのか楽しみです。 Providerを必要とするUIライブラリは何個も入れるとカニバるのかなと思うので、早い者勝ちだなとも思います。 まとめ 特にまとまりのない記事ですが、とりあえずGitHub公開というミッションは達成したので後の担当の皆さんに期待しております。 2日目は @sassy_watson さんです。よろしくお願いします!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Harmoware-VIS APIリファレンス

前提 Harmoware-VISについては別記事 「Harmoware-VISの紹介」 「Harmoware-VIS はじめに」 をご覧ください。 APIリファレンス Harmoware-VISのAPIのリファレンスを解説します。 Properties ルートで受け取る「props」オブジェクト内の項目を以下に示します。 actions 以下のPropertiesを更新するためのメソッド群 詳細は各項目の欄を参照 animatePause (Boolean) ・Default : false trueでアニメーションが一時停止。 値更新:actions.setAnimatePause animateReverse (Boolean) ・Default : false trueでアニメーションを再生。 値更新:actions.setAnimateReverse bounds (Object) ・Default : ※後述 シミュレーション領域の境界位置。 移動体データ設定時(actions.setMovesBase)に設定データ内のboundsをコピー 設定データ内にboundsが無い場合はDefault値のまま ※boundsのDefault値 westlongitiude(number) 最西端の経度位置 Default : 0 eastlongitiude(number) 最東端の経度位置 Default : 0 southlatitude(number) 最南端の緯度位置 Default : 0 northlatitude(number) 最北端の緯度位置 Default : 0 defaultPitch (Number) ・Default : 30 視点ピッチ(角度)のデフォルト値。 値更新:actions.setDefaultPitch 値更新:actions.setDefaultViewport sample.js actions.setDefaultViewport({defaultPitch:45, defaultZoom: 15}) defaultZoom (Number) ・Default : 11.1 デフォルトのズーム値。 値更新:actions.setDefaultViewport depotsBase (Object Array) ・Default : [] 停留場の位置情報の登録データフォーマット。 (Harmoware-VIS提供の「DepotsLayer」用のデータ) 値更新:actions.setDepotsBase ※depotsBaseのフォーマット(setDepotsBaseで設定するデータのフォーマット) sample.json [ { "position": [999.9999, 999.9999, 999], ...anyKey, ・・・・・・ },・・・・・・ ] or [ { "longitude": 999.9999, "latitude": 99.9999, ...anyKey, ・・・・・・ },・・・・・・ ] position (number[], required) 停留場の位置情報 [ 経度、緯度、[標高] ](標高は省略可) longitude(number, optional) 停留場の位置情報 [ 経度 ] latitude(number, optional) 停留場の位置情報 [ 緯度 ] type (string, optional) 停留場の種別を識別する際に設定 ...any key (any, optional) 必要に応じて設定。 depotsData (Object Array) ・Default : [] 停留場の位置情報の表示データフォーマット。 DepotsLayer表示用データとしてdepotsBaseを元に作成されます。 基本的に「depotsBaseのフォーマット」と同じフォーマット(※)ですが、getDepotsOptionFuncにコールバック関数を登録してカスタマイズが可能です。 ※depotsBaseでの位置情報がlongitudeとlatitudeで定義されている場合はpositionに変換されます。 sample.json [ { "settime": 9999999999, "position": [999.9999, 999.9999, 999], ...anyKey, ・・・・・・ },・・・・・・ ] settime(number) このdepotsDataを作成した時点のシミュレーション日時のUNIX時間(秒)を編集 position (number[]) depotsBaseより編集 ...any key (any, optional) depotsBaseのposition、longitude、latitude以外の項目とコールバック関数(getDepotsOptionFunc)より編集 getDepotsOptionFunc (Function) ・Default : null 停留場情報の表示データをカスタマイズするコールバック関数を設定。 値更新:actions.setDepotsOptionFunc オプショングラフの動的更新時などで使用。 コールバック関数の書式 <P>(props: P, i: number) => object 戻り値のobjectはdepotsData生成時にマージされます。 props(object) 最新のpropsデータ i(number) depotsBase配列の処理中要素インデックス getMovesOptionFunc (Function) ・Default : null 移動体情報の表示データをカスタマイズするコールバック関数を設定。 値更新:actions.setMovesOptionFunc オプショングラフの動的更新時などで使用。 コールバック関数の書式 <P>(props: P, i: number, j: number) => object 戻り値のobjectはmovedData生成時にマージされます。 props(object) 最新のpropsデータ i(number) movesbase配列の処理中要素インデックス j(number) movesbase配列の処理中要素のoperationの処理中要素インデックス つまり movesbase[i].operation[j] を指す leading (Number) ・Default : 100 シミュレーション開始時刻の前に追加する空白時間(単位:秒) 値更新:actions.setLeading movesbase (Object Array) ・Default : [] 移動体の経路情報のデータ。 ※movesbaseのフォーマット sample.json [ { "type": "xxxxx", "movesbaseidx": 9999999999, "departuretime": 9999999999, "arrivaltime": 9999999999, "operation": [ { "elapsedtime": 9999999999, "position": [999.9999, 999.9999, 999], "direction": 999, ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ },・・・・・・ ] type(string) 移動体の識別情報を編集(定義有無は元データによる) movesbaseidx(number) movesbase配列のインデックス値を編集 departuretime(number) 移動体の出発時のelapsedtimeの値をUNIX時間(秒)で編集 arrivaltime(number) 移動体の到着時のelapsedtimeの値をUNIX時間(秒)で編集 operation(object[]) 移動体のルートと経過時間を移動体毎の配列で設定 elapsedtime(number) positionの通過時間をUNIX時間(秒)で設定 position (number[]) 移動体のelapsedtime時点の位置情報 [ 経度、緯度、標高 ] direction(number) 移動体の方向(角度) ...any key (any, required) setMovesBaseなどのデータ設定用のメソッドで設定したデータの項目より編集 ※移動体の経路情報のデータ設定用のメソッド 値更新:actions.setMovesBase 値更新:actions.updateMovesBase ※設定するデータのフォーマット sample.json // フォーマット例1 { "timeBegin": 9999999999, "timeLength": 99999, "elapsedtimeMode": "UNIXTIME", "bounds": { "northlatitude": 99.99999, "southlatitude": 99.99999, "westlongitiude" 999.99999, "eastlongitiude": 999.99999, }, "movesbase": [ { "type": "xxxxx", "operation": [ { "elapsedtime": 99999, "position": [999.9999, 999.9999, 999], "longitude": 999.9999, "latitude": 99.999, ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ } // フォーマット例2 [ { "type": "xxxxx", "operation": [ { "elapsedtime": 9999999999, "position": [999.9999, 999.9999, 999], "longitude": 999.9999, "latitude": 99.999, ...anyKey, ・・・・・・ },・・・・・・ ], ...anyKey, ・・・・・・ },・・・・・・ ] timeBegin(number, optional) シミュレーション開始日時のUNIX時間(秒) timeLength(number, optional) シミュレーション開始から終了までの時間長(秒) elapsedtimeMode(string, optional) movesbase.operationのelapsedtimeをUNIX時間(秒)で指定する場合は"UNIXTIME"を設定 bounds(object, optional) シミュレーションエリアの範囲宣言(東西南北端) movesbase(object[], required) 移動体のルートと経過時間、識別情報などを設定 type(string, optional) 移動体の識別情報を設定 operation(object[], required) 移動体のルートと経過時間を移動体毎の配列で設定 elapsedtimeの値が異なる2個以上の要素が無い場合は表示対象外 elapsedtime(number, required) フォーマット例1の場合は上記のtimeBeginからの経過時間(秒)で設定 但し、timeBeginが未定義の場合はUNIX時間(秒)で設定 更に、timeBeginが定義済かつelapsedtimeModeに"UNIXTIME"が定義済の場合はUNIX時間(秒)で設定 フォーマット例2の場合はUNIX時間(秒)で設定 position (number[], required) 移動体のelapsedtime時点の位置情報 [ 経度、緯度、[標高] ](標高は省略可) 位置情報をpositionまたはlongitude-latitudeで指定 positionとlongitude-latitudeの両方が定義された場合はpositionを優先で採用 longitude(number, required) 移動体のelapsedtime時点の位置情報 [ 経度 ] latitude(number, required) 移動体のelapsedtime時点の位置情報 [ 緯度 ] ...any key (any, required) 必要に応じて設定。 movedData (Object Array) ・Default : [] MovesLayer表示用データとしてmovesbaseを元に作成されます。 ※movedDataのフォーマット sample.json [ { "settime": 9999999999, "movesbaseidx": 9999999999, "type": "xxxxx", "position": [999.9999, 999.9999, 999], "sourcePosition": [999.9999, 999.9999, 999], "targetPosition": [999.9999, 999.9999, 999], "direction": 999, "color": [999, 999, 999], "sourceColor": [999, 999, 999], "targetColor": [999, 999, 999], ...anyKey, ・・・・・・ }, ・・・・・・ ] settime(number) このmovedDataを作成した時点のシミュレーション日時のUNIX時間(秒)を編集 movesbaseidx(number) このmovedDataに対応するmovesbase配列のインデックス値を編集 type(string) movesbaseより編集(定義有無はmovesbaseによる) position (number[]) movesbaseより編集 sourcePosition(number[]) 上記positionと同値 targetPosition(number[]) 次のデータのpositionと同値 direction(number) 移動体の方向(角度) color(number[]) movesbaseより編集(未定義時は[0,255,0]:”green”) sourceColor(number[]) 上記colorと同値 targetColor(number[]) 次のデータのcolorと同値(未定義時は[0,255,0]:”green”) ...any key (any, optional) setMovesBaseなどのデータ設定用のメソッドで設定したデータの項目とコールバック関数(getMovesOptionFunc)より編集 ExtractedData (any) ・Default : undefined getExtractedDataFunc関数で生成&登録したデータ。 getExtractedDataFunc (Function) ・Default : null 再生時の画面更新タイミングで表示させるデータを生成&登録する関数。 値更新:actions.setExtractedDataFunc movesbaseではコントロールできないカスタムデータを処理する関数 コールバック関数の書式 <P>(props: P) => any 戻り値の値がそのままExtractedDataに設定されます。 props(object) 最新のpropsデータ clickedObject (Object Array) ・Default : null クリックされた移動オブジェクト参照データの配列。 Harmoware-VIS標準機能で使用する情報。 クリックしたアイコンの情報を保持。 値更新:actions.setClicked routePaths (Object Array) ・Default : [] クリックされた移動オブジェクトの経路データの配列。 Harmoware-VIS標準機能で使用する情報。 クリックしたアイコンの経路情報を保持。 値更新:actions.setRoutePaths ※routePathsのフォーマット sample.json [ { "movesbaseidx": 9999999999, "sourcePosition": [999.9999, 999.9999, 999], "targetPosition": [999.9999, 999.9999, 999], "routeColor": [999, 999, 999], "routeWidth": 999, }, ・・・・・・ ] movesbaseidx(number) このroutePathsに対応するmovesbase配列のインデックス値を編集 sourcePosition(number[]) 線分始端位置 [ 経度、緯度、標高 ] targetPosition(number[]) 線分終端位置 [ 経度、緯度、標高 ] routeColor(number[]) 線分色 routeWidth(number) 線分幅(マップ縮尺でのメートル) secperhour (Number) ・Default : 180 再生速度(秒/時) 値更新:actions.setSecPerHour secperhour値を更新時にはmultiplySpeed値も対応する値に更新 multiplySpeed (Number) ・Default : 20 再生速度(倍速) 値更新:actions.setMultiplySpeed multiplySpeed値を更新時にはsecperhour値も対応する値に更新 settime (Number) ・Default : 0 再生時間(UNIX時間) 再生時には再生時刻に自動で更新 任意の値に更新する場合は以下のメソッド使用 値更新:actions.setTime 値を加減算する場合は以下のメソッド使用() 値更新:actions.addMinutes sample.js actions.addMinutes(10) // 指定の単位は分 timeBegin (Number) ・Default : 0 シミュレーション開始時刻(UNIX時間) 移動体の経路情報のデータ設定時に自動で設定 間接更新:actions.setMovesBase 間接更新:actions.updateMovesBase 任意の値に更新する場合は以下のメソッド使用 値更新:actions.setTimeBegin timeLength (Number) ・Default : 0 シミュレーション時間長(秒) 移動体の経路情報のデータ設定時に自動で設定 timeBegin値の時刻よりtimeLength後がシミュレーション終了時刻 間接更新:actions.setMovesBase 間接更新:actions.updateMovesBase 任意の値に更新する場合は以下のメソッド使用 値更新:actions.setTimeLength trailing (Number) ・Default : 180 シミュレーション終了後に追加する空白時間(単位:秒) 値更新:actions.setTrailing viewport (Object) ・Default : ※後述 マップ表示の視点情報(ビューポート)を指定 値更新:actions.setViewport ※viewportのDefault値 longitude(number,option) マップ表示中心経度 Default : 136.906428 latitude(number,option) マップ表示中心経度 Default : 136.906428 zoom(number,option) マップ表示ズーム値 Default : 11.1 maxZoom(number,option) マップ表示ズーム最大値 Default : 18 minZoom(number,option) マップ表示ズーム最小値 Default : 5 pitch(number,option) マップ表示視点角度値 Default : 30 bearing(number,option) マップ表示視点方角値 Default : 0 transitionDuration(number,option) マップ表示視点移動時間(ミリ秒) Default : 0 マップ位置移動時にかける時間(ミリ秒)を設定。 ゼロ以上の場合は移動がアニメーション化する ...any key (any, optional) その他react-map-glで扱う視点情報 linemapData (Object) ・Default : [] 線画LineMapLayer表示用データフォーマット 値更新:actions.setLinemapData ※linemapDataのフォーマット sample.json [ { "sourcePosition": [999.9999, 999.9999, 999], "targetPosition": [999.9999, 999.9999, 999], "color": [999, 999, 999], "path": [[999.9999, 999.9999, 999], ・・・・・・], "polygon": [[999.9999, 999.9999, 999], ・・・・・・], "coordinates": [[999.9999, 999.9999, 999], ・・・・・・], "elevation": 999, "strokeWidth": 999, "dash": [99, 99], }, ・・・・・・ ] sourcePosition(number[],option) LineMapLayer内のLineLayerで表示する直線始端位置 [ 経度、緯度、標高 ] targetPosition(number[],option) LineMapLayer内のLineLayerで表示する直線終端位置 [ 経度、緯度、標高 ] color(number[],option) 線分色 path(number[][],option) LineMapLayer内のPathLayerで表示する線分パス情報([ 経度、緯度、標高 ]の配列) polygon(number[][],option) LineMapLayer内のPolygonLayer(3D)で表示する線分パス情報([ 経度、緯度、標高 ]の配列) coordinates(number[][],option) LineMapLayer内のPolygonLayer(2D)で表示する線分パス情報([ 経度、緯度、標高 ]の配列) elevation(number,option) LineMapLayer内のPolygonLayerで表示する高さ情報 strokeWidth(number,option) LineMapLayer内のLineLayerとPathLayerで表示する線幅情報(マップ縮尺でのメートル) dash(number[],option) LineMapLayer内のPathLayerで表示する破線指示 破線指示情報の設定値の詳細はこちら loading (Boolean) ・Default : false trueで読み込み中のアイコンを表示。 (Harmoware-VIS提供のLoadingIconを使用時に有効) 値更新:actions.setLoading inputFileName (Object) ・Default : {} 画面表示用の入力ファイル名(※後述) Harmoware-VIS提供のMovesInputDepotsInputLinemapInputコンポーネントを用いてファイルデータを読み込んだ場合にファイル名を設定 ユーザで別のファイルを使用する際に使用しても構いません。 値更新:actions.setInputFilename sample.js actions.setInputFilename({customFileName:"xxxxxxxxxx.xxx"}) sample.json { "movesFileName": "xxxxxxxxxx.json", "depotsFileName": "xxxxxxxxxx.json", "linemapFileName": "xxxxxxxxxx.json", } movesFileName(number[],option) MovesInputコンポーネントでファイルを読み込んだ場合にファイル名を設定 depotsFileName(number[],option) DepotsInputコンポーネントでファイルを読み込んだ場合にファイル名を設定 linemapFileName(number[],option) LinemapInputコンポーネントでファイルを読み込んだ場合にファイル名を設定 noLoop (Boolean) ・Default : false trueでシミュレーション終了後に一時停止する。(ループしない) 値更新:actions.setNoLoop initialViewChange (Boolean) ・Default : true trueで移動体データを読み込んだ後、マップ表示の視点をデータ群の平均的な中心位置に移動 falseの場合は移動体データを読み込み後、マップ表示の視点移動しない(そのまま) 値更新:actions.setInitialViewChange iconGradation (Boolean) ・Default : false trueで移動体アイコンのカラー(movedData)変更を経過時間で遷移するグラデーション化 ⇒A地点(赤指定)→B地点へ(青指定)の場合、A→B移動中に徐々に色が変化 falseの場合、B地点到着まで赤のままで到着すると青に変化 値更新:actions.setIconGradationChange getCombinedReducer reduxのcreateStoreに渡すことができるHarmoware-VISのリデューサ関数を取得します。 また、引数にリデューサ関数を追加で記述することで、Harmoware-VISのリデューサ関数と統合することができます。 sample.js import { render } from 'react-dom'; import { getCombinedReducer } from 'harmoware-vis'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import React from 'react'; import App from './containers'; import 'harmoware-vis/scss/harmoware.scss'; // 個別リデューサ関数の追加がない場合 const store = createStore(getCombinedReducer({customReducer})); // 個別リデューサ関数を追加する場合の例 import customReducer from './customReducer'; const store = createStore(getCombinedReducer({customReducer})); render( <Provider store={store}> <App /> </Provider>, document.getElementById('app') ); Container React.Componentを継承したクラスです。 Harmoware-VISライブラリのベースとなるコンポーネントで、settimeとanimation frameを更新します。 renderメソッドでDOMを出力してください。 sample.js // using mapbox import React from 'react'; import { Container, connectToHarmowareVis, HarmoVisLayers, ... } from 'harmoware-vis'; class App extends Container { render() { const { viewport, actions, ... } = this.props; return ( <HarmoVisLayers viewport={viewport} actions={actions} mapboxApiAccessToken={ ... } layers={[ ... ]} /> ); } } export default connectToHarmowareVis(App); connectToHarmowareVis connectToHarmowareVisの状態をコンテナ・コンポーネントのpropに同期させるためのUtilityです。 sample.js import { Container, connectToHarmowareVis } from 'harmoware-vis'; class App extends Container { ・・・ } // 追加のアクションが無い場合 export default connectToHarmowareVis(App); // 追加のアクションが有る場合 import * as moreActions from '../actions'; export default connectToHarmowareVis(App, moreActions); HarmoVisLayers deck.glのLayerクラスを継承したレイヤーを、mapboxで取得した地図上に表示します。 sample.js <HarmoVisLayers viewport={this.props.viewport} actions={this.props.actions} mapboxApiAccessToken={MAPBOX_TOKEN} layers={ [ ... ] } mapGlComponents={ this.getComponentsDom(this.props.movedData) } // Example /> visible(Boolean,option) ・Default : true trueの場合地図レイヤーを表示 actions(object,required) Propertiesのactionsを設定 APIリファレンスはリンク先を参照 viewport(object,required) Propertiesのviewportを設定 APIリファレンスはリンク先を参照 mapboxApiAccessToken(string,required) mapbox.comのアクセストークンを設定 但し、上記visibleでfalseを設定した場合は有効なアクセストークンである必要はない mapStyle(string,option) ・Default : 'mapbox://styles/mapbox/dark-v8' style URL を設定(Mapboxの style URL の情報はリンク先を参照) layers(array,required) レイヤーインスタンスの配列 レイヤーは、deck.glのLayerクラスを継承したレンダリングコンポーネントです。 onViewportChange(Function,option) ・Default : PropertiesのactionsのsetViewport viewportを更新するインターフェスを設定 mapGlComponents(any,option) ・Default : null "react-map-gl" カスタムデータ追加インターフェース カスタムデータ追加に関する情報は”react-map-gl”のリンク先を参照 mapboxAddLayerValue(mapboxgl.Layer[],option) ・Default : ※下記参照 Mapboxマップに重ねるレイヤーを設定 Mapboxマップに重ねるレイヤーに関する情報はリンク先を参照 mapboxAddLayerValue_Default.json [{ "id": "3d-buildings", "source": "composite", "source-layer": "building", "filter": ["==", "extrude", "true"], "type": "fill-extrusion", "paint": { "fill-extrusion-color": "#888", "fill-extrusion-height": ["interpolate", ["linear"], ["zoom"], 5, 0, 5.05, ["get", "height"]], "fill-extrusion-base": ["interpolate", ["linear"], ["zoom"], 5, 0, 5.05, ["get", "min_height"]], "fill-extrusion-opacity": 0.6 }, },{ "id": "sky", "type": "sky", "paint": { "sky-type": "atmosphere", "sky-atmosphere-sun": [180.0, 60.0], "sky-atmosphere-sun-intensity": 5 } }] mapboxAddSourceValue(object {id:string, source:object}[],option) ・Default : undefined Mapboxマップに重ねるソースを設定 Mapboxマップに重ねるソースに関する情報はリンク先を参照 terrain(Boolean,option) ・Default : false trueで3Ⅾ地形表示(下記terrainSourceとsetTerrainのデータを使用) Mapboxでの3Ⅾ地形表示に関する情報はリンク先を参照 terrainSource(object{id:string,source:object},option) ・Default : ※下記参照 terrainSource_Default.js { id:'mapbox-dem', source:{ 'type': 'raster-dem', 'url': 'mapbox://mapbox.mapbox-terrain-dem-v1', }} setTerrain(object { source:string, exaggeration?:number},option) ・Default : ※下記参照 setTerrain_Default.js {source:'mapbox-dem'} Layersリファレンス Harmoware-VISのLayersリファレンスについては別記事「Harmoware-VIS Layersリファレンス」をご覧ください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを基本からまとめてみた【18】【React Hook (useContext) ③】

useContextはどういう時に使用するか コンポーネントをまたいで値を渡す時。 propsで渡すと孫コンポーネントに渡す時に、一度、子や孫コンポーネントを経由しなければならなくなりバケツリレーになる。(propドリル) さらにContextのいい点はContextで管理している状態や関数をそれぞれの子コンポーネントで共通して扱えるということ。 Contextの概念図 以下の図は、AuthContextという認証関連の変数や操作ロジックを扱ったContextと、それを実際に使用する2つのコンポーネントとの関係性を図示したもの。 ・ログインしているかどうかを示す変数であるisAuthがAuthContextで一元的に管理されている ・この子コンポーネントはisAuthを使えるようになり、isAuthの値に応じた処理を書くことができるようになる ・コンポーネントAではloginHandlerを呼び出すことで、AuthContext内のisAuthをtrueに変更することができる Contextの使い方 今回はこういうファイル構成にしてComponentAとComponentBでそれぞれ共通の変数を扱うようにする。 実際にはありえないがまぁ今回はよしとする。 ├── App.jsx ├── AuthContext.jsx ├── ComponentA.jsx └── ComponentB.jsx Step 1.Contextの作成 まずは子コンポーネントで使う変数や関数を管理するためのContextを作成する。 ここではAuthContextを作成する。 ポイントは以下の2つ ①createContextでContextオブジェクトを作成し、外部で使えるようにexportする ②子コンポーネントをContextオブジェクトのProviderでラップすることでvalueに記載した変数や関数を子コンポーネントで使用できるようにする AuthContext.jsx import React, { useState, createContext } from 'react'; // Contextオブジェクトを作成し、exportする export const AuthContext = createContext(); const AuthContextProvider = props => { const [isAuth, setIsAuth] = useState(false); const login = () => { setIsAuth(true); } return ( // Providerで子コンポーネントをラップする // valueに子コンポーネントで使いたい変数や関数を与える <AuthContext.Provider value={{login, isAuth}}> {props.children} </AuthContext.Provider> ) } export default AuthContextProvider; createContext は Contextオブジェクトを作り出していて、これによって Provider とConsumer ができる。 Provider は与える側、Consumer は受け取る側。 hooks ではConsumer は useContext で代用される。 Step 2.Providerを適切な位置に配置する App.jsx import { ComponentA } from "./ComponentA"; import { ComponentB } from "./ComponentB"; import AuthContextProvider from "./AuthContext"; function App() { return ( <BrowserRouter> <AuthContextProvider> <Switch> <Route path="/a" component={ComponentA} /> <Route path="/b" component={ComponentB} /> </Switch> </AuthContextProvider> </BrowserRouter> ); } export default App; 子コンポーネントであるComponentAとComponentBではStep1のAuthContextProviderで定義した変数と関数を使える準備ができたということになる。 Step 3.子コンポーネントでContextを使う ComponentA ①useContextをimoprt ②useContextの引数にAuthContextで作成したContextオブジェクト(Step 1で作成)を入れる ③使いたいオブジェクトを取り出す(ここではisAuthとlogin) ComponentA.jsx import React, { useContext } from "react"; import { AuthContext } from "./AuthContext"; import { Link } from "react-router-dom"; const ComponentA = () => { const { isAuth, login } = useContext(AuthContext); return ( <div className="auth"> <h2>ComponentA</h2> {isAuth ? ( <h2>認証されました</h2> ) : ( <> <h2>認証されてません</h2> <button onClick={login}>ログイン</button> </> )} <Link to="/b">ComponentBへ</Link> </div> ); }; export default ComponentA; ここではisAuthの真偽によって表示する内容を変えている。 さらにonClickイベントでログイン関数を呼び出している。 ComponentB ComponentB.jsx import React, { useContext } from "react"; import { AuthContext } from "./AuthContext"; const ComponentB = () => { const { isAuth } = useContext(AuthContext); return ( <div> <h2>ComponentB</h2> {isAuth ? <h2>認証されました</h2> : <h2>認証されてません</h2>} <Link to="/a">ComponentAへ</Link> </div> ); }; export default ComponentB; ComponentBも同様にisAuthを取り出して表示内容を変えている。 Contextによって何ができるようになったのか ①ComponentAでログインボタンを押すことでlogin関数が呼び出される ②AuthContextのisAuthがfalseからtrueに変更される ③ComponentA, ComponentBでそれぞれ変更されたisAuthの値を使えるようになった つまりComponentAによる値の変更をComponentBで検知できるようになった。 参考サイト useContextの使い方
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS Amplify の React チュートリアルをやってみる

はじめに AWS Amplifyで手頃なチュートリアル見つけたのでやってみました React + Amplify + AppSync + DynamoDB でサーバーレスなToDoアプリを作成します https://docs.amplify.aws/start/q/integration/react/ AWS Amplifyとは スケーラブルなモバイルアプリケーションとウェブアプリケーションをすばやく構築するもの BackendとUIをまとめて構築、管理できる 引用元:https://aws.amazon.com/jp/amplify/ AWS Amplify でToDoアプリを作ってみる 0. 前提環境 Node.js v12.x以降 NPM V5.x 以降 git v2.14.1以降 1. amplify/cliをインストールしてセットアップする amplify/cliをインストール $ sudo npm install -g @aws-amplify/cli amplify/cliのセットアップ $ amplify configure Follow these steps to set up access to your AWS account: Sign in to your AWS administrator account: https://console.aws.amazon.com/ AWSコンソールにadministratorでログイン AWSコンソールにログイン後、ターミナルに戻ってEnter regionとuser name を入力すると、AWSコンソールのIAM画面がブラウザに表示される Press Enter to continue Specify the AWS Region ? region: us-east-1 Specify the username of the new IAM user: ? user name: amplify-user Complete the user creation using the AWS console https://console.aws.amazon.com/iam/home?region=us-east-1#/users$new?step=final&accessKey&userNames=amplify-user&permissionType=policies&policies=arn:aws:iam::aws:policy%2FAdministratorAccess ユーザー名、AWS認証情報タイプの選択はデフォルトのまま次のステップへ アクセス許可はデフォルトのまま次のステップへ タグはデフォルトのまま次のステップへ ユーザー情報を確認しユーザーの作成 ユーザー作成完了 「アクセスキーID」と「シークレットアクセスキー」はあとで使うので控えておく ターミナルに戻ってEnter 先ほど控えた「アクセスキーID」と「シークレットアクセスキー」を入力 Press Enter to continue Enter the access key of the newly created user: ? accessKeyId: ******************** ? secretAccessKey: **************************************** This would update/create the AWS Profile in your local machine ? Profile Name: default Successfully set up the new user. 2. Reactアプリを作成する Reactアプリを作成する ※2021/11/29現在 @latest無しだとインストールに失敗した。こちらのバグレポートが関係しているよう $ npx create-react-app@latest react-amplified ~~~省略~~ We suggest that you begin by typing: cd react-amplified npm start Happy hacking! ローカルで画面を確認する $ cd react-amplified $ npm start ~~省略~~ Compiled successfully! You can now view react-amplified in the browser. Local: http://localhost:3000 On Your Network: http://192.168.11.6:3000 Note that the development build is not optimized. To create a production build, use npm run build. Reactアプリ画面が表示される 3. バックエンドを初期化する バックエンドを初期化する デフォルトのままEnter $ amplify init Note: It is recommended to run this command from the root of your app directory ? Enter a name for the project reactamplified The following configuration will be applied: Project information | Name: reactamplified | Environment: dev | Default editor: Visual Studio Code | App type: javascript | Javascript framework: react | Source Directory Path: src | Distribution Directory Path: build | Build Command: npm run-script build | Start Command: npm run-script start ? Initialize the project with the above configuration? Yes Using default provider awscloudformation ? Select the authentication method you want to use: AWS profile For more information on AWS Profiles, see: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html ? Please choose the profile you want to use default Adding backend environment dev to AWS Amplify app: dxleXXXX ~~省略~~ Some next steps: "amplify status" will show you what you've added already and if it's locally configured or deployed "amplify add <category>" will allow you to add features like user login or a backend API "amplify push" will build all your local backend resources and provision it in the cloud "amplify console" to open the Amplify Console and view your project status "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud Pro tip: Try "amplify add api" to create a backend API and then "amplify publish" to deploy everything 4. Amplifyライブラリをインストールする Amplifyライブラリをインストールする $ npm install aws-amplify @aws-amplify/ui-react@1.x.x 5. フロントエンドを設定する src/index.jsのimportの最下部に以下を追加 import Amplify from "aws-amplify"; import awsExports from "./aws-exports"; Amplify.configure(awsExports); 追記後のsrc/index.js src/index.js import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import Amplify from "aws-amplify"; import awsExports from "./aws-exports"; Amplify.configure(awsExports); ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); 6. GraphQL APIとデータベースを作成する GraphQL API と データベースを作成する デフォルトのままEnter、最後にエディタを選択する $ amplify add api ? Select from one of the below mentioned services: GraphQL ? Here is the GraphQL API that we will create. Select a setting to edit or continue Continue ? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description) ⚠️ WARNING: your GraphQL API currently allows public create, read, update, and delete access to all models via an API Key. To configure PRODUCTION-READY authorization rules, review: https://docs.amplify.aws/cli/graphql/authorization-rules GraphQL schema compiled successfully. Edit your schema at /XXXX/react-amplified/amplify/backend/api/reactamplified/schema.graphql or place .graphql files in a directory at /XXXX/react-amplified/amplify/backend/api/reactamplified/schema ✔ Do you want to edit the schema now? (Y/n) · yes ? Choose your default editor: (Use arrow keys) ❯ Visual Studio Code Android Studio Xcode (Mac OS only) Atom Editor Sublime Text IntelliJ IDEA Vim (via Terminal, Mac OS only) (Move up and down to reveal more choices) 選択したエディタにGraphQLスキーマが表示される 7. APIのデプロイ APIをデプロイする $ amplify push ~~省略~~ GraphQL endpoint: https://dgsXXXX.appsync-api.us-east-1.amazonaws.com/graphql GraphQL API KEY: da2-vkaXXXX APIのステータスを確認する $ amplify status Current Environment: dev ┌──────────┬────────────────┬───────────┬───────────────────┐ │ Category │ Resource name │ Operation │ Provider plugin │ ├──────────┼────────────────┼───────────┼───────────────────┤ │ Api │ reactamplified │ No Change │ awscloudformation │ └──────────┴────────────────┴───────────┴───────────────────┘ GraphQL endpoint: https://dgsXXXX.appsync-api.us-east-1.amazonaws.com/graphql GraphQL API KEY: da2-vkaXXXX 8. フロントエンドからAPIに接続する src/App.jsを以下に置き換える src/App.js /* src/App.js */ import React, { useEffect, useState } from 'react' import Amplify, { API, graphqlOperation } from 'aws-amplify' import { createTodo } from './graphql/mutations' import { listTodos } from './graphql/queries' import awsExports from "./aws-exports"; Amplify.configure(awsExports); const initialState = { name: '', description: '' } const App = () => { const [formState, setFormState] = useState(initialState) const [todos, setTodos] = useState([]) useEffect(() => { fetchTodos() }, []) function setInput(key, value) { setFormState({ ...formState, [key]: value }) } async function fetchTodos() { try { const todoData = await API.graphql(graphqlOperation(listTodos)) const todos = todoData.data.listTodos.items setTodos(todos) } catch (err) { console.log('error fetching todos') } } async function addTodo() { try { if (!formState.name || !formState.description) return const todo = { ...formState } setTodos([...todos, todo]) setFormState(initialState) await API.graphql(graphqlOperation(createTodo, {input: todo})) } catch (err) { console.log('error creating todo:', err) } } return ( <div style={styles.container}> <h2>Amplify Todos</h2> <input onChange={event => setInput('name', event.target.value)} style={styles.input} value={formState.name} placeholder="Name" /> <input onChange={event => setInput('description', event.target.value)} style={styles.input} value={formState.description} placeholder="Description" /> <button style={styles.button} onClick={addTodo}>Create Todo</button> { todos.map((todo, index) => ( <div key={todo.id ? todo.id : index} style={styles.todo}> <p style={styles.todoName}>{todo.name}</p> <p style={styles.todoDescription}>{todo.description}</p> </div> )) } </div> ) } const styles = { container: { width: 400, margin: '0 auto', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: 20 }, todo: { marginBottom: 15 }, input: { border: 'none', backgroundColor: '#ddd', marginBottom: 10, padding: 8, fontSize: 18 }, todoName: { fontSize: 20, fontWeight: 'bold' }, todoDescription: { marginBottom: 0 }, button: { backgroundColor: 'black', color: 'white', outline: 'none', fontSize: 18, padding: '12px 0px' } } export default App ローカルで確認する $ npm start ToDo画面が表示される 9. 認証を追加する 認証を追加する デフォルトでEnter $ amplify add auth ~~省略~~ Some next steps: "amplify push" will build all your local backend resources and provision it in the cloud "amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud デプロイ $ amplify push ~~省略~~ All resources are updated in the cloud amplifyのステータスでAuthが追加されていることを確認する $ amplify status ~~省略~~ Current Environment: dev ┌──────────┬────────────────────────┬───────────┬───────────────────┐ │ Category │ Resource name │ Operation │ Provider plugin │ ├──────────┼────────────────────────┼───────────┼───────────────────┤ │ Api │ reactamplified │ No Change │ awscloudformation │ ├──────────┼────────────────────────┼───────────┼───────────────────┤ │ Auth │ reactamplified91c68478 │ No Change │ awscloudformation │ └──────────┴────────────────────────┴───────────┴───────────────────┘ GraphQL endpoint: https://dgsXXXX.appsync-api.us-east-1.amazonaws.com/graphql GraphQL API KEY: da2-vkaXXXX 10. ログイン画面作成 src/App.jsを修正 10-1. importに以下を追加 import { withAuthenticator } from '@aws-amplify/ui-react' 10-2. デフォルトのエクスポートをwithAuthenticator、メインコンポーネントをラップするように変更します。 export default withAuthenticator(App) 10-3. 修正後のsrc/App.js src/App.js /* src/App.js */ import React, { useEffect, useState } from 'react' import Amplify, { API, graphqlOperation } from 'aws-amplify' import { createTodo } from './graphql/mutations' import { listTodos } from './graphql/queries' import { withAuthenticator } from '@aws-amplify/ui-react' import awsExports from "./aws-exports"; Amplify.configure(awsExports); const initialState = { name: '', description: '' } const App = () => { const [formState, setFormState] = useState(initialState) const [todos, setTodos] = useState([]) useEffect(() => { fetchTodos() }, []) function setInput(key, value) { setFormState({ ...formState, [key]: value }) } async function fetchTodos() { try { const todoData = await API.graphql(graphqlOperation(listTodos)) const todos = todoData.data.listTodos.items setTodos(todos) } catch (err) { console.log('error fetching todos') } } async function addTodo() { try { if (!formState.name || !formState.description) return const todo = { ...formState } setTodos([...todos, todo]) setFormState(initialState) await API.graphql(graphqlOperation(createTodo, {input: todo})) } catch (err) { console.log('error creating todo:', err) } } return ( <div style={styles.container}> <h2>Amplify Todos</h2> <input onChange={event => setInput('name', event.target.value)} style={styles.input} value={formState.name} placeholder="Name" /> <input onChange={event => setInput('description', event.target.value)} style={styles.input} value={formState.description} placeholder="Description" /> <button style={styles.button} onClick={addTodo}>Create Todo</button> { todos.map((todo, index) => ( <div key={todo.id ? todo.id : index} style={styles.todo}> <p style={styles.todoName}>{todo.name}</p> <p style={styles.todoDescription}>{todo.description}</p> </div> )) } </div> ) } const styles = { container: { width: 400, margin: '0 auto', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: 20 }, todo: { marginBottom: 15 }, input: { border: 'none', backgroundColor: '#ddd', marginBottom: 10, padding: 8, fontSize: 18 }, todoName: { fontSize: 20, fontWeight: 'bold' }, todoDescription: { marginBottom: 0 }, button: { backgroundColor: 'black', color: 'white', outline: 'none', fontSize: 18, padding: '12px 0px' } } export default withAuthenticator(App) ローカルで確認する $ npm start ログイン画面が表示される  11. アプリをデプロイする アプリをデプロイする デフォルトのままEnter $ amplify add hosting ? Select the plugin module to execute Hosting with Amplify Console (Managed hosting with custom domains, Continuous deployment) ? Choose a type Manual deployment You can now publish your app using the following command: Command: amplify publish 12. アプリを公開する アプリを公開する $ amplify publish ~~省略~~ Zipping artifacts completed. Deployment complete! https://dev.dxlXXXX.amplifyapp.com 「amplify publish」後に表示されたURLにアクセスし、ログイン画面を表示する ※アカウントを作成していない場合は、Create Accountからアカウントを作成する ログイン後、ToDo画面が表示される Amplify コンソール Amplifyで作ったアプリを管理できるコンソール 今回はチュートリアルを試しただけなのでここには使わない リソースのクリーンアップ $ amplify delete $ amplify status おわりに Cloudformationでインフラのコードを作成するするのに比べ、準備するコード量が少なく使いやすいと感じた 認証周りなど細かい設定がどこまでできるかはまだ分からない 次はAWS Amplify を使って継続的デプロイメント環境を作ろうと思う https://docs.aws.amazon.com/amplify/latest/userguide/multi-environments.html#standard 参考 https://docs.amplify.aws/start/getting-started/installation/q/integration/react/#option-1-watch-the-video-guide https://github.com/facebook/create-react-app/issues/10601 https://dev.classmethod.jp/articles/react-amplify-appsync-dynamodb-tutorial/ https://docs.aws.amazon.com/amplify/latest/userguide/multi-environments.html#standard
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React Context APIの概念と使い方

業務のコードでContextが使われているので勉強しようと思いましたが、公式ドキュメントはクラスコンポーネントを知っていないとなかなか難しいものでした。 色々試して、業務でもさわって、大分Contextについては理解できたかなと思うのでまとめておきます。 概要 ReactのContextの概念や考え方、使い方を説明します。 本記事の説明で書いたコードを載せておきますので、実際の挙動も試していただければと思います。 まずは全体像を掴み、その後具体的な実装方法を説明していきます。 React Context APIとは Contextとは、Propsのバケツリレーをすることなく「state」と「stateを変更するメソッド」をアプリケーション全体で使えるようにするための、ReactのAPIです。 コンポーネントを跨いで値を渡す時や、複数のコンポーネントで同じ値を参照したい時に有用です。 Contextを使うために最低限知っておく必要があるのは、次の3つの要素です。 Contextオブジェクト Context Provider Context Consumer Contextオブジェクト コンポーネントのツリー上、直接の親子関係にない離れたコンポーネント間で、同じ値を共有するためのものです。Contextオブジェクトを作り、そのContextからデータを取得して使ったり、データを変更するというように使います。 以下のようにcreateContext()を使って生成します。 export const CountContext = createContext(); // Contextオブジェクト生成 console.log(CountContext); 試しにconsole.logで出力してみると、こんな感じ。 Consumer と Provider を持っていることがわかります。 Context Provider Contextオブジェクトが所持する要素で、対象のContextの適用範囲(Contextが持つ値を使うことができるコンポーネントの範囲)を決める役割を持ちます。 以下のようにProviderで子要素を囲むことで、コンポーネントツリー上でContext Providerの内側(下層)にある全てのコンポーネントから、valueに渡した値や関数を利できます。 return ( <CountContext.Provider value={{ count, setCount }}> {children} </CountContext.Provider> ); このvalueに渡した値が更新されると、配下にあるコンポーネントは全て再レンダリングされる点には注意が必要です。(親コンポーネントが再レンダリングされると、子コンポーネントも再レンダリングされるので) Context Consumer Context Providerと同じくContextオブジェクトに備わった要素で、Contextの値を利用したいところで使います。 コンポーネントツリー上で自身より外側(上層)にあるContext ProviderのContextに紐づけられた値にアクセスすることができます。 useContextとは Consumerを簡単シンプルに使えるhooksです。 以下のように書いて、Providerのvalueに渡した値や関数の中から使いたいものを受け取れます。 const { Contextから受け取る値や関数 } = useContext(Contextオブジェクト名); ここから、「Contextオブジェクト・Providerコンポーネント・useContext」を使ってContext APIを利用する具体的な方法を説明します。 Context APIの使い方 全体の流れ Contextを使う流れは、以下の3stepです。 ① createContext()を使ってContextオブジェクトを生成する。 ② Providerコンポーネントをコンポーネントツリー上に設置する。 ③ 子孫コンポーネントでContextオブジェクトを使う。 これらを具体的にみていきます。 step① Contextオブジェクトを生成する まずは子コンポーネントで使う変数や関数を管理するためのContextを作成します。 createContext()を使えばどこでもContextオブジェクトを作ることはできるわけですが、contextsディレクトリを切ってその中にContextのファイルを作ることが多いかと思います。 今回はcontexts/CountContext.jsxでContextを管理していきます。 // src/contexts/CountContexts.jsx import { useState, createContext } from "react"; // Contextオブジェクトを生成する export const CountContext = createContext(); // 生成したContextオブジェクトのProviderを定義する export const CountProvider = ({children}) => { const [count, setCount] = useState(0) return ( <CountContext.Provider value={{ count, setCount }}> {children} </CountContext.Provider> ); } ポイントは以下の2つです。 ① createContext()でContextオブジェクトを作成し、外部で使えるようにexportする。 ② 子孫コンポーネントをContextオブジェクトのProviderで囲むことで、valueに渡した変数や関数を子孫コンポーネントで使用できるようにする。 このときのProviderは、 <Contextオブジェクト名.Provider value={ 子孫コンポーネントに渡す変数・関数 }> というように書きます。 ※createContext()の引数に初期値を与えることも可能ですが、この初期値はuseContextがProviderを見つけられなかったときのみ参照するとのこと。 Contextを使う以上そのようなことは起こらないはずなので、初期値の設定は通常必要ないのかなと思います。 const MyContext = React.createContext(defaultValue); コンテクストオブジェクトを作成します。React がこのコンテクストオブジェクトが登録されているコンポーネントをレンダーする場合、ツリー内の最も近い上位の一致する Provider から現在のコンテクストの値を読み取ります。 defaultValue 引数は、コンポーネントがツリー内の上位に対応するプロバイダを持っていない場合のみ使用されます。このようなデフォルト値は、ラップしない単独でのコンポーネントのテストにて役に立ちます。補足: undefined をプロバイダの値として渡しても、コンシューマコンポーネントが defaultValue を使用することはありません。 引用:コンテクスト - React step② Providerコンポーネントをコンポーネントツリー上に設置する ①でexportしたProviderコンポーネントを、コンポーネントツリー上に設置します。 Providerコンポーネントを設置した内側にある全てのコンポーネント(設置したコンポーネントの全ての子孫コンポーネント)から、Contextを参照することができます。 今回は以下のようなツリー構造にしたいと思いますので、まずはコンポーネントツリーの一番上層に位置するApp.jsにProviderを設置します。 App (Providerを設置) | ComponentA (Contextを使う子コンポーネント) | ComponentB (Contextを使う子コンポーネント) // src/App.js import { ComponentA } from "./components/ComponentA"; import { CountProvider } from "./contexts/CountContexts"; export default function App() { return ( <div> <CountProvider> <ComponentA /> // ComponentAの配下にあるコンポーネントでContextを利用可能 </CountProvider> </div> ); } これによりの内側にある全てのコンポーネントで、step①のCountProviderで定義した変数と関数を使えるよう準備ができたということになります。 step③ 子孫コンポーネントでContextオブジェクトを使う 親コンポーネントにProviderを設置したので、子コンポーネント「ComponentA」と、孫コンポーネント「ComponentB」を作成します。 ComponentA・ComponentBの両方から、CountContextの値を参照できるように書いていきます。 まずはComponentAを作ります。 // src/components/ComponentA.jsx import { useContext } from 'react' import { ComponentB } from './ComponentB' import { CountContext } from "../contexts/CountContexts"; export const ComponentA = () => { // const { Contextから受け取る値や関数 } = useContext(Contextオブジェクト名); const { count, setCount } = useContext(CountContext); return ( <div> <p> ComponentA:{count} <button onClick={() => setCount(count + 1)}> + </button> </p> <ComponentB /> </div> ); } ここでのポイントは3つです。 ① useContextをimoprtする。 ② useContextの引数にCountContextで作成したContextオブジェクト(Step 1で作成)を入れる。 ③ 使いたいオブジェクトを取り出す。(ここではcountとsetCount) あとは、取り出したオブジェクトをコンポーネント内で好きに使うことができます。 ComponentBも作ります。 // src/components/ComponentB.jsx import { useContext } from "react"; import { CountContext } from "../contexts/CountContexts"; export const ComponentB = () => { const { count, setCount } = useContext(CountContext); return ( <div> <p> ComponentB:{count} <button onClick={() => setCount(count + 1)}> + </button> </p> </div> ); }; するとブラウザにはこのように表示されています。 Contextによって何ができるようになったのか? ① ComponentA・ComponentBの両方で、ContextのcountとsetCount()を使えるようになった。 ② ComponentA・ComponentB双方での値の変更を、互いに検知できるようになった。 ComponentA・ComponentBのどちらの+ボタンを押しても、表示しているcountは同じように増えて同じ値を表示します。 補足①:ヘルパー関数を自作する ここまでの例では、子孫コンポーネントそれぞれで毎回 const { count, setCount } = useContext(CountContext);と記述していますが、これは子孫コンポーネントで使い回す処理なので、予め共通化してヘルパー関数を自作しておくという方法もよく見られます。 その場合はまずContextファイルでヘルパー関数を定義し、exportしておきます。 // src/contexts/CountContexts.jsx import React, { useState, createContext, useContext } from "react"; export const CountContext = createContext(); // ヘルパー関数を定義してexport export const useCountContext = () => useContext(CountContext); export const CountProvider = ({ children }) => { const [count, setCount] = useState(0); return ( <CountContext.Provider value={{ count, setCount }} > {children} </CountContext.Provider> ); }; 子孫コンポーネントは以下のように変わります。 // src/components/ComponentA.jsx //不要になる import React, { useContext } from "react"; //不要になる import { CountContext } from "../contexts/CountContexts"; import { ComponentB } from "./ComponentB"; import { useCountContext } from "../contexts/CountContexts"; // 追記 export const ComponentA = () => { //不要になる const { count, setCount } = useContext(CountContext); const { count, setCount } = useCountContext(); // 追記 return ( <div> <p> ComponentA:{count} <button onClick={() => setCount(count + 1)}> + </button> </p> <ComponentB /> </div> ); }; 補足②:アプリケーション内で複数のContextを使う アプリケーション内で複数のContextを使うこともできます。 その場合はProviderコンポーネントをネストさせます。 以下のようにProviderをネストさせて設置すると、MainContainerコンポーネント配下のコンポーネントではCountContextとUserContextの両方を参照することができます。 import { CountProvider } from "./contexts/CountContext"; import { UserProvider } from "./contexts/UserContext"; import { MainContainer } from "./components/MainContainer"; export default function App() { return ( <div className="App"> <div> <CountProvider> <UserProvider> <MainContainer /> </UserProvider> </CountProvider> </div> </div> ); } このようにネストさせた場合、MainContainer配下の全てのコンポーネントは、CountContextかUserContextどちらかの値が更新される度に再レンダーされることになります。(Contextの値を参照していない子孫コンポーネントであっても再レンダーされる。) memo化して不要なレンダリングは防ぐ注意が必要です。    Contextを複数使う場合のコード例も載せておきます。 Contextの値が更新されることや、console.logを仕込むなどしてレンダリングの挙動も試して見ていただければと思います。 さいごに 一点だけ不便だなと思ったのが、ページ遷移するとContextのstateの値はクリアされてしまうこと。 業務でログイン中のユーザー情報をcontextのstateに保管していたのですが、そこからReact routerなどを使って別のページに遷移するとstateはnullになってしまいます。 Reduxを使えばページ遷移先に値を送れると聞いたので、次はReduxを勉強してみようと思います。 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React customHooks を利用してロジックとUIを切り離すまでをやってみた(5/5)〜不要なレンダリングが走らないための工夫(検証 & 改善編)〜

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 前回は、不要なレンダリングが走らないための工夫を紹介するための準備をしました。 (ReactのmemoやuseCallbackを使った最適化を説明したいがためのコンポーネントの配備) 今回は、その実践編をやっていきます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成 不要なレンダリングが走らないための工夫(準備編) 不要なレンダリングが走らないための工夫(検証 & 改善編) ? 今回はコレ 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 検証用のための記述を追加 長いので折りたたみます(components/CreateUser.tsx) components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' import { Child } from './Child' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return ( <> {console.log('CreateUser rendered')} <div className="mb-3 flex flex-col justify-center items-center"> <label>Text</label> <input className="px-3 py-2 border border-gray-300" type="text" value={text} onChange={handleTextChange} /> </div> <form className="flex flex-col justify-center items-center " onSubmit={handleSubmit} > <label>Username</label> <input className="mb-3 px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={username} onChange={usernameChange} /> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" type="submit" > Submit </button> </form> <Child printMsg={printMsg} handleSubmit={handleSubmit} /> </> ) } 長いので折りたたみます(components/Child.tsx) components/Child.tsx import { ChangeEvent, FormEvent, VFC } from 'react' interface Props { printMsg: () => void } export const Child: VFC<Props> = ({ printMsg }) => { return ( <> {console.log('Child rendered')} <p>Child Component</p> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" onClick={printMsg} > click </button> </> ) } 長いので折りたたみます(hooks/useCreateForm.ts) hooks/useCreateForm.ts import { useState, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' import { CREATE_USER } from '../queries/queries' import { CreateUserMutation } from '../types/generated/graphql' export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) const handleTextChange = (e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) } const usernameChange = (e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) } const printMsg = () => { console.log('Hello') } const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') } return { text, handleSubmit, username, usernameChange, printMsg, handleTextChange, } } 実際に検証 以下のタイミングで、console.logが発火していることが確認できます。 components/CreateUser.tsx レンダリング時 console.log('CreateUser rendered')が発火 components/Child.tsx レンダリング時 console.log('Child rendered')が発火 hooks/useCreateForm.ts 「click」ボタンのクリック時 console.log('Hello')が発火 課題 パフォーマンスの問題があります。 例えば「Text」の直下にあるinputに何か入力しただけでも console上にChild renderedが表示されレンダリングされていると分かります。 なぜかというと、親のコンポーネント(components/CreateUser.tsx)のstateが変更されれば、その中になるコンポーネントは全て再度レンダリングされる仕様だからです。 課題の解決策の概要 components/Child.tsxのReactmemo化 hooks/useCreateForm.ts の全ての関数をuseCallback 化 components/Child.tsx components/Child.tsx import { ChangeEvent, FormEvent, memo, VFC } from 'react' interface Props { printMsg: () => void } export const Child: VFC<Props> = memo(({ printMsg }) => { return ( <> {console.log('Child rendered')} <p>Child Component</p> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" onClick={printMsg} > click </button> </> ) }) hooks/useCreateForm.ts hooks/useCreateForm.ts import { useState, useCallback, ChangeEvent, FormEvent } from 'react' import { useMutation } from '@apollo/client' import { CREATE_USER } from '../queries/queries' import { CreateUserMutation } from '../types/generated/graphql' export const useCreateForm = () => { const [text, setText] = useState('') const [username, setUsername] = useState('') const [insert_users_one] = useMutation<CreateUserMutation>(CREATE_USER, { update(cache, { data: { insert_users_one } }) { const cacheId = cache.identify(insert_users_one) cache.modify({ fields: { users(existingUsers, { toReference }) { return [toReference(cacheId), ...existingUsers] }, }, }) }, }) const handleTextChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setText(e.target.value) }, []) const usernameChange = useCallback((e: ChangeEvent<HTMLInputElement>) => { setUsername(e.target.value) }, []) const printMsg = useCallback(() => { console.log('Hello') }, []) const handleSubmit = useCallback( async (e: FormEvent<HTMLFormElement>) => { e.preventDefault() try { await insert_users_one({ variables: { name: username, }, }) } catch (err) { alert(err.message) } setUsername('') }, [username] ) return { text, handleSubmit, username, usernameChange, printMsg, handleTextChange, } } useCallbackのdepsの指定も大事ですが、長くなるので、下記の記事読んで、なんで今回のdepsの指定はこうなんだ?があれば疑問を解消しておいてください。 また、親(components/CreateUser.tsx)から子(components/Child.tsx)へpropsとして関数handleSubmitを渡したら ユーザーがブラウザ上でusernameを変えるたびに関数handleSubmitは再度レンダリングされます <Child printMsg={printMsg} handleSubmit={handleSubmit} /> それはつまり、components/Child.tsxが、usernameを変えるたびに再度レンダリングされるということになります。 ここらへんは、ちゃんと理解して最適化をしていきましょう! 検証 親要素のinputの結果、親にあるstateが変化しても、Child renderedの発火、つまり子コンポーネントのレンダリングはされなくなりました。 まとめ ReactのmemoやuseCallbackを使った最適化ができました。 全体を通すと、React customHooks を利用してロジックとUIを切り離すまで一連をこれで学べ方かと思います。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactHookForm(V7)でフォームの簡単バリデーション!(実装例&Gif付き)

React と Typescript を使用します。 ReactHookForm とは ReactHookForm とは「高性能で柔軟かつ拡張可能な使いやすいフォームバリデーションライブラリ」を掲げた入力フォームの管理に特化した React 向けのライブラリです。状態管理を DOM で行う非制御コンポーネントでフォームの値を扱うことで他よりも高速なフォームライブラリを可能にしています。 ほかのフォームライブラリとの比較 ReactHookForm のインストール npm install react-hook-form [基本編]バリデーション 下記の条件で 3 つのフォームにバリデーションをかけた例です。 必須 5 文字以上の文字列 10 以下の数字 type Input = { required: string; maxLength: string; maxNumber: number; }; const App = (): JSX.Element => { const { register, handleSubmit, formState: { errors }, } = useForm<Input>({ criteriaMode: "all", shouldFocusError: false, }); const onSubmit: SubmitHandler<Input> = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> trueの場合必須  <br /> <input {...register("required", { required: true })} /> {errors.required && "文字が入力されていません"} <br /> 5文字以上の文字列 <br /> <input {...register("maxLength", { maxLength: 5 })} /> {errors.maxLength && "5文字以上が入力されています"} <br /> 10以内ならOK <br /> <input type="number" {...register("maxNumber", { max: 10 })} /> {errors.maxNumber && "10以上の数字が入力されています"} <br /> <input type="submit" /> </form> ); }; 実際はこのような動きになります。 上から順番に見ていきましょう。 まず最初に useForm で初期化します。 const { register, handleSubmit, formState: { errors }, } = useForm<Input>({ criteriaMode: "all", shouldFocusError: false, }); useForm から各メソッドを受け取っています。 register : 入力または選択された要素を登録し検証します handleSubmit : 検証が成功するとフォーム内のデータを受け取ります formState: { errors } : バリデーションエラーを受け取ります useForm の引数にオプションを設定できます。 criteriaMode firstError(default) : 上から順番に検証され最初のエラーが返されます all : 全てのエラーが返されます shouldFocusError true : 最初のエラーがフォーカスされます false : フォーカスされなくなります 検証用に handleSubmit で返された値をコンソールに表示しています。 const onSubmit: SubmitHandler<Input> = (data) => console.log(data); // handleSubmitの引数に使われています。 <form onSubmit={handleSubmit(onSubmit)}> input に入力した値が register に登録された検証内容で検証されます。 <input {...register("required", { required: true })} /> <input {...register("maxLength", { maxLength: 5 })} /> <input type="number" {...register("maxNumber", { max: 10 })} /> register でエラーが発生した場合、その register に設定した key で error から取り出すことができます。{error.key} エラーが存在しているかどうかでエラーメッセージを表示しています。 {errors.required && "文字が入力されていません"} {errors.maxLength && "5文字以上が入力されています"} {errors.maxNumber && "10より大きい値の数字が入力されています"} [応用編]バリデーション type Input = { conditions: string; }; const App = (): JSX.Element => { const { register, handleSubmit, formState: { errors }, } = useForm<Input>({ mode: "onChange", criteriaMode: "all", shouldFocusError: false, }); const onSubmit: SubmitHandler<Input> = (data) => console.log(data); return ( <form onSubmit={handleSubmit(onSubmit)}> 必須&5文字以下&A-Zの文字列 <br /> <input {...register("conditions", { required: true, maxLength: 5, pattern: /[A-Z]/, })} /> <br /> {errors.conditions?.types?.required && "文字が入力されていません"} {errors.conditions?.types?.maxLength && "5文字以上が入力されています"} {errors.conditions?.types?.pattern && "A-Z以外の文字が含まれています"} <br /> <input type="submit" /> </form> ); }; このような動きになります。 基本編との違いは submit を押さなくても onChange でバリデーションが行われる 一つのフォームで条件を3つに増やしそれぞれ複数のエラーを表示させる というところです。 実際に変更されたコードを見ていきましょう。 const { register, handleSubmit, formState: { errors }, } = useForm<Input>({ mode: "onChange", criteriaMode: "all", shouldFocusError: false, }); mode: "onChange"を追加しました。 このようにすることで submit を押さなくてもエラーが表示されるのでリアクティブな感じに実装できます。 <input {...register("conditions", { required: true, maxLength: 5, pattern: /[A-Z]/, })} /> <br /> {errors.conditions?.types?.required && "文字が入力されていません"} {errors.conditions?.types?.maxLength && "5文字以上が入力されています"} {errors.conditions?.types?.pattern && "A-Z以外の文字が含まれています"} 上記の実装は一つのフォームで複数の条件とその条件に対応したエラーを表示させるというものです。 types でどの条件のエラーかを判別することができます。 さいごに 2パターンのバリデーションを実装してみました。 他にもさまざまなバリデーションに対応できるように、多くのメソッドが実装されていて拡張性の高いフォームライブラリとなっています。Formik や ReduxForm に比べると新しいライブラリで日本語の記事が少ないこともありますが、勢いのあるライブラリだと思います。 ぜひ使ってみてはいかがでしょうか。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[WIP] React customHooks を利用してロジックとUIを切り離すまでをやってみた(4/n)

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 前回でUI部分となるコンポーネント作成はできたので 今回は、ロジックとUIは切り離せたましたが、不要なレンダリングが走らないための工夫を説明するための準備をしていきます。 具体的には、ReactのmemoやuseCallbackを使った最適化をやっていきます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成 不要なレンダリングが走らないための工夫(準備編) ? 今回はコレ 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 Childコンポーネントの作成 useCallbackを使った最適化を説明したいだけの目的ですが、Child.tsxファイルを作成します。 components/ └── Child.tsx hooks/ pages/ 中身 components/Child.tsx import { ChangeEvent, FormEvent, VFC } from 'react' interface Props { printMsg: () => void // 引数がなくて返り値が何もないという意味 } export const Child: VFC<Props> = ({ printMsg }) => { return ( <> <p>Child Component</p> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" onClick={printMsg} > click </button> </> ) } 次に、components/CreateUser.tsxに先ほど作成したChild.tsxをimportします。 長いので折りたたみます(components/CreateUser.tsx) components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' import { Child } from './Child' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return ( <> <div className="mb-3 flex flex-col justify-center items-center"> <label>Text</label> <input className="px-3 py-2 border border-gray-300" type="text" value={text} onChange={handleTextChange} /> </div> <form className="flex flex-col justify-center items-center " onSubmit={handleSubmit} > <label>Username</label> <input className="mb-3 px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={username} onChange={usernameChange} /> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" type="submit" > Submit </button> </form> <Child printMsg={printMsg} /> </> ) } components/CreateUser.tsxで、components/Child.tsxをimportして <Child />に、関数printMsgとhandleSubmitを渡しています。 CreateUserをimportしたページを作成 まずは、以下のファイルを作成します。 components/ hooks/ pages/ └── hooks-memo.tsx pages/hooks-memo.tsx import { VFC } from 'react' import { Layout } from '../components/Layout' import { CreateUser } from '../components/CreateUser' const HooksMemo: VFC = () => { return ( <Layout title="Hooks memo"> <CreateUser /> </Layout> ) } export default HooksMemo 次回、検証するので、そのための準備をして終わります。 $ yarn build $ yarn start まとめ 今回は、不要なレンダリングが走らないための工夫(ReactのmemoやuseCallbackを使った最適化を説明したいがためのコンポーネントの配備)をしました。 次回 実際に検証をしつつ、ReactのmemoやuseCallbackを使った最適化をしていきます。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React customHooks を利用してロジックとUIを切り離すまでをやってみた(4/5)〜不要なレンダリングが走らないための工夫(準備編)〜

はじめに React customHooks を利用してロジックとUIを切り離すまでをやってみたシリーズ 前回でUI部分となるコンポーネント作成はできたので 今回は、ロジックとUIは切り離せたましたが、不要なレンダリングが走らないための工夫を説明するための準備をしていきます。 具体的には、ReactのmemoやuseCallbackを使った最適化をやっていきます。 これまで Hasuraにユーザーのデータと仮定してデータを作成し データを外部APIとして取得できるようにして、Next.js(apollo)を使って、クエリやミューテーションできるようにしました Hasuraから取得したユーザーデータの一覧ページと詳細ページを作りながら、SGとISRを学んできました このシリーズの目次 このシリーズの方針 customHooks を利用してロジックとUIを切り離すまで ロジックの作成 コンポーネント(UI)作成 不要なレンダリングが走らないための工夫(準備編)? 今回はコレ 不要なレンダリングが走らないための工夫(検証 & 改善編) 最終的なゴール 以下のような構成のアプリを作ることです。 目的(最終的なゴールを達成する) 仕事で使っている技術のキャッチアップと復習 使う可能性がある技術の理解度向上 Childコンポーネントの作成 useCallbackを使った最適化を説明したいだけの目的ですが、Child.tsxファイルを作成します。 components/ └── Child.tsx hooks/ pages/ 中身 components/Child.tsx import { ChangeEvent, FormEvent, VFC } from 'react' interface Props { printMsg: () => void // 引数がなくて返り値が何もないという意味 } export const Child: VFC<Props> = ({ printMsg }) => { return ( <> <p>Child Component</p> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" onClick={printMsg} > click </button> </> ) } 次に、components/CreateUser.tsxに先ほど作成したChild.tsxをimportします。 長いので折りたたみます(components/CreateUser.tsx) components/CreateUser.tsx import { VFC } from 'react' import { useCreateForm } from '../hooks/useCreateForm' import { Child } from './Child' export const CreateUser: VFC = () => { const { handleSubmit, username, usernameChange, printMsg, text, handleTextChange, } = useCreateForm() return ( <> <div className="mb-3 flex flex-col justify-center items-center"> <label>Text</label> <input className="px-3 py-2 border border-gray-300" type="text" value={text} onChange={handleTextChange} /> </div> <form className="flex flex-col justify-center items-center " onSubmit={handleSubmit} > <label>Username</label> <input className="mb-3 px-3 py-2 border border-gray-300" placeholder="New user ?" type="text" value={username} onChange={usernameChange} /> <button className="my-3 py-1 px-3 text-white bg-indigo-600 hover:bg-indigo-700 rounded-2xl focus:outline-none" type="submit" > Submit </button> </form> <Child printMsg={printMsg} /> </> ) } components/CreateUser.tsxで、components/Child.tsxをimportして <Child />に、関数printMsgとhandleSubmitを渡しています。 CreateUserをimportしたページを作成 まずは、以下のファイルを作成します。 components/ hooks/ pages/ └── hooks-memo.tsx pages/hooks-memo.tsx import { VFC } from 'react' import { Layout } from '../components/Layout' import { CreateUser } from '../components/CreateUser' const HooksMemo: VFC = () => { return ( <Layout title="Hooks memo"> <CreateUser /> </Layout> ) } export default HooksMemo 次回、検証するので、そのための準備をして終わります。 $ yarn build $ yarn start まとめ 今回は、不要なレンダリングが走らないための工夫(ReactのmemoやuseCallbackを使った最適化を説明したいがためのコンポーネントの配備)をしました。 次回 実際に検証をしつつ、ReactのmemoやuseCallbackを使った最適化をしていきます。 アウトプット100本ノック実施中
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む