【React】カスタムフックを使ったリファクタリング

本記事の内容について
---
Laravel×Reactで作成したポートフォリオ「[MATRIXFLOW](https://matrixflow-d2987557bad9.herokuapp.com/)」で
Reactのコードをカスタムフックを使ってリファクタリングしたため、アウトプットします。

https://qiita.com/maixhashi/items/5da649e290ec13e71465

https://matrixflow-d2987557bad9.herokuapp.com/

リファクタリング Before/After
---
ポートフォリオ「[MATRIXFLOW](https://matrixflow-d2987557bad9.herokuapp.com/)」では、MatrixViewコンポーネントというコンポーネントでワークフローを表す表を描画しています。
MatrixViewコンポーネントのリファクタリングを例として記します。


### リファクタリング前
以下は、[リファクタリング前のMatrixViewコンポーネント](https://github.com/maixhashi/matrixflow/blob/1dfa40760e41c5a7bc62170db4fcb81fb0db0336/src/resources/js/Components/MatrixView.jsx)の一部抜粋です。
 MatrixViewコンポーネントのロジック部分のコードが100行ほど続き、
 実際のMatrixViewコンポーネントの描画部分(`return(`以降)がようやく現れる。。

``` React:MatrixView.jsx(リファクタリング前)
const MatrixView = ({ onAssignFlowStep, onMemberAdded, onFlowStepAdded, workflowId }) => {
    const [isModalOpen, setIsModalOpen] = useState(false);
    const [isModalforAddCheckListFormOpen, setIsModalforAddCheckListFormOpen] = useState(false);
    const [selectedMember, setSelectedMember] = useState(null);
    const [selectedStepNumber, setSelectedStepNumber] = useState(null);
    const [maxFlowNumber, setMaxFlowNumber] = useState(0);
    const [orderedMembers, setOrderedMembers] = useState([]);
    
    const dispatch = useDispatch();
    
    const members = useSelector((state) => state.members);
    const flowsteps = useSelector((state) => state.flowsteps);
    const isAddFlowstepModalOpen = useSelector((state) => state.modal.isAddFlowstepModalOpen);
    
    useEffect(() => {
        dispatch(fetchMembers(workflowId));
        dispatch(fetchFlowsteps(workflowId));
    }, [dispatch, workflowId]);

    useEffect(() => {
        if (flowsteps.length > 0) {
            const maxFlowNumber = Math.max(0, ...flowsteps.map(step => step.flow_number));
            setMaxFlowNumber(maxFlowNumber);
        } else {
            setMaxFlowNumber(0);
        }
    }, [flowsteps]);

    useEffect(() => {
        setOrderedMembers(members);
    }, [members]);

    const handleMemberAdded = async (newMember) => {
        await onMemberAdded(newMember); 
        dispatch(fetchMembers()); 
    };
        

    const openAddFlowStepModal = (member, stepNumber) => {
        setSelectedMember(member);
        setSelectedStepNumber(stepNumber);
        setIsModalOpen(true);
    };

    const handleOpenAddFlowstepModalonMatrixView = (member, stepNumber) => {
        setSelectedMember(member);
        setSelectedStepNumber(stepNumber);
        dispatch(openAddFlowStepModal());
    };

    const closeAddFlowStepModal = () => {
        setSelectedMember(null);
        setSelectedStepNumber(null);
        setIsModalOpen(false);
    };

    const openAddCheckListModal = (member, stepNumber) => {
        setSelectedMember(member);
        setSelectedStepNumber(stepNumber);
        setIsModalforAddCheckListFormOpen(true);
    };

    const closeAddCheckListModal = () => {
        setSelectedMember(null);
        setSelectedStepNumber(null);
        setIsModalforAddCheckListFormOpen(false);
    };

    const moveRow = async (fromIndex, toIndex) => {
        const updatedMembers = [...orderedMembers];
        const [movedMember] = updatedMembers.splice(fromIndex, 1);
        updatedMembers.splice(toIndex, 0, movedMember);
        setOrderedMembers(updatedMembers);

        const response = await saveOrderToServer(updatedMembers);
        if (response.success) {
            console.log('Order saved successfully');
            dispatch(fetchFlowsteps());
        } else {
            console.error('Error saving order:', response.error);
        }
    };

    const saveOrderToServer = async (updatedMembers) => {
        try {
            const response = await axios.post('/api/save-order', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ member_ids: updatedMembers.map(member => member.id) }),
            });

            const data = await response.json();
            return data;
        } catch (error) {
            console.error('Error saving order:', error);
            return { success: false, error };
        }
    };

    const handleUpdateFlowStepNumber = async (flowStepId, newFlowNumber) => {
        // ReduxのupdateFlowStepNumberをdispatch
        dispatch(updateFlowStepNumber({ flowStepId, newFlowNumber }));
        dispatch(fetchFlowsteps(workflowId));
    };

    const handleMemberDelete = (memberId) => {
        dispatch(deleteMember(memberId))
            .then(() => {
                dispatch(fetchMembers(workflowId));
            })
            .catch((error) => {
                console.error('Error deleting member:', error);
            });
    };

    return (
        <DndProvider backend={HTML5Backend}>
            <div className="matrix-container">
             ...

```

### リファクタリング後
[リファクタリング後のMatrixViewコンポーネント](https://github.com/maixhashi/matrixflow/blob/ca6513a997eb7f600032a28081c71b869d2e99ee/src/resources/js/Components/MatrixView.jsx)は次の通りになります。

 - `useState`で定義するローカルステート
 - `useSelector`で定義するグローバルステート
 - イベントハンドラ
 をまとめてカスタムフック`useMatrixView`から参照するようにリファクタリングしました。

``` React:MatrixView.jsx(リファクタリング後)
const MatrixView = ({ onAssignFlowStep, onFlowStepAdded }) => {
    const { 
        // Local State
        isHovered, setIsHovered, isEditingToolsystemName, setIsEditingToolsystemName, updatedToolsystemName, setUpdatedToolsystemName,
        isModalforAddCheckListFormOpen, selectedStepNumber, maxFlowNumber, orderedMembers,
    
        // Global State
        dataBaseIconPositions, flowstepPositions, selectedMember,
        flowsteps, isAddFlowstepModalOpen, isUpdateFlowstepModalOpen, workflowId,
    
        // Event Handler
        handleMemberAdded, handleOpenAddFlowstepModalonMatrixView, openAddCheckListModal, closeAddCheckListModal, moveRow,
        handleUpdateFlowStepNumber, handleUpdateToolsystemName, handleMemberDelete, handleSetSelectedToolsystem,
    } = useMatrixView();
    

    return (
        <DndProvider backend={HTML5Backend}>
            <div className="matrix-container">
            ...
```

[カスタムフック`useMatrixView.jsx`](https://github.com/maixhashi/matrixflow/blob/935d379b7b9809b53c4e54bbe856441d02c39aee/src/resources/js/Hooks/useMatrixView.js)の内容は次の通りです。(一部抜粋)

``` React: カスタムフック useMatrixView.jsx
import { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchMembers, deleteMember } from '../store/memberSlice';
import { fetchFlowsteps, updateFlowStepNumber } from '../store/flowstepsSlice';
import { updateToolsystemForFlowstep } from '../store/toolsystemSlice';
import { setDataBaseIconPositions } from '../store/positionSlice';
import { setSelectedMember, setSelectedToolsystem } from '../store/selectedSlice';
import { openAddFlowstepModal } from '../store/modalSlice';

export const useMatrixView = () => {
  const [isHovered, setIsHovered] = useState(false);
  ...

  return { 
    // Local State
    isHovered, setIsHovered, isEditingToolsystemName, setIsEditingToolsystemName, updatedToolsystemName, setUpdatedToolsystemName,
    isModalforAddCheckListFormOpen, setIsModalforAddCheckListFormOpen, selectedStepNumber, setSelectedStepNumber,
    maxFlowNumber, setMaxFlowNumber, orderedMembers, setOrderedMembers,

    // Global State
    dataBaseIconPositions, flowstepPositions, selectedMember, selectedFlowstep, selectedToolsystem,
    members, flowsteps, isAddFlowstepModalOpen, isUpdateFlowstepModalOpen, workflowId,

    // Event Handler
    handleMemberAdded, handleOpenAddFlowstepModalonMatrixView, openAddCheckListModal, closeAddCheckListModal, moveRow,
    handleUpdateFlowStepNumber, handleUpdateToolsystemName, handleMemberDelete, handleSetSelectedToolsystem,
  };
};
```
 
実装方法について
---
### 使用技術
 - React 18.3.1
   - Redux 

### 実装手順
#### カスタムフックのJSXファイルを作成

- 格納フォルダ・カスタムフックファイルを作成 `src/resources/js/Hooks/[カスタムフックファイル.jsx]`
    - カスタムフックのファイルの名前は `use[カスタムフックを使用するコンポーネント名].jsx`としました
    - [React 公式doc](https://ja.react.dev/learn/reusing-logic-with-custom-hooks#hook-names-always-start-with-use) のとおり、`use`から始まるファイル名にします
 
https://ja.react.dev/learn/reusing-logic-with-custom-hooks#hook-names-always-start-with-use

#### コンポーネントのステート、イベントハンドラ部分（描画部分以外）をカスタムフックファイルに移管する

 - `useState`で定義するローカルステート
 - `useSelector`で定義するグローバルステート
 - `useEffect`で定義する状態監視
 - イベントハンドラ
 - その他ロジックのために使用する定数
をカスタムフックファイルに移管します

 移管した全てのローカルステート、グローバルステート、イベントハンドラ、定数は
 ひとまず全て`export`設定

 #### コンポーネント上でカスタムフックからステート、イベントハンドラ、定数を参照する
 カスタムフックを使用するコンポーネント上で、カスタムフック内で定義したステート、イベントハンドラ、定数を参照して呼び出します。

 コンポーネント描画部分（`return(`以降）で使用されないステートなどは削除
 ※コンポーネントの内部ロジックには使用されるが、描画には関わらないもの

スクショのようにVisual Studio Code で表示されるものが該当
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/606327/1c160def-6393-af58-08d2-6f7d80f78f8e.png)


終わりに
---
リファクタリング、良き

リファクタリングで次のようなことも発見することができ、改善に繋がりました
 - イベントハンドラの命名の仕方が揃っていないことを発見
     - → `handle[何する][〇〇を]`という命名に統一  
 - Reduxでグローバルステートとして定義したものが一部ローカルステートとして残っていることを発見
     - → モーダルの開閉フラグを全てRedux管理に統一

**カスタムフックは...良いぞ**

もう1つのポートフォリオ「PlotForge」について
---
もう1つポートフォリオ「PlotForge」もリリースしております！
こちらもご覧いただけますと幸いです。

https://qiita.com/maixhashi/items/e9a3503913b16635905f