20220131のReactに関する記事は8件です。

Next.js(React)をざっくり始める

こんにちは!いまだにreturnの綴りが怪しいフロントエンドエンジニアです。 今回は自分の備忘録も兼ねて、Next.jsの概念についてまとめたいと思います。(Reactと重複する内容有/今回は概念だけですので、詳しい書き方については書いていません。) その前にまず私の状況 今までVue.jsしかやったことがなく、React系を始めて1か月経ったところです。 そのためかなり知識が未熟なところがあります。他のブログ/本/動画などを参考にし、補助的にこちらを見て頂ければと思います。 そのような状況で今回記事を書いた理由は、  ・「とりあえず書き始めたい!」という人向けの記事が少ないように感じたため  ・TypeScriptのNext.jsの資料が少ない というところです。 サイトパフォーマンス等一旦気にせず、ざっくり始めたい人向けになるべく分かりやすく説明したいと思います。 目次 1.基本的な概念 2.ページについて 3.拡張子について 4.use/memoで囲む 5.型について 6.APIについて 7.まとめ 8.おまけ 1. 基本的な概念 ■ファイルの中身 大体1ファイルの中身はこのような構成です。拡張子については後ほど説明します。 ■コンポーネントについて Nextは「とにかく部品を作って、それをページに埋めていく!」という考えのもと作っていきます。(ここで言う「ページ」は実際にサイトとして見せる用のファイルの事) 例えば、ボタンを3つ使いたいページを作る際に。。。 そのため、部品(コンポーネントファイル等)を1つ作成して、それを使いたい度に呼び出してページに埋めていきます。 ■コンポーネント以外の部品達 「部品」はコンポーネントの他にも色々あります。  ・コンポーネント:HTMLにて埋め込む部品(ボタン/フォーム/ヘッダー等々…)  ・カスタムhook:色々なところで使いたいメソッド  ・プロバイダー:色々なところで使いたい値  ・タイプ:型をまとめておく  等々… それぞれのファイルの置き場所はこのような感じです。 hooksフォルダを作成して、その中にカスタムhookファイルを作成するイメージ ※ちなみにカスタムhookのカスタムは、Nextに元々用意されているhookがあるので、自分で作成したhookの事をカスタムhookと呼んでいます。 ■コンポーネントの作り方 細かいところは後ほど説明していきます。一旦コンポーネントの書き方のみ紹介です。 コンポーネント //Buttonというコンポーネント //export const コンポーネント名:型(()=>{ページの内容}) export const Button: FC = memo(() => { //これを埋め込んだ際に返すHTML(このHTMLが表示される) return ( <><button>ボタン</button></> ); }); ページ <!--埋め込みたいところにこれを書けば埋め込める--> <Button/> という事でこのように色々な部品を埋め込んで1ページを作成していくイメージになります。 ■コンポーネントに値を渡す:Props/Children しかしここでこのような事に気が付きませんでしょうか。。 このような問題を解決するために、Propsという概念が存在します。 Propsは一旦コンポーネント内の値を「仮」にしておいて、ページで使う際に入れる値を決定出来るシステムです。 コンポーネント //最初に受け取るPropsの型を宣言 export type Props = { label: string; }; //propsを使用する際には()の中にpropsを入れる export const Button: FC<Props> = memo((props) => { //Propsで受け取る値を宣言 const { label } = props; //これを埋め込んだ際に返すHTML(このHTMLが表示される) //ここのlabel部分に渡された値が入る return ( <><button>{label}</button></> ); }); ページ <!--ボタンコンポーネントのラベルに「検索」を渡す--> <Button label={検索} /> 同じようにChildrenという概念も存在します。 これは挟んで値を渡してあげるやり方です。渡す値が大きい範囲の際はこちら利用すると良いかも。 コンポーネント //ここの書き方は固定 type Props = { children: ReactNode; }; //propsを使用する際には()の中にpropsを入れる export const Button: FC<Props> = memo((props) => { //Propsで受け取る値を宣言 const { children } = props; return ( <><button>{children}</button></> ); }); ページ <!--ボタンコンポーネントのchildrenに「検索」を渡す--> <!--挟んだ値が渡される--> <Button>検索</Button> 1のまとめ ・Nextは部品を作成してページに埋めていく ・コンポーネント:HTML/hooks:メソッド/providers:値/types:型 ・コンポーネントはProps/Childrenで値を渡せる 2. ページについて 先程「ページはサイト表示用ファイル」と述べましたが、超厳密にいうと表示しているのは「ページを埋めたレイアウトコンポーネントを埋めた_app.jsx」です。ここを理解するのもなかなか難しい。。 それぞれの書き方も見ていきたいと思います。 ■ページファイル コンポーネントは上でexportしてたけど、ページは下でexportする。 //const ページ名 :型 const Page: NextPage = () => { return ( <>ページの内容</> ); }; export default Page; ■レイアウトコンポーネント Childrenのところにページファイルが入る export const Layout: FC<Props> = memo((props) => { const { children } = props; return ( <> <Header/> <!--ヘッダーコンポーネント--> <main>{children}</main> <Footer /> <!--フッターコンポーネント--> </> ); }); ■_app.tsx Headに書く事なければ初期のままでOK function MyApp({ Component, pageProps }: AppProps) { return ( <> <Head> <title>サイトのタイトル</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="/image/favicon.ico" /> </Head> <Component {...pageProps} /> </> ); } export default MyApp; 2のまとめ ・全体に表示させたい内容はレイアウトコンポーネントにて ・全体の設定(Headタグ等)は_app.tsxにて ・コンポーネントは上でexport/ページは下でexport 3. 拡張子について ファイルの拡張子なのですが、.ts(.js)と.tsx(.jsx)が存在します。 この違いは「そのファイルは、最後何でreturnしているか」です。 ・値/メソッド等、TypeScript形式でreturn→.ts(カスタムhook等) return num1; ・HTMLの形でreturn→.tsx(ページ/コンポーネント等) return (<button>{label}</button>); 3のまとめ ・「そのファイルは、最後何でreturnしているか」でファイルの拡張子を決める ・HTML → .tsx ・値/メソッド → .ts 4. use/memoで囲む Nextでは以下のuse/memoを使って、変数等を囲む必要があります。 それぞれの理由を説明すると長くなるのでここでは置いておきます。 囲む対象 使うもの メソッド外で宣言している変数 useState 関数 useCallback コンポーネント memo (※使いたいときのみ)関数:値の変更→発動        useEffect 囲む/囲まないを場合によって判断する必要がある様ですが、その判断基準が人によって割とまちまちなため、一旦は全てにつけて問題ないかと思います。とにかく、コンポーネントはmemo/変数はuseState/関数はuseCallbackで囲む!です。(useEffectは使いたいときのみ…) ■変数:useState //この先aaaの中身を初期値から書き換えない時 const [aaa] = useState("初期値") //この先aaaの中身を書き替える予定の時 //後ろの値はset変数名の形にする //letでなく、constでOK const [aaa,setAaa] = useState("初期値"); //aaaの中身を書きかえる時 setAaa("書き換えたい内容") ///NG! aaa = "書き換えたい内容" メソッド外で宣言している変数にはuseState必要だけど、 とあるメソッド内でしか使っていない場合は不要 //num1はuseState必要 const [num1,setNum1] = useState(0) const aaa = useCallback(()=>{ //num2はこのメソッド内でしか使っていないのでuseStateいらない const num2 = 1; setNum1(num2); },[num1,setNum1]) ■関数:useCallback 後ろの[]には、処理に使っている値が入ります。 VScodeのESLintを使っていれば恐らく下線が引かれるので、クイックフィックスで直せます。。 //囲まない場合 console.log("関数"); const aaa =()=>{console.log("関数")} //囲む場合 useCallback()=>{console.log("関数");},[]); const aaa = useCallback(()=>{console.log("関数");},[]); ■コンポーネント:memo これを export const Button: FC = () => { return ( <><button>ボタン</button></> ); }; memo()で囲んでこうする コンポーネント export const Button: FC = memo(() => { return ( <><button>ボタン</button></> ); }); ■useEffect []の中の値が変更された際に発動する。 []を空にするとmount時(このページが作られる際)に自動発動 useEffect()=>{console.log("関数");},[num1]); const aaa = useEffect(()=>{console.log("関数");},[num1]); 4のまとめ ・コンポーネントはmemo/変数はuseState/関数はuseCallbackで囲む! ・useEffectは値の中身が変更されたタイミングで発動/始めに自動発動させることが出来る 5. 型について ここがNextの資料はTypeScriptで書かれているものが少なく、苦労した点です。。 ざっくり、覚えておくと良さそうな型を紹介します。 ■ページファイル NextPageをつける const AaaPage: NextPage = () => {} ■コンポーネントファイル FCとVFCがあるようですが、私はFCを使用しています。 //propsなし export const Button: FC = memo(() => {}) //propsあり export const Button: FC<Props> = memo((props) => {}) ■useStateで型を設定する時 const [aaa]=useState<string>(""); ■propsで定義する際に役に立ちそうな型 export type Props = { title: string; //ここに入れる型 }; //メソッド method: () => void; //文字 mozi: string;  //数 id: number; //true・false isOpen: boolean; //2種類のどちらかしか入らない時  color: "赤" | "青";  //onChange(ユーザが値を入力して、値の中身が変わったら発動する) onChange: ChangeEventHandler<HTMLInputElement>; ■APIでとってきた値に型を設定する type.ts //types>type.tsで型の設定をしておく export type Mailtype = { email: string; }; ページ //APIで取得したデータ:dataの中の「メール」に型をつける const data: Mailtype = data.userMail; 6. APIについて 皆さんおなじみ(?)axiosやfetchで呼べるのですが、こちらも「APIを呼ぶタイミング」によって囲まなくてはいけません。ここについてはまだ私も勉強不足なので異なる点があればご教示ください。 とにかく  ・SSG→静的なページの初期表示用(早い)  ・SSR→動的なページ初期表示用(遅い)  ・useSWR→動的なページ リアルタイム更新用(遅い) という認識です。APIの種類によってこれらを使い分ける事がミソです。 7. まとめ 1. Nextは部品を作成してページに埋めていく ・コンポーネント:HTML ・hooks:メソッド ・providers:値 ・types:型 コンポーネントはProps/Childrenで値を渡せる 2. ・全体に表示させたい内容はレイアウトコンポーネント ・全体の設定(Headタグ等)は_app.tsx ・コンポーネントは上でexport ・ページは下でexport 3. 「そのファイルは、最後何でreturnしているか」でファイルの拡張子を決める ・値、メソッド → ts ・HTML → tsx 4. ・コンポーネントはmemo ・変数はuseState ・関数はuseCallbackで囲む! 6. APIの呼び方はタイミングによって書き方が異なる。 ・SSG→静的なページの初期表示用(早い) ・SSR→動的なページ初期表示用(遅い) ・useSWR→動的なページ リアルタイム更新用(遅い) 今後追加したい項目: ・onChange/onClick等イベントハンドラーについて ・mapで回す ここまで記事をお読みいただきありがとうございました! 8. おまけ Next.js/Reactを勉強するにあたり読んだ資料達 ■本 ■Next.js公式チュートリアル ■YouTube (さいごに: 色々教えてくれたお二人ありがとうございました!@suzu1997 @hiroki-yama-1118)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを基本からまとめてみた【24】【Styled Components and Smooth Scroll 】

