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

【React Hook Form v7】ネスト化された構造で簡単にバリデーションを実施する

個人開発進めていく中で、ユーザー情報の登録画面を作る中で進行状況を表すウィザード(MUIでいうStepperみたいな)を作る場合に、ネスト化された構造でバリデーションを実施する必要があったのでメモとして残します。 MUI v5とReact Hook Form v7の導入に関しては以下の記事でとても分かりやすくまとめられています。本記事もこの記事に記載されているコードを元に作成致しました。是非参考にしてください。 今回は以下のような状況で、簡単にバリデーションを実施してみます。 App.tsx <Stack spacing={3}> // ネスト化されたコンポーネントにおいても簡単にバリデーションを実装したい <EmailInput /> <NameInput /> <PasswordTInput /> <Button color="primary" variant="contained" size="large" onClick={handleSubmit(onSubmit)} > 作成 </Button> </Stack> 方法 useFormContextを使う(それだけです) このカスタムフックを使うと、フォームのコンテキストにアクセスできるようになります。 useFormContext は、深くネストされた構造体の中で使うことを想定しており、 コンテキストを prop として渡すのが面倒な場合に使います。- React Hook Form ただし正しく機能させるには、上記のコードをFormProviderコンポーネントでラップする必要がある為、コードを修正した形が以下になります。 App.tsx import { useForm, FormProvider } from "react-hook-form"; // フォームの型 export interface FormInputState { ・・・・ } // バリデーションルール export const schema = yup.object({ email: yup .string() .required("必須内容です") .email("正しいメールアドレスを入力して下さい"), ・・・・ }); export default function App() {  // FormProviderはすべてのuseFormメソッドを必要とするため、 // 変数metohdsにuseFormメソッドを値をして渡した後に、オブジェクトの分割代入をしています。 const methods = useForm<StateFormInput>({ resolver: yupResolver(schema), }); const { handleSubmit } = methods; // フォーム送信時の処理 const onSubmit: SubmitHandler<SampleFormInput> = (data) => { console.log(data); }; return ( <FormProvider {...methods} >     <Stack spacing={3}> <EmailInput /> <NameInput /> <PasswordTInput /> <Button color="primary" variant="contained" size="large" onClick={handleSubmit(onSubmit)} > 登録 </Button> </Stack> </FormProvider> ); } また、ネスト化されたコンポーネントでのコードは以下。 NameInputコンポーネントおよびPasswordInputコンポーネントも同様に作成します。 EmailInput.tsx import { useFormContext } from "react-hook-form"; export default function EmailInput() { const { register, formState: { errors }, } = useFormContext(); return ( <> <TextField required label="Email" type="email" {...register("email")} error={"email" in errors} helperText={errors.email?.message} /> </> ); } 動作確認 フォームの送信は問題なく出来てます。 バリデーションチェックも問題なし。 結論 ネスト化された構造でもたった数行でバリデーションを実施する事が出来ました。 いや~React Hook Form便利ですね。 でも、「その他の用途としても使い回したいコンポーネントの場合にバリデーションを行うロジックのみを切り離して使えないか?」という疑問が湧きました。 以下の記事がこの疑問に大いに参考になりました。本当に感謝です。 こんな内容が無料で読めるなんて、便利な世の中になったものですね、、、
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(React Hooks) カウントアップ・カウントダウンを実装してみた

