0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-16

本記事の内容について

Laravel×Reactで作成したポートフォリオ「MATRIXFLOW」で
Reactのコードをカスタムフックを使ってリファクタリングしたため、アウトプットします。

リファクタリング Before/After

ポートフォリオ「MATRIXFLOW」では、MatrixViewコンポーネントというコンポーネントでワークフローを表す表を描画しています。
MatrixViewコンポーネントのリファクタリングを例として記します。

リファクタリング前

以下は、リファクタリング前のMatrixViewコンポーネントの一部抜粋です。
MatrixViewコンポーネントのロジック部分のコードが100行ほど続き、
実際のMatrixViewコンポーネントの描画部分(return(以降)がようやく現れる。。

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コンポーネントは次の通りになります。

  • useStateで定義するローカルステート
  • useSelectorで定義するグローバルステート
  • イベントハンドラ
    をまとめてカスタムフックuseMatrixViewから参照するようにリファクタリングしました。
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の内容は次の通りです。(一部抜粋)

カスタムフック 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 のとおり、useから始まるファイル名にします

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

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

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

コンポーネント上でカスタムフックからステート、イベントハンドラ、定数を参照する

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

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

スクショのようにVisual Studio Code で表示されるものが該当
image.png

終わりに

リファクタリング、良き

リファクタリングで次のようなことも発見することができ、改善に繋がりました

  • イベントハンドラの命名の仕方が揃っていないことを発見
    • handle[何する][〇〇を]という命名に統一
  • Reduxでグローバルステートとして定義したものが一部ローカルステートとして残っていることを発見
    • → モーダルの開閉フラグを全てRedux管理に統一

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

もう1つのポートフォリオ「PlotForge」について

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?