RTQ_React05 - wahei628/ShareSuke GitHub Wiki
https://www.notion.so/05_-5add9440a44c4c6dbfdd35269b5ea71c
<aside> 📎 目次
</aside>
05_todo_detail
- useCallbackフックとuseRefフックの機能
- コンポーネント、型定義の復習
- カスタムフックの実装
- ItemCardコンポーネントに置き換える (https://www.notion.so/1-ItemCard-99440e12b46d473e83119eb74333432b?pvs=21) -- 2: ItemCardコンポーネントの実装 (https://www.notion.so/2-ItemCard-542f8be6245b4fdfb881f9084fd53d94?pvs=21)
先にindex.tsxファイルにItemCardコンポーネントを作成する想定でコードを書いていく。 このコンポーネントはToDoを1つ1つ表示するためのもの。 下記のようにimport文を追加して、該当箇所を置き換える。
[ app/javascript/react/features/todos/index.tsx ] を以下のように書き換える
import React, { useState } from "react";
import { Item, Category } from "./types";
import ItemCard from "./components/ItemCard"; // 追加
// 省略
return (
<div className="my-4 flex max-h-[90%] overflow-x-auto">
{categories.map((category) => (
<div
key={category.id}
className="ml-4 max-h-[100%] flex-none self-start overflow-y-auto rounded border border-gray-200 bg-base-300"
>
<div className="sticky top-0 z-10 flex rounded bg-base-300 p-2">
<div className="flex-1">{category.name}</div>
</div>
<div
className="min-h-[100px] w-[350px]"
>
{filteredItems(category.id).map((item) => (
<div key={item.id}>
// ここから変更
<ItemCard
item={item}
/>
// ここまで変更
</div>
))}
</div>
</div>
))}
</div>
);
元の [ app/javascript/react/features/todos/index.tsx ]
import React, { useState } from "react"; // feature/04 追加
import { Item, Category } from "./types"; // feature/04 追加
import ItemCard from "./components/ItemCard"; // 追加
// 初期アイテムモックリストの定義
const initialItems = [
{ id: 1, title: 'アイテム1', content: 'アイテム1', category: 'NoStatus', assignee: '未割り当て', position: 10, category_id: 1},
{ id: 2, title: 'アイテム2', content: 'アイテム2', category: 'NoStatus', assignee: '未割り当て', position: 20, category_id: 1 },
{ id: 3, title: 'アイテム3', content: 'アイテム3', category: 'NoStatus', assignee: 'らんてくん', position: 30, category_id: 1 },
{ id: 4, title: 'アイテム4', content: 'アイテム4', category: 'NoStatus', assignee: '未割り当て', position: 40, category_id: 1 },
{ id: 5, title: 'アイテム5', content: 'アイテム5', category: 'NoStatus', assignee: 'らんくん', position: 50, category_id: 1 },
{ id: 6, title: 'アイテム6', content: 'アイテム6', category: 'NoStatus', assignee: '未割り当て', position: 60, category_id: 1 },
{ id: 7, title: 'アイテム7', content: 'アイテム7', category: 'NoStatus', assignee: '未割り当て', position: 70, category_id: 1 },
{ id: 8, title: 'アイテム8', content: 'アイテム8', category: 'NoStatus', assignee: '未割り当て', position: 80, category_id: 1 },
{ id: 9, title: 'アイテム9', content: 'アイテム9', category: 'InProgress', assignee: '未割り当て', position: 10, category_id: 3 },
{ id: 10, title: 'アイテム10', content: 'アイテム10', category: 'Done', assignee: '未割り当て', position: 10, category_id: 4 }
];
// 初期カテゴリーモックリストの定義
const initialCategories = [
{ id: 1, name: 'NoStatus'},
{ id: 2, name: 'Backlog'},
{ id: 3, name: 'InProgress'},
{ id: 4, name: 'Done'}
];
export default function Todos() {
const [items, setItems] = useState<Item[]>(initialItems);
const [categories, setCategories] = useState<Category[]>(initialCategories);
const filteredItems = (categoryId: number) => {
return items
.filter(
(item) =>
item.category_id === categoryId
).sort((a, b) => a.position - b.position)
}
return (
<div className="my-4 flex max-h-[90%] overflow-x-auto">
{categories.map((category) => (
<div
key={category.id}
className="ml-4 max-h-[100%] flex-none self-start overflow-y-auto rounded border border-gray-200 bg-base-300"
>
<div className="sticky top-0 z-10 flex rounded bg-base-300 p-2">
<div className="flex-1">{category.name}</div>
</div>
<div
className="min-h-[100px] w-[350px]"
>
{filteredItems(category.id).map((item) => (
<div key={item.id}>
<div className="m-2 rounded border border-gray-200 bg-base-100 hover:bg-base-200">
<div className="relative flex min-h-[80px] flex-col">
<div className="link m-2 mr-10 break-words text-sm">{item.title}</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
);
}
変更した箇所
import
import ItemCard from "./components/ItemCard"; // 追加
ItemCard コンポーネントを から ItemCard 関数を インポート
{ 波括弧 } をつけていないので、これから ItemCard コンポーネントを作成するが、そのときに default export とする
コンポーネント関数の呼び出し & props ⇒ Item を設定
<ItemCard item={item}/>
⇒ props とは、任意の名前で設定しておける渡す引数みたいなもの。
オブジェクト方式で複数個設定しておける。( 気がする )
after
<div key={item.id}>
<ItemCard item={item}/>
</div>
before
<div key={item.id}>
<div className="m-2 rounded border border-gray-200 bg-base-100 hover:bg-base-200">
<div className="relative flex min-h-[80px] flex-col">
<div className="link m-2 mr-10 break-words text-sm">{item.title}</div>
</div>
</div>
</div>
この、before のゴチャついている <div></div> の部分をコンポーネント化する。次。
-
コンポーネントファイルを作成する
[ app/favascritpt/featrues/todos/components/ItemCard.tsx ]
app └-- javascript └----- react ├----- entrypoints │ └----- todo_app.tsx └----- features | todos ├------ index.tsx ├------ types ---- index.ts └------ components --- ItemCard.tsx
-
コードを書く
<aside> 📌 カリキュラムのコードをコピペする前の整理
① 取り敢えず書いてみよう
[ app/favascritpt/featrues/todos/components/ItemCard.tsx ]
import React from "react";
export const ItemCard =(props) => { return ( <div className="m-2 rounded border border-gray-200 bg-base-100 hover:bg-base-200"> <div className="relative flex min-h-[80px] flex-col"> <div className="link m-2 mr-10 break-words text-sm">{item.title}</div> </div> </div> ); };
・ ItemCard 関数を インポートするようにかいたので、ItemCard 関数を定義 ・ とりあえず 引数は Udemy と同じように ( props ) としてみた。
これで、取り敢えず前の “ゴチャついている<div>タグ” をコンポーネント化できた。
しかし、これは React の書き方であり、TypeScript の強みである「型の管理」 ができていない。次。
■ TypeScript の強みである「型の管理」 をする
② interface を使用して ジェネリック型を作成する
interface ItemCardProps { item: Item; // `item` は `Item` 型 }
∴ 全体のコードは以下のようになる
[ app/favascritpt/featrues/todos/components/ItemCard.tsx ]
import React from 'react'; import { Item } from '../types';
interface ItemCardProps { item: Item; //
item
はItem
型 }//
React.FC<ItemCardProps>
を使用して、props がItemCardProps
型であることを保証 export const ItemCard: React.FC<ItemCardProps> = ({ item }) => { return ( <div className="m-2 rounded border border-gray-200 bg-base-100 hover:bg-base-200"> <div className="relative flex min-h-[80px] flex-col"> <div className="link m-2 mr-10 break-words text-sm">{item.title}</div> </div> </div> ); };
-
[ 備考 ] React.FC ⇒ React Function Component
React.FC
は「Function Component」の略で、Reactの関数コンポーネントを定義する際に使用する型です。これは、TypeScriptでReactコンポーネントを定義する際に、そのコンポーネントのpropsに型安全を提供するためによく使われます。- 明示的な型定義:コンポーネントが受け取るpropsの型を明確にすることができます。
-
childrenのサポート:
React.FC
を使用すると、自動的にchildren
プロパティがprops型に含まれるため、子要素を持つことができます。 -
デフォルトの型:
React.FC
はReact.FunctionComponent
の略で、デフォルトの型としてPropsWithChildren<T>
を使用します。これにより、children
propが含まれることを意味します。
import React from 'react'; import { Item } from '../types';
interface ItemCardProps { item: Item; }
// React.FC を使用して関数コンポーネントを定義 const ItemCard: React.FC<ItemCardProps> = ({ item }) => { return ( <div className="m-2 rounded border border-gray-200 bg-base-100 hover:bg-base-200"> <div className="relative flex min-h-[80px] flex-col"> <div className="link m-2 mr-10 break-words text-sm">{item.title}</div> </div> </div> ); };
export default Item-1 Card;
React.FC
の使用は推奨されるスタイルの一つですが、必ずしも必要とされるわけではありません。特に、コンポーネントがchildren
を受け取らない場合や、より単純な型定義が適している場合は、普通の関数としてコンポーネントを定義することもあります。その場合、型定義を直接関数のパラメータに適用することで、同様の型安全を提供することができます。例:
interface ItemCardProps { item: Item; }
// 通常の関数として型を直接適用 const ItemCard = ({ item }: ItemCardProps) => { return ( <div className="m-2 rounded border border-gray-200 bg-base-100 hover:bg-base-200"> <div className="relative flex min-h-[80px] flex-col"> <div className="link m-2 mr-10 break-words text-sm">{item.title}</div> </div> </div> ); };
export default ItemCard;
どの方式を選択するかは、プロジェクトのスタイルガイドやチームの好み、または特定のケースに応じた技術的な要件によって異なります。
</aside>
カリキュラムの配布コードをコピペしてみる
import React from "react"; import Modal from "./ui/Modal"; // 後ほど実装 import useModal from "../hooks/useModal"; // 後ほど実装 import { Item } from "../types";
interface ItemCardProps { item: Item; }
export default function ShowItem({ item, } : ItemCardProps) { const { modalRef, openModal } = useModal(); // 後ほど実装 return ( <div> <div className="m-2 rounded border border-gray-200 bg-base-100 hover:bg-base-200"> <div className="relative flex min-h-[80px] flex-col"> <div className="link m-2 mr-10 break-words text-sm"onClick={openModal}>{item.title}</div> </div> </div> <Modal modalRef={modalRef}> <div className="flex"> <h3 className="my-2 flex-1 border-b border-gray-300 text-3xl font-bold"> Show </h3> </div> <div className="mb-2 text-lg">Title</div> <div className="rounded-lg border border-solid border-gray-300"> <div className="m-2 mx-4">{item.title}</div> </div> <div className="mb-2 text-lg">Content</div> <div className="rounded-lg border border-solid border-gray-300 pb-10 text-sm"> <div className="m-2 mx-4">{item.content}</div> </div> <div className="mb-2 text-lg">Assignee</div> <div className="badge badge-outline badge-lg">{item.assignee}</div> </Modal> </div> ); }
interface ItemCardProps
で型を定義することで、propsで渡されてきた情報に問題ないかをチェックしてくれる変わり果ててて何がなんだか… きっと後でわかるんだろう。次。
-
ちゅうもーく !!!!! ItemCard.tsxのコンポーネントのコード !!!!
<Modal modalRef={modalRef}>
// 省略
</Modal>
<Modal>
で
詳細情報が囲まれているッ !!
UIライブラリのDaisyUIを利用してモーダルのコンポーネントを実装
「モーダル外をクリックすることでモーダルウィンドウを閉じる 」
これを作成していく。
下記はモーダル表示のbuttonの要素と表示させるモーダルのdialogの要素 ( daisyUI )
<button className="btn" onClick={()=>document.getElementById('my_modal_2').showModal()}>open modal</button>
<dialog id="my_modal_2" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg">Hello!</h3>
<p className="py-4">Press ESC key or click outside to close</p>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
↓
↓ 変更
↓
UI的に<dialog>
部分を変更
<div className="modal-box h-2/3">
の要素内がモーダルウィンドウの中身になるので、モーダル内に表示させる要素は共通化できるように children を使用する。
childrenプロパティを使用して、モーダル内に表示させる要素を共通化。
<dialog ref={modalRef} className="modal">
<div className="modal-box h-2/3">
{children}
<form method="dialog">
<button className="btn btn-circle btn-ghost btn-sm absolute right-2 top-2">
✕
</button>
</form>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
下記のコード部分で、画面外クリックした場合でもモーダルを閉じることができる。
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
ファイル作成
app/javascript/react/features/todos/components/Modal/ui/Modal.tsx
app
└-- javascript
└----- react
├----- entrypoints
│ └----- todo_app.tsx
└----- features
|
todos
├------ index.tsx
├------ types ---- index.ts
└------ components --- ItemCard.tsx
│
ui
└---- Modal.tsx
コード写し
[ app/javascript/react/features/todos/components/Modal/ui/Modal.tsx ]
import React from 'react'
type Props = {
children: React.ReactNode
modalRef: React.RefObject<HTMLDialogElement>
}
export default function Modal({ children, modalRef }: Props) {
return (
<dialog ref={modalRef} className="modal">
<div className="modal-box h-2/3">
{children}
<form method="dialog">
<button className="btn btn-circle btn-ghost btn-sm absolute right-2 top-2">
✕
</button>
</form>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
)
}
💡
これだけではまだ、モーダルを使用は❌ ⇒ モーダルの表示/非表示を状態管理する機能が必要だから。
⭐ それがコード上の
modalRef
というカスタム hooks になる。
⇒⇒⇒ ここでは、「 定義しただけ 」 という認識で OK ??
色々きになるとこ
アロー関数で書くと、こうなる
const Modal = ({ children, modalRef }: Props) => { // 省略 }; default export Modal
型をまとめる
type Props = { children: React.ReactNode modalRef: React.RefObject<HTMLDialogElement> }
オブジェクトにして、型をまとめる。
React.ReactNode、React.RefObject<HTMLDialogElement> こいつらは、React を使う上で便利にするように作られた型、使用される型、という認識で今はOKだと思う。
はい次。
ファイルを作成
app/javascript/react/features/todos/components/Modal/hooks/useModal.ts
app
└-- javascript
└----- react
├----- entrypoints
│ └----- todo_app.tsx
└----- features
|
todos
├------ index.tsx
├------ types ---- index.ts
├------ components --- ItemCard.tsx
│ │
│ ui
│ └---- Modal.tsx
hooks
└------ useModal.ts
コード写し
[ app/javascript/react/features/todos/components/Modal/hooks/useModal.ts ]
import { useRef, useCallback } from 'react'
export default function useModal() {
const modalRef = useRef<HTMLDialogElement>(null)
const openModal = useCallback(() => {
const modal = modalRef.current
if (modal && modal.showModal) {
modal.showModal()
}
}, [])
const closeModal = useCallback(() => {
const modal = modalRef.current
if (modal && modal.close) {
modal.close()
}
}, [])
return { modalRef, openModal, closeModal }
}
・ openModal関数 ⇒ ボタン要素などに追加 ⇒ モーダルを開くことが可能 ・ closeModal関数 ⇒ 同様、モーダルを閉じることが可能
React Hooksは、関数コンポーネントで状態管理やライフサイクル機能などを使えるようにするための機能。HooksはReact 16.8で導入され、それ以前のクラスコンポーネントでしか使用できなかった多くの機能を関数コンポーネントでも利用できるようにしました。
useState
: コンポーネントの状態を管理useEffect
: 副作用(データの取得、購読など)を扱うuseContext
: Reactのコンテキストを関数コンポーネントで利用useRef
: DOM要素への参照や、レンダー間での値の保持を可能にuseCallback
: 関数をメモ化し、依存する値に変更があった場合にのみ関数を再生成
カスタムHooksは、これらの基本的なHooksを組み合わせて、特定の機能をカプセル化し、再利用可能なもの。useModal
というカスタムHookは、モーダルダイアログの参照を作成し、そのダイアログを開閉する機能を提供している。
<aside> 📌 カリキュラムページでの説明
useRefはReactコンポーネント内のDOMノードやその他のReact要素への参照を保持するために使用されます。
一般的にDOM要素の参照の際に利用され、DOM要素に直接アクセスして操作できます。
このコードではDialogの要素を参照して操作するために利用しています。
コールバック関数をメモ化して、同じコールバック関数のインスタンスを再利用します。
メモ化とは、関数の実行結果をキャッシュしておくことで、同じ引数が与えられた場合に再計算をせずにキャッシュされた結果を返す手法です。
これにより、子コンポーネントが余分に再レンダリングされるのを防ぐため、パフォーマンスを向上のために利用されます。
</aside>
まぁ、コードの詳しいことは省いて、そんな便利な機能があるんだぁ
くらいにとどめておこうっと。
最後に、daisyUI のモーダルを反映させるために 以下のコマンドを実行した。
docker compose exec web yarn build:css
はい OK。
テストコード
docker compose exec web yarn jest