はじめに React Hooksでタイマー機能を実装しました。(useCounter.tsx ※コードはこちら) demoのgifのようにカウントアップとカウントダウンの両パターンに対応してます。 コード全体はこちら↓(demoに示したもの) demo カウントアップとカウントダウンの両方に対応 使い方 step must/want 説明 (★1)useCounter.tsxを読み込み must カウントを表示したいtsxファイルに読み込む (★2)必要な変数、関数を取り込む must 必要な変数、関数を取り込む ※補足参照 (★3)カウントを表示 must カウントを表示したい箇所にcountを記載 (★4)カウント終了を検知 want カウント終了を検知してなにかする ※(★1)~(★4)はコード内の記載と対応 [補足] (★2)必要な変数、関数を取り込む 読み込み例 最低限、countとstartCountがあればOK const {count, isTimerEnd, initCount, startCount} = useCounter(); 取り込める変数 名前 Type 概要 count number カウント情報 isTimerEnd boolean true: カウントが終了したfalse:カウント中 or リセット状態 取り込める関数 名前 引数・返り値 概要 initCount f(引数なし):返り値なし カウントをリセット(countを0にして、isTimerEndをfalseとする) startCount f( upLimit?:number, downLimit?:number, direction?:string):返り値なしupLimit:カウント上限downLimit:カウント下限direction:どちらにカウントするか("up"の場合、下限⇒上限に向かってカウント、"down"の場合、上限⇒下限に向かってカウント) カウントを開始する (startCountの呼び出し例) //引数なし:10⇒0のカウントダウンになる(デフォルト設定) //デフォルト設定は、useCounter.tsx(4,5,6行目で変更可) startCount(); //100⇒0のカウントダウン startCount(100, 0, "down"); //0⇒100のカウントアップ startCount(100, 0, "up"); 呼び出し元のコード例(demoで示したもの) App.tsx import React from 'react'; import './App.css'; import { useEffect, useState } from 'react'; import useCounter from './useCounter' //(★1)useCounter.tsxを読み込み const EndDom:React.VFC<{isShow:boolean}>= (props) => { if(props.isShow) { return <p className="color-red">カウント終了!</p> } return <></> } function App() { const {count, isTimerEnd, initCount, startCount} = useCounter(); //(★2)必要な変数、関数を取り込む const [startTime, setStartTime] = useState<number>(0); const [endTime, setEndTime] = useState<number>(0); const start = (startTime:number, endTime:number) => { const direction = (startTime>endTime) ? "down" : "up"; const upLimit = (direction==="down") ? startTime : endTime; const downLimit = (direction==="down") ? endTime : startTime; startCount(upLimit, downLimit, direction) } const reset = () => { initCount(); } const handleChangeStartTime = (e:React.ChangeEvent<HTMLInputElement>) => { setStartTime(parseInt(e.target.value)); } const handleChangeEndTime = (e:React.ChangeEvent<HTMLInputElement>) => { setEndTime(parseInt(e.target.value)); } return ( <div className="App"> <main> <section className='counter-input mb10'> <input onChange={(e)=>handleChangeStartTime(e)} type="text" placeholder='開始時間(sec)'/> <input onChange={(e)=>handleChangeEndTime(e)} type="text" placeholder='終了時間(sec)'/> </section> <section className='counter-button'> <button onClick={()=>start(startTime, endTime)} className='button mb10'>START</button> <button onClick={()=>reset()} className='button mb10' disabled={(isTimerEnd===true) ? false : true}>RESET</button> </section> <section className={(isTimerEnd===true) ? 'counter-value color-red' : 'counter-value'}> {count} //(★3)カウントを表示 </section> <EndDom isShow={isTimerEnd===true}/> //(★4)カウントが終了したら何かする </main> </div> ); } export default App; useCounter.tsx useCounter.tsx import { useEffect, useState } from "react"; //10秒⇒0秒のカウントダウンがデフォルト //デフォルト変更可 const UPLIMIT = 10; const DOWNLIMIT = 0; const DIRECTION = "down"; const useCounter = () => { const [timeEnd, setTimeEnd] = useState(0);//終了時間 const [direction, setDirection] = useState("down");//カウント方向 const [count, setCount] = useState(0);//カウント情報 const [timerFlag, setTimerFlag] = useState(false);//カウント開始フラグ const [isTimerEnd, setIsTimerEnd] = useState(false);//カウント終了フラグ useEffect(() => { if (timerFlag) { const id = setTimeout(() => { setCount((beforecount) => { console.log('timer・・・') if(direction==="up"){ if(beforecount===timeEnd-1) { setTimerFlag(false); setIsTimerEnd(true); return (beforecount + 1); } if(beforecount<timeEnd){ return (beforecount + 1); } return beforecount; }else{ if(beforecount===timeEnd+1) { setTimerFlag(false); setIsTimerEnd(true); return (beforecount - 1); } if(beforecount>timeEnd){ return (beforecount - 1); } return beforecount; } }); }, 1000); return () => clearTimeout(id); } return; },[count, timerFlag]); //開始時間セット const setStart = (upLimit:number, downLimit:number, direction:string) => { const start = (direction==="up") ? downLimit : upLimit; setCount(start) } //終了時間セット const setEnd = (upLimit:number, downLimit:number, direction:string) => { const end = (direction==="up") ? upLimit : downLimit; setTimeEnd(end); } //タイマースタート const startCount = (upLimit:number=UPLIMIT, downLimit:number=DOWNLIMIT, direction:string=DIRECTION) => { console.log('start count'); setStart(upLimit, downLimit, direction);//開始時間セット setEnd(upLimit, downLimit, direction);//終了時間セット setDirection(direction);//カウントアップ("up") or カウントダウン("down")をセット setIsTimerEnd(false);//カウント終了フラグをfalse setTimerFlag(true);//タイマー開始 }; const initCount = () => { console.log('init count!'); setIsTimerEnd(false); setCount(0); } return { count, isTimerEnd , initCount, startCount }; }; export default useCounter;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでenvを導入したらエラーが起きた