環境の準備 ①ターミナルでreactアプリケーションを作成する。 $ npx create-react-app <プロジェクト名> % cd <プロジェクト名> % npm start ② logo.svg,setupTests.js,index.css, App.test.js,reportWebVaitals.js を削除する。 ③ 必要な環境を整える。 $ npm install --save styled-components $ npm install react-router-dom $ npm install react-scroll $ npm install react-icons --save 公式サイト:react-icons コンポーネント・ファイル構成 src ├── components ├── Navbar └── index.js  //全てを保存する   └── NavbarElements.js  //在庫部品を保管する └── Sidebar └── index.js  //全てを保存する   └── SidebarElements.js  //在庫部品を保管する ├── images ├── .js ├── .js ├── .js ├── .js ├── pages ├── .js └── .js ├── videos ├── .js └── .js ├── App.css ├── App.js ├── index.js 参考サイト React Website using Styled Components and Smooth Scroll - Beginner Project Fully Responsive
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactを基本からまとめてみた【24】【Styled Components and Smooth Scroll 】【随時更新】

環境の準備 ①ターミナルでreactアプリケーションを作成する。 $ npx create-react-app <プロジェクト名> % cd <プロジェクト名> % npm start ② logo.svg,setupTests.js,index.css, App.test.js,reportWebVaitals.js を削除する。 ③ 必要な環境を整える。 $ npm install --save styled-components $ npm install react-router-dom $ npm install react-scroll $ npm install react-icons --save 公式サイト:react-icons コンポーネント・ファイル構成 src ├── components ├── Navbar ├── index.js  //全てを保存する   └── NavbarElements.js  //在庫部品を保管する └── Sidebar ├── index.js  //全てを保存する   └── SidebarElements.js  //在庫部品を保管する ├── images ├── .js ├── .js ├── .js ├── .js ├── pages ├── index.js └── .js ├── videos ├── .js └── .js ├── App.css ├── App.js ├── index.js 参考サイト React Website using Styled Components and Smooth Scroll - Beginner Project Fully Responsive
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactの再レンダリングの仕組み

