⇧ 割に合わない気がする...
専門家による論文の査読で8年以上かかってるらしいのに、1.4億円ってのは夢が無いと感じてしまう...
大金であることは間違いないのだけど、才能ある人たちが8年も費やしている問題に対する費用対効果として考えた場合にどうなのかなと。
映画『グッド・ウィル・ハンティング/旅立ち(監督: ガス・ヴァン・サント)』に出てくるような数学に才能のある人たちだけに許されるような神々の遊び、といったところでしょうか...
映画は良い内容です。
⇧1997年公開で、今が2023年だから、26年経ってるのか...時の流れを感じますな...
Reactで入力は複数ページのFormで、最終的に全てのForm入力内容を一括で送信したかったが...
Reactで、せっかく「SPA(Single Page Application)」を実現できるということで、
⇧ 上記サイト様のように、複数画面のFormの入力内容を、一度に送信したいですと。
業務システムとかで、入力内容が複数ページにならざるを得ないこと、あるあるな要件だとは思うのだけど、Reactでの実例がほとんど無いのに驚き。
例えば、IPAが公開しているデータベースの設計に関する問題で、
⇧ 上図のような「概念データモデリング」があるのだけど、
- 出荷指示
- 出荷指示梱包明細
- 出荷指示店舗別梱包明細
- 出荷指示梱包明細
というような入れ子構造になってそうではありますと。
なので、
⇧ 上図のようなイメージで、複数ページの<form>の内容をまとめて一度に送信したいですと。
と思ったけど、計算処理とかは、サーバーサイドでやった方が良いような気がしてきたので、
みたいな流れにして、計算処理はサーバーサイドに任せた方が良いんかな?
サーバーサイドで計算した結果の③をフロントエンド側で状態管理できれば、サーバーサイドとの通信を減らせるってことですかね。
値を変更した場合は、サーバーサイドとの通信が発生することにはなるけども、ページを表示する度にサーバーサイドとの通信を発生せずに済むかと。
Reactで<form>のデータを取り扱う方法は大別して2種類あるらしい
ネットの情報によりますと、
In React, there are two ways of handling form data:
- Controlled Components: In this approach, form data is handled by React through the use of hooks such as the
useState
hook. - Uncontrolled Components: Form data is handled by the Document Object Model (DOM) rather than by React. The DOM maintains the state of form data and updates it based on user input.
https://www.freecodecamp.org/news/how-to-build-forms-in-react/
⇧ 上記サイト様が仰るには、
- Controlled Components
- Uncontrolled Components
の2種類に大別できるということらしく、
「2. Uncontrolled Components」ってのは、従来のHTML5(いまは、HTML Living Standardってのに取って代わったらしい)の<form>の入力内容をそのまま扱うってことになるんかね?
一方、「1. Controlled Components」って方では、<form>の入力内容をReact側で管理するってことらしい。
サーバーサイドと都度、通信を発生させないためには、フロントエンド側で状態管理して、各ページの<form>の入力内容を保持する必要があると思われる。
状態管理が上手くいかず...
う~む、どうも、画面を戻った時に、入れ子の配列の内容がリセットされてしまう...
2023年7月8日(土)追記:↓ ここから
ちなみに、Reactで状態管理する方法としては、
⇧ 上記サイト様によりますと、4つぐらいあるらしく、今回、利用しているのは、useContextというReactのHooksの1つということになるっぽい。
2023年7月8日(土)追記:↑ ここまで
そもそも、Reactの使い方を良く分かっていないので、凡ミスとかなのかもしらんけど、いまいち、どう実装すれば良いのかが皆目見当が付かないこともあり、解決に時間がかかりそうなので、途中経過を掲載。
Reactのプロジェクトとしては、
⇧ 上記の時のものを流用した感じです。
今回、追加したソースコードは以下。
■my-react-spa-app\src\components\model\dto\shipment\inventory-shipment-packing-store-item-dto.ts
/** * 出荷指示店舗別梱包明細 * */ export interface IInventoryShipmentPackingStoreItemDto { /** 入力行項番(出荷指示梱包明細と紐づけ) */ itemNumber: number; /** 出荷指示番号 */ shipmentOrderNumber: string; /** 出荷指示梱包明細番号 */ shipmentPackingItemOrderNumber: string; /** チェーン法人コード */ chainCorporateCode: string; /** 梱包対象チェーン店舗コード */ packingChainStoreCode: string; /** 店舗名 */ storeName: string; /** 店舗別発注数 */ numberOfOrders: number; /** 店舗別出荷数 */ numberOfShipments: number; // } // class InventoryShipmentPackingStoreItemDto implements IInventoryShipmentPackingStoreItemDto { // itemNumber: number; // shipmentOrderNumber: string; // shipmentPackingItemOraderNumber: string; // chainCorporateCode: string; // packingChainStoreCode: string; // storeName: string; // numberOfOrders: number; // numberOfShipments: number; // constructor(itemNumber: number // , shipmentOrderNumber: string // , shipmentPackingItemOraderNumber: string // , chainCorporateCode: string // , packingChainStoreCode: string // , storeName: string // , numberOfOrders: number // , numberOfShipments: number) { // this.itemNumber = itemNumber; // this.shipmentOrderNumber = shipmentOrderNumber; // this.shipmentPackingItemOrderNumber = shipmentPackingItemOrderNumber; // this.chainCorporateCode = chainCorporateCode; // this.packingChainStoreCode = packingChainStoreCode; // this.storeName = storeName; // this.numberOfOrders = numberOfOrders; // this.numberOfShipments = numberOfShipments; // } }
■my-react-spa-app\src\components\model\dto\shipment\inventory-shipment-packing-item-dto.ts
import { IInventoryShipmentPackingStoreItemDto } from './inventory-shipment-packing-store-item-dto'; /** * 出荷指示梱包明細 * */ export interface IInventoryShipmentPackingItemDto { /** 入力行項番 */ itemNumber: number; /** 出荷指示番号 */ shipmentOrderNumber: string; /** 出荷指示梱包明細番号 */ shipmentPackingItemOrderNumber: string; /** 発注数 */ numberOfOrders: number; /** 出荷数 */ numberOfShipments: number; /**出荷指示梱包店舗別明細 */ inventoryShipmentPackingStoreItemDtoArr: IInventoryShipmentPackingStoreItemDto[]; // } // class InventoryShipmentPackingItemDto implements IInventoryShipmentPackingItemDto { // itemNumber: number; // shipmentOraderNumber: string; // shipmentPackingItemOraderNumber: string; // numberOfOrders: number; // numberOfShipments: number; // inventoryShipmentPackingStoreItemDto: Array<inventoryshipmentpackingstoreitemdto>; // constructor(itemNumber: number // , shipmentOraderNumber: string // , shipmentPackingItemOraderNumber: string // , numberOfOrders: number // , numberOfShipments: number // , inventoryShipmentPackingStoreItemDto: Array<inventoryshipmentpackingstoreitemdto>) { // this.itemNumber = itemNumber; // this.shipmentOraderNumber = shipmentOraderNumber; // this.shipmentPackingItemOraderNumber = shipmentPackingItemOraderNumber; // this.inventoryShipmentPackingStoreItemDto = inventoryShipmentPackingStoreItemDto; // this.numberOfOrders = numberOfOrders; // this.numberOfShipments = numberOfShipments; // } }
■my-react-spa-app\src\components\model\dto\shipment\inventory-shipment-dto.ts
// my-react-spa-app\src\components\model\dto\shipment\inventory-shipment-dto.ts import { IInventoryShipmentPackingItemDto } from './inventory-shipment-packing-item-dto'; /** * 出荷指示 * */ export interface IInventoryShipmentDto { /** 出荷指示番号 */ shipmentOraderNumber: string; /** チェーン法人コード */ chainCorporateCode: string; /** チェーンDCコード */ chainDcCode: string; /** 締め年月日 */ closingDate: string; /** 回目 */ nthTime: number; /** 出庫指示番号 */ goodsIssueNumber: number; /** 出荷指示梱包明細 */ inventoryShipmentPackingItemDto: IInventoryShipmentPackingItemDto[]; // } // class InventoryShipmentDto implements IInventoryShipmentDto { // shipmentOraderNumber: string; // chainCorporateCode: string; // chainDcCode: string; // closingDate: string; // nthTime: number; // goodsIssueNumber: number; // inventoryShipmentPackingItemDto: Array<inventoryshipmentpackingitemdto>; // constructor(shipmentOraderNumber:string // ,chainCorporateCode: string // ,chainDcCode: string // ,closingDate: string // ,nthTime: number // ,goodsIssueNumber: number // ,inventoryShipmentPackingItemDto: Array<inventoryshipmentpackingitemdto>) { // this.shipmentOraderNumber = shipmentOraderNumber; // this.chainCorporateCode = chainCorporateCode; // this.chainDcCode = chainDcCode; // this.closingDate = closingDate; // this.nthTime = nthTime; // this.goodsIssueNumber = goodsIssueNumber; // this.inventoryShipmentPackingItemDto = inventoryShipmentPackingItemDto; // } }
■my-react-spa-app\src\page\shipment\create\context.tsx
import React, { createContext, useState, Dispatch, SetStateAction } from 'react'; import { IInventoryShipmentPackingItemDto } from '../../../components/model/dto/shipment/inventory-shipment-packing-item-dto'; import { IInventoryShipmentPackingStoreItemDto } from '../../../components/model/dto/shipment/inventory-shipment-packing-store-item-dto'; // export interface IInventoryShipmentPackingItemDto { // itemNumber: number; // shipmentOrderNumber: string; // shipmentPackingItemOrderNumber: string; // numberOfOrders: number; // numberOfShipments: number; // inventoryShipmentPackingStoreItemDto: IInventoryShipmentPackingStoreItemDto[]; // } // export interface IInventoryShipmentPackingStoreItemDto { // itemNumber: number; // shipmentOrderNumber: string; // shipmentPackingItemOrderNumber: string; // chainCorporateCode: string; // packingChainStoreCode: string; // storeName: string; // numberOfOrders: number; // numberOfShipments: number; // } interface IShipmentItemsContext { shipmentItems: IInventoryShipmentPackingItemDto[]; setShipmentItems: Dispatch<SetStateAction<IInventoryShipmentPackingItemDto[]>>; } export const ShipmentItemsContext = createContext<IShipmentItemsContext | undefined>(undefined); export const ShipmentItemsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [shipmentItems, setShipmentItems] = useState<IInventoryShipmentPackingItemDto[]>([]); return ( <ShipmentItemsContext.Provider value={{ shipmentItems, setShipmentItems }}> {children} </ShipmentItemsContext.Provider> ); };
■my-react-spa-app\src\page\shipment\create\page1.tsx
import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { IInventoryShipmentPackingItemDto } from '../../../components/model/dto/shipment/inventory-shipment-packing-item-dto'; import { ShipmentItemsContext } from './context'; const Page1: React.FC = () => { const navigate = useNavigate(); const { shipmentItems, setShipmentItems }: any = useContext(ShipmentItemsContext); console.log("shipmentItems"); console.dir(shipmentItems); useEffect(() => { // 1ページ目に遷移した際に、shipmentItemsが未初期化の場合のみ初期化する if (shipmentItems.length === 0) { const initialShipmentItems: IInventoryShipmentPackingItemDto[] = Array.from( { length: 10 }, (_, index) => ({ itemNumber: index + 1, shipmentOrderNumber: '', shipmentPackingItemOrderNumber: '', numberOfOrders: 0, numberOfShipments: 0, inventoryShipmentPackingStoreItemDtoArr: [], }) ); console.log("initialShipmentItems"); console.dir(initialShipmentItems); setShipmentItems([...initialShipmentItems]); } }, [shipmentItems, setShipmentItems]); const handleShipmentItemChange = ( index: number , event: React.ChangeEvent<HTMLInputElement> ) => { event.preventDefault(); const { name, value } = event.target; console.log("handleShipmentItemChange"); console.dir(shipmentItems); setShipmentItems(shipmentItems.map((item: IInventoryShipmentPackingItemDto, itemIndex: number) => { let propertyName = name.split("_")[0]; if (index === itemIndex) { return { ...item ,shipmentOrderNumber: propertyName === 'shipmentOrderNumber'? value: item.shipmentOrderNumber ,shipmentPackingItemOrderNumber: propertyName === 'shipmentPackingItemOrderNumber'? value: item.shipmentPackingItemOrderNumber }; } else { return { ...item } } })) }; const handleNext = (index: number, e: React.MouseEvent<HTMLButtonElement>) => { e.preventDefault(); const clickedButtonParentRow = e.currentTarget.closest('tr'); const hiddenInput = clickedButtonParentRow?.querySelector( `input[name="itemNumber_${index}"]` ) as HTMLInputElement; const hiddenInputValue = hiddenInput?.value; const selectedRowIndex = Number(hiddenInputValue) -1; navigate('/page2', { state: { shipmentItems, selectedRowIndex } }); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // 1ページ目のフォームと2ページ目のフォームのデータをサーバーサイドに送信する処理 // ... // データ送信後に1ページ目に遷移する navigate('/page1'); }; return ( <div> <h2>ページ1</h2> <form onSubmit={handleSubmit}> <table> <thead className="content-header" style={{ tableLayout: 'fixed' }}> <tr> <th>No.</th> <th>出荷指示番号</th> <th>出荷指示梱包明細番号</th> <th></th> </tr> </thead> <tbody className="content-body"> {/* 1ページ目のフォームを10回ループ */} {Array.from({ length: 10 }, (_, index) => ( <tr key={index}> <td> <label> {index + 1} <span style={{ display: 'none' }}>Item Number:</span> <input type="hidden" id={`itemNumber_${index}`} name={`itemNumber_${index}`} value={shipmentItems[index]?.[`itemNumber`]} readOnly /> </label> </td> <td> <label> <span style={{ display: 'none' }}>Shipment Order Number:</span> <input type="text" id={`shipmentOrderNumber_${index}`} name={`shipmentOrderNumber_${index}`} value={shipmentItems[index]?.[`shipmentOrderNumber`] || ''} onChange={(e) => handleShipmentItemChange(index, e)} /> </label> </td> <td> <label> <span style={{ display: 'none' }}>Shipment Packing Item Order Number:</span> <input type="text" id={`shipmentPackingItemOrderNumber_${index}`} name={`shipmentPackingItemOrderNumber_${index}`} value={shipmentItems[index]?.[`shipmentPackingItemOrderNumber`] || ''} onChange={(e) => handleShipmentItemChange(index, e)} /> </label> </td> <td> <button type="button" onClick={(e) => handleNext(index, e)}> NEXT </button> </td> </tr> ))} </tbody> </table> <button type="submit">Submit</button> </form> </div> ); }; export default Page1;
■
import React, { useContext, useEffect, useState } from "react"; //import { render } from 'react-dom'; import { createRoot } from "react-dom/client"; import { useNavigate, useLocation } from "react-router-dom"; import { ShipmentItemsContext } from "./context"; import { IInventoryShipmentPackingItemDto } from "../../../components/model/dto/shipment/inventory-shipment-packing-item-dto"; import { IInventoryShipmentPackingStoreItemDto } from "../../../components/model/dto/shipment/inventory-shipment-packing-store-item-dto"; const Page2: React.FC = () => { const location = useLocation(); const navigate = useNavigate(); const shipmentItemsContext = useContext(ShipmentItemsContext); console.log("page2■ShipmentItemsContext"); console.dir(shipmentItemsContext); // const [isRender, setIsRender] = useState(true); // const [onloaded, setOnloaded] = useState(false); // const [isInit, setIsInit] = useState(false); const [isReadyRender, setIsReadyRender] = useState(false); if (!shipmentItemsContext) { // ShipmentItemsContextが未定義の場合のエラーハンドリング return null; } // 選択された行の番号 const selectedRowIndex = location.state?.selectedRowIndex || 0; // let stateShipmetItemArr: IInventoryShipmentPackingItemDto[] = location.state?.shipmentItems || []; console.log("location.state"); console.dir(location.state); //let [shipmentItems, setShipmentItems] = useState< const [shipmentItems, setShipmentItems] = useState< IInventoryShipmentPackingItemDto[] >( location.state && location.state.shipmentItems ? location.state.shipmentItems : [] ); console.log("shipmentItemsContext"); console.dir(shipmentItemsContext); // サーバーサイドからデータが取得された想定 let initShipmentItems: any = []; let tableBody: JSX.Element[] = [] as JSX.Element[]; let storeData = [ { packingChainStoreCode: "01", storeName: "保谷店" }, { packingChainStoreCode: "02", storeName: "大泉学園店" }, { packingChainStoreCode: "03", storeName: "練馬駅前店" }, { packingChainStoreCode: "04", storeName: "石神井公園店" }, { packingChainStoreCode: "05", storeName: "石神井台店" }, { packingChainStoreCode: "06", storeName: "練馬店" }, { packingChainStoreCode: "07", storeName: "中村橋店" }, { packingChainStoreCode: "08", storeName: "成増店" }, { packingChainStoreCode: "09", storeName: "田無店" }, { packingChainStoreCode: "10", storeName: "東中野店" }, { packingChainStoreCode: "11", storeName: "高円寺店" }, { packingChainStoreCode: "12", storeName: "笹塚店" }, { packingChainStoreCode: "13", storeName: "上池袋店" }, { packingChainStoreCode: "14", storeName: "東新宿店" }, { packingChainStoreCode: "15", storeName: "新宿店" }, { packingChainStoreCode: "16", storeName: "中野店" }, { packingChainStoreCode: "17", storeName: "三鷹店" }, { packingChainStoreCode: "18", storeName: "武蔵境店" }, { packingChainStoreCode: "19", storeName: "吉祥寺店" }, { packingChainStoreCode: "20", storeName: "西荻窪店" }, ]; React.useEffect(() => { const isAreadyInit: boolean = shipmentItems.every(item => item.inventoryShipmentPackingStoreItemDtoArr.length !== 0) if (isAreadyInit) { return; } console.log("【useEffect】【start】初回実行"); // 選択された行の情報を取得 // const selectedRow = location.state.shipmentItems[selectedRowIndex]; const copyShipmentItems = JSON.parse(JSON.stringify(shipmentItems)); console.log("copyShipmentItems"); console.dir(copyShipmentItems); const initialShipmentItems: IInventoryShipmentPackingItemDto[] = //location.state.shipmentItems.map((item: any, itemIndex: number) => { copyShipmentItems.map((item: any, itemIndex: number) => { // 店舗別の情報(更新用) let inventoryShipmentPackingStoreItemDtoArr: IInventoryShipmentPackingStoreItemDto[] = []; // 店舗別発注数合計 let totalStoreNumberOfOrders = 0; // 店舗別出荷数合計 let totalStoreNumberOfShipments = 0; // 店舗別の情報(作業用) const inventoryShipmentPackingStoreItemDtoWorkArr = item[selectedRowIndex]?.inventoryShipmentPackingStoreItemDtoArr || []; // サーバーからのデータを設定していく storeData.forEach((store: any, storeIndex: number) => { // 店舗別の情報 // const selected = inventoryShipmentPackingStoreItemDtoWorkArr.some( // (storeItem: any) => // storeItem?.packingChainStoreCode === store.packingChainStoreCode // ); // 選択されてる行の場合だけ実施 if (itemIndex === selectedRowIndex) { // 店舗別の発注数、出荷数の合計を算出する item.inventoryShipmentPackingStoreItemDtoArr.forEach( (itemStore: any, itemStoreIndex: number) => { // 発注数の合計 if (Number.isInteger(itemStore.numberOfOrders)) { totalStoreNumberOfOrders += itemStore.numberOfOrders; } // 出荷数の合計 if (Number.isInteger(itemStore.numberOfShipments)) { totalStoreNumberOfShipments += itemStore.numberOfShipments; } } ); } // 店舗別の情報(更新用)を作成する inventoryShipmentPackingStoreItemDtoArr.push({ itemNumber: location.state.shipmentItems[itemIndex].itemNumber, shipmentOrderNumber: "", shipmentPackingItemOrderNumber: "", chainCorporateCode: "", packingChainStoreCode: store.packingChainStoreCode, storeName: store.storeName, numberOfOrders: inventoryShipmentPackingStoreItemDtoWorkArr[storeIndex] ?.numberOfOrders || 0, numberOfShipments: inventoryShipmentPackingStoreItemDtoWorkArr[storeIndex] ?.numberOfShipments || 0, }); }); // 選択されてる明細行の情報を引き続く let itemNumber: number = location.state.shipmentItems[itemIndex].itemNumber; let shipmentOrderNumber: string = itemIndex === selectedRowIndex ? item?.shipmentOrderNumber : ""; let shipmentPackingItemOrderNumber: string = itemIndex === selectedRowIndex ? item?.shipmentPackingItemOrderNumber : ""; return { itemNumber: itemNumber, shipmentOrderNumber: shipmentOrderNumber, shipmentPackingItemOrderNumber: shipmentPackingItemOrderNumber, numberOfOrders: totalStoreNumberOfOrders, numberOfShipments: totalStoreNumberOfShipments, inventoryShipmentPackingStoreItemDtoArr: inventoryShipmentPackingStoreItemDtoArr, }; }); console.log("initialShipmentItems"); console.dir(initialShipmentItems); // shipmentItemsを更新する //shipmentItems = initialShipmentItems; setShipmentItems( initialShipmentItems.map((shipmentItem, index) => { return { ...shipmentItem, numberOfOrders: shipmentItem.numberOfOrders, numberOfShipments: shipmentItem.numberOfShipments, inventoryShipmentPackingStoreItemDtoArr: shipmentItem.inventoryShipmentPackingStoreItemDtoArr.map( (store, storeIndex) => { return { ...store, storeName: store.storeName, packingChainStoreCode: store.packingChainStoreCode, numberOfOrders: store.numberOfOrders, numberOfShipments: store.numberOfShipments, }; } ), }; }) ); initShipmentItems = initialShipmentItems; console.log("【useEffect】【end】初回実行"); //setIsInit(true); }, []); // shipmentItemsが更新された後に実行される処理 React.useEffect(() => { console.log("【useEffect】【start】shipmentItemsの更新"); console.log("setShipmentItems(initialShipmentItems);が実行されました。"); console.log("shipmentItems"); console.dir(shipmentItems); console.log("initShipmentItems"); console.dir(initShipmentItems); // <tbody>の部分を作成する //tableBody = renderFormFields(); //setIsRender(true); console.log("isAready"); //console.dir(isRender); console.log("tableBody"); console.dir(tableBody); console.dir(typeof tableBody); tableBody.map((element: JSX.Element) => { console.dir(React.isValidElement(element)); }); // レンダリングの準備ができた setIsReadyRender(true); // if (isInit) { // console.log("tbody更新"); // console.log("updated"); // const tbody = document.getElementById("tbody"); // if (tbody) { // const root = createRoot(tbody); // root.render(renderFormFields()); // } // } console.log("【useEffect】【end】shipmentItemsの更新"); }, [shipmentItems]); // <input>の値が変更されたら実施 const handleInputChange = ( selectedItemIndex: number, event: React.ChangeEvent<HTMLInputElement> ) => { event.preventDefault(); const { name, value } = event.target; console.log("name"); console.log(name); console.log("value"); console.log(value); let propertyName = name.split("_")[0]; const copyForUpdateShipments = JSON.parse(JSON.stringify(shipmentItems)); const updateShipments: IInventoryShipmentPackingItemDto[] = copyForUpdateShipments.map((item: IInventoryShipmentPackingItemDto, index: number) => { // 店舗別の情報を更新 const workIInventoryShipmentPackingStoreItemDtoArr = item.inventoryShipmentPackingStoreItemDtoArr.map((store: IInventoryShipmentPackingStoreItemDto, storeIndex: number) => { if (storeIndex === selectedItemIndex) { return { ...store, numberOfOrders: propertyName === 'numberOfOrders'? value: store.numberOfOrders, numberOfShipments: propertyName === 'numberOfShipments'? value: store.numberOfShipments } } else { return { ...store } } }); return { ...item, inventoryShipmentPackingStoreItemDtoArr: workIInventoryShipmentPackingStoreItemDtoArr } }); setShipmentItems(updateShipments); // shipmentItems = updatedShipmentItems; //renderFormFields(); }; console.log("handleInputChange■shipmentItems"); console.dir(shipmentItems); console.dir(shipmentItems[selectedRowIndex]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // フォームの送信処理 }; // <tbody>部分を作成する処理 const renderFormFields = () => { const formFields: JSX.Element[] = []; console.log("renderFormFields■shipmentItems"); console.dir(shipmentItems[selectedRowIndex]); { /* 店舗の数だけ繰り返し */ } for (let i = 0; i < storeData.length; i += 4) { const rowFields: JSX.Element[] = []; { /* 1行に4店舗 */ } for (let j = i; j < i + 4; j++) { if (j >= storeData.length) { break; } let itemIndex = j; rowFields.push( <td> <div style={{ display: "flex" }}> <div style={{ minWidth: "128px" }}> {/* 梱包対象チェーン店舗コード */} <div> <span> { shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `packingChainStoreCode` ] } </span> <input style={{ width: "100%", boxSizing: "border-box" }} type="hidden" id={`packingChainStoreCode_${itemIndex}`} key={`packingChainStoreCode_${itemIndex}`} name={`packingChainStoreCode_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `packingChainStoreCode` ] } /> </div> {/* 店舗名 */} <div> <span> { shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `storeName` ] } </span> <input style={{ width: "100%", boxSizing: "border-box" }} type="hidden" id={`storeName_${itemIndex}`} key={`storeName_${itemIndex}`} name={`storeName_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `storeName` ] } /> </div> </div> <div> {/* 店舗別発注数 */} <div> <input style={{ width: "100%", boxSizing: "border-box" }} type="text" id={`numberOfOrders_${itemIndex}`} key={`numberOfOrders_${itemIndex}`} name={`numberOfOrders_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `numberOfOrders` || "" ] } onChange={(e) => handleInputChange(itemIndex, e)} /> </div> {/* 店舗別出荷数 */} <div> <input style={{ width: "100%", boxSizing: "border-box" }} type="text" id={`numberOfShipments_${itemIndex}`} key={`numberOfShipments_${itemIndex}`} name={`numberOfShipments_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `numberOfShipments` || "" ] } onChange={(e) => handleInputChange(itemIndex, e)} /> </div> </div> </div> </td> ); } formFields.push(<tr key={i}>{rowFields}</tr>); } return formFields; }; // <thead>部分を作成する処理 const renderTableHeaders = () => { const headers: JSX.Element[] = []; const cells: JSX.Element[] = []; for (let i = 0; i < 4; i++) { cells.push( <th key={i}> {/* 適切なヘッダーテキストを表示 */} <div style={{ display: "flex" }}> <div style={{ minWidth: "128px" }}> <span>店舗コード</span> <br /> <span>店舗名</span> </div> <div style={{ minWidth: "128px" }}> <span>店舗別発注数</span> <br /> <span>店舗別出荷数</span> </div> </div> </th> ); } headers.push(<tr key={0}>{cells}</tr>); return headers; }; // const onPageLoaded = () => { // setOnloaded(true); // }; // if (document.readyState === "complete") { // if (!onloaded) { // () => onPageLoaded(); // } // } else { // window.addEventListener("load", onPageLoaded); // window.removeEventListener("load", onPageLoaded); // } // // 初期化が済んでいれば実施 // useEffect(() => { // const tbody = document.getElementById("tbody"); // console.log("tbody"); // console.dir(tbody); // if (tbody && tbody.childNodes.length === 0) { // console.log("dom loaded"); // console.log("レンダリング時:shipmentItems"); // console.dir(shipmentItems); // const root = createRoot(tbody); // root.render(tableBody); // // tableBody.map((element) => { // // root.render(element); // // }); // //setOnloaded(false); // } // }, [isInit]); // 画面の描画 return ( <div> {/* {isRender ? (*/} {isReadyRender ? ( <> <h2>Page 2</h2> <form onSubmit={handleSubmit}> <table> <thead>{renderTableHeaders()}</thead> <tbody id="tbody">{renderFormFields()}</tbody> </table> <div> <button type="submit">次へ</button> <button type="button" onClick={() => navigate(-1)}> 戻る </button> </div> </form> </> ) : ( <> <p>...loading</p> </> )} </div> ); }; export default Page2;
■my-react-spa-app\src\App.tsx
// src/App.tsx import { useState } from "react"; import reactLogo from "./assets/react.svg"; import viteLogo from "/vite.svg"; import "./App.css"; import { useRoutes, BrowserRouter, Routes, Route, RouteObject, Outlet, } from "react-router-dom"; //import Router from './routes/router'; import React from "react"; import Router from "./routes/router"; import Page1 from "./page/shipment/create/page1"; import Page2 from "./page/shipment/create/page2"; import { ShipmentItemsProvider } from "./page/shipment/create/context"; const App: React.FC = () => { const [count, setCount]: [ number, React.Dispatch<React.SetStateAction<number>> ] = React.useState<number>(0); return ( <div className="App"> {/** <div> <a href="https://vitejs.dev" target="_blank"> <img src={viteLogo} className="logo" alt="Vite logo" /> </a> <a href="https://reactjs.org" target="_blank"> <img src={reactLogo} className="logo react" alt="React logo" /> </a> </div> <h1>Vite + React</h1> <div className="card"> <button onClick={() => setCount((count) => count + 1)}> count is {count} </button> <p> Edit <code>src/App.tsx</code> and save to test HMR </p> </div> <p className="read-the-docs"> Click on the Vite and React logos to learn more </p> */} {/* ルーター追加 */} {/* <BrowserRouter> <Router/> </BrowserRouter> */} {/** <>{router}</> */} <BrowserRouter> <Router /> <ShipmentItemsProvider> <Routes> <Route path="/page1" element={<Page1 />} /> <Route path="/page2" element={<Page2 />} /> </Routes> </ShipmentItemsProvider> </BrowserRouter> </div> ); }; export default App;
で、開発用サーバーを起動して、ブラウザのURLに直打ちでアクセスして、
『Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://reactjs.org/link/controlled-components』
って早速、怒られるという...
とりあえず、今回はコンソールの警告は無視して、適当に入力し、「NEXT」ボタンを押下。
最初の画面からの情報は維持できてるっぽい。
⇧ 店舗ごとの配列も20個、作成されている。
で、「戻る」ボタンを押下して、最初の画面に遷移すると、
入れ子の店舗ごとの配列が空になってしまう...
う~ん、Reactよう分からん...
解決に時間がかかりそうな気がする...
2023年7月8日(土)追記:↓ ここから
ChatGTPに聞いたら、解決できました。
■my-react-spa-app\src\page\shipment\create\page2.tsx
import React, { useContext, useEffect, useState } from "react"; //import { render } from 'react-dom'; import { createRoot } from "react-dom/client"; import { useNavigate, useLocation } from "react-router-dom"; import { ShipmentItemsContext } from "./context"; import { IInventoryShipmentPackingItemDto } from "../../../components/model/dto/shipment/inventory-shipment-packing-item-dto"; import { IInventoryShipmentPackingStoreItemDto } from "../../../components/model/dto/shipment/inventory-shipment-packing-store-item-dto"; const Page2: React.FC = () => { const location = useLocation(); const navigate = useNavigate(); const shipmentItemsContext = useContext(ShipmentItemsContext); console.log("page2■ShipmentItemsContext"); console.dir(shipmentItemsContext); // const [isRender, setIsRender] = useState(true); // const [onloaded, setOnloaded] = useState(false); // const [isInit, setIsInit] = useState(false); const [isReadyRender, setIsReadyRender] = useState(false); if (!shipmentItemsContext) { // ShipmentItemsContextが未定義の場合のエラーハンドリング return null; } // 選択された行の番号 const selectedRowIndex = location.state?.selectedRowIndex || 0; // let stateShipmetItemArr: IInventoryShipmentPackingItemDto[] = location.state?.shipmentItems || []; console.log("location.state"); console.dir(location.state); //let [shipmentItems, setShipmentItems] = useState< // const [shipmentItems, setShipmentItems] = useState< // IInventoryShipmentPackingItemDto[] // >( // location.state && location.state.shipmentItems // ? location.state.shipmentItems // : [] // ); const { shipmentItems, setShipmentItems }: any = useContext(ShipmentItemsContext); console.log("shipmentItemsContext"); console.dir(shipmentItemsContext); // サーバーサイドからデータが取得された想定 let initShipmentItems: any = []; let tableBody: JSX.Element[] = [] as JSX.Element[]; let storeData = [ { packingChainStoreCode: "01", storeName: "保谷店" }, { packingChainStoreCode: "02", storeName: "大泉学園店" }, { packingChainStoreCode: "03", storeName: "練馬駅前店" }, { packingChainStoreCode: "04", storeName: "石神井公園店" }, { packingChainStoreCode: "05", storeName: "石神井台店" }, { packingChainStoreCode: "06", storeName: "練馬店" }, { packingChainStoreCode: "07", storeName: "中村橋店" }, { packingChainStoreCode: "08", storeName: "成増店" }, { packingChainStoreCode: "09", storeName: "田無店" }, { packingChainStoreCode: "10", storeName: "東中野店" }, { packingChainStoreCode: "11", storeName: "高円寺店" }, { packingChainStoreCode: "12", storeName: "笹塚店" }, { packingChainStoreCode: "13", storeName: "上池袋店" }, { packingChainStoreCode: "14", storeName: "東新宿店" }, { packingChainStoreCode: "15", storeName: "新宿店" }, { packingChainStoreCode: "16", storeName: "中野店" }, { packingChainStoreCode: "17", storeName: "三鷹店" }, { packingChainStoreCode: "18", storeName: "武蔵境店" }, { packingChainStoreCode: "19", storeName: "吉祥寺店" }, { packingChainStoreCode: "20", storeName: "西荻窪店" }, ]; React.useEffect(() => { const isAreadyInit: boolean = shipmentItems.every((item: IInventoryShipmentPackingItemDto) => item.inventoryShipmentPackingStoreItemDtoArr.length !== 0) if (isAreadyInit) { return; } console.log("【useEffect】【start】初回実行"); // 選択された行の情報を取得 // const selectedRow = location.state.shipmentItems[selectedRowIndex]; const copyShipmentItems = JSON.parse(JSON.stringify(shipmentItems)); console.log("copyShipmentItems"); console.dir(copyShipmentItems); const initialShipmentItems: IInventoryShipmentPackingItemDto[] = //location.state.shipmentItems.map((item: any, itemIndex: number) => { copyShipmentItems.map((item: any, itemIndex: number) => { // 店舗別の情報(更新用) let inventoryShipmentPackingStoreItemDtoArr: IInventoryShipmentPackingStoreItemDto[] = []; // 店舗別発注数合計 let totalStoreNumberOfOrders = 0; // 店舗別出荷数合計 let totalStoreNumberOfShipments = 0; // 店舗別の情報(作業用) const inventoryShipmentPackingStoreItemDtoWorkArr = item[selectedRowIndex]?.inventoryShipmentPackingStoreItemDtoArr || []; // サーバーからのデータを設定していく storeData.forEach((store: any, storeIndex: number) => { // 店舗別の情報 // const selected = inventoryShipmentPackingStoreItemDtoWorkArr.some( // (storeItem: any) => // storeItem?.packingChainStoreCode === store.packingChainStoreCode // ); // 選択されてる行の場合だけ実施 if (itemIndex === selectedRowIndex) { // 店舗別の発注数、出荷数の合計を算出する item.inventoryShipmentPackingStoreItemDtoArr.forEach( (itemStore: any, itemStoreIndex: number) => { // 発注数の合計 if (Number.isInteger(itemStore.numberOfOrders)) { totalStoreNumberOfOrders += itemStore.numberOfOrders; } // 出荷数の合計 if (Number.isInteger(itemStore.numberOfShipments)) { totalStoreNumberOfShipments += itemStore.numberOfShipments; } } ); } // 店舗別の情報(更新用)を作成する inventoryShipmentPackingStoreItemDtoArr.push({ itemNumber: location.state.shipmentItems[itemIndex].itemNumber, shipmentOrderNumber: "", shipmentPackingItemOrderNumber: "", chainCorporateCode: "", packingChainStoreCode: store.packingChainStoreCode, storeName: store.storeName, numberOfOrders: inventoryShipmentPackingStoreItemDtoWorkArr[storeIndex] ?.numberOfOrders || 0, numberOfShipments: inventoryShipmentPackingStoreItemDtoWorkArr[storeIndex] ?.numberOfShipments || 0, }); }); // 選択されてる明細行の情報を引き続く let itemNumber: number = location.state.shipmentItems[itemIndex].itemNumber; let shipmentOrderNumber: string = itemIndex === selectedRowIndex ? item?.shipmentOrderNumber : ""; let shipmentPackingItemOrderNumber: string = itemIndex === selectedRowIndex ? item?.shipmentPackingItemOrderNumber : ""; return { itemNumber: itemNumber, shipmentOrderNumber: shipmentOrderNumber, shipmentPackingItemOrderNumber: shipmentPackingItemOrderNumber, numberOfOrders: totalStoreNumberOfOrders, numberOfShipments: totalStoreNumberOfShipments, inventoryShipmentPackingStoreItemDtoArr: inventoryShipmentPackingStoreItemDtoArr, }; }); console.log("initialShipmentItems"); console.dir(initialShipmentItems); // shipmentItemsを更新する //shipmentItems = initialShipmentItems; setShipmentItems( initialShipmentItems.map((shipmentItem, index) => { return { ...shipmentItem, numberOfOrders: shipmentItem.numberOfOrders, numberOfShipments: shipmentItem.numberOfShipments, inventoryShipmentPackingStoreItemDtoArr: shipmentItem.inventoryShipmentPackingStoreItemDtoArr.map( (store, storeIndex) => { return { ...store, storeName: store.storeName, packingChainStoreCode: store.packingChainStoreCode, numberOfOrders: store.numberOfOrders, numberOfShipments: store.numberOfShipments, }; } ), }; }) ); initShipmentItems = initialShipmentItems; console.log("【useEffect】【end】初回実行"); //setIsInit(true); }, []); // shipmentItemsが更新された後に実行される処理 React.useEffect(() => { console.log("【useEffect】【start】shipmentItemsの更新"); console.log("setShipmentItems(initialShipmentItems);が実行されました。"); console.log("shipmentItems"); console.dir(shipmentItems); console.log("initShipmentItems"); console.dir(initShipmentItems); // <tbody>の部分を作成する //tableBody = renderFormFields(); //setIsRender(true); console.log("isAready"); //console.dir(isRender); console.log("tableBody"); console.dir(tableBody); console.dir(typeof tableBody); tableBody.map((element: JSX.Element) => { console.dir(React.isValidElement(element)); }); // レンダリングの準備ができた setIsReadyRender(true); // if (isInit) { // console.log("tbody更新"); // console.log("updated"); // const tbody = document.getElementById("tbody"); // if (tbody) { // const root = createRoot(tbody); // root.render(renderFormFields()); // } // } console.log("【useEffect】【end】shipmentItemsの更新"); }, [shipmentItems]); // <input>の値が変更されたら実施 const handleInputChange = ( selectedItemIndex: number, event: React.ChangeEvent<HTMLInputElement> ) => { event.preventDefault(); const { name, value } = event.target; console.log("name"); console.log(name); console.log("value"); console.log(value); let propertyName = name.split("_")[0]; const copyForUpdateShipments = JSON.parse(JSON.stringify(shipmentItems)); const updateShipments: IInventoryShipmentPackingItemDto[] = copyForUpdateShipments.map((item: IInventoryShipmentPackingItemDto, index: number) => { // 店舗別の情報を更新 const workIInventoryShipmentPackingStoreItemDtoArr = item.inventoryShipmentPackingStoreItemDtoArr.map((store: IInventoryShipmentPackingStoreItemDto, storeIndex: number) => { if (storeIndex === selectedItemIndex) { return { ...store, numberOfOrders: propertyName === 'numberOfOrders'? value: store.numberOfOrders, numberOfShipments: propertyName === 'numberOfShipments'? value: store.numberOfShipments } } else { return { ...store } } }); return { ...item, inventoryShipmentPackingStoreItemDtoArr: workIInventoryShipmentPackingStoreItemDtoArr } }); setShipmentItems(updateShipments); // shipmentItems = updatedShipmentItems; //renderFormFields(); }; console.log("handleInputChange■shipmentItems"); console.dir(shipmentItems); console.dir(shipmentItems[selectedRowIndex]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); // フォームの送信処理 }; // <tbody>部分を作成する処理 const renderFormFields = () => { const formFields: JSX.Element[] = []; console.log("renderFormFields■shipmentItems"); console.dir(shipmentItems[selectedRowIndex]); { /* 店舗の数だけ繰り返し */ } for (let i = 0; i < storeData.length; i += 4) { const rowFields: JSX.Element[] = []; { /* 1行に4店舗 */ } for (let j = i; j < i + 4; j++) { if (j >= storeData.length) { break; } let itemIndex = j; rowFields.push( <td> <div style={{ display: "flex" }}> <div style={{ minWidth: "128px" }}> {/* 梱包対象チェーン店舗コード */} <div> <span> { shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `packingChainStoreCode` ] } </span> <input style={{ width: "100%", boxSizing: "border-box" }} type="hidden" id={`packingChainStoreCode_${itemIndex}`} key={`packingChainStoreCode_${itemIndex}`} name={`packingChainStoreCode_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `packingChainStoreCode` ] } /> </div> {/* 店舗名 */} <div> <span> { shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `storeName` ] } </span> <input style={{ width: "100%", boxSizing: "border-box" }} type="hidden" id={`storeName_${itemIndex}`} key={`storeName_${itemIndex}`} name={`storeName_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `storeName` ] } /> </div> </div> <div> {/* 店舗別発注数 */} <div> <input style={{ width: "100%", boxSizing: "border-box" }} type="text" id={`numberOfOrders_${itemIndex}`} key={`numberOfOrders_${itemIndex}`} name={`numberOfOrders_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `numberOfOrders` || "" ] } onChange={(e) => handleInputChange(itemIndex, e)} /> </div> {/* 店舗別出荷数 */} <div> <input style={{ width: "100%", boxSizing: "border-box" }} type="text" id={`numberOfShipments_${itemIndex}`} key={`numberOfShipments_${itemIndex}`} name={`numberOfShipments_${itemIndex}`} value={ shipmentItems[selectedRowIndex] ?.inventoryShipmentPackingStoreItemDtoArr[itemIndex]?.[ `numberOfShipments` || "" ] } onChange={(e) => handleInputChange(itemIndex, e)} /> </div> </div> </div> </td> ); } formFields.push(<tr key={i}>{rowFields}</tr>); } return formFields; }; // <thead>部分を作成する処理 const renderTableHeaders = () => { const headers: JSX.Element[] = []; const cells: JSX.Element[] = []; for (let i = 0; i < 4; i++) { cells.push( <th key={i}> {/* 適切なヘッダーテキストを表示 */} <div style={{ display: "flex" }}> <div style={{ minWidth: "128px" }}> <span>店舗コード</span> <br /> <span>店舗名</span> </div> <div style={{ minWidth: "128px" }}> <span>店舗別発注数</span> <br /> <span>店舗別出荷数</span> </div> </div> </th> ); } headers.push(<tr key={0}>{cells}</tr>); return headers; }; // const onPageLoaded = () => { // setOnloaded(true); // }; // if (document.readyState === "complete") { // if (!onloaded) { // () => onPageLoaded(); // } // } else { // window.addEventListener("load", onPageLoaded); // window.removeEventListener("load", onPageLoaded); // } // // 初期化が済んでいれば実施 // useEffect(() => { // const tbody = document.getElementById("tbody"); // console.log("tbody"); // console.dir(tbody); // if (tbody && tbody.childNodes.length === 0) { // console.log("dom loaded"); // console.log("レンダリング時:shipmentItems"); // console.dir(shipmentItems); // const root = createRoot(tbody); // root.render(tableBody); // // tableBody.map((element) => { // // root.render(element); // // }); // //setOnloaded(false); // } // }, [isInit]); // 画面の描画 return ( <div> {/* {isRender ? (*/} {isReadyRender ? ( <> <h2>Page 2</h2> <form onSubmit={handleSubmit}> <table> <thead>{renderTableHeaders()}</thead> <tbody id="tbody">{renderFormFields()}</tbody> </table> <div> <button type="submit">次へ</button> <button type="button" onClick={() => navigate(-1)}> 戻る </button> </div> </form> </> ) : ( <> <p>...loading</p> </> )} </div> ); }; export default Page2;
⇧ page2.tsxのuseContextのコーディングがイケてなかったらしい...
何にせよ、
⇧ page2.tsxの内容がpage1.tsxへ引き継げました。
コンソールの警告は相変わらず消えんけど...
2023年7月8日(土)追記:↑ ここまで
それにしても、必要な処理を終わらせるまで、レンダリングが開始されないように制御しなきゃいかんとか、Reactって面倒くさ過ぎる気がするんだけど...
兎にも角にも、Reactのルールってのが分からな過ぎる...
毎度モヤモヤ感が半端ない...
今回はこのへんで。