前提 フロントエンドでwebpackを使用して開発をしている 下記が設定していたwebpack.config.js const path = require("path") const HtmlWebpackPlugin = require("html-webpack-plugin") module.exports = { mode: "development", entry: path.resolve(__dirname, "./src/index.tsx"), output: { path: path.resolve(__dirname, "dist"), filename: "index.js", }, resolve: { modules: [path.resolve(__dirname, "node_modules")], extensions: [".ts", ".tsx", ".js"], }, module: { rules: [ { test: [/\.ts$/, /\.tsx$/], use: [ { loader: "babel-loader", options: { presets: ["@babel/preset-env", "@babel/preset-react", "@babel/preset-typescript"], plugins: ["@babel/plugin-transform-runtime"], }, }, ], }, { test: /\.png/, type: "asset/resource", }, ], }, plugins: [ new HtmlWebpackPlugin({ template: path.resolve(__dirname, "./src/index.html"), }), ], devServer: { port: 8111, static: { directory: path.join(__dirname, "dist"), }, }, } 今まで yarn add dotenvコマンドを実行し、envを使用する際に下記のように記入していた index.ts import dotenv from "dotenv" dotenv.config()        process.env.〇〇 エラー ターミナルでnpx webpack serve --config webpack.config.jsを叩いてブラウザで描画させようとしたら、 ERROR in ./node_modules/dotenv/lib/main.jsというエラーが出た... 解決策 yarn add -D dotenv-webpack コマンドを実行し、dotenv-webnpackを導入 webpack.config.jsに追加 const Dotenv = require("dotenv-webpack") module.exports = { 省略 plugins: [ new Dotenv(), ], } index.ts const { TEST } = process.env //分割代入 const test = TEST 必要だと思っていた下記を記入しなくてよくなった! import dotenv from "dotenv" dotenv.config()        最後に 解決するまで意外と時間がかかってしまったので作成しました。 今まではNode.jsだったのでenvの使い方が違っていました... 参考記事
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactのチュートリアルをやってみた②

はじめに Reactを使って画面作成をしてみたいと思うようになり、サイトを調べてチュートリアル を試しにやってみました。 ここにかかれていることをサイト内にあるスターターコード を使って試したものになります。 ○×ゲームを作ることが題材となっています。 スターターコードにチュートリアルの内容に従いコードを書いていくことで動きを確認することはできました。 Reactのチュートリアルをやってみた① に投稿させていただいた第2弾になります。 前回は、Propsとstateについて、理解した内容をイメージにしてみました。 今回は、ゲーム完成までのコーディングを行うまでに学んだ以下の内容について纏めてみたいと思います。 State のリフトアップ(親:Boardが子:Squareをコントロールできるようにする) 関数コンポーネント 番手の処理 勝ち負けの判定 前回のおさらい Propsは、子のコンポーネントに対して引数を受け渡すのに利用される stateは、自コンポーネント内で情報を保持し記憶しておくことができる 前回は、SquareとBoardのコンポーネントを準備し、Squareでイベントを検知して、情報を記憶しておき、Boardを再描画することで、Squareで記憶した値を表示できることを理解しました。イメージにするとこんな感じだと理解しました。          今回理解したこと Stateのリフトアップ 今、行っているチュートリアルでは○×ゲームを完成させることにありますので、Squareコンポーネントで記憶している内容をBoardコンポーネントで収集し、盤面の内容から勝ち負けを判定できるようにする必要があります。 そのため、以下のイメージにあるようなことをできるようにする必要があります。          そのためには、以下のイメージのように、親のコンポーネントでstateを宣言して、Propsを使って子のコンポーネントに情報を展開することで、親コンポーネントで情報を管理し、子コンポーネント間も親を通して情報のやり取りをできるようにすることでStateのリフトアップを行います。          チュートリアルの○×ゲームでは、クリックのイベントも親コンポーネントで定義し、子コンポーネントの振る舞いを決めます。子コンポーネントは、クリックイベントが発生したら親コンポーネントが定義したイベントに従うようにしています。 コンポーネントのイメージと処理の流れをイメージしてみると、このような感じになると理解しました。       関数コンポーネント Reactでは、rendaerメソッドだけを有して、stateを持たないコンポーネントは、React.componentを継承する代わりに、Propsを引数として受け取り、表示すべき内容だけを返す(return)できるように関数としてていぎすることができる。 クラスコンポーネントとして書くより、関数コンポーネントはコーディングが楽になる。 実際、stateのリフトアップした結果を見てみると、実際にやっていることは、renderメソッドだけになっています。       番手の処理 番手の処理、つまり”○” と ”×” の攻め手の切り替えが?の処理までだと実現できていないので、実現しましょう!という話になります。 ここは、stateで真偽(True/False)を記憶して、その評価の結果によって、”○”と”×”を切り替えられるようにしていくことで実現できます。 Boardコンポーネントのstateに変数:xIsNextをtrueを初期地として追加 イベントが発生するたびに真偽の変更と、"○"と"×"の切り替えができるようにhandleClick関数に処理を追加 squares[i]に変数:xIsNextの真偽によって、"○" か"×"をセット setStateメソッドで、squares[i]をstate.squaresに、変数:xIsNextに変数:xIsNextではない値をstateの情報として記憶する Boardコンポーネントのレンダー内にconstで定義されている今、どっちの番かを表示するstatusにも変数:xIsNextの真偽によって、"○" か"×"を切り替えができるように書き換える 勝ち負けの判定 勝ち負けの判定をするためのcomponentなどの描画などが目的の関数コンポートンとは異なるヘルパー関数を用意するとあった。 ここでは、処理のイメージ化して書いてきたけど、純粋にロジックなのでコードベースで理解をした。 コードは以下のようにfunctionで関数であることを宣言して、関数名(引数)としてreturnで処理結果を返している。 function calculateWinner(squares) { const lines = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6], ]; for (let i = 0; i < lines.length; i++) { const [a, b, c] = lines[i]; if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; } } return null; } 一方の呼び出し側のBoardコンポートでは、以下のように書き換えることで、勝ち負けの判定の結果に基づき、constで定義していたstatusをlet(再代入可能な変数に定義しなおしている)のstatusに対して、ゲームが続行で、手番がどっちか、もしくは決着がついて、Winnerが誰になったのか、文字列を再代入する形で表現できるようにしている。 render() { const winner = calculateWinner(this.state.squares); let status; if (winner) { status = 'Winner: ' + winner; } else { status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); } まとめ 準備するコンポーネントが 'state' を定義し情報を記憶させておくべきコンポーネントか否かでコンポーネントクラスで定義するのか、関数コンポーネントとして定義するのかを整理しながら、まずは動かすためにコンポーネントクラスで定義したとしても、やりたいことを整理していく中で リファクタリング しながら実装することがポイントになると理解した。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む