本記事の内容について
Laravel×Reactで作成したポートフォリオ「MATRIXFLOW」で
Reactのコードをカスタムフックを使ってリファクタリングしたため、アウトプットします。
リファクタリング Before/After
ポートフォリオ「MATRIXFLOW」では、MatrixViewコンポーネントというコンポーネントでワークフローを表す表を描画しています。
MatrixViewコンポーネントのリファクタリングを例として記します。
リファクタリング前
以下は、リファクタリング前のMatrixViewコンポーネントの一部抜粋です。
MatrixViewコンポーネントのロジック部分のコードが100行ほど続き、
実際のMatrixViewコンポーネントの描画部分(return(
以降)がようやく現れる。。
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
から参照するようにリファクタリングしました。
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
の内容は次の通りです。(一部抜粋)
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 で表示されるものが該当
終わりに
リファクタリング、良き
リファクタリングで次のようなことも発見することができ、改善に繋がりました
- イベントハンドラの命名の仕方が揃っていないことを発見
- →
handle[何する][〇〇を]
という命名に統一
- →
- Reduxでグローバルステートとして定義したものが一部ローカルステートとして残っていることを発見
- → モーダルの開閉フラグを全てRedux管理に統一
カスタムフックは...良いぞ
もう1つのポートフォリオ「PlotForge」について
もう1つポートフォリオ「PlotForge」もリリースしております!
こちらもご覧いただけますと幸いです。