20210917のReactに関する記事は10件です。

useState の仕組み

最近Reactを勉強し始め、useStateの仕組みが理解しづらかったのでまとめておきます。 0. なぜuseState()が必要なのか Reactではページ更新時に一度ページ全体をレンダリングした後、個々のコンポーネントを再レンダリングしたい場合、任意の関数コンポーネントを個別で再実行しなければいけません。 その際に、イベントリスナーをonClick などで指定すると呼び出した関数だけを実行してくれた後に元の関数コンポーネントを再実行してくれません。(回帰処理を試してみましたがErrorになりました。) そこでuseStateで返ってきたset関数を使うことで、値を保持、更新しつつ元の関数コンポーネントを再実行(再レンダリング)することができます。 1.importしておく import React, { useState } from 'react'; 2.useStateの基本的な動き useState() は引数にinitalValueを渡してあげます。(型はなんでもOK) <引数> React側で渡された値をどこかのメモリに記憶しておいてくれます。詳しくは、以下のブログで解説されてましたが、流し読みしちゃいました。 https://sbfl.net/blog/2019/02/09/react-hooks-usestate/ <戻り値> 戻り値は、現在の状態と、状態更新用の関数を配列で返してくれるので、Destructuring Array で受け取ってあげます。この時、 const [好きな変数名, set好きな変数名] = useState(initialValue); と命名してあげるのが一般的なようです。 以下使用例です。 const Title = (props) => { const [title, setTitle] = useState(props.title); const clickHandler = () => { setTitle('Updated!'); } return ( <div> <h2>{title}</h2> <button onClick={clickHandler}>Change Title</button> <div/> ); } 3.最後に 自分がわかりづらかったと感じたところだけをまとめたので、より詳しい解説や文法はReactの公式や他の記事を探してください。以下に自分が参考にした記事と教材をまとめたのでご参考にどうぞ。 https://sbfl.net/blog/2019/02/09/react-hooks-usestate/ https://udemy.com/course/react-the-complete-guide-incl-redux
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.js+tailwindcssにgoogleFont読み込ませる!

googleFontをtailwindで使用する為の設定の仕方を紹介します。 こちら課題の一環でLGMTを一定数取らないといけない試験でぜひLGMTを押していただけるとありがたいです。 googlefontでフォントを選択して@importの方でコピーする! styles/globals.scssに読み込む @tailwind base; @tailwind components; @tailwind utilities; //読み込む @import url('https://fonts.googleapis.com/css2?family=Lato:wght@700&family=Noto+Sans+JP:wght@500&display=swap'); tailwind.config.jsに読み込む module.exports = { theme: { extend: { fontFamily: { roboto: ["Roboto"], }, }, }, } 使用する。 font-roboto font-(設定したやつ)で使える <div className="font-roboto"> テキスト </div>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】react-three-fibarで3D表現をする