現在Reactを学んでいるWeb系への転職を目指している者です。 ポートフォリオをSPA化するために学んでいます。 自分用のメモのために残します。 再レンダリングされるときの条件 1.stateが更新されたコンポーネントは再レンダリングされる。 2.propsが変更されたコンポーネントは再レンダリングされる。 3.再レンダリングされたコンポーネント配下の子要素はすべて再レンダリングされる。 大きくこの3つの条件でレンダリングされます。 検証のためにはcodesandboxを使用します。 1.stateが更新されたコンポーネントは再レンダリングされる。 再レンダリングされるとconsole.logでレンダリングされましたというのが表示されるようなコードを作成しました。 App.jsx import "./styles.css"; import React, { useState } from "react"; const style = { margin: "20px", padding: "20px", backgroundColor: "pink" }; const textBox = { width: "100%" }; export default function App() { const [text, setText] = useState(""); const onChangeText = (event) => setText(event.target.value); console.log("レンダリング"); return ( <> <div style={style}> <input style={textBox} value={text} onChange={onChangeText} placeholder="文字を入力してください" /> </div> </> ); } このコードではテキストボックスに文字入力されるたびにstateが更新されます。 画面を表示した状態ではレンダリングは1回されているので、コンソールにはレンダリングの文字が1回表示されます。 今回は文字を5回入力したので、stateが5回更新されます。 表示したときのものと合わせて計6回コンソールにレンダリングの文字が表示されます。 2.propsが更新されたコンポーネントは再レンダリングされる。 先ほどのテキストボックスをコンポーネント化しました。 改修したコードはこちらになります。 TextBox.jsx const style = { margin: "20px", padding: "20px", backgroundColor: "pink" }; const textBox = { width: "100%" }; export const TextBox = (props) => { const { text, onChangeText } = props; console.log("TextBoxコンポーネントが更新された"); return ( <div style={style}> <input style={textBox} value={text} onChange={onChangeText} placeholder="文字を入力してください" /> </div> ); }; App.jsx import "./styles.css"; import { TextBox } from "./components/TextBox"; import React, { useState } from "react"; const style = { margin: "20px", padding: "20px", backgroundColor: "pink" }; export default function App() { const [text, setText] = useState(""); const onChangeText = (event) => setText(event.target.value); console.log("レンダリング"); return ( <> <h1 style={style}>レンダリングの条件を理解する</h1> <TextBox text={text} onChangeText={onChangeText} /> </> ); } この状態で文字を入力すると子コンポーネントであるTextBox.jsxに記述した「TextBoxコンポーネントがレンダリングされた」というコンソールも表示されていることがわかります。 3.再レンダリングされたコンポーネント配下の子要素はすべて再レンダリングされる。 ここからが注意しなければならない部分になります。 まずは新たなコンポーネントを作成します。 ChildArea.jsx const style = { margin: "20px", padding: "20px", backgroundColor: "pink" }; const textBox = { width: "100%" }; export const TextBox = (props) => { const { text, onChangeText } = props; console.log("TextBoxコンポーネントがレンダリングされました"); return ( <div style={style}> <input style={textBox} value={text} onChange={onChangeText} placeholder="文字を入力してください" /> </div> ); }; 新たなコンポーネント作成に伴い既存のコードも改修したので、載せます。 TextBox.jsx const style = { margin: "20px", padding: "20px", backgroundColor: "pink" }; const textBox = { width: "100%" }; export const TextBox = (props) => { const { text, onChangeText } = props; console.log("TextBoxコンポーネントがレンダリングされました"); return ( <div style={style}> <input style={textBox} value={text} onChange={onChangeText} placeholder="文字を入力してください" /> </div> ); }; App.js import "./styles.css"; import { TextBox } from "./components/TextBox"; import { ChildArea } from "./components/ChildArea"; import React, { useState } from "react"; const style = { margin: "20px", padding: "20px", backgroundColor: "pink" }; const buttonStyle = { margin: "20px", padding: "20px" }; export default function App() { const [text, setText] = useState(""); const [display, setDisplay] = useState(false); const onChangeText = (event) => setText(event.target.value); const onClickChanged = () => setDisplay(!display); console.log("レンダリング"); return ( <> <h1 style={style}>レンダリングの条件を理解する</h1> <TextBox text={text} onChangeText={onChangeText} /> <button style={buttonStyle} onClick={onClickChanged}> 表示 </button> <br /> <ChildArea display={display} /> </> ); } これにより表示ボタンを押すと画像が表示される機能が追加されました。 画像を表示させる。 画像を非表示させる。 ここでの問題というのは再レンダリングされたコンポーネントの子コンポーネントはすべて再レンダリングされてしまいます。 コンソールを確認してみると。 ・文字をテキストボックスに入力した場合 ・表示ボタンをクリックした場合 どちらも関係ないコンポーネントまで再レンダリングされてしまいます。 要するにすべてのコンポーネントが呼び出されています。 子要素がすべて再レンダリングされる場合の解決方法。 コンポーネントをmemoで囲む。 memoで囲まれた関数はpropsが渡された時のみ、再レンダリングされるという指定がされます。 memo化の方法 ChildArea.jsx // memo化に必要 import { memo } from "react"; const imageStyle = { height: "100px", width: "100px" }; // 関数をmemo()で囲んであげればmemo化が完成。 export const ChildArea = memo((props) => { const { display } = props; console.log("ChildAreaがレンダリングされました"); return ( <> {display ? ( <img style={imageStyle} src="images/account.jpeg" alt="Logo" /> ) : null} </> ); }); 今回はchildAreaコンポーネントをmemo化しました。 これで文字を入力した場合にChildAreaコンポーネントが再レンダリングされることはないはずです。 確認してみると。 試しの文字を二回入力しました。 ページが開かれた際に呼び出されたのみで、再レンダリングはされていないことがわかります。 これでpropsが渡されたときのに再レンダリングされる機能の実装ができました。 memo化の注意点 memo化したコンポーネントに関数を渡す場合は、渡す関数は再レンダリングの際に再生成されるpropsと判断されるためmemo化がうまく機能しなくなります。 検証するために表示ボタンもChildAreaコンポーネントに組み込みます。 ChildArea.jsx import { memo } from "react"; const imageStyle = { height: "100px", width: "100px" }; const buttonStyle = { margin: "20px", padding: "20px" }; export const ChildArea = memo((props) => { const { display, onClickChanged } = props; console.log("ChildAreaがレンダリングされました"); return ( <> <button style={buttonStyle} onClick={onClickChanged}> 表示 </button> <br /> {display ? ( <img style={imageStyle} src="images/account.jpeg" alt="Logo" /> ) : null} </> ); }); App.js import "./styles.css"; import { TextBox } from "./components/TextBox"; import { ChildArea } from "./components/ChildArea"; import React, { useState } from "react"; const style = { margin: "20px", padding: "20px", backgroundColor: "pink" }; export default function App() { const [text, setText] = useState(""); const [display, setDisplay] = useState(false); const onChangeText = (event) => setText(event.target.value); const onClickChanged = () => setDisplay(!display); console.log("レンダリング"); return ( <> <h1 style={style}>レンダリングの条件を理解する</h1> <TextBox text={text} onChangeText={onChangeText} /> {/* 表示ボタンをChildAreaコンポーネントに追加したため関数をpropsとして渡しています。 */} <ChildArea display={display} onClickChanged={onClickChanged} /> </> ); } このコードで注目して欲しいのは、ChildAreaコンポーネントに関数をpropsとして渡しているということです。 関数をpropsでコンポーネントに渡すとmemo化がうまく働かなくなります。 試しに文字を入力した際のコンソールを確認してみます。 memo化したはずなのに再レンダリングが起きてしまっています。 関数をpropsで渡した際にmemo化がうまく動いていない原因 原因は新たなpropsが渡されていると判断されてしまっているからです。 なぜなら、関数はレンダリングされるたびに再生成されていると判断されているため、同じ名前の関数でもレンダリングされる前とは別の関数として認識されてしまうからです。 そのため、memoがうまく機能しません。 改善方法 useCallBackを使用する。 memo化したコンポーネントに渡す関数を、useCallBackで囲むことで、第二引数に指定した配列の値が変化する時のみ再生成されると設定できる。 配列が空の場合は最初のレンダリング時から再生成されなくなります。 それでは、useCallbackで関数を囲みます。 App.js const onClickChanged = useCallback(() => setDisplay(!display), [display]); これにより変数であるdisplayが変化する時のみonClickChangedが再生成されるように設定できました。 それではまた、試しに文字を入力した際のコンソールを確認してみます。 今回はしっかりとmemo化が行われていることがわかります。 まとめ Reactを扱う際に基本的な部分だったと思いますが、基本が大切だと思うのでメモを残しました。 この記事が私と同じくReactを勉強し始めた人の助けになったらうれしいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-hook-formを用いて、二つの入力欄にまたがるvalidationを実装する方法

