20210605のReactに関する記事は11件です。

【React】Contextを使用しているコンポーネントのテストを実装

Testing Library学習中ということで、今回はContextの状態を使用しているコンポーネントでテストを行う方法についてまとめました。 実装 以下のコンポーネントで構成された画面を考えます。 Scoopsで個数を入力するとScoops total(小計)、Toppingsを選択するとToppings total(小計)、それぞれの合計がGrand totalとして表示されます。 選択した個数を更新する関数や小計や合計の状態をContextで管理します。 Contextの実装 Contextオブジェクト(OrderDetails)とContext.Provider(OrderDetails.Provider)を以下のように作成します。 formatCurrencyは金額を小数2桁(ex. 0.00, 2.00)で表示するようにフォーマットする関数です。 OrderDetails.jsx import { createContext, useContext, useState, useMemo, useEffect } from 'react'; import { pricePerItem } from '../constants'; import { formatCurrency } from '../utilities'; const OrderDetails = createContext(); export function useOrderDetails() { const context = useContext(OrderDetails); if (!context) { throw new Error( 'useOrderDetails must be used within an OrderDetailsProvider' ); } return context; } function calculateSubtotal(optionType, optionCounts) { let optionCount = 0; for (const count of optionCounts[optionType].values()) { optionCount += count; } return optionCount * pricePerItem[optionType]; } export function OrderDetailsProvider(props) { const [optionCounts, setOptionCounts] = useState({ scoops: new Map(), toppings: new Map(), }); const zeroCurrency = formatCurrency(0); const [totals, setTotals] = useState({ scoops: zeroCurrency, toppings: zeroCurrency, grandTotal: zeroCurrency, }); useEffect(() => { const scoopsSubtotal = calculateSubtotal('scoops', optionCounts); const toppingsSubtotal = calculateSubtotal('toppings', optionCounts); const grandTotal = scoopsSubtotal + toppingsSubtotal; setTotals({ scoops: formatCurrency(scoopsSubtotal), toppings: formatCurrency(toppingsSubtotal), grandTotal: formatCurrency(grandTotal), }); }, [optionCounts]); const value = useMemo(() => { function updateItemCount(itemName, newItemCount, optionType) { const newOptionCounts = { ...optionCounts }; // update option count for this item with the new value const optionCountsMap = optionCounts[optionType]; optionCountsMap.set(itemName, parseInt(newItemCount)); setOptionCounts(newOptionCounts); } return [{ ...optionCounts, totals }, updateItemCount]; }, [optionCounts, totals]); return <OrderDetails.Provider value={value} {...props} />; } App以下のコンポーネントで状態と更新関数を使うために、作成したProviderを以下のようにラッピングします。 App.js function App() { return ( <Container> <OrderDetailsProvider> <OrderEntry /> </OrderDetailsProvider> </Container> ); } ラッピングされたコンポーネント内でContextを使用するために、const [orderDetails, updateItemCount] = useOrderDetails()でorderDetailsとupdateItemCountを読み込みます。 更新された小計はtotal: {orderDetails.totals[optionType]}のように表示されます。 Options.jsx export default function Options({ optionType }) { const [items, setItems] = useState([]); const [error, setError] = useState(false); const [orderDetails, updateItemCount] = useOrderDetails(); // optionType is 'scoop' or 'toppings' useEffect(() => { axios .get(`http://localhost:3030/${optionType}`) .then((response) => setItems(response.data)) .catch((error) => setError(true)); }, [optionType]); if (error) { return <AlertBanner />; } // TODO: replace `null` with ToppingOption when available const ItemComponent = optionType === 'scoops' ? ScoopOption : ToppingOption; const title = optionType[0].toUpperCase() + optionType.slice(1).toLowerCase(); //頭文字だけ大文字 const optionItems = items.map((item) => ( <ItemComponent key={item.name} name={item.name} imagePath={item.imagePath} updateItemCount={(itemName, newItemCount) => updateItemCount(itemName, newItemCount, optionType) } /> )); return ( <> <h2>{title}</h2> <p>{formatCurrency(pricePerItem[optionType])} each</p> <p> {title} total: {orderDetails.totals[optionType]} </p> <Row>{optionItems}</Row> </> ); } テストの実装 Contextの状態を利用するコンポーネントをテストする際、renderで仮想DOMを生成するときに第2引数に{ wrapper: OrderDetailsProvider }を与えてあげる必要があります。 testUpdates.test.jsx import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Options from '../Options'; import { OrderDetailsProvider } from '../../../contexts/OrderDetails'; import OrderEntry from '../OrderEntry'; test('update scoop subtotal when scoops change', async () => { render(<Options optionType="scoops" />, { wrapper: OrderDetailsProvider }); // make sure total starts out $0.00 const scoopsSubtotal = screen.getByText('Scoops total: $', { exact: false }); expect(scoopsSubtotal).toHaveTextContent('0.00'); // update vanilla scoops to 1 and check the subtotal const vanillaInput = await screen.findByRole('spinbutton', { name: 'Vanilla', }); userEvent.clear(vanillaInput); userEvent.type(vanillaInput, '1'); expect(scoopsSubtotal).toHaveTextContent('2.00'); // update chocolate scoops to 2 and check subtotal const chocolateInput = await screen.findByRole('spinbutton', { name: 'Chocolate', }); userEvent.clear(chocolateInput); userEvent.type(chocolateInput, '2'); expect(scoopsSubtotal).toHaveTextContent('6.00'); }); renderを別ファイルでカスタムすることで、Contextがない場合と同様にrender(<Options optionType="scoops" />として使用することもできます。 testing-library-utils.jsx import { render } from '@testing-library/react'; import { OrderDetailsProvider } from '../contexts/OrderDetails'; const renderWithContext = (ui, options) => render(ui, { wrapper: OrderDetailsProvider }, ...options); // re-export everything export * from '@testing-library/react'; // override render method export { renderWithContext as render }; 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactでドラッグソートを実装する

react-beautiful-dndというライブラリを使用してシンプルなドラッグソートをReactで実装する手順を記します。 ここのチュートリアルをTypeScriptを用いてシンプルにまとめた内容となっています。 最終的には以下のような感じで動きます。 事前準備 まずはcreate-react-appでTypeScriptのReact環境を作ります。 npx create-react-app sort-sample --template typescript cd sort-sample 次に必要なライブラリをインストールします。 react-beautiful-dndと、スタイル記述用のstyled-componentsです。 npm install --save react-beautiful-dnd npm install --save styled-components # TypeScriptの型定義ファイル npm install --save-dev @types/react-beautiful-dnd npm install --save-dev @types/styled-components 実装手順 タスクリストの初期データをsrc/initial-data.tsとして作成します。 initial-data.ts interface Data { tasks: { id: string; content: string; }[]; taskOrder: string[]; } export const initialData: Data = { tasks: [ { id: "task-1", content: "first task" }, { id: "task-2", content: "second task" }, { id: "task-3", content: "third task" }, { id: "task-4", content: "fourth task" }, { id: "task-5", content: "fifth task" }, ], taskOrder: ["task-1", "task-2", "task-3", "task-5"], }; initialData.tasksが各タスク情報を保持し、initialData.taskOrderが現在のタスクの順番を表します。 ここではタスクは全部で5つあり、そのうち4つ(task-1, task-2, task-3, task-5)がタスクリストに表示されます。 次に、src/index.tsxを以下のように作成します。 index.tsx import React, { useState } from "react"; import ReactDOM from "react-dom"; import { initialData } from "./initial-data"; const App = () => { const [data, setData] = useState(initialData); let tasks: { id: string; content: string }[]; try { // 順番通りにtaskを取得する tasks = data.taskOrder.map((taskId) => { const target = data.tasks.find((v) => v.id === taskId); if (target == null) throw new Error("task not found."); return target; }); } catch (error) { console.log(error.message); return <></>; } return <></>; }; ReactDOM.render(<App />, document.getElementById("root")); まだ順番通りにtaskを取得しているだけです。 次に1つのタスクを表すコンポーネントをsrc/Task.tsxとして作成します。 Task.tsx import React from "react"; import { Draggable } from "react-beautiful-dnd"; import styled from "styled-components"; const Container = styled.div` border: 1px solid lightgrey; border-radius: 2px; padding: 8px; margin-bottom: 8px; background-color: white; display: flex; `; const Handle = styled.div` width: 20px; height: 20px; background-color: orange; border-radius: 4px; margin-right: 8px; `; interface Props { id: string; index: number; content: string; } export const Task: React.FC<Props> = ({ id, index, content }) => ( <Draggable draggableId={id} index={index}> {(provided) => ( <Container {...provided.draggableProps} ref={provided.innerRef}> <Handle {...provided.dragHandleProps} /> {content} </Container> )} </Draggable> ); Handleはソートするときにマウスで掴む部分に相当します。 ドラッグできる要素はDraggableで囲み、それぞれに個別のdraggableIdを指定します。 draggableIdにはindex.tsxのdata.tasks[n].idを渡すようにしたいので、index.tsxをそのように修正します。 index.tsx import React, { useState } from "react"; import ReactDOM from "react-dom"; import { initialData } from "./initial-data"; + import { Task } from "./Task"; + import { DragDropContext, Droppable } from "react-beautiful-dnd"; + import styled from "styled-components"; + const Container = styled.div` + margin: 8px; + border: 1px solid lightgrey; + border-radius: 2px; + `; + const Title = styled.h3` + padding: 8px; + `; + const TaskList = styled.div` + padding: 8px; + `; const App = () => { const [data, setData] = useState(initialData); let tasks: { id: string; content: string }[]; try { // 順番通りにtaskを取得する tasks = data.taskOrder.map((taskId) => { const target = data.tasks.find((v) => v.id === taskId); if (target == null) throw new Error("task not found."); return target; }); } catch (error) { console.log(error.message); return <></>; } - return <></>; + return ( + <DragDropContext> + <Container> + <Title>TODOリスト</Title> + <Droppable droppableId="column"> + {(provided) => ( + <TaskList ref={provided.innerRef} {...provided.droppableProps}> + {tasks.map((task, index) => ( + <Task + key={task.id} + id={task.id} + index={index} + content={task.content} + /> + ))} + {provided.placeholder} + </TaskList> + )} + </Droppable> + </Container> + </DragDropContext> + ); }; ReactDOM.render(<App />, document.getElementById("root")); タスクリストをDroppableで囲み、それをDragDropContextで囲みます。 今回はDroppableが1つですが、Droppableが2つ以上ある場合は別のDroppableにもドラッグで移動できるようになります。 最後に、ドラッグ時の挙動をonDragEnd関数として実装します。 index.tsx import React, { useState } from "react"; import ReactDOM from "react-dom"; import { initialData } from "./initial-data"; import { Task } from "./Task"; + import { DragDropContext, DropResult, Droppable } from "react-beautiful-dnd"; import styled from "styled-components"; const Container = styled.div` margin: 8px; border: 1px solid lightgrey; border-radius: 2px; `; const Title = styled.h3` padding: 8px; `; const TaskList = styled.div` padding: 8px; `; const App = () => { const [data, setData] = useState(initialData); + const onDragEnd = (result: DropResult) => { + const { destination, source, draggableId } = result; + + // ドラッグがキャンセルされた場合 + if (!destination) { + return; + } + // ドラッグ先が現在の場所と同じ場合 + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + return; + } + + // 新しい順番を保持する配列を生成する + const newTaskOrder = Array.from(data.taskOrder); + // 移動元の要素を削除する + newTaskOrder.splice(source.index, 1); + // 移動先の位置にdraggableIdを挿入する + newTaskOrder.splice(destination.index, 0, draggableId); + + const newData = { + ...data, + taskOrder: newTaskOrder, + }; + + setData(newData); + }; + let tasks: { id: string; content: string }[]; try { // 順番通りにtaskを取得する tasks = data.taskOrder.map((taskId) => { const target = data.tasks.find((v) => v.id === taskId); if (target == null) throw new Error("task not found."); return target; }); } catch (error) { console.log(error.message); return <></>; } return ( + <DragDropContext onDragEnd={onDragEnd}> <Container> <Title>TODOリスト</Title> <Droppable droppableId="column"> {(provided) => ( <TaskList ref={provided.innerRef} {...provided.droppableProps}> {tasks.map((task, index) => ( <Task key={task.id} id={task.id} index={index} content={task.content} /> ))} {provided.placeholder} </TaskList> )} </Droppable> </Container> </DragDropContext> ); }; ReactDOM.render(<App />, document.getElementById("root")); ドラッグがキャンセルされた場合、onDragEnd()のdestinationまたはcombineはnullに設定されます。 動作確認 npm start まとめ 簡単にドラッグソートを実装することができました。 今回はシンプルなソートですが、Droppableを増やせばかんばんツールのようなドラッグソートも実装できるようです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScriptでLPを作ってみた。

はじめに おはようございます。こんにちは。こんばんは。 Watataku です。 今回の記事は React を使って簡単に LP を制作してみたいと思います。 自分的に React を学びたての方で、アウトプットに「何を作ろうか」と困っている方におすすめだと思っています。 ちなみに今回は TypeScript で書いていきます。 制作物 →https://react-ts-lp-five.vercel.app リポジトリ →https://github.com/watataku8911/react-ts-lp React で LP を作る LP.html <div class="card"> <div class="inner"> <img src="./images/image01.jpeg" class="img" /> <div class="description"> <h1>コンテンツ1</h1> <p> テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト </p> </div> </div> </div> <div class="card"> <div class="inner"> <img src="./images/image02.jpeg" class="img" /> <div class="description"> <h1>コンテンツ2</h1> <p> テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト </p> </div> </div> </div> <div class="card"> <div class="inner"> <img src="./images/image03.jpeg" class="img" /> <div class="description"> <h1>コンテンツ3</h1> <p> テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト </p> </div> </div> </div> コンテンツが増えていくたびに上記のような<div>地獄になりコードが読みにくくなります。 なのでそれを React で解決させます。 解決法 データ(data..json)とビュー(Card.tsx)を分け、ループさせる。 data.json [ { "title": "コンテンツ1", "description": "テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト", "imagePath": "./images/image01.jpeg" }, { "title": "コンテンツ2", "description": "テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト", "imagePath": "./images/image02.jpeg" }, { "title": "コンテンツ3", "description": "テキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキストテキスト ", "imagePath": "./images/image03.jpeg" } ] Card.tsx import React from "react" import styles from "./Card.module.css" type Props = { title: string description: string imagePath: string } const Card = (props: Props) => { return( <section className={styles.contents}> <div className={styles.inner}> <img src={props.imagePath} alt={props.title} className={styles.img} /> <div className={styles.description}> <h1 className={styles.title}>{props.title}</h1> <p className={styles.text}>{props.description}</p> </div> </div> </section> ) } export default Card; App.tsx import React, {ComponentProps} from 'react'; import data from "./data.json" const App = () => { return( <div className="card"> {data.map((item: ComponentProps<typeof Card>, index: number) => { return( <Card title={item.title} description={item.description} imagePath={item.imagePath} key={index} /> ) })} </div> ) } このようにdata.json(データ)、Card.tsx(ビュー)を分けることにより、 効率が良くなりまたコードの可読性も良くなります。 スクロールアニメーションの実装 LP でよく見かけると思いますが、スクロールしていくと要素が出現していくあれです。 これを React で実装するためにライブラリを入れていきます。 animate.cssとreact-inview-monitorです。 $ npm install animate.css react-inview-monitor Card.tsx import React from "react" import styles from "./Card.module.css" import InViewMonitor from "react-inview-monitor" type Props = { title: string description: string imagePath: string } const Card = (props: Props) => { return( <section className={styles.contents}> <div className={styles.inner}> <InViewMonitor classNameNotInView='hidden' classNameInView='animate__animated animate__fadeInLeft slower'> <img src={props.imagePath} alt={props.title} className={styles.img} /> </InViewMonitor> <div className={styles.description}> <InViewMonitor classNameNotInView='hidden' classNameInView='animate__animated animate__fadeInRight slower'> <h1 className={styles.title}>{props.title}</h1> </InViewMonitor> <InViewMonitor classNameNotInView='hidden' classNameInView='animate__animated animate__fadeInUp slower'> <p className={styles.text}>{props.description}</p> </InViewMonitor> </div> </div> </section> ) } export default Card; このようにreact-inview-monitor モジュールを読み込み、スクロールアニメーションさせたい要素にInViewMonitor コンポーネントで囲んでやれば簡単にスクロールアニメーションが実装できます。 ただし、今回の場合react-inview-monitor モジュールを読み込んだ時点でエラーが出ます。なぜでしょう? それは今回 Typescript で書いているため、「型がない」とエラーが出ます。 外部ライブラリを使う時の型定義がない時の解決法 2種類解決策があります。 まずひちつめにtsconfig.jsonで"noImplicitAny": falseとしてあげ暗黙的 any を許してあげる。 でもこれをしちゃうと Typescript で書いている意味がなくなっちゃいますよね(笑) なので、新たに型定義ファイル(.d.ts)を作ってやる。 reacy-inview-monitor.d.ts declare module 'react-inview-monitor' { export default function InViewMonitor( children?: any, classNameNotInView?: string, classNameInView?: string, classNameAboveView?: string, intoViewMargin?: string, onInView?: Function, onNotInView?: Function ): JSX.Element } この作成したファイルをsrc/@types フォルダを作成し、その中で保存するとreact-inview-monitor モジュールが無事動くようになり、スクロールアニメーションが実装できます。 フォームの実装 お問い合わせなどフォームの実装方法です。実際のコード const Form = () => { const [firstName, setFirstName] = useState(""); const inputFirstName = useCallback( (event) => { setFirstName(event.target.value); }, [setFirstName] ); return ( <input type="text" name="firstName" placeholder="firstNameを入力してください。" value={firstName} onChange={inputFirstName} /> ); }; テキストボックスに入力があるたびにonChange(inputFirstName)が実行されて、setFirstNameでstate(firstName)を更新してます。 つまり入力した内容がfirstNameになっています。 React ではテキストボックスを置いておくだけでは入力ができず、上記のように実装してあげないといけないのです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react+Material-UI+Styled Components Part0

Material-UI + Styled Componentsでスタイリングを少しでも楽に コピペしたいコピペしたいコピペしたいコピペしたいコピペしたいコピペしたいコピペしたいコピペで完結させたい… って常に思ってる。とくにCSS。 細部の調整は必要でもできうる限り楽したい。 できうる限り何とかしてみた。 とはいうもののmaterial-UIもstyled components使った事無いので何回かに分けて使い方まとめる 今回は導入方法絞る ざっくり説明 Material-UI Google が提唱する Material Design に準拠したコンポーネントライブラリ Material-UIのCSSの記述がオブジェクトスタイルがめんどくさい Styled Components CSS 記法のまま CSS in JSを行うことができるライブラリ 今回はこの二つを組み合わせて楽する 使い方 Material-UIのインストール yarn add --save @material-ui/core @material-ui/icons styled-componentsのインストール yarn add --save-dev styled-components @types/styled-components babel-plugin-styled-components cross-env typescript-styled-plugin 上記の詳細は以下 @material-ui/core material-uiのコアライブラリ @material-ui/icons Font Awesomeのmaterial-ui版 (要らないなら省く) @types/styled-components styled-componentsを使ってTypescriptを使うためのライブラリ。 babel-plugin-styled-components styled-componentsのclass名を分かりやすくしてくれるライブラリ。 cross-env MacとWindowsで同じコマンドで環境変数を設定するためのライブラリ。 typescript-styled-plugin VS Code 上でテンプレート リテラル による CSS 定義の入力補完に必要。 引用元
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Mock Service Workerで模擬したサーバレスポンスをテストに活用する

現在、Testing Libraryについて学習中です。 今回はサーバのレスポンスをMock Service Workerで模擬し、Testing Libraryで作成したテストに活用することに挑戦してみました。 Mock Service Workerとは Mock Service Worker(msw)は、ネットワーク呼び出し(APIリクエスト)をインターセプトして特定のレスポンスを返すことを目的としたライブラリです。 mswを利用することで、サーバとの通信を行わずにレスポンスを利用したテストをできたり、SPA開発時のモックサーバとして活用することができます。 実装 以下のコンポーネントで構成された画面を考えます。 ScoopOptionの画像とアイテム名がサーバから取得するデータです。 また、サーバからデータを取得できなかったとき、Options内で警告バナー(AlertBanner)を表示するようにします。 以下の2パターンを確認するためのテストを作成しました。 モックサーバの模擬データを正しく取得できているかどうか サーバからのデータ取得の失敗を模擬して警告が表示されるかどうか セットアップ mswのセットアップは以下の流れで行います。 ライブラリのインストール handlersの作成 test serverの作成 setUpTestsの作成(create-react-appの場合) ライブラリのインストール 以下のコマンドでライブラリをインストールします。 npm install msw handlersの作成 handlersはモックするリクエストとレスポンスの中身を指定するものです。 REST APIとGraphQlのどちらにも対応しており、今回はREST APIで作成します。 ctxでレスポンスの返し方を決めることができ、例えばctx.jsonとすることでjson形式に変換しています。 handlers.js // src/mocks/handlers.js import { rest } from 'msw'; export const handlers = [ rest.get('http://localhost:3030/scoops', (req, res, ctx) => { return res( ctx.json([ { name: 'Chocolate', imagePath: '/images/chocolate.png' }, { name: 'Vanilla', imagePath: '/images/vanilla.png' }, ]) ); }), ]; test serverの作成 作成したhandlersを使用して、実際のモックサーバ(test server)を作成します。 server.js // src/mocks/server.js import { setupServer } from 'msw/node'; import { handlers } from './handlers'; // handlersを元にモックサーバを作成 export const server = setupServer(...handlers); setUpTestsの作成 create-react-appの場合はsetUpTestsに以下の内容を記述します。 テスト中のモックサーバの設定を行います。 setupTests.js // src/setupTests.js import { server } from './mocks/server.js' // モックサーバのlistenをすべてのテストの前に一回だけ行う beforeAll(() => server.listen()) // 他のテストに影響を与えないようにテストごとにhandlersをリセットする afterEach(() => server.resetHandlers()) // すべてのテストが終了したらモックサーバをcloseする afterAll(() => server.close()) テスト1の作成 モックサーバの模擬データを正しく取得できているかどうかを確認するテストを作成します。 テストするファイルの中身は以下のようになっています。 Options.jsx import axios from 'axios'; import { useEffect, useState } from 'react'; import Row from 'react-bootstrap/Row'; import ScoopOption from './ScoopOption'; import ToppingOption from './ToppingOption'; import AlertBanner from '../common/AlertBanner'; import { pricePerItem } from '../../constants'; export default function Options({ optionType }) { const [items, setItems] = useState([]); const [error, setError] = useState(false); // optionType is 'scoop' or 'toppings' useEffect(() => { axios .get(`http://localhost:3030/${optionType}`) .then((response) => setItems(response.data)) .catch((error) => setError(true)); }, [optionType]); if (error) { return <AlertBanner />; } // TODO: replace `null` with ToppingOption when available const ItemComponent = optionType === 'scoops' ? ScoopOption : ToppingOption; const title = optionType[0].toUpperCase() + optionType.slice(1).toLowerCase(); //頭文字だけ大文字 const optionItems = items.map((item) => ( <ItemComponent key={item.name} name={item.name} imagePath={item.imagePath} /> )); return ( <> <h2>{title}</h2> <p>{pricePerItem[optionType]} each</p> <Row>{optionItems}</Row> </> ); } テストの中身は以下のようになります。 Options.test.jsx import { render, screen } from '@testing-library/react'; import Options from '../Options'; test('displays image for each scoop option from server', async () => { render(<Options optionType="scoops" />); // 画像が2個取得できていれば成功 const scoopImages = await screen.findAllByRole('img', { name: /scoop$/i }); expect(scoopImages).toHaveLength(2); // 画像のaltが設定されていれば成功 const altText = scoopImages.map((element) => element.alt); expect(altText).toEqual(['Chocolate scoop', 'Vanilla scoop']); }); 前項のsetUpTestsの作成でテストを実行したときにモックサーバをlistenするような設定を行いました。 そのため、Optionsコンポーネントではhttp://localhost:3030/${optionType}からサーバのデータを取得しているのですが、テスト実行中はモックサーバからデータを取得するようになっています。 テスト2の作成 サーバからのデータ取得の失敗を模擬して、警告が表示されるかどうかをテストします。 OrderEntry.test.jsx import { render, screen, waitFor } from '@testing-library/react'; import OrderEntry from '../OrderEntry'; import { rest } from 'msw'; import { server } from '../../../mocks/server'; test('handles error for scoops and toppings routes', async () => { // handlersを上書きする server.resetHandlers( rest.get('http://localhost:3030/scoops', (req, res, ctx) => res(ctx.status(500)) ), ); render(<OrderEntry />); await waitFor(async () => { const alerts = await screen.findAllByRole('alert'); expect(alerts).toHaveLength(1); }); }); エラーレスポンス(500)を返すように、server.resetHandlersを使ってhandlersを上書きします。 また、警告アラート(AlertBanner)はデータを取得できなかったとき(axiosがエラーをなげたとき)に非同期で表示されます。 そのようなケースでは、要素の取得にawait find[All]Byを使用します。 しかし、これだけだとWarningがでるので、waitForも併用します。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Expected an assignment or function call and instead saw an expression no-unused-expressionsの対処法

症状 Reactのコードをコンパイルした時に、以下のエラーが発生しました。 error Expected an assignment or function call and instead saw an expression no-unused-expressions そのまま翻訳すると、 「割り当てまたは関数呼び出しが予期されていましたが、代わりに未使用の式のない式が表示されました」 エラーが出たソースは以下です。 Hoge.jsx import React ,{ Fragment} from 'react';1 export const Hoge= () => { return( <Fragment> "Fuga" </Fragment> ) } 解決策 importの後に1が入っていたため、起きていました。 1を取り除いたら、エラーが解消されました。 Hoge.jsx import React ,{ Fragment} from 'react'; export const Hoge= () => { return( <Fragment> "Fuga" </Fragment> ) }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

できるだけモダンな技術を使用してWEBサービスを作った話

はじめに ここ一ヶ月半くらいかけて比較的新しいWeb制作技術を取り入れたサービスを個人的に作ってみましたので、使用した技術やサービスの紹介をしたいと思います。 作ったサービス フォーラムがメインのSNS 詳細は伏せるが、デザインはredditを意識して作成した。 ※イメージ図 開発を行った経緯 トレンドのフロントエンド技術のSPAとクラウドサービスの理解を深めるために開発に着手しました。 使用した技術やサービス できるだけ新しいものを選定 フロントエンド関連  React.js SPA(シングルページアプリケーション)で作りたかったことと、VueやAngulerに比べて昨今の勢いが好調なところが採用のポイント。 本当はNext.jsで作る予定だったが、Firebaseとの親和性が低く、開発元であるVercelのホスティングサービスを利用することが前提に近かったため、今回は見送りした。 使用したライブラリや機能 Reactの機能やライブラリはトレンドの変化に変わるので、開発開始時点でのトレンドを把握してから着手した方が良いと思う。 hook React 16.8 (2020年)で追加された新機能です。state(状態管理用変数)やライフサイクル(constructor,didmount) などの Reactの機能を、クラスを書かずに使えるようになる現時点での必須機能です。 React Router アクセスされたURLによって、表示を切り替えることができるライブラリ。 これを使わないとソースコードがゴチャゴチャし、大規模開発になると管理が大変になります。 Redux ブラウザアプリケーションのための状態管理コンテナライブラリ。 R- eact単体の機能だとstate(静的変数とかcookieに近いイメージ)は自身のコンポーネントで使用するか、props(引数)を通して子コンポーネントへ送ることしかできないが、Reduxを使うとstateを高域変数の様に扱うことができる様になる。 管理方法もフレームワーク化されているため、煩雑な扱いでコードの可読性が低下することもない。 フレームワーク自体が複雑な欠点があり、理解にはかなり時間を費やしたかも・・・ ※以降の開発ではReactの提供する状態管理機能のRecoilを使った方が学習コストが低そうなので、そちらの方がおすすめ、現在はベータ版だが、そろそろ正式リリースされる予定。 使用言語 Typescript JavaScriptに型制約を追加したもの。 コードの可読性の向上や、意図しないバグを防いだりできる。 JavaScriptと構文はほぼ同じなので、学習コストは低い。 型の記述方法はC#とかJavaとかの言語とは少し異なるので、最初は違和感を感じるが慣れの問題。 interfaceの機能があるが、他の言語とは用途が少し異なり、拡張可能な型定義の機能を持っており、プロパティの定義に使われたりすることが多い。ちなみに拡張しない場合はTypedで定義する。 CSSフレームワーク Tailwind CSS 最近のトレンドらしいのでメインはこれを使用 簡単に扱うことができるが、日本語情報はまだまだ少ないので、BootStorapの方が初心者向け。 BootStrapと同じような感覚で使用できるが、比較すると色々な所が少し細かく設定できるイメージ。 個人的にはデザインスキルがまだまだ低いので、これに限らずCSS関連はいつも苦戦する。 Material UI Reactと親和性が高い(らしい)。 テキストボックスのプレースホルダがカッコよく動くのでそこだけ利用した。 静的解析ツール ESlint Javascript/Typescript用静的解析ツール - ファイル内のバグチェックやコーディングスタイルの一貫性を保つために使用 バックエンド関連 Firebase 手軽に一定範囲まで無料で使えるので個人開発で人気のクラウドサービス スマホアプリやWeb用のクラウドサービス、元々別会社だったがGoogleに買収されたのでGCPと同じサービスがあったりする。 今回は使っていないがAndroidのPUSH通知はFirebaseしか使えなかったりするらしい(ホント?) oauth機能が簡単に扱えるため、oauth機能はかなり人気で、オンプレで制作してoauthだけFirebaseとか結構あるらしい。 各サービスに無料枠があり、個人で使う分には変な作り方をしない限り無料で収まる。開発中のテストでも無料枠の1%を超えることがなかった。 使用したFirebaseの機能 Authentication 簡単にログイン機能を実装できて、自分で個人情報の管理をしなくて良いので使用した。 googleとかtwitterとかの外部認証を簡単に実装できる。 GUIライブラリreact-firebaseuiを使うとさらに簡単に実装できる。 RealtimeDatabase Json形式のデータベース 帯域に対して課金される。同じデータベースサービスのFireStoreでは、アクセス回数で課金されるので、頻繁にReadする用途に向いている。 Googleの推奨はオブジェクト形式のFirestoreで当初はFireStoreを使用していたが、課金方式的に用途に適していなかったので途中でこちらに変更した。 単純なクエリが設定できないので、粗い条件で一旦取得して、ローカルのTSでソートやフィルタをかけて使う。この辺はFireStoreも変わらない。 Storage ファイルストレージサービス FIrebaseのファイル管理はこれ一択、画像の保存に使用した。 Hosting Reactのプロジェクトを格納するのに使用 静的なファイルはここに格納する。 デフォルトで設定するとPublicフォルダにアクセスしようとするので、Buildフォルダにアクセスするように設定することが重要 Function サーバーレスコンピューティングサービス SlackやTwitterのAPIと組み合わせてメッセージを飛ばせるようにした。 ドメイン取得サービス ムームードメイン 前から使っていたので、今回もここから取得 以前初期費用が安い".xyz"を使ったことがあったが、年間維持費を意識していなかったので失敗したので、使いたいドメインが空いていたこともあり、今回は1000円程度で維持できる.comで取得した。 外部サービス slack API 問い合わせ画面で問い合わせをするとSlackでメッセージが飛ぶように設定 twitter API Webサービスの各種情報発信用にTwitterの連携したアカウントでツイートするように設定 開発環境 windows環境 wsl2 + vscode(remote wsl) Windows上でほぼ完全なLinux(Ubuntu)を動作させることができ、Windowsのファイルシステムにそのままアクセスできたりする。※GUI版も2021年にリリースされたが、こちらは使用していない。 react開発環境のnodeがバージョン制約にかなり厳しいので、仮想環境として利用した。 2つ以上環境を作りたい場合、環境が作れるか謎。どうすれば良いか調べてないが、多分そういう用途だとDockerでやった方が良さそう。 ※途中で何回かnode関連のアップデートをしてしまい、環境再構築に手間取ったので、すぐに復元できるDockerは偉大。 Mac(M1)環境 Docker + vscode(remote container) みんな大好きDocker remote containerを使うとローカル作業とほぼ同じ感覚で開発することができる。 最初にdockerファイルを作るのが面倒 dockerHubにイメージを保管できるので、環境が消えても安心(Dockerファイルはgitで管理?) バージョン管理 GitHub 選定理由は人気で情報が多いから。 次回以降は競合のGitLabで作ろうと思う。 2020年にCI/CD機能GitHub Actionsが追加されたので、こちらも実装し、git pushを実行するとreactプロジェクトのビルドとFirebaseへのデプロイが自動で実行される様にしてみた。 必須VSCode拡張機能 Prettier 大人気の自動整形拡張機能 ソースコードを保存すると自動で整形されるので、整形忘れを防止できる。 (個人で使ったが)グループでの開発に向いており、バージョン管理のソースコードが荒れにくく減る。 タスク管理 Trello プロジェクトのタスクをチームで管理するためのサービス。 トヨタの生産管理方式“カンバン”の運用ができる。 タスク(チケット)発行が楽で5秒で発行できる。 スマホ版もしっかり作られている。 使いたかったサービス Webデザインツール Figma トレンドのWebデザインツールらしい デザイン通りにCSSを組める自信が無いのと、デザインは重視してなかったので使わなかった。 デザインできる人は尊敬します。 感想など SPAはほぼ知識ゼロに近い状態からスタートしたので、慣れるまでかなり時間が掛かったのと、学習コストが結構高かった。 reactは2年前に少し触ったが使用するライブラリや機能のトレンドが代わり過ぎてて知識が使い物にならないので、フロントエンドの流れの速さに驚いた。自分の持ってた2年前の書籍が使いものにならない、書籍買う人は注意。 全体的に触ったことがないものばかりだったため、インプット量が多く、辛い道のりだったが、今回で一気にWEB系の技術や知識などの引き出しが増えたので少し自信がついた。 今回の進め方について 今回は開始前にラーニングピラミットやアクティブラーニングなどの学習方法について学んでから開発を進めていた。Qiitaに最近ちょくちょく投稿し始めたのもそれの関係。 アクティブラーニングを意識し、ほぼ書籍を読まずに手探りで進め、分からなかったところを中心に調べる方式で進めたが、体験と結びつけて学習ができたためか、かなり効率的に吸収できた気がする。 着手前に本を読んだ場合、記憶力が悪いので後半に差し掛かると本の前半の内容を忘れてしまうので、こちらの方が効率的に感じた。 これまでは、勉強してから手を動かしていたので非効率だったことに後悔するも、これからに期待 以上
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

第 8 章 何はなくともコンポーネント メモ

コンポーネントのメンタルモデル React ではコンポーネントをどう考えればいいか。これはよくいわれることだけど、 JavaScript の関数のようなものと考えるのが一番近い。 props を引数として受け取り、戻り値として React Elementsを返す関数 返されたReact Elementsがそのコンポーネントのレンダリング結果 になる。 ただしコンポーネントが通常の関数とちがうのは、個々に『状態』を持つことができるところ 関数コンポーネントも、 仮想 DOM の差分検出処理エンジンによって React Elements ごとに状態を保持する空間が用意される そのコンポーネントが持つ状態のことを state と呼ぶ props と state が同じである限りはその返す React Elements が変 わることはないんだけど、そのどちらか一方または両方が変わると返す React Elements も変わって くる。つまりコンポーネントのレンダリングに差分が発生する。 React の差 分検出処理エンジンは、仮想 DOM 内の React Elements すべてを監視していて、そのどれかの props またはその保持している state の値に差分を検出すると、そのコンポーネントのレンダリング処理を 再実行するようになってる。 コンポーネントと Props state というのは極力コンポーネントに持たせるべきではないもの? 純粋関数とは、引数が同じなら必ず同じ戻り値を返す関数のこと。 React にとって理想的 なコンポーネントとは、props が同じなら必ずレンダリング結果が同じになるコンポーネント props とはコンポーネントにとっての関数に対する引数のようなもの React に特有の概念で『properties(プロパティ)』を短くして props と呼んでる そして JSX が生成する React Elements を通してコールされたコンポーネント側では、それを { 属性名: 属性値 } の形式の props というオ ブジェクトとして受け取る。 受け取り方には 2 つ コンポーネントの実装が関数だった場合はその関数の第 1 引数として渡される クラスだった場合は初期化時にそのメンバーオブジェクト props として設定される import { VFC } from 'react'; import CharacterList, { Character } from './CharacterList'; import './App.css'; const App: VFC = () => { const characters: Character[] = [ { id: 1, name: '桜木花道', grade: 1, height: 189.2, }, { id: 2, name: '流川 楓', grade: 1, height: 187, }, { id: 3, name: '宮城リョータ', grade: 2, height: 168, }, { id: 4, name: '三井 寿', grade: 3, }, { id: 5, name: '赤木剛憲', grade: 3, height: 197, }, ]; return ( <div className="container"> <header> <h1>『SLAM DUNK』登場人物</h1> </header> <CharacterList school="湘北高校" characters={characters} /> </div> ); }; export default App; VFC これは VoidFunctionComponent インターフェー スのエイリアスで、FunctionComponent の関数の props からそのコンポーネントの子要素が格納される children を除いたものなの 従来の FC で定義された関数コンポーネントだと、子要素の操作が必要ない場合でも暗黙の内に props の中に子要素のオブジェクトが渡されてた。 import { VFC } from 'react'; import { Header, Icon, Item } from 'semantic-ui-react'; export type Character = { id: number; name: string; grade: number; height?: number; }; type Props = { school: string; characters: Character[]; }; const CharacterList: VFC<Props> = (props) => { const { school, characters } = props; return ( <> <Header as="h2">{school}</Header> <Item.Group> {characters.map((character) => ( <Item key={character.id}> <Icon name="user circle" size="huge" /> <Item.Content> <Item.Header>{character.name}</Item.Header> <Item.Meta>{character.grade}年生</Item.Meta> <Item.Meta> {character.height ? character.height : '???'} cm </Item.Meta> </Item.Content> </Item> ))} </Item.Group> </> ); }; export default CharacterList; Props という型エイリアスを定義しているところ。この型はコンポーネントを定義する関数宣言の型適用で使われてる こうやって FC に型引数を渡すことで、そのコンポーネントの props の型を指定できる props の型を設定することで、そのコンポーネントを JSX でマウントするときに必要な属性値と その型に縛りが発生する。だから App.tsx で をマウントするとき、school と chracter の属性値をそれぞれ Props で定義されている適正な型で記述しないと怒られてしまう 関数コンポーネントでは、レンダリングしたい内容を戻り値として return で返す <>...</> これはフラグメントといって React.Fragment のシンタックスシュガー でくくっても表示結果は同じなんだけど、そうすると HTML ソース に意味のない 階層ができてしまう。でもフラグメントにしておくとそれが避けられる た だ、 とちがって必ず中身のノードが必要なので、処理の結果、中身がなくなる可能性のある ときは使っちゃダメ Character 型の定義で height? と要素名にクエスチョンマーク これは『※その要素は省略できます』ってこと だから三井さんの height 値は設定されて ないのに型チェックで怒られてない。設定されてなければ参照値は undefined なので、三項演算子 で '???' という文字列が返ってる JSX で要素 をループ処理によって記述する場合、各要素にユニークな値を key 属性として設定しなければならない 仮想 DOM の差分検出処理で再レンダリングを効率的にするために必要 クラスコンポーネントで学ぶ State コンポーネントをクラスで表現する 今となってはクラスコンポーネントでしかできないことはほんのわずかしか残ってな いし、それも今後すべて関数コンポーネントでサポートされる予定になってる Facebook の React 開発チームが公式に推奨してるのも関数コンポーネント ・this の挙動が不可解で、そのためにコードが冗長になりがち ・minify やホットリローディング、さらに今後導入を検討しているコンポーネントの AOT コンパイルなどにおいて、クラスは最適化が困難で動作も不安定 ・ライフサイクルメソッドを用いると機能的に関連しているはずのコードがバラバラに記述され ることになり、可読性が落ちる ・状態を分離するのが難しく、ロジックを再利用するのが難しい クラスコンポーネントに State を持たせる 関数コンポーネントに state を持たせるためにはちょっとしたマジックが必要になるんだけど、ク ラスコンポーネントは簡単に state が持てる。props と同様、型引数に state の型を渡せば、メンバー 変数state からstateにアクセスできるようになる import { Component, ReactElement } from 'react'; import { Button, Card, Statistic } from 'semantic-ui-react'; import './App.css'; type State = { count: number }; class App extends Component<unknown, State> { constructor(props: unknown) { super(props); this.state = { count: 0 }; } reset(): void { this.setState({ count: 0 }); } increment(): void { this.setState((state) => ({ count: state.count + 1 })); } render(): ReactElement { const { count } = this.state; return ( <div className="container"> <header> <h1>カウンター</h1> </header> <Card> <Statistic className="number-board"> <Statistic.Label>count</Statistic.Label> <Statistic.Value>{count}</Statistic.Value> </Statistic> <Card.Content> <div className="ui two buttons"> <Button color="red" onClick={() => this.reset()}> Reset </Button> <Button color="green" onClick={() => this.increment()}> +1 </Button> </div> </Card.Content> </Card> </div> ); } } export default App; ひとつめは props の型で、このコンポーネントには props が必要ないので unknown を渡して る。デフォルト値は空オブジェクト {} なんだけど、{} の型は TypeScript の解釈では『null 以外の あらゆるオブジェクト』になってしまうため、@typescript-eslint/ban-typesのルールで使用が禁じ られてるの。だからプロパティを持てないオブジェクトの型である unknown がここではよりふさわしい Component に渡してる 2 つめの型引数だけど、これが state の型になる お約束としてスーパークラスに props を渡すのを忘れないように その下で定義しているメンバーメソッド reset と increment の中でその値を操作してる。 気をつけなきゃいけないのは、this.state の値を直接書き換えないこと。直接代入していいのはコ ンストラクタの中だけで、それ以外では値の設定には必ず setState メソッドを使うようにする setState メソッドの使い方について説明しておくと、そ の引数には次の 2 種類が設定できるようになってる i. state 内の変更したい要素名をキーに、値をその値にしたオブジェクト e.g. {count:0} ii. (prevState,props?)=>newState形式の、以前のstate(必要であればpropsも)を引数として 受け取って新しい state を返す関数 e.g. (state,props)=>({foo:state.foo+props.bar}) React ではイベントハンドラには通常、そのイベントが起きたときに実行したい関数を設定する この () => this.increment() というのは、引数を受け取らず increment メソッドを実行する無名関数 reset = (e: SyntheticEvent) => { e.preventDefault(); this.setState({ count: 0 }); }; increment = (e: SyntheticEvent) => { e.preventDefault(); this.setState((state) => ({ count: state.count + 1 })); }; React が提供している SyntheticEvent という型で定義されるイベントオブジェクト イベントハンドラのコールバックに引数として渡されるオブジェクトの型 たとえばこれが a要素だったりするとクリックで ページ移動が起きてしまうので、それをキャンセルするためにこういう記述が必要になる 他にも実用的な使い方だと、 要素で選択した値を受け取りたい場合は、その onChange 値に設定した関数内で同様にイベントハンドラ e を引数に定義しておけば、e.target.value から参照できたりする コンポーネントのライフサイクル コン ポーネントにおけるライフサイクルとは、まずマウントして初期化され、次にレンダリングされた後、 何らかのきっかけで再レンダリングされ、最後にアンマウントされるまでの過程をいう クラスコンポーネントにはライフサイクルの各フェーズに対応したライフサイクルメソッド (lifecycle methods)というものがあり、そこに必要な処理を登録しておける。 1. Mounting フェーズ ...... コンポーネントが初期化され、仮想 DOM にマウントされるまでの フェーズ。このフェーズで初めてコンポーネントがレンダリングされる 2. Updating フェーズ ...... 差分検出処理エンジンが変更を検知してコンポーネントが再レン ダリングされるフェーズ 3. Unmounting フェーズ ...... コンポーネントが仮想 DOM から削除されるフェーズ 4. Error Handling フェーズ ...... 子孫コンポーネントのエラーを検知、捕捉するフェーズ import { Component, ReactElement } from 'react'; import { Button, Card, Icon, Statistic } from 'semantic-ui-react'; import './App.css'; const LIMIT = 60; type State = { timeLeft: number }; class App extends Component<unknown, State> { timerId: NodeJS.Timer | null = null; constructor(props: unknown) { super(props); this.state = { timeLeft: LIMIT }; } componentDidMount = (): void => { this.timerId = setInterval(this.tick, 1000); }; componentDidUpdate = (): void => { const { timeLeft } = this.state; if (timeLeft === 0) this.reset(); }; componentWillUnmount = (): void => { if (this.timerId) clearInterval(this.timerId); }; tick = (): void => this.setState((prevState) => ({ timeLeft: prevState.timeLeft - 1 })); reset = (): void => this.setState({ timeLeft: LIMIT }); render = (): ReactElement => { const { timeLeft } = this.state; return ( <div className="container"> <header> <h1>タイマー</h1> </header> <Card> <Statistic className="number-board"> <Statistic.Label>time</Statistic.Label> <Statistic.Value>{timeLeft}</Statistic.Value> </Statistic> <Card.Content> <Button color="red" fluid onClick={this.reset}> <Icon name="redo" /> Reset </Button> </Card.Content> </Card> </div> ); }; } export default App; setInterval()というのは JavaScript の組み込み関数で、第 1 引数の関数を第 2 引数のミリ秒ご と延々と実行し続けるようにするもの ライフサイクルメソッドはたくさんあるけど、使う機会があるのはほぼ下記 componentDidMount, shouldComponentUpdate, componentDidUpdate, componentWillUnmount の 4 つくらい Presentational Component と Container Component ひとつのコンポーネントを『Presentational Component』お よび『Container Component』というものの 2 種類に分割しようというデザインパターンがある presentational component というのは『presentational(表現に関する)』の名前のとおり、純粋に見た目だけを責務とするコンポーネントのこと。 container component というのはそれをコンテナのごとく抱合してロジックを追加するためのコンポーネントのこと React の公式ドキュメントには『React の流儀(Thinking in React)』という章があって、そこで 公式が推奨するコンポーネントの正しい作り方が説明されてる 1. デザインモックから始め、その UI をコンポーネントの構造に分解して落とし込む 2. ロジックを除外した、静的に動作するバージョンを作成する 3. UI を表現するために最低限必要な「状態」を特定する 4. 3 の「状態」をどこに配置すべきかを決める 5. 階層構造を逆のぼって考え、データが上階層から流れてくるようにする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Next.js】 コンポーネントを絶対パスとエイリアスで指定する方法

はじめに こんにちわ、dtakkiyです! Next.jsでプロジェクトが大きくなってきた場合に、コンポーネントを相対パスでリンクさせているとファイルの移動などでミスが出やすくなるかもしれません。またネストが深いとコードの可読性にも問題があります。 そこで今回は、設定ファイルで/srcフォルダを基準に絶対パスで指定する方法を紹介します。 またエイリアスを使った指定方法も紹介します。 Before import React from 'react' import Button from '../../../components/home/Button' // 相対パスで指定 import Title from '../../../components/home/Title' // 相対パスで指定 const About = () => { return ( <div> <Title /> <Button /> </div> ) } export default About After import React from 'react' import Button from '/src/components/home/Button' // 絶対パスで指定 import Title from '@/components/home/Title' // エイリアスで指定 const About = () => { return ( <div> <Title /> <Button /> </div> ) } export default About ネストも浅くなり、だいぶスッキリしました! 動作検証の環境 Next.js: 10.2 Javascript 設定方法 まず基準となる親フォルダ/srcを作成します。 $ mkdir src 次にjsconfig.jsonを作成します。 $ touch jsconfig.json 作成したJSONファイルに以下のキーを設定します。 今回は/srcを基準のフォルダに指定しました。 jsonconfig.json { "compilerOptions": { "baseUrl": "/src", "paths": { "@/components/*": ["src/components/*"] } } } 参考サイト Absolute Imports and Module path aliases
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】Testing Libraryでポップアップの表示テスト

Testing Library学習中ということで、いろんなテストケースを記事にまとめています。 今回はマウスオーバーでポップアップが表示されるケースについてまとめました。 テスト実装中に遭遇したWarningの解消にかなり手こずったので、同様の問題にぶつかっている方がいたら記載した解消法が参考になるかと思います。 実装 Terms and Conditionの部分をマウスオーバーしたときに、No ice cream will actually be deliveredというポップアップが表示されるケースを考えます。 テストするファイルのコンポーネントSummaryFormの中身は以下のようになっています。 スタイルにはreact-bootstrapを使用しています。 SummaryForm.jsx export default function SummaryForm() { const [tcChecked, setTcChecked] = useState(false); const popover = ( <Popover id="termsandconditions-popover"> <Popover.Content>No ice cream will actually be delivered</Popover.Content> </Popover> ); const checkboxLabel = ( <span> I agree to <OverlayTrigger placement="right" overlay={popover}> <span style={{ color: 'blue' }}> Terms and Conditions</span> </OverlayTrigger> </span> ); return ( <Form> <Form.Group controlId="terms-and-conditions"> <Form.Check type="checkbox" checked={tcChecked} onChange={(e) => setTcChecked(e.target.checked)} label={checkboxLabel} /> </Form.Group> <Button variant="primary" type="submit" disabled={!tcChecked}> Confirm order </Button> </Form> ); } 同じ階層にtestフォルダを作成し、その中にSummaryForm.test.jsxを作成します。 SummaryForm.test.jsx test('popover responds to hover', async () => { // SummaryFormの仮想DOMにアクセス render(<SummaryForm />); // ポップアップが初期状態で隠れていれば成功 const nullPopover = screen.queryByText( /no ice cream will actually be delivered/i ); expect(nullPopover).not.toBeInTheDocument(); // マウスオーバーを模擬 const termsAndConditions = screen.getByText(/terms and conditions/i); userEvent.hover(termsAndConditions); // ポップアップが表示されていれば成功 const popover = screen.getByText(/no ice cream will actually be delivered/i); expect(popover).toBeInTheDocument(); // マウスオーバーの解除を模擬 userEvent.unhover(termsAndConditions); await waitForElementToBeRemoved(() => screen.queryByText(/no ice cream will actually be delivered/i) ); }); 要素へのマウスオーバーのイベント模擬には、userEventを使います。 userEventを使うことで細かいブラウザ操作の模擬を行うことができるため、fireEventよりもテストでの使用に適しています。 また、テストファイル内ではqueryByText getByTextというクエリメソッドを使い分けています。 query getにあたる部分をコマンド、Textにあたる部分をクエリタイプと呼ぶのですが、それぞれの部分で以下のようなことを指定しています。 コマンド get: DOMに要素があることを期待しており、要素がなければエラーを返す query: DOMに要素がないことを期待しており、要素がなければnullを返す find: 要素が非同期で現れることを期待しており、Promiseを返す クエリタイプ Role: すべての要素 AltText: imgやinputなどのText以外の要素 Text: divやspanなどのForm外の要素 クエリタイプの使い分けについては、公式Docsに記載されている優先度に従います。 最後に、await waitForElementToBeRemovedの記述についての補足です。 アサーションについてはexpect(popover).toBeInTheDocument()で終わっているのですが、userEvent.unhover(termsAndConditions)以降の記述がないと以下のWarningに遭遇します。 このWarningはテスト内でDOMの更新を行った場合に発生します。 ただ、Warningに従ってact()でのラッピングを行う必要はなく(Testing Libraryですでに行っているため)、代わりにテスト終了後の状態を決めるような操作を行ってあげる必要があります。 userEvent.unhover(termsAndConditions)でマウスオーバーを解除したときのイベントを発生させ、await waitForElementToBeRemoved(...)で要素が消えた後のコールバックを非同期で実行しています。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでページリダイレクトする方法

はじめに こんにちわ、dtakkiyです! Next.jsでページリダイレクトができるか調べました。 以下の手順は、Next.js バージョン10.2.3で動作確認しています。 そもそもリダイレクトとは? リダイレクトとは、WEBページに訪れたユーザを別のURLに自動的に転送するための仕組みです。 設定方法 next.config.jsの作成 まずNext.jsプロジェクトのルートフォルダにnext.config.jsファイルを作成します。 $ touch next.config.js next.config.jsの設定 作成したnext.config.jsにredirectsキーを指定することでリダイレクトを設定できます。 例えば/aboutから/topへリダイレクトさせたい場合は、以下のように設定します。 next.config.js module.exports = { async redirects() { return [ { source: '/about', // 転送元のページ destination: '/top', // 転送先のページ 宛先のパスを指定する permanent: true, }, ] }, } それ以外の設定例については、next.config.jsを参照下さい。 ソース
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む