RTQ_React05 - wahei628/ShareSuke GitHub Wiki

https://www.notion.so/05_-5add9440a44c4c6dbfdd35269b5ea71c

<aside> 📎 目次


</aside>

ブランチ

05_todo_detail


学習の意識ポイント

  • useCallbackフックとuseRefフックの機能
  • コンポーネント、型定義の復習
  • カスタムフックの実装

実践フェーズ

  1. ItemCardコンポーネントに置き換える (https://www.notion.so/1-ItemCard-99440e12b46d473e83119eb74333432b?pvs=21) -- 2: ItemCardコンポーネントの実装 (https://www.notion.so/2-ItemCard-542f8be6245b4fdfb881f9084fd53d94?pvs=21)

1. ItemCardコンポーネントに置き換える

⇒ Todo1つ1つをItemCardとしてコンポーネントに切り出す

先に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> ); }

変更した箇所

  1. import

    import ItemCard from "./components/ItemCard";  // 追加
    

    ItemCard コンポーネントを から ItemCard 関数を インポート

    { 波括弧 } をつけていないので、これから ItemCard コンポーネントを作成するが、そのときに default export とする

  2. コンポーネント関数の呼び出し & 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> の部分をコンポーネント化する。次。


2: ItemCardコンポーネントの実装

  1. コンポーネントファイルを作成する

    [ app/favascritpt/featrues/todos/components/ItemCard.tsx ]

    app 
     └-- javascript 
    				└----- react                         
    								 ├----- entrypoints  
    								 │           └----- todo_app.tsx
    								 └----- features
    														 |
    													 todos	 
    														 ├------ index.tsx
    													   ├------ types ---- index.ts
    													   └------ components --- ItemCard.tsx
    
  2. コードを書く

    <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; // itemItem 型 }

    // React.FC&lt;ItemCardProps&gt; を使用して、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に型安全を提供するためによく使われます。

      React.FCの利点

      1. 明示的な型定義:コンポーネントが受け取るpropsの型を明確にすることができます。
      2. childrenのサポートReact.FCを使用すると、自動的にchildrenプロパティがprops型に含まれるため、子要素を持つことができます。
      3. デフォルトの型React.FCReact.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 を使うかどうか

      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で渡されてきた情報に問題ないかをチェックしてくれる

    変わり果ててて何がなんだか… きっと後でわかるんだろう。次。


3. モーダル機能の実装手順

ちゅうもーく !!!!! 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>

4. モーダルコンポーネントの実装

モーダルコンポーネントを作成する

ファイル作成

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関数 ⇒ 同様、モーダルを閉じることが可能


Hooks とは

React Hooksは、関数コンポーネントで状態管理やライフサイクル機能などを使えるようにするための機能。HooksはReact 16.8で導入され、それ以前のクラスコンポーネントでしか使用できなかった多くの機能を関数コンポーネントでも利用できるようにしました。

  • useState: コンポーネントの状態を管理
  • useEffect: 副作用(データの取得、購読など)を扱う
  • useContext: Reactのコンテキストを関数コンポーネントで利用
  • useRef: DOM要素への参照や、レンダー間での値の保持を可能に
  • useCallback: 関数をメモ化し、依存する値に変更があった場合にのみ関数を再生成

Custom Hooks

カスタムHooksは、これらの基本的なHooksを組み合わせて、特定の機能をカプセル化し、再利用可能なもの。useModal というカスタムHookは、モーダルダイアログの参照を作成し、そのダイアログを開閉する機能を提供している。

<aside> 📌 カリキュラムページでの説明


useRef

useRefはReactコンポーネント内のDOMノードやその他のReact要素への参照を保持するために使用されます。

一般的にDOM要素の参照の際に利用され、DOM要素に直接アクセスして操作できます。

このコードではDialogの要素を参照して操作するために利用しています。

useCallback

コールバック関数をメモ化して、同じコールバック関数のインスタンスを再利用します。

メモ化とは、関数の実行結果をキャッシュしておくことで、同じ引数が与えられた場合に再計算をせずにキャッシュされた結果を返す手法です。

これにより、子コンポーネントが余分に再レンダリングされるのを防ぐため、パフォーマンスを向上のために利用されます。

</aside>

まぁ、コードの詳しいことは省いて、そんな便利な機能があるんだぁ

くらいにとどめておこうっと。


最後に、daisyUI のモーダルを反映させるために 以下のコマンドを実行した。

docker compose exec web yarn build:css

Untitled

はい OK。

テストコード

docker compose exec web yarn jest
⚠️ **GitHub.com Fallback** ⚠️