概要  こんにちは!今日はreact-hook-formを用いて、やや複雑なvalidationを実装する方法を説明します。 react-hook-formでは、色々なvalidationが簡単に書けるようになっています。 今回は、二つの入力欄にまたがるvalidationを解説します。具体的には名字と名前に入力された文字列の長さが合計で 20未満になるようなvalidationを実装します。 よく使いそうな簡単なvalidationは、以前他の記事で解説したので参照してみてください!(RequireやmaxLengthなど) 具体的な方法 1. watchを宣言する。 (例) const { register, handleSubmit, watch, formState: { errors }, } = useForm<ValuesType>({ mode: 'onSubmit', reValidateMode: 'onChange', }) まずはreact-hook-formのimportを忘れずに行ってください。 その後、useFormを使って各変数の宣言をする際に、watchを宣言するようにします。これにより、validationに入力されている値がいつでも参照できるようになります。 ここでValuesTypeには、入力されるデータの型が格納されています。 2. validationを記述 (例) <input placeholder="姓" type="text" defaultValue="あ" {...register('user.last_name', { required: '必須項目です', validate: { lessThanTwenty: (value) => value.length + watch('user.first_name', '').length < 20 || '漢字氏名を合わせて20文字未満で入力してください', }, })} /> 例のようにregisterの第二引数にvalidationを記述していきます。 今回重要なのはvalidateの中身です。validateの中身を例のように記述することで、漢字氏名合わせて20文字未満でないとエラーが出るようになります。このようにvalidateの内容を記述してあげると自由にvalidationを記述することができます。 また具体的なvalidationの内容を書いた後に||を記述し、その後にエラーメッセージを書くことで、どのようなエラーメッセージを表示するかも記述できます! まとめ 今日は、やや複雑なvalidationの書き方を解説しました。 validateを用いることで、色々なvalidationがかけるようになるので、是非参考にしてみてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel+React&PKCEのSPA環境構築メモ