概要 Three.jsのReact用ライブラリ react-three-fibar を使用して、簡単な3D表現を実装します。 公式ドキュメント react-three-fibarは、Three.jsのReact用パッケージで、ほぼ100%の互換性があります。 Three.jsのドキュメントはかなり充実しているので、react-three-fibarのドキュメントに載っていないことがあれば、そちらを参照しましょう。 環境 react - 17.0.2 typescript - 4.4.3 three - 0.132.2 react-three/fiber - 7.0.7 react-three/drei - 7.8.2 インストール いつものように、プロジェクトフォルダを用意してcreate-react-appでプロジェクトを作成します。 cmd npx create-react-app . --template typescript --use-npm 必要なパッケージをインストールします。 cmd npm i three @react-three/fiber @react-three/drei npm i -D @types/three react-three/dreiは、react-three-fiberに対して便利なヘルパー関数を提供します。 実装 キャンバスの作成・オブジェクトの配置 import React, { VFC } from 'react'; import { DoubleSide } from 'three'; import { OrbitControls } from '@react-three/drei'; import { Canvas } from '@react-three/fiber'; export const ThreeDemo: VFC = () => { return ( <Canvas camera={{ fov: 50, position: [0, 3, 10] }}> <Contents /> </Canvas> ) } const Contents: VFC = () => { return ( <> {/* control */} <OrbitControls /> {/* light */} <directionalLight position={[5, 5, 5]} /> {/* box 1 */} <mesh position={[0, 2, 0]}> <boxGeometry args={[1, 1, 1]} /> <meshPhongMaterial color="blue" /> </mesh> {/* box 2 */} <mesh position={[1, 3, 2]} scale={0.5}> <boxGeometry args={[1, 1, 1]} /> <meshPhongMaterial color="red" /> </mesh> {/* floor */} <mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]}> <planeGeometry args={[10, 10]} /> <meshStandardMaterial color="#E5E5E5" side={DoubleSide} /> </mesh> </> ) } Canvas 視野角(fov)やカメラ位置(position)を設定します。ほかに背景色やDPRなどを設定できます。 OrbitControls このコンポーネントを追加すると、マウスでカメラの回転、移動、ズームを行うことができます。 どれかしらの機能だけを使う/使わないの設定もできます。 directionalLight ライトです。位置や強さ、色などを設定することができます。 オブジェクト geometryではオブジェクトの形状を、materialでは色や材質を定義します。 それをmeshタグで囲います。meshでは、オブジェクトの位置や回転を定義します。 <mesh position={[0, 2, 0]}> <boxGeometry args={[1, 1, 1]} /> <meshPhongMaterial color="blue" /> </mesh> 実装例では、Contentsコンポーネントを作成してCanvasタグの中で参照しています。 これは意図的にやっていて、Canvas内のコンポーネントでしか使用できないカスタムフックがあるためです。(後ほど紹介しています) 影 オブジェクト間に落ちる影も簡単に実装することができます。 import React, { VFC } from 'react'; import { DoubleSide } from 'three'; import { OrbitControls } from '@react-three/drei'; import { Canvas } from '@react-three/fiber'; export const ThreeDemo: VFC = () => { return ( <Canvas camera={{ fov: 50, position: [0, 3, 10] }} shadows> <Contents /> </Canvas> ) } const Contents: VFC = () => { return ( <> {/* control */} <OrbitControls /> {/* light */} <directionalLight position={[5, 5, 5]} intensity={1} // 光の強さ shadow-mapSize-width={2048} // 描画精度 shadow-mapSize-height={2048} castShadow /> {/* box 1 */} <mesh position={[0, 2, 0]} castShadow receiveShadow> <boxGeometry args={[1, 1, 1]} /> <meshPhongMaterial color="blue" /> </mesh> {/* box 2 */} <mesh position={[1, 3, 2]} scale={0.5} castShadow receiveShadow> <boxGeometry args={[1, 1, 1]} /> <meshPhongMaterial color="red" /> </mesh> {/* floor */} <mesh position={[0, 0, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow> <planeGeometry args={[10, 10]} /> <meshStandardMaterial color="#E5E5E5" side={DoubleSide} /> </mesh> </> ) } 影を入れるためには、まずCanvasにshadowsプロパティを追加します。 次に、lightやオブジェクトに、castShadow・receiveShadowを追加します。 castShadowは、それが影を他に落とすかの設定です。receiveShadowは、それが影を受けるかの設定です。 例えば、directionalLightは、影を他に落とすけど自分自身は影を受けないので、castShadowだけ設定します。boxオブジェクトは、影を他に落とすし自分自身も影を受けるので、castShadow・receiveShadowどちらも設定しています。 このため、Box1にはBox2の影が落ちていて、Box1自身も床に影を落としています。 影の解像度が低いときは、lightのshadow-mapSize-width・shadow-mapSize-heightで調整します。 オブジェクトの回転 import React, { useRef, VFC } from 'react'; import { DoubleSide } from 'three'; import { OrbitControls } from '@react-three/drei'; import { Canvas, useFrame } from '@react-three/fiber'; export const ThreeDemo: VFC = () => { return ( <Canvas camera={{ fov: 50, position: [0, 3, 10] }} shadows> <Contents /> </Canvas> ) } const Contents: VFC = () => { const boxRef = useRef<any>(null) useFrame(({ clock }) => { const a = clock.getElapsedTime() boxRef.current.rotation.x = a * 1 boxRef.current.rotation.y = a * 1 boxRef.current.rotation.z = a * 0 }) return ( <> {/* control */} ・・・ {/* light */} ・・・ {/* box 1 */} <mesh ref={boxRef} position={[0, 2, 0]} castShadow receiveShadow> <boxGeometry args={[1, 1, 1]} /> <meshPhongMaterial color="blue" /> </mesh> {/* box 2 */} <mesh position={[1, 3, 2]} scale={0.5} castShadow receiveShadow> <boxGeometry args={[1, 1, 1]} /> <meshPhongMaterial color="red" /> </mesh> {/* floor */} ・・・ </> ) } フレームアニメーションは、useFrameを使うことで実装できます。 Box1のmeshにboxRefを指定することで、Box1が回転します。 useFrameは、Canvas内のコンポーネントでのみ使用できます。 ヘルパーオブジェクト Three.jsには、グリッドやライトを視覚化するためのヘルパーオブジェクトが用意されています。 import React, { useRef, VFC } from 'react'; import { DirectionalLightHelper, DoubleSide } from 'three'; import { OrbitControls, useHelper } from '@react-three/drei'; import { Canvas, useFrame } from '@react-three/fiber'; export const ThreeDemo: VFC = () => { return ( <Canvas camera={{ fov: 50, position: [0, 3, 10] }} shadows> <Contents /> </Canvas> ) } const Contents: VFC = () => { const lightRef = useRef() useHelper(lightRef, DirectionalLightHelper) ・・・ return ( <> {/* control */} ・・・ {/* light */} <directionalLight ref={lightRef} position={[5, 5, 5]} intensity={1} // 光の強さ shadow-mapSize-width={2048} // 描画精度 shadow-mapSize-height={2048} castShadow /> {/* box 1 */} ・・・ {/* box 2 */} ・・・ {/* floor */} ・・・ {/* grid */} <gridHelper position={[0, 0.01, 0]} args={[10, 10, 'red', '#4C4C4C']} /> </> ) } DirectionalLightHelper lightRefをdirectionalLightのプロパティに追加することで、ライトを可視化します。 ヘルパーの生成には、useHelperを使用します。 useHelperは、Canvas内のコンポーネントでのみ使用できます。 gridHelper argsでは、[大きさ, 分割数, 真ん中の線の色, 全体の線の色]を指定しています。 まとめ react-three-fiberを使うと、簡単に動く3D表現ができてとても楽しいです。 ですが、Three.jsのラッパーパッケージなので、Three.jsでの情報はあってもreact-three-fiberの情報がなかなかないです... 実装例は、CodeSandBoxを参照するといいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【エラー】TypeError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': Value is not of type 'long'.(執筆途中)

はじめに Dicomデータをsagittal, coronal表示させる時にタイトルのエラーが出たので記録する。 エラーについて :の右側を読むと、「long型じゃありません」とのこと。つまり、該当箇所の変数の型が本来ならlong型のところそうなってないみたい。 左側の”TypeError: Failed to execute 'getImageData' on 'CanvasRenderingContext2D'”については初めて見るエラーだったので詳しく調べてみることにする。 CanvasRenderingContext2D.getImageData() getImageDataというのは、Canvas2D APIのCanvasRenderingContext2Dというメソッドにある関数である。 この関数は、canvasで指定された部分の基礎となるピクセルデータを表すImageDataオブジェクトを返す。 この関数は引数に抽出するImageDataのx座標、y座標、幅、高さを持つことができます。 MDN Web Docs: CanvasRenderingContext2D.getImageData() この関数に表示させたいImageDataの各座標を渡して、ImageDataを出力するイメージになる。 エラー全体 上記のことを踏まえると今回出ているこのエラーは、getImageData()に渡している引数がlong型になっていないということになるのか...? 実際にこのエラー分をまるまるコピペしてググってみたところ、 ローカル環境で参照する場合はセキュリティ機構の使用によりエラーが発生する Webサーバーを配置してhttpでアクセスする必要がある。 後者の方は、実際にhttpアクセスで読み込んだデータを表示しているシステムを参考にしているの考えにくいと感じた。 最後に かなり雑な調べ方になってしまったが、エラーの概要は掴むことができた。 渡ってきたデータをlong型に変換する方法、実際にサーバーを立ててデータを読んでみる方法、両方とも試してみてどうだったのか、後日記載することにする。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

備忘録: React-Testing-library × Jestを使ったテスト

はじめに テスト対象のコンポーネントの作成 rtl_lessonディレクトリの作成し、ターミナルにて下記のコマンドを実行し、アプリを作成する $ npx create-react-app . --template redux 1. 簡単なテスト はじめに簡単なテストとして各要素が存在するかのテストを作成します。 そのため、テスト対象となるコンポーネントとして、srcディレクトリ直下にRender.jsファイルを作成する src/Render.js import React from "react"; const Render = () => { return ( <div> <h1>React Testing Library Lesson</h1> <input type="text" /> <button>Click1</button> <button>Click2</button> <p>Udemy</p> <span>@React</span> </div> ); }; export default Render; src/Render.test.js import React from "react"; import { render, screen } from "@testing-library/react"; import Render from "./Render"; describe("Rendering", () => { it("Should render all the elements correctly", () => { render(<Render />); expect(screen.getByRole("heading")).toBeTruthy(); expect(screen.getByRole("textbox")).toBeTruthy(); expect(screen.getAllByRole("button")[0]).toBeTruthy(); expect(screen.getAllByRole("button")[1]).toBeTruthy(); expect(screen.getByText("Udemy")).toBeTruthy(); expect(screen.queryByText("Indeed")).toBeNull(); expect(screen.getByTestId("copyright")).toBeTruthy(); }); }); publicフォルダーのindex.htmlが大元になっている 2. userEventとモック関数を使った簡単なテスト src/App.js import React from "react"; import logo from "./logo.svg"; import { Counter } from "./features/counter/Counter"; import "./App.css"; import RenderInput from "./RenderInput"; //追記行 function App() {     /* 以下の3行を追記 */ const output = (text) => { console.log(text);  };  return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <Counter /> <RenderInput outputConsole={output} />                      {/* 追記行 */} <p> Edit <code>src/App.js</code> and save to reload. </p> <span> <span>Learn </span> <a className="App-link" href="https://reactjs.org/" target="_blank" rel="noopener noreferrer"> React </a> <span>, </span> <a className="App-link" href="https://redux.js.org/" target="_blank" rel="noopener noreferrer"> Redux </a> <span>, </span> <a className="App-link" href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer"> Redux Toolkit </a> ,<span> and </span> <a className="App-link" href="https://react-redux.js.org/" target="_blank" rel="noopener noreferrer"> React Redux </a> </span> </header> </div> ); } export default App; src/RenderInput.js import React, { useState } from "react"; const RenderInput = ({ outputConsole }) => { const [input, setInput] = useState(""); const outputValue = () => { if (input) { outputConsole(input); } }; const updateValue = (e) => { setInput(e.target.value); }; return ( <> <input type="text" placeholder="Enter" value={input} onChange={updateValue} /> <button onClick={outputValue}>Console</button> </> ); }; export default RenderInput; src/RenderInput.test.js import React from "react"; import { screen, render, cleanup } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import RenderInput from "./RenderInput"; afterEach(() => cleanup()); describe("Rendering", () => { it("Should render all the elements correctly", () => { render(<RenderInput />); expect(screen.getByRole("textbox")).toBeTruthy(); expect(screen.getByRole("button")).toBeTruthy(); expect(screen.getByPlaceholderText("Enter")).toBeTruthy(); }); }); describe("Input form onChange event", () => { it("Should update input value correctly", () => { render(<RenderInput />); const inputValue = screen.getByPlaceholderText("Enter"); userEvent.type(inputValue, "test"); expect(inputValue.value).toBe("test"); }); }); describe("Console button conditionally triggered", () => { it("Should not trigger output function", () => { const outputConsole = jest.fn(); render(<RenderInput outputConsole={outputConsole} />); userEvent.click(screen.getByRole("button")); expect(outputConsole).not.toHaveBeenCalled(); }); it("Should trigger output function", () => { const outputConsole = jest.fn(); render(<RenderInput outputConsole={outputConsole} />); const inputValue = screen.getByPlaceholderText("Enter"); userEvent.type(inputValue, "test"); userEvent.click(screen.getByRole("button")); expect(outputConsole).toHaveBeenCalledTimes(1); }); }); 3. listに対する簡単なテスト src/App.js import React from "react"; import logo from "./logo.svg"; import { Counter } from "./features/counter/Counter"; import "./App.css"; import RenderInput from "./RenderInput"; import FrameworkList from "./FrameworkList"; //追記行 function App() { //以下のdata定数を追記 const data = [ { id: 1, item: "React", }, { id: 2, item: "Angular", }, { id: 3, item: "Vue", }, ]; const output = (text) => { console.log(text); }; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <Counter /> <RenderInput outputConsole={output} /> /* Frameworkコンポーネントを追記 */ <FrameworkList frameworks={data} /> <p> Edit <code>src/App.js</code> and save to reload. </p> <span> <span>Learn </span> <a className="App-link" href="https://reactjs.org/" target="_blank" rel="noopener noreferrer"> React </a> <span>, </span> <a className="App-link" href="https://redux.js.org/" target="_blank" rel="noopener noreferrer"> Redux </a> <span>, </span> <a className="App-link" href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer"> Redux Toolkit </a> ,<span> and </span> <a className="App-link" href="https://react-redux.js.org/" target="_blank" rel="noopener noreferrer"> React Redux </a> </span> </header> </div> ); } export default App; src/FrameworkList.js import React from "react"; const FrameworkList = (props) => { if (!props.frameworks || !props.frameworks.length) { return <h1>No data!</h1>; } return ( <> <ul> {props.frameworks.map(({ id, item }) => ( <li key={id}>{item}</li> ))} </ul> </> ); }; export default FrameworkList; src/FrameworkList.test.js import React from "react"; import { screen, cleanup, render } from "@testing-library/react"; import FrameworkList from "./FrameworkList"; import { afterEach } from "@jest/globals"; afterEach(() => cleanup()); describe("Rendering the list with props", () => { it("Should render No data ! when no data propped", () => { render(<FrameworkList />); expect(screen.getByText("No data!")).toBeInTheDocument(); }); it("Should render list item correctly", () => { const dummyData = [ { id: 1, item: "React dymmy" }, { id: 2, item: "Angular dymmy" }, { id: 3, item: "Vue dymmy" }, ]; render(<FrameworkList frameworks={dummyData} />); const frameworkItems = screen .getAllByRole("listitem") .map((item) => item.textContent); const dummyItems = dummyData.map((ele) => ele.item); expect(frameworkItems).toEqual(dummyItems); expect(screen.queryByText("No data!")).toBeNull(); }); }); 4. useEffectを使ったテスト $ npm install axios src/useEffectRender.js import React, { useState, useEffect } from "react"; import axios from "axios"; const UseEffectRender = () => { const [user, setUser] = useState(null); const fetchJSON = async () => { const res = await axios.get("https://jsonplaceholder.typicode.com/users/1"); return res.data; }; useEffect(() => { const fetchUser = async () => { const json_user = await fetchJSON(); setUser(json_user); }; fetchUser(); }, []); return ( <> {user ? ( <p> I am {user.username} : {user.email} </p> ) : null} </> ); }; export default UseEffectRender; src/useEffectRender.test.js import React from "react"; import { screen, render } from "@testing-library/react"; import UseEffectRender from "./UseEffectRender"; describe("useEffect rendering", () => { it("Should render only after function resolved", async () => { render(<UseEffectRender />); expect(screen.queryByText(/I am/)).toBeNull(); expect(await screen.findByText(/I am/)).toBeInTheDocument(); }); }); 5. モックAPIを使ったテスト $ npm install msw src/MockServer.js import React, { useState } from "react"; import axios from "axios"; const MockServer = () => { const [clicked, setClicked] = useState(false); const [username, setUsername] = useState(""); const [error, setError] = useState(""); const fetchUser = async () => { try { const res = await axios.get( "https://jsonplaceholder.typicode.com/users/1" ); const { username } = res.data; setUsername(username); setClicked(true); } catch { setError("Fetching Failed!"); } }; const buttonText = clicked ? "Loaded" : "Start Fetch"; return ( <> <button onClick={fetchUser} disabled={clicked}> {buttonText} </button> {username && <h3>{username}</h3>} {error && <p data-testid="error">{error}</p>} </> ); }; export default MockServer; src/MockServer.test.js import React from "react"; import { screen, render, cleanup } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { rest } from "msw"; import { setupServer } from "msw/node"; import MockServer from "./MockServer"; const server = setupServer( rest.get("https://jsonplaceholder.typicode.com/users/1", (req, res, ctx) => { return res(ctx.status(200), ctx.json({ username: "Bred dummy" })); }) ); beforeAll(() => server.listen()); afterEach(() => { server.resetHandlers(); cleanup(); }); afterAll(() => server.close()); describe("Mocking API", () => { it("[Fetch success]Should display fetched data correctly and button disable", async () => { render(<MockServer />); userEvent.click(screen.getByRole("button")); expect(await screen.findByText("Bred dummy")).toBeInTheDocument(); expect(screen.getByRole("button")).toHaveAttribute("disabled"); }); it("[Fetch failure]Should display error msg, no render heading and button abled", async () => { server.use( rest.get( "https://jsonplaceholder.typicode.com/users/1", (req, res, ctx) => { return res(ctx.status(404)); } ) ); render(<MockServer />); userEvent.click(screen.getByRole("button")); expect(await screen.findByTestId("error")).toHaveTextContent( "Fetching Failed!" ); expect(screen.queryByRole("heading")).toBeNull(); expect(screen.getByRole("button")).not.toHaveAttribute("disabled"); }); }); 6. ReduxToolKitを使用したテスト(Reducerのテスト) app/store.js import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "../features/counter/counterSlice"; import customCounterReducer from "../features/customCounter/customCounterSlice"; export const store = configureStore({ reducer: { counter: counterReducer, customCounter: customCounterReducer, }, }); src/customCounterSlice.js import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import axios from "axios"; const sleep = (msec) => { const start = new Date(); while (new Date() - start < msec); }; export const fetchDummy = createAsyncThunk("fetch/dummy", async (num) => { await sleep(2000); return num; }); export const fetchJSON = createAsyncThunk("fetch/api", async () => { const res = await axios.get("https://jsonplaceholder.typicode.com/users/1"); const { username } = res.data; return username; }); const initialState = { mode: 0, value: 0, username: "", }; export const customCounterSlice = createSlice({ name: "customCounter", initialState, reducers: { increment: (state) => { switch (state.mode) { case 0: state.value += 1; break; case 1: state.value += 100; break; case 2: state.value += 10000; break; default: break; } }, decrement: (state) => { state.value -= 1; }, incrementByAmount: (state, action) => { switch (state.mode) { case 0: state.value += action.payload; break; case 1: state.value += 100 * action.payload; break; case 2: state.value += 10000 * action.payload; break; default: break; } }, }, extraReducers: (builder) => { builder.addCase(fetchDummy.fulfilled, (state, action) => { state.value = 100 + action.payload; }); builder.addCase(fetchDummy.rejected, (state, action) => { state.value = 100 - action.payload; }); builder.addCase(fetchJSON.fulfilled, (state, action) => { state.username = action.payload; }); builder.addCase(fetchJSON.rejected, (state, action) => { state.username = "anonymous"; }); }, }); export const { increment, decrement, incrementByAmount } = customCounterSlice.actions; export const selectCount = (state) => state.customCounter.value; export const selectUsername = (state) => state.customCounter.username; export default customCounterSlice.reducer; 非同期関数をテストするために、fetchDummy関数はsleep関数の引数に受けった値分だけ待機して値を返している src/Reducer.test.js import reducer, { increment, incrementByAmount, fetchDummy, } from "../src/features/customCounter/customCounterSlice"; describe("Reducer of ReduxToolKit", () => { describe("increment action", () => { let initialState = { mode: 0, value: 1, }; it("Should increment by 1 with mode 0", () => { const action = { type: increment.type }; const state = reducer(initialState, action); expect(state.value).toEqual(2); }); it("Should increment by 100 with mode 1", () => { initialState.mode = 1; const action = { type: increment.type }; const state = reducer(initialState, action); expect(state.value).toEqual(101); }); it("Should increment by 10000 with mode 2", () => { initialState.mode = 2; const action = { type: increment.type }; const state = reducer(initialState, action); expect(state.value).toEqual(10001); }); }); describe("incrementByAmount action", () => { let initialState = { mode: 0, value: 1, }; it("Should increment by payload value with mode 0", () => { const action = { type: incrementByAmount.type, payload: 2 }; const state = reducer(initialState, action); expect(state.value).toEqual(3); }); it("Should increment by 100 * payload value with mode 1", () => { initialState.mode = 1; const action = { type: incrementByAmount.type, payload: 2 }; const state = reducer(initialState, action); expect(state.value).toEqual(201); }); it("Should increment by 10000 * payload value with mode 2", () => { initialState.mode = 2; const action = { type: incrementByAmount.type, payload: 2 }; const state = reducer(initialState, action); expect(state.value).toEqual(20001); }); }); describe("extraReducers", () => { const initialState = { mode: 0, value: 0, }; it("Should output 100 + payload when fulfilled", () => { const action = { type: fetchDummy.fulfilled.type, payload: 5 }; const state = reducer(initialState, action); expect(state.value).toEqual(105); }); it("Should output 100 - payload when rejected", () => { const action = { type: fetchDummy.rejected.type, payload: 5 }; const state = reducer(initialState, action); expect(state.value).toEqual(95); }); }); }); 7. Integrationテストの作成 src/App.js import React from "react"; import logo from "./logo.svg"; import { Counter } from "./features/counter/Counter"; import "./App.css"; import RenderInput from "./RenderInput"; import FrameworkList from "./FrameworkList"; import UseEffectRender from "./UseEffectRender"; import MockServer from "./MockServer"; import Redux from "./Redux"; function App() { const data = [ { id: 1, item: "React", }, { id: 2, item: "Angular", }, { id: 3, item: "Vue", }, ]; const output = (text) => { console.log(text); }; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <Counter /> <RenderInput outputConsole={output} /> <FrameworkList frameworks={data} /> <UseEffectRender /> <MockServer /> <Redux /> <p> Edit <code>src/App.js</code> and save to reload. </p> <span> <span>Learn </span> <a className="App-link" href="https://reactjs.org/" target="_blank" rel="noopener noreferrer"> React </a> <span>, </span> <a className="App-link" href="https://redux.js.org/" target="_blank" rel="noopener noreferrer"> Redux </a> <span>, </span> <a className="App-link" href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer"> Redux Toolkit </a> ,<span> and </span> <a className="App-link" href="https://react-redux.js.org/" target="_blank" rel="noopener noreferrer"> React Redux </a> </span> </header> </div> ); } export default App; src/Redux.js import React, { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { selectCount, increment, decrement, incrementByAmount, } from "./features/customCounter/customCounterSlice"; const Redux = () => { const [number, setNumber] = useState(0); const count = useSelector(selectCount); const dispatch = useDispatch(); return ( <> <h3>Redux Integration Test</h3> <div> <button onClick={() => dispatch(increment())}>+</button> <span data-testid="count-value">{count}</span> <button onClick={() => dispatch(decrement())}>-</button> <button onClick={() => dispatch(incrementByAmount(number | 0))}> IncrementByAmount </button> <input type="text" placeholder="Enter" value={number} onChange={(e) => setNumber(e.target.value)}></input> </div> </> ); }; export default Redux; src/Redux.test.js import React from "react"; import { render, screen, cleanup } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { Provider } from "react-redux"; import Redux from "./Redux"; import { configureStore } from "@reduxjs/toolkit"; import customCounterReducer from "./features/customCounter/customCounterSlice"; afterEach(() => { cleanup(); }); describe("Redux Integration Test", () => { let store; beforeEach(() => { store = configureStore({ reducer: { customCounter: customCounterReducer, }, }); }); it("Should display value with increment by 1 per click", () => { render( <Provider store={store}> <Redux /> </Provider> ); userEvent.click(screen.getByText("+")); userEvent.click(screen.getByText("+")); userEvent.click(screen.getByText("+")); expect(screen.getByTestId("count-value")).toHaveTextContent(3); }); it("Should display value with decrement by 1 per click", () => { render( <Provider store={store}> <Redux /> </Provider> ); userEvent.click(screen.getByText("-")); userEvent.click(screen.getByText("-")); expect(screen.getByTestId("count-value")).toHaveTextContent(-2); }); it("Should display value with incrementByAmount", () => { render( <Provider store={store}> <Redux /> </Provider> ); userEvent.click(screen.getByText("-")); userEvent.click(screen.getByText("-")); expect(screen.getByTestId("count-value")).toHaveTextContent(2); }); it("Should display value with incrementByAmount", () => { render( <Provider store={store}> <Redux /> </Provider> ); userEvent.type(screen.getByPlaceholderText("Enter"), "30"); userEvent.click(screen.getByText("IncrementByAmount")); expect(screen.getByTestId("count-value")).toHaveTextContent(30); }); }); ```src/ReduxAsync.js import React from "react"; import { useSelector, useDispatch } from "react-redux"; import { selectCount, selectUsername, fetchDummy, fetchJSON, } from "./features/customCounter/customCounterSlice"; const ReduxAsync = () => { const count = useSelector(selectCount); const username = useSelector(selectUsername); const dispatch = useDispatch(); return ( <div> <span data-testid="count-value">{count}</span> <button onClick={() => dispatch(fetchDummy(5))}>fetchDummy</button> {username && <h1>{username}</h1>} <button onClick={() => dispatch(fetchJSON())}>fetchJSON</button> </div> ); }; export default ReduxAsync; src/ReducxAsync.test.js import React from "react"; import { render, screen, cleanup } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { rest } from "msw"; import { setupServer } from "msw/node"; import { Provider } from "react-redux"; import { configureStore } from "@reduxjs/toolkit"; import customCounterReducer from "../src/features/customCounter/customCounterSlice"; import ReduxAsync from "./ReduxAsync"; const server = setupServer( rest.get("https://jsonplaceholder.typicode.com/users/1", (req, res, ctx) => { return res(ctx.status(200), ctx.json({ username: "Bred dummy" })); }) ); beforeAll(() => server.listen()); let store; beforeEach(() => { store = configureStore({ reducer: { customCounter: customCounterReducer, }, }); }); afterEach(() => { server.resetHandlers(); cleanup(); }); afterAll(() => server.close()); describe("ReduxAsync Test", () => { it("Should display value with 100 + payload", async () => { render( <Provider store={store}> <ReduxAsync /> </Provider> ); expect(screen.getByTestId("count-value")).toHaveTextContent(0); userEvent.click(screen.getByText("fetchDummy")); expect(await screen.findByTestId("count-value")).toHaveTextContent(105); }); }); describe("Redux Async API Mocking", () => { it("[Fetch success] Should display username in h3 tag", async () => { render( <Provider store={store}> <ReduxAsync /> </Provider> ); expect(screen.queryByText("Bred dummy")).toBeNull(); userEvent.click(screen.getByText("fetchJSON")); expect(await screen.findByText("Bred dummy")).toBeInTheDocument(); }); it("[Fetch failed] Should display username in h3 tag", async () => { server.use( rest.get( "https://jsonplaceholder.typicode.com/users/1", (req, res, ctx) => { return res(ctx.status(404)); } ) ); render( <Provider store={store}> <ReduxAsync /> </Provider> ); expect(screen.queryByText("Bred dummy")).toBeNull(); userEvent.click(screen.getByText("fetchJSON")); expect(await screen.findByText("anonymous")).toBeInTheDocument(); }); }); 8. Custom Hooksを使用したテスト $ npm i @testing-library/react-hooks $ npm i react-test-renderer src/App.js import React from "react"; import logo from "./logo.svg"; import { Counter } from "./features/counter/Counter"; import "./App.css"; import RenderInput from "./RenderInput"; import FrameworkList from "./FrameworkList"; import UseEffectRender from "./UseEffectRender"; import MockServer from "./MockServer"; import Redux from "./Redux"; import ReduxAsync from "./ReduxAsync"; import CustomHooks from "./CustomHooks"; function App() { const data = [ { id: 1, item: "React", }, { id: 2, item: "Angular", }, { id: 3, item: "Vue", }, ]; const output = (text) => { console.log(text); }; return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <Counter /> <RenderInput outputConsole={output} /> <FrameworkList frameworks={data} /> <UseEffectRender /> <MockServer /> <Redux /> <ReduxAsync /> <CustomHooks /> <p> Edit <code>src/App.js</code> and save to reload. </p> <span> <span>Learn </span> <a className="App-link" href="https://reactjs.org/" target="_blank" rel="noopener noreferrer"> React </a> <span>, </span> <a className="App-link" href="https://redux.js.org/" target="_blank" rel="noopener noreferrer"> Redux </a> <span>, </span> <a className="App-link" href="https://redux-toolkit.js.org/" target="_blank" rel="noopener noreferrer"> Redux Toolkit </a> ,<span> and </span> <a className="App-link" href="https://react-redux.js.org/" target="_blank" rel="noopener noreferrer"> React Redux </a> </span> </header> </div> ); } export default App; src/customHooks.js import React from "react"; import { useCounter } from "./useCounter"; const CustomHooks = () => { const { count, increment, decrement, double, triple, reset } = useCounter(3); return ( <> <p>{count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> <button onClick={triple}>triple</button> <button onClick={double}>double</button> <button onClick={reset}>reset</button> </> ); }; export default CustomHooks; src/useCounter.js import { useState } from "react"; export const useCounter = (initialCount) => { const [count, setCount] = useState(initialCount); const increment = () => { setCount((count) => count + 1); }; const decrement = () => { setCount((count) => count - 1); }; const double = () => { setCount((count) => count * 2); }; const triple = () => { setCount((count) => count * 3); }; const reset = () => { setCount(0); }; return { count, increment, decrement, double, triple, reset }; }; src/useCounter.test.js import { useCounter } from "./useCounter"; import { act, renderHook } from "@testing-library/react-hooks"; import { cleanup } from "@testing-library/react"; afterEach(() => cleanup()); describe("useCounter custom Hook", () => { it("Should increment by 1", () => { const { result } = renderHook(() => useCounter(3)); expect(result.current.count).toBe(3); act(() => { result.current.increment(); }); expect(result.current.count).toBe(4); }); it("Should decrement by 1", () => { const { result } = renderHook(() => useCounter(3)); expect(result.current.count).toBe(3); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(2); }); it("Should double by 3", () => { const { result } = renderHook(() => useCounter(3)); expect(result.current.count).toBe(3); act(() => { result.current.double(); }); expect(result.current.count).toBe(6); }); it("Should triple by 3", () => { const { result } = renderHook(() => useCounter(3)); expect(result.current.count).toBe(3); act(() => { result.current.triple(); }); expect(result.current.count).toBe(9); }); it("Should reset to 0", () => { const { result } = renderHook(() => useCounter(3)); expect(result.current.count).toBe(3); act(() => { result.current.reset(); }); expect(result.current.count).toBe(0); }); }); customHooksのテストをする場合はrenderHook()を使用する customHooks内の関数を実行する場合はact()内で関数を実行して使用する tips create-react-appコマンドで作成するとデフォルトで, React Testing LibraryとJestが導入されている Roleの確認 本記事の作成時のJestのバージョンは26.2 テスト実行時にテストタイトルも表示したい場合はpackage.json内のtests項目を編集"test": "react-scripts test --env=jsdom --verbose" afterEachはテスト(it)が終了した直後にrenderによって取得したコンポーネントをアンマウントしてくれる jsonplaceholder たぶんこれが一番分かりやすいと思います React + Redux のフロー図解
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

はじめに 学習するに至った経緯 2020年より、未経験からエンジニアへの転職を目指し、某プログラミングスクールへ通う。入学後、『Ruby』を未経験から学ぶ人が多いのと『Ruby』の求人が思っていた以上に少ないので、卒業後、フロントエンドのエンジニアを目指す事に。 Reactの学習した事を言語化し、認識の深化による備忘録として記載。 参考サイト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

基礎から学ぶReact/React Hooks学習メモ 6-1 環境構築とアプリ作成準備

6-1 Create React AppでReact開発環境を準備する 参考 基礎から学ぶReact/React Hooks Create React Appの実行 # nodeのバージョン確認 node -v # yarnのインストール npm install -g yarn # アプリケーションを作成したディレクトリに移動して、create-react-app プロジェクト名 npx create-react-app todo-app # 作成されたディレクトリに移動 cd todo-app # アプリ起動 yarn start Visual Studio Codeを利用する インストーラーをダウンロード&インストール 不要ファイルの削除と修正 todo-app/ ┣ node_modules/ ┣ public/ ┣ src/ ┃ ┣ App.css ← 削除 ┃ ┣ App.js ┃ ┣ App.test.js ← 削除 ┃ ┣ index.css ← 削除 ┃ ┣ index.js ┃ ┣ logo.svg ← 削除 ┃ ┣ reportWebVitals.js ← 削除 ┃ ┗ setupTests.js ← 削除 ┣ .gitignore ┣ package.json ┣ README.md ┗ yarn.lock App.js function App() { return <p>これからTODOアプリを実装します!</p>; } export default App; index.js import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById("root") ); ToDoアプリの仕様 TODOを完了/未完了別にリスト化して表示 TODOの追加ができる TODOの状態(完了/未完了)をボタンクリックで変更できる TODOの削除ができる モックサーバーでTODO情報を更新管理できる モックサーバーの準備 db.json { "todos": [ { "id": 1, "content": "Create react appをインストールする", "done": true }, { "id": 2, "content": "JSON Server仮のAPIを作成する", "done": false }, { "id": 3, "content": "Chakra UIをインストールする", "done": false } ] } ポート3100番で起動 npx json-server --watch db.json --port 3100 axiosのインストール yarn add axios axiosの使用例 const todoDataUrl = "http://localhost:3100/todos" // axios.get(URL)でgetリクエスト送信 axios.get(todoDataUrl) ulidのインストール ユニークなIDを付与するために、ulidをインストールする。ソート可能でランダムなIDを生成する。 yarn add ulid
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material-UI V5(MUI)へのマイグレーション

はじめに ちょっと前からMaterial-UI使っているので浅い知識で色々試行中ですが・・・ v5が2021/09に正式リリースになったので、マイグレーションしてみました。 v5機能 MUI Versionsに記載があります リリースノートによると npmで200M DL/月→ 950M DL/月 コントリビューターは600人以上 この数字はv4→v5の1年間での成長だと思いますが、エコシステムとして順調に成長している感じですね。 個人的には色々コンポーネント試用しましたが、直感的で使いやすいと感じていますし、 ドキュメントも充実していて十分業務でも使えます。 https://deploy-preview-27912--material-ui.netlify.app/blog/material-ui-is-now-mui/ パッケージ見ればわかりますが名称が「MUI」ですね ロゴも変更されてます v5へのマイグレーション 実装量も少ないので、移行ガイドに従ってやってみました。 手修正な部分もありますが基本は「@mui/codemod@next」でマイグレーションできます。 基本的なコンポーネントは互換性があるように見えます。 スタイル周りで非互換や非推奨がありそうですが、まずは移行させて動作確認してを繰り返しですかね? まだ新しいコンポーネントなどは試していませんが。今後試していきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】コンポーネントのメモ化で怒られる / ESLint: Component definition is missing display name(react/display-name)

はじめに コンポーネントのメモ化を試みたところESLintで怒られた、のでその時の対処法。 メモ化 memo(コンポーネント); 「コンポーネントの定義に表示名がない」と言われている ESLint: Component definition is missing display name(react/display-name) 開発環境でのデバッグ時に、複数のコンポーネントが存在する場合エラーメッセージからのデバッグが難しくなってしまう。 ということがあってこのように怒られるらしい。 サンプルコード 怒られる // ESLint: Component definition is missing display name(react/display-name) export const Foo: React.VFC = memo(function foo() { return <div>memoサンプル</div>; }); 解決策 無名関数から普通の名前付き関数にする export const Foo: React.VFC = memo(function foo() { return <div>memoサンプル</div>; }); ESLintを部分的に disable にする // eslint-disable-next-line react/display-name export const Foo: React.VFC = memo(() => { return <div>memoサンプル</div>; }); 最後に ESLintの設定でそもそも回避できると思いますが、そこまで調べられておらず。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React+TypeScript+VS Codeでイベント処理関数に適切な型を「調査」して「定義」する方法

イベント処理の型を覚えるのは大変 フォームやインプットなどのイベントハンドラに渡すイベント処理関数に対する型は、 FormEventHandler<HTMLFormElement> ChangeEventHandler<HTMLInputElement> といった型のエイリアスを呼びだして定義するか、 (event: FormEvent<HTMLFormElement>) => void (event: ChangeEvent<HTMLInputElement>) => void のように型のエイリアスと組み合わせて定義します。 このほかにもエイリアスが存在するため、全てを覚えるのは大変です。 仮に一字一句覚えたとしても誤って不適切な型を定義するとエラーの元になります。 本記事では、VS Codeの機能を活用して適切な型の調査方法と定義方法について紹介します。 また応用編として、Material-UIの定義方法について紹介します。 前提 VS Codeは2021/9/15時点の最新版 Reactプロジェクトのモジュール構成は次の通り(Node.jsはv15.9.0) package.json(一部抜粋) { "dependencies": { "@emotion/react": "^11.4.1", "@emotion/styled": "^11.3.0", "@mui/icons-material": "^5.0.0-rc.0", "@mui/material": "^5.0.0-rc.0", "@mui/styled-engine": "^5.0.0-rc.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.1.0", "@testing-library/user-event": "^13.2.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-scripts": "4.0.3" }, "devDependencies": { "@types/jest": "^27.0.1", "@types/node": "^16.7.10", "@types/react": "^17.0.19", "@types/react-dom": "^17.0.9", "@types/react-router-dom": "^5.1.8", "typescript": "^4.4.3" }, } tsconfig.jsonは"strict": trueを設定して、厳格に型をチェックしています。 そもそも型を定義しないと何が起こるのか? 簡単なフォームとイベント処理関数を型定義しないで実装したとします。 App.tsx import { memo, VFC } from 'react'; const App: VFC = memo(() => { // formタグのイベント処理 const handleOnSubmit = (event) => { console.log(event); }; // inputタグのイベント処理 const handleOnChange = (event) => { console.log(event); }; return ( <form onSubmit={handleOnSubmit}> <input type="text" name="text" onChange={handleOnChange} /> <input type="submit" value="submit" /> </form> ); }); export default App; このときのVS Codeでは、各イベント処理のeventに対して次のエラーが出ます。 (eventの上にマウスカーソルをホバーすると見ることができます。) また、yarn startで実行すると、VS Codeのコンソールと表示用のブラウザに次のエラーが表示されます。 /(中略)/App.tsx TypeScript error in /(中略)/App.tsx(5,27): Parameter 'event' implicitly has an 'any' type. TS7006 3 | const App: VFC = memo(() => { 4 | // formタグのイベント処理 > 5 | const handleOnSubmit = (event) => { | ^ 6 | console.log(event); 7 | }; 8 | これはeventに対して暗黙的にany型が定義されていることが原因です。 jsxで保存してyarn startで実行すると正常動きますが、TypeScriptではこのような暗黙的なany型の定義があると許してくれません。厳しい… このエラーを回避するにはイベント処理関数に型を定義するか、次のようにイベントハンドラに直接コールバックを書く必要があります。 <form onSubmit={(event) => {console.log(event);}}> この状態でeventにマウスカーソルをホバーすると、eventに対してFormEventHandler<HTMLFormElement>の型が自動的に定義されていることがわかります。便利! ただし、リファクタリングでコールバック関数の部分を抜き出して、イベント処理関数として定義しようとすると再びエラーが発生するため、イベント処理関数への型定義は避けて通れません。 ではどうするのか? 型の調査方法 実は、前章でヒントが書かれています。 それは マウスカーソルをホバー です!! formタグのonSubmitとinputタグのonChangeにそれぞれマウスカーソルをホバーすると、VS Codeの機能として次のヒントが表示されます。 formonSubmit inputonChange ここで重要になるのが、onSubmitやonChangeに定義されている型です。 一部省略しますが、次のように型が定義されています。 (省略).onSubmit?: React.FormEventHandler<HTMLFormElement> | undefined (省略).onChange?: React.ChangeEventHandler<HTMLInputElement> | undefined React.FormEventHandleやReact.ChangeEventHandlerは'react'モジュールで用意されている型のエイリアスです。 これは、冒頭に述べた型のエイリアスそのもです。 冒頭のエイリアス(再掲) FormEventHandler<HTMLFormElement> ChangeEventHandler<HTMLInputElement> 型の定義方法 これらの型のエイリアスを使ってイベント処理関数に対して型を定義することで、イベントハンドラに直接コールバックを書いたときのようにeventに対して適切な型が自動的に設定されるようになります。 まず、ホバーした際に表示されるヒントの上にマウスカーソルを移動して、エイリアスの部分をドラッグ&ドロップすると文字列が選択できます。 これをコピーして、イベント処理関数を定義している変数に対してエイリアスを貼り付けて型を定義します。これだけ。 作業の様子は次のようになります。 型を定義した瞬間にhandleOnSubmit関数の引数eventのエラー表示(赤波線)が消えます。 このeventにマウスカーソルをホバーすると、自動的に型定義されていることが確認できます。 これは、formタグのイベントハンドラにコールバックを設定したときと同じ型が定義されています。 handleOnChangeも同様の手順で型を定義してコードを整理すると、次のようになります。 App.tsx import { ChangeEventHandler, FormEventHandler, memo, VFC } from 'react'; const App: VFC = memo(() => { // formタグのイベント処理 const handleOnSubmit: FormEventHandler<HTMLFormElement> = (event) => { console.log(event); }; // inputタグのイベント処理 const handleOnChange: ChangeEventHandler<HTMLInputElement> = (event) => { console.log(event); }; return ( <form onSubmit={handleOnSubmit}> <input type="text" name="text" onChange={handleOnChange} /> <input type="submit" value="submit" /> </form> ); }); export default App; 型のエイリアスFormEventHandlerとChangeEventHandlerは、'react'モジュールに格納されているため、次のように呼び出して使うことができます。 import { ChangeEventHandler, FormEventHandler, memo, VFC } from 'react'; ここでFormEventHandlerとChangeEventHandlerについて簡単に説明します。 FormEventHandlerとChangeEventHandlerの正体 これらもマウスカーソルをホバーすることでヒントを見ることができます。 FormEventHandler ChangeEventHandler それぞれ右辺を抽出すると、次のようになります。 (event: FormEvent<T>) => void (event: ChangeEvent<T>) => void ここで<T>はジェネリクスなので、<HTMLFormElement>と<HTMLInputElement>がそれぞれ代入されて、次のようになります。 (event: FormEvent<HTMLFormElement>) => void (event: ChangeEvent<HTMLInputElement>) => void つまりFormEventHandlerとChangeEventHandlerは戻り値無しの関数の型がエイリアスとして定義されていたわけです。 型定義の恩恵 ここまで、型の調査方法と定義方法について説明しました。 JavaScript( jsx )で書くよりも手間が掛かるため面倒ですが、型を定義することで手間に見合った恩恵を受けることができます。 それぞれのイベント処理に入れたconsole.log(event)に.を入力してみてください。 すると次のようにインテリセンスが働きます。これが一つ目の恩恵です。 JavaScript( jsx )で同じことをやろうとすると、上記の候補は一つも出ません。 出てくるの使ったことがある語句程度です。 次に、それぞれのイベント処理関数に入れたconsole.log(event)をconsole.log(event.target.value)に変更します。 するとhandleOnSubmit側のvalueにエラーが発生します。 valueにマウスカーソルをホバーすると、エラーメッセージが次のように表示されます。 これは、formのevent.targetにはそもそもvalueのプロパティが存在しないからです。 (formの場合、event.currentTargetからFormDataに投げるなどして加工する必要があります) このエラー表示が二つ目の恩恵です。 応用編:Material-UIの定義方法 ここまではformやinputなどのhtmlタグに対しての説明でした。 Reactには様々なUIフレームワークが存在します。 これらを使う場合もしっかりと型を定義する必要があります。 本章では応用編としてUIフレームワークの一つ、Material-UIの定義方法を説明します。 ソースコード 最初と同様に簡単なインプットフォームと、イベント処理関数を定義したとします。 SignIn.tsx import { memo, VFC } from 'react'; import { Box, Button, Container, createTheme, CssBaseline, TextField, ThemeProvider } from '@mui/material'; const theme = createTheme(); const SignIn: VFC = memo(() => { const handleOnSubmit = (event) => { console.log(event); }; const handleOnChange = (event) => { console.log(event); }; return ( <ThemeProvider theme={theme}> <Container component="main" maxWidth="xs"> <CssBaseline /> <Box sx={{marginTop: 8, display: 'flex', flexDirection: 'column', alignItems: 'center'}} > <Box component="form" onSubmit={handleOnSubmit} noValidate sx={{ mt: 1 }}> <TextField margin="normal" required fullWidth id="user" label="ニックネーム" name="user" autoFocus onChange={handleOnChange} /> <Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}> はじめる </Button> </Box> </Box> </Container> </ThemeProvider> ); }); export default SignIn; この中で、フォームに関する記述は次のようになります。 フォームのみを抜粋 <Box component="form" onSubmit={handleOnSubmit} noValidate sx={{ mt: 1 }}> <TextField margin="normal" required fullWidth id="user" label="ニックネーム" name="user" autoFocus onChange={handleOnChange} /> <Button type="submit" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}> はじめる </Button> </Box> 型の調査 BoxのonSubmitとTextFieldのonChangeにマウスカーソルをホバーすると、次のようになります。 BoxonSubmit TextFieldonChange BoxのonSubmitはコピーアンドペーストで何とかなりそうですが、TextFieldのonChangeは特にエイリアスで定義されているわけではなく、onChange:(event:any)=>voidとなっています。 これはonSubmitに対して3つの関数が定義されていることに起因しています。 command(windowsはctrl)を押しながらクリックすると、次のように展開されます。 これはイベントハンドラに入れるモノによって決まります。 今回の場合、型が未定義のイベント処理関数を入れたことで、3つのうち1つの関数が確定して最終的に(event:any)=>voidが表示されています。 しかし、これではダメです。 では、どうするのか? 答えは、コールバックを入れるです。 コールバックを入れた状態でマウスカーソルをホバーすると、次のようになります。 型を定義 これらの情報をもとに型を定義すると、次のようになります。 const handleOnSubmit:FormEventHandler<HTMLFormElement> = (event) => { console.log(event); }; const handleOnChange:(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void = (event) => { console.log(event); }; まとめ イベント処理関数に適切な型を「調査」して「定義」する方法と応用方法について説明しました。 まとめると、型調査はマウスカーソルをホバーで型のエイリアスまたは関数の型を表示して、この情報をもとに型を定義する流れとなります。 しかし、イベントハンドラに複数の関数が定義されている場合、型調査で(event:any)=>voidと表示されることがあります。 その場合は、イベントハンドラにコールバック関数を入れて変化を確認してみてください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む