先日書いた Laravel+Vue&PKCEのSPA環境構築メモ のReact版も作成してみました。 ※ Laravel側は全く同じなので割愛し、見出しを合わせて差分のところだけ記載しています https://github.com/pei-miyapei/laravel-react-spa (こちらは簡単なAPIの実装とUIフレームワーク(Ant Design)を導入した状態です) 構成 API server コンテナ(SPA バックエンド、ユーザー管理) (割愛) SPA クライアントコンテナ(SPA フロントエンド) Vite React 17 TypeScript react-router js-pkce React+Vite+TypeScript環境の構築 Vue版との違いは、react-tsのテンプレートを使用しているというだけです。 あとはおなじ。 bash yarn yarn create vite temp --template react-ts vite.config.ts import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], server: { host: true, port: 3000, // https: true, }, }); SPA側の実装 react-router, js-pkceをインストール bash yarn add react-router-dom@6 js-pkce ルーティングの設定 client/src/routes/Router.tsx import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { About } from '../views/About'; import { AuthorizationCallback } from '../views/Auth/AuthorizationCallback'; import { Home } from '../views/Home'; export const Router = () => { return ( <BrowserRouter> <Routes> <Route path='/' element={<Home />} /> <Route path='/about' element={<About />} /> </Routes> </BrowserRouter> ); }; App.tsxのコンテンツはreact-routerの中身(<router>)のみにしておきます ※ Vue版とファイル構成も合わせているだけで、別にApp.tsxを使う必要はありません client/src/App.tsx import './App.css'; import { Router } from './routes/Router'; function App() { return <Router />; } export default App; MasterPage.tsx(Layout)を追加しておく 未認証時に丸ごと非表示にしたいため、 別途レイアウト用のVueを作成してその中にコンテンツページを表示するようにしておきます。 Vue版ではLayout.vueになっています。 ※ ant-designを入れた際にLayoutというコンポーネント名が被ったため… client/src/views/MasterPage.tsx import { Link, Outlet } from 'react-router-dom'; export const MasterPage = () => { return ( <div id="nav"> <Link to='/'>Home</Link> | <Link to='/about'>About</Link> </div> <Outlet /> ); }; このコンポーネントの入れ子のルーティングは以下のように書けます client/src/routes/Router.tsx import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { About } from '../views/About'; import { Home } from '../views/Home'; import { MasterPage } from '../views/MasterPage'; export const Router = () => { return ( <BrowserRouter> <Routes> <Route path='/' element={<MasterPage />}> <Route path='/' element={<Home />} /> <Route path='/about' element={<About />} /> </Route> </Routes> </BrowserRouter> ); }; ネストされたルートについての詳細はこちら https://reactrouter.com/docs/en/v6/getting-started/tutorial#nested-routes トークンを保持する入れ物 これに関してはCookieなどに保存する場合は不要です。 ここでは変数内に持っているだけという状態で実装するため、その入れ物です。 (タブを閉じたら消えます) Vue版ではProvide/Injectという機能を使用しました。 Reactにも同様のContextという機能があり、これを使用します。 client/src/store/AuthContext.tsx import { createContext, useContext, useState } from 'react'; class AuthTokens { constructor(public accessToken = '', public refreshToken = '') {} } // ProviderProps const authProps = ( tokens = new AuthTokens(), handleSetTokens = (accessToken = '', refreshToken = '') => {} ) => { const hasToken = () => tokens.accessToken !== ''; return { tokens, handleSetTokens, hasToken }; }; export type AuthProps = ReturnType<typeof authProps>; // Context let AuthContext = createContext(authProps()); // Provider export const AuthProvider = ({ children }: any) => { const [tokens, setTokens] = useState(new AuthTokens()); const handleSetTokens = (accessToken = '', refreshToken = '') => { setTokens({ ...tokens, ...new AuthTokens(accessToken, refreshToken) }); }; const props = authProps(tokens, handleSetTokens); return <AuthContext.Provider value={props}>{children}</AuthContext.Provider>; }; // Consumer export const useAuthContext = () => useContext(AuthContext); Vue版だと上記をプラグインの機能を使用してアプリ全体に適用しました。 ReactではProviderの子要素で使用することができるので、こちらも最上位から適用します client/src/main.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import { AuthProvider } from './store/AuthContext'; // ← 追記 ReactDOM.render( <React.StrictMode> <AuthProvider> {/* ← 追記 */} <App /> </AuthProvider> </React.StrictMode>, document.getElementById('root') ); ページの作成 残りの トークンチェック&認可リクエストするガワページ 認可コードコールバック用ページ を作成します。 トークンチェック&認可リクエストするガワページ 認可が必要なページにかませて使用 トークンがなければ認可リクエストを発行(if ~) トークンがなければデフォルトスロットの中身(コンテンツ)の描画を行わない(v-if) トークンは先ほど作成した、プラグインでprovideされた入れ物を召喚して確認(injectAuth) というコンポーネント client/src/components/Auth/AuthGuard.tsx import PKCE from 'js-pkce'; import { useAuthContext } from '../../store/AuthContext'; export const AuthGuard = ({ children }: any) => { const { hasToken } = useAuthContext(); if (!hasToken()) { const pkce = new PKCE({ client_id: '1', // `php artisan passport:client --public` したときのIDです redirect_uri: location.origin + '/auth/callback', // 戻ってくるURL authorization_endpoint: 'http://localhost/server/oauth/authorize', // Laravel側の認可エンドポイント requested_scopes: '*', }); location.replace(pkce.authorizeUrl()); return <></>; } else { return <>{children}</>; } }; を作成。 認証が必要なページにこのコンポーネントを導入します。 今回はコンテンツページは全部認証が必要なページとして、 最初に作成したグローバルメニューを持ったMasterPage.tsxに仕込みます client/src/views/MasterPage.tsx import { Link, Outlet } from 'react-router-dom'; import { AuthGuard } from '../components/Auth/AuthGuard'; export const MasterPage = () => { return ( <AuthGuard> <div id="nav"> <Link to='/'>Home</Link> | <Link to='/about'>About</Link> </div> <Outlet /> </AuthGuard> ); }; 認可コードコールバック用ページ Laravelで認証後戻ってくるページ。受け取った認可コードとトークンを交換します。 Laravel側で指定した /auth/callback のページになります。 js-pkceのexchangeForAccessTokenというメソッドでトークンを受け取れます。 それを用意した入れ物(injectAuth)に保存(auth.setToken)し、 ここでは再度トップ画面に戻ります client/src/views/Auth/AuthorizationCallback.tsx import PKCE from 'js-pkce'; import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuthContext } from '../../store/AuthContext'; export const AuthorizationCallback = () => { const pkce = new PKCE({ client_id: '1', redirect_uri: location.origin + '/auth/callback', token_endpoint: 'http://localhost/server/oauth/token', }); const { handleSetTokens } = useAuthContext(); const navigate = useNavigate(); useEffect(() => { pkce.exchangeForAccessToken(document.location.href).then((response) => { handleSetTokens(response.access_token, response.refresh_token); // 認証後に遷移するページへ navigate('/', { replace: true }); }); }, []); return <></>; }; このページを /auth/callback のURLで公開します client/src/routes/Router.tsx import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { About } from '../views/About'; import { AuthorizationCallback } from '../views/Auth/AuthorizationCallback'; // ← 追加 import { Home } from '../views/Home'; import { MasterPage } from '../views/MasterPage'; export const Router = () => { return ( <BrowserRouter> <Routes> <Route path='/auth/callback' element={<AuthorizationCallback />} /> {/* ← 追加 */} <Route path='/' element={<MasterPage />}> <Route path='/' element={<Home />} /> <Route path='/about' element={<About />} /> </Route> </Routes> </BrowserRouter> ); }; 一応完了 というわけでとりあえずこれでVue版同様、 React側にアクセスするとトークンがないためLaravelに遷移、 認証・承認後トークンをゲットして、ページがみられるようになると思います。 (これはAntDesignなどが入ってるので見た目違うと思いますが…) 締め Reactはまだ全然でして… おかしな点などがありましたらこっそりご教示ください…orz
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

フロントエンドLT vol.1 ひとことイベントレポート

こんにちは!みやぞんです! 2022年1月27日にオンラインで開催された、弊社Another works主催 フロントエンドLTのイベントレポートになります! どれも非常に内容の濃いLTでしたので、タイトルと感想を一言書かせていただきます! LTタイトル (サイト掲載順) 命名規則のレビューしたくない話(そるさん) 日本一簡単投稿!? LINEフォトアルバムアプリを自作してみた(沖中がいこつさん) 「immer」で簡単!イミュータブル!(hakkeiさん) 令和最新版FCM(morishinさん) 状態管理ライブラリRecoilについて話します(まっちさん) ISR完全に理解した(みやぞん(私)) StoryBook駆動で新規システムの開発を試してみた(kght6123さん) Blitz.jsで作るエンタープライズサービス(Yuta Okadaさん) みんなRef使ってる?(ひらまささん) React Testing LibraryのQueryについて整理してみた(古市武尊さん) 命名規則のレビューしたくない話 - そるさん(株式会社Another works) あるディレクトリのは以下のクラス名や関数名、ファイル名を縛りたい ymlやjsonでかけて、ディレクトリごとにルールを設定できたりファイル名をクラス名に使うなどのルールを設定したい そこで命名規則判定ツール「namelint」を自作! ドキュメント管理コストやレビューコストが大幅に削減された話でした! npm packageが公開されているのでぜひ触ってみてください! 日本一簡単投稿!? LINEフォトアルバムアプリを自作してみた - 沖中がいこつさん(個人開発) なんとエンジニアではなくスーパー勤務の店長さんです! 「のこるん」というLINEフォトアルバムアプリをご自身でいちから開発しておられるとのこと! 日常の思い出をより簡単に残せるようにするのに最適なアプリで、 お子さんがアプリを使用している映像などでもその手軽さが伝わってきました! クラファン開催中です! 「immer」で簡単!イミュータブル! - hakkeiさん(株式会社Another works) Reactが公式で推奨するイミュータブル実装のためのライブラリ「immer」についての発表でした! Reactのためのhooksなども提供されていて非常に便利...! cloneDeepとの比較もわかりやすく非常に勉強になりました。 圧倒的にパフォーマンスが良い! 令和最新版FCM - morishinさん(株式会社Magic Moment) Firebase Cloud Messagingについての発表です! Reactを使ってプッシュ通知ができるところまでのご紹介と、ハマリポイントについて。 私自身FCMを使ったことがなかったのですが、結構大きなハマリポイントあるんだなあと思いました...。 使う機会があったらmorishinさんの発表を見返そうと思いました! 朝考えた発表とのことでしたが、情報量がすごかったですw 状態管理ライブラリRecoilについて話します - まっちさん(株式会社Another works) ステート管理ライブラリのRecoilについてのお話! 弊社では状態管理にRedaxを使用していますが、Recoilはそれよりもシンプルにかける場面もありそうでした! Redaxとの比較が非常にわかりやすかったです! ISR完全に理解した - みやぞん(私)(株式会社Another works) ISRについての発表です! ISRの選定理由やその挙動について深堀りした結果を発表しました! 速い!簡単!すごい! そんな機能でございます。 発表内容は以下の記事にまとめています! StoryBook駆動で新規システムの開発を試してみた - kght6123さん(虎の穴ラボ株式会社) StoryBook駆動で新規システムの開発を試してみた StoryBookを使った開発についての発表です! コンポーネントの実際のUIをGUIで確認できるだけでなく、使用をマークダウンで書くことができたり、APIが必要なコンポーネントに関してはモックを用意することができたりと、、!すごい! 弊社でもStoryBookを一部導入しているので非常に勉強になりました。 Blitz.jsで作るエンタープライズサービス - Yuta Okadaさん(株式会社BOOSTRY) Blitz.jsはまだまだ記事も出回っていないこともあり未知でしたが、Railsに変わるフルスタックフレームワークということで個人的にすごく興味がありました。 プロダクトにすでに導入しているということが驚きでした!超最先端! みんなRef使ってる? - ひらまささん(株式会社Another works) 前回の外部LTの続編! Refの歴史や、Refのユースケース、サンプルFormをRefを使って実装するなど、コードベースでわかりやすく説明してくれました。 Refはいいぞ! React Testing LibraryのQueryについて整理してみた - 古市武尊さん(コネヒト株式会社) testing libraryのQueryについての注意点などを発表いただきました! getByxxxにもたくさんあって、使い分けが大切であると知り勉強になりました。 getByTestIdの存在は知らなかったです...。要素内のテキストが動的な場合に非常に便利! おわりに 今回は80名を超えるエンジニアにご参加いただき、大盛況となりました! また第二弾も近々開催しようと思っていますのでお楽しみに!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQLとコードジェレータでスキーマファーストな開発

フロントエンドとバックエンドを結合テスト ...動かない みたいな手戻りは最小限に抑え、効率的な開発を行っていきましょう。 この記事では、GraphQLのスキーマ定義からコード生成してフロントエンドとバックエンドを実装するスキーマファーストな開発について紹介しようと思います。 また、この記事で紹介するコードは以下のリポジトリに置いてます。 技術スタック TypeScript バックエンド、フロントエンド共にTypeScriptで実装しています。 GraphQL.js GraphQLのJavaScriptライブラリです。 React フロントエンドはcreate-react-appで作りました。 Apollo GraphQL Client フロントエンドのGraphQLクライアントとしてApolloを使います。 以下で紹介するGraphQL Code GeneratorでスキーマからReact hooksをコード生成して利用します。 Fastify サーバーは、Fastifyを採用しました。また、FastifyでGraphQLサーバーを作るのにmercuriusというAdapterを使います。 GraphQL Code Generator スキーマ定義からフロントエンド、バックエンドのコードを自動生成するツールです。 Prisma DBクライアントです。prismaはスキーマ定義をもとにClientを生成したり、マイグレーションを実行してテーブルを作ったり、CLIコマンドが充実しています。 GraphQLのスキーマ定義 サンプルのリポジトリでは、backend/ディレクトリ下にあります。 schema.graphql scalar DateTime type Post { id: Int! title: String! content: String published: Boolean! author: User viewCount: Int! createdAt: DateTime! } type User { id: Int! email: String! name: String } type Query { allUsers: [User!]! draftsByUser(input: UserUniqueInput): [Post!] feed(q: String, sort: FeedSort, skip: Int, take: Int): [Post!]! } type Mutation { createDraft(authorEmail: String!, data: PostCreateInput!): Post deletePost(id: Int!): Post incrementPostViewCount(id: Int!): Post signup(data: UserCreateInput!): User togglePublishPost(id: Int!): Post } enum SortOrder { asc desc } input UserUniqueInput { email: String id: Int } input FeedSort { updatedAt: SortOrder! } input PostCreateInput { title: String! content: String } input UserCreateInput { email: String! name: String } このスキーマ定義をもとにクライアントサイドとサーバーサイドをそれぞれ実装していきます。 バックエンドの実装 graphql-codegenでResolverの型を生成 以下の公式ドキュメントを参考にGraphQL Code Generatorを使ってみます。 GraphQL Code Generatorで自動生成するのにプラグインを指定します。今回使ったプラグインは、 @graphql-codegen/typescript @graphql-codegen/typescript-resolvers です。 設定ファイル codegen.yml overwrite: true schema: "./schema.graphql" generates: src/lib/generated/graphql.ts: plugins: - typescript - "typescript-resolvers" 以下のコマンドを実行してResolverの型を生成します  yarn graphql-codegen --config codegen.yml Resolverを実装 Resolverの型が生成されたら、それをもとに実装していきます。 resolvers.ts export const resolvers: Resolvers<Context> = { Query: { allUsers: (_parent, _args, context, _info) => { return context.prisma.user.findMany() }, ... }, Mutation: { ... } } PrismaClientをContextを通してResolverで受け取ります。 Contextの実装は以下。 context.ts const prisma = new PrismaClient({ log: ['query', 'info', 'warn', 'error'], }) export interface Context { prisma: PrismaClient } export const context: Context = { prisma } export const buildContext = async ( _req: FastifyRequest, _reply: FastifyReply ): Promise<Context> => { return { prisma } } サーバーはFastifyを使いました。FastifyでGraphQLサーバーを実装するのにmercuriusというAdapterを使います。 上で実装したcontextとresolversも一緒に渡します。 server.ts import { resolvers } from './resolvers' import { buildContext } from './context' // Load schema from the file const schema = loadSchemaSync(join(__dirname, '../schema.graphql'), { loaders: [new GraphQLFileLoader()] }) // Add resolvers to the schema const schemaWithResolvers = addResolversToSchema({ schema, resolvers }) const fastify = Fastify() fastify.register(mercurius, { schema: schemaWithResolvers, context: buildContext, graphiql: true }) fastify.listen(4000, '0.0.0.0', (err, addr) => { if (err) { console.error(err) process.exit(1) } console.info(`Server listening on ${addr}`) }) あっという間にGraphQLサーバーが実装出来ちゃいましたね server.tsを実行すると、GraphQLサーバーが立ち上がります。 GraphiQLも立ち上がる設定にしてあるので、ブラウザで以下のURLで開きます http://localhost:4000/graphiql フロントエンドの実装 フロントエンドの実装を始める前に、モックサーバーを立ち上げてみます。 ApolloServerを使うことでGraphQLのスキーマからお手軽にモックサーバーを立ち上げられます。 スキーマ定義からモックサーバーを立ち上げ サンプルのリポジトリではfrontend/mock/ディレクトリ下にあります。 server.ts // Load schema from the file const schema = loadSchemaSync(join(__dirname, '../../server/schema.graphql'), { loaders: [new GraphQLFileLoader()] }) const server = new ApolloServer({ schema,     mocks: true }) server.listen().then(({ url }) => console.log(`? Server ready at ${url}`) ) server.tsを実行したら、ブラウザで以下のURLからApolloStudioを開けます。 http://localhost:4000 graphql-codegenでApolloクライアント生成 バックエンドと同じようにGraphQL Code Generatorでコード生成します。 以下の公式ドキュメントに従いながら進めました。 使ったプラグインは @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo です。 @graphql-codegen/typescript-react-apollo はApolloClientのReact hooksを生成するプラグインです。 今回は使用しませんでしたが、hooks以外にもComponentやHOCなども生成出来たり、その他の設定も豊富に用意されてあり、便利そうです。詳しくは公式のドキュメントを参照。 設定ファイル codegen.yml overwrite: true schema: "../backend/schema.graphql" documents: "graphql/**/*.graphql" generates: src/graphql/types.ts: plugins: - "typescript" - "typescript-operations" - "typescript-react-apollo" documents documentsは、リクエストするときに発行するクエリを定義します。 サンプルのリポジトリでは、frontend/graphql/下にあります。 post.graphql query feed($q: String!, $take: Int, $skip: Int, $sort: SortOrder!) { feed(q: $q, sort: { updatedAt: $sort }, take: $take, skip: $skip) { id title content viewCount createdAt author { id name } } } コード生成すると、ApolloClientでリクエストするフックが生成されます yarn graphql-codegen --config codegen.yml GraphQLサーバーへのリクエストを実装 上のコマンドでuseFeedQueryが生成されました。 これを使って、データを取得・表示するコンポーネントを作ります。 Feed.tsx export const Feed: React.FC<FeedProps> = ({ q, take, skip, sort }) => { const { loading, error, data } = useFeedQuery({ variables: { q, take, skip, sort } }) if (loading) { return (<div>loading...</div>) } if (error) { return (<div>{error.message}</div>) } const posts = data?.feed ?? [] return ( <div className='Feed'> {posts.map((post) => ( <div className='Feed-Row'> <a className='Feed-Row-Title' href='/'>{post.title}</a> {' '} <span className='Post-Views'>{post.viewCount}views</span> <div> <span className='Post-Author'>author: {post.author?.name}</span> {' '} <DateTime className='Post-DateTime' datetime={post.createdAt} /> </div> </div> ))} </div> ) } 動作チェック こちらのサンプルでは、docker-composeで立ち上がるようになっています。 docker-compose up ブラウザでhttp://localhost:3000を開いてみます。 サーバーからデータを取得して表示出来ていますね! いかがだったでしょうコードジェネレータを活用することで型安全に開発が進められ、尚且つ大幅に実装量を減らすことが出来ましたね 課題 以上で概ね型安全に実装出来たのですが、残念ながら完全には型安全になっていません。。。 問題は、schema.graphqlで定義したcustom scalarのDateTimeです。 バックエンドでは graphql-scalars というライブラリを使ってresolversでDateTimeResolverを渡してやることで、serializeしてレスポンスを返してくれています。 resolvers.ts export const resolvers: Resolvers<Context> = { ..., DateTime: DateTimeResolver, } 一方、クライアントサイドでGraphQL Code Generatorで生成された型を見てみると、DateTimeがany型になっていました。 types.ts export type Scalars = { ID: string; String: string; Boolean: boolean; Int: number; Float: number; DateTime: any; //←これ }; custom scalarのDateTimeをdeserializeしてDate型に変換する処理もコード生成して欲しいのですが、どうすればいいのか分からず。。。 現状方法ないんでしょうかね、、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む