「[[.NET 開発基盤部会 Wiki>http://dotnetdevelopmentinfrastructure.osscons.jp]]」は、「[[Open棟梁Project>https://github.com/OpenTouryoProject/]]」,「[[OSSコンソーシアム .NET開発基盤部会>https://www.osscons.jp/dotNetDevelopmentInfrastructure/]]」によって運営されています。 -戻る --[[React]] -> [[Redux]] -戻る([[React]] -> [[Redux]]) --...[[Reactの5thステップ]] --[[Reactのファースト・ステップ2]] --[[Reactのセカンド・ステップ2]] --Reactのサード・ステップ2 *目次 [#bf704575] #contents *概要 [#rbd0d9e0] 引き続き[[Redux]]対応を行う。 *詳細 [#k95528b7] **プロンプト [#gad08276] CrudSampleとCrudSample2があるが、構成が複雑なCrudSample2をサンプリングし同様の修正方法でCrudSampleも対応 以下のCrudSample2のmessageをRedux対応させたい。修正方法を教えてください。 .\src\store\index.ts .\src\store\counterSlice.ts .\src\pages\CrudSample2.tsx .\src\components\CrudSample2\Buttons.tsx .\src\components\CrudSample2\DropDownLists.tsx .\src\components\CrudSample2\Inputs.tsx .\src\components\CrudSample2\Outputs.tsx ``` import { configureStore } from '@reduxjs/toolkit' import counterReducer from './counterSlice' export const store = configureStore({ reducer: { counter: counterReducer, }, }) // 型エクスポート(useSelector / useDispatch で使用) export type RootState = ReturnType<typeof store.getState> export type AppDispatch = typeof store.dispatch ``` ``` import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' interface CounterState { value: number } const initialState: CounterState = { value: 0, } const counterSlice = createSlice({ name: 'counter', initialState, reducers: { increment: (state) => { state.value += 1 }, decrement: (state) => { state.value -= 1 }, incrementByAmount: (state, action: PayloadAction<number>) => { state.value += action.payload }, reset: (state) => { state.value = 0 }, }, }) export const { increment, decrement, incrementByAmount, reset } = counterSlice.actions export default counterSlice.reducer ``` ``` import * as React from 'react'; import constants from '../const'; import common from '../common.ts'; import oauth_oidc from '../touryo/oauth_oidc'; import Inputs from '../components/CrudSample2/Inputs'; import DropDownLists from '../components/CrudSample2/DropDownLists'; import Buttons from '../components/CrudSample2/Buttons'; import Outputs from '../components/CrudSample2/Outputs'; // ===== 型定義 ===== interface DdlState { ddlDap: string; ddlMode1: string; ddlMode2: string; ddlIso: string; ddlExRollback: string; ddlOrder: string; ddlOrderSequence: string; } interface ShipperState { shipperID: string; companyName: string; phone: string; } interface CrudSample2State { message: string; ddl: DdlState; shipper: ShipperState; shippers: ShipperState[]; loading: boolean; } // ===== コンポーネント ===== export class CrudSample2 extends React.Component<Record<string, never>, CrudSample2State> { constructor(props: Record<string, never>) { super(props); this.state = { message: '', ddl: { ddlDap: 'SQL', ddlMode1: 'individual', ddlMode2: 'static', ddlIso: 'NT', ddlExRollback: '-', ddlOrder: 'c1', ddlOrderSequence: 'A', }, shipper: { shipperID: '', companyName: '', phone: '', }, shippers: [ { shipperID: '', companyName: '', phone: '', }, ], loading: true, }; } // ===== render ===== render() { const containerStyle: React.CSSProperties = { textAlign: 'left' }; const div0Style: React.CSSProperties = {}; const div1Style: React.CSSProperties = { display: 'inline-block' }; const div2Style: React.CSSProperties = { display: 'inline-block' }; return ( <div style={containerStyle}> <h1>CRUD sample</h1> <p>This component demonstrates CRUD.</p> <div style={div0Style}> <DropDownLists onChangeDdl={(e) => this.receiveDDLChanged(e)} /> </div> <div style={div1Style}> <Inputs shipper={this.state.shipper} onChangeInput={(e) => this.receiveInputChanged(e)} /> </div> <div style={div2Style}> <Outputs loading={this.state.loading} shippers={this.state.shippers} message={this.state.message} /> </div> <div> <Buttons onClickButton={(e) => this.receiveButtonClick(e)} /> </div> </div> ); } // ─── 子コンポーネントからのイベント受信 ──────────────────── receiveDDLChanged(ddl: Partial<CrudSample2State>): void { this.setState(ddl as CrudSample2State); } receiveInputChanged(shipper: Partial<CrudSample2State>): void { this.setState(shipper as CrudSample2State); } receiveButtonClick(actionType: string): void { switch (actionType) { case 'SelectCount': this.selectCount(); return; case 'SelectAll_DT': this.selectAll_DT(); return; case 'SelectAll_DS': this.selectAll_DS(); return; case 'SelectAll_DR': this.selectAll_DR(); return; case 'SelectAll_DSQL': this.selectAll_DSQL(); return; case 'Select': this.select(); return; case 'Insert': this.insert(); return; case 'Update': this.update(); return; case 'Delete': this.delete(); return; default: return; } } // ===== WebAPI イベントハンドラ ===== selectCount() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'SelectCount', oauth_oidc.createHttpRequestHeader(false), 'ddlDap=' + this.state.ddl.ddlDap + '&ddlMode1=' + this.state.ddl.ddlMode1 + '&ddlMode2=' + this.state.ddl.ddlMode2 + '&ddlExRollback=' + this.state.ddl.ddlExRollback, (data) => { if (data.message) { this.setState({ message: JSON.stringify(data.message) }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } selectAll_DT() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'SelectAll_DT', oauth_oidc.createHttpRequestHeader(false), 'ddlDap=' + this.state.ddl.ddlDap + '&ddlMode1=' + this.state.ddl.ddlMode1 + '&ddlMode2=' + this.state.ddl.ddlMode2 + '&ddlExRollback=' + this.state.ddl.ddlExRollback, (data) => { if (data.result) { this.setState({ message: '', shippers: data.result as ShipperState[], loading: false }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } selectAll_DS() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'selectAll_DS', oauth_oidc.createHttpRequestHeader(false), 'ddlDap=' + this.state.ddl.ddlDap + '&ddlMode1=' + this.state.ddl.ddlMode1 + '&ddlMode2=' + this.state.ddl.ddlMode2 + '&ddlExRollback=' + this.state.ddl.ddlExRollback, (data) => { if (data.result) { this.setState({ message: '', shippers: data.result as ShipperState[], loading: false }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } selectAll_DR() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'selectAll_DR', oauth_oidc.createHttpRequestHeader(false), 'ddlDap=' + this.state.ddl.ddlDap + '&ddlMode1=' + this.state.ddl.ddlMode1 + '&ddlMode2=' + this.state.ddl.ddlMode2 + '&ddlExRollback=' + this.state.ddl.ddlExRollback, (data) => { if (data.result) { this.setState({ message: '', shippers: data.result as ShipperState[], loading: false }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } selectAll_DSQL() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'selectAll_DSQL', oauth_oidc.createHttpRequestHeader(false), 'ddlDap=' + this.state.ddl.ddlDap + '&ddlMode1=' + this.state.ddl.ddlMode1 + '&ddlMode2=' + this.state.ddl.ddlMode2 + '&ddlExRollback=' + this.state.ddl.ddlExRollback + '&orderColumn=' + this.state.ddl.ddlOrder + '&orderSequence=' + this.state.ddl.ddlOrderSequence, (data) => { if (data.result) { this.setState({ message: '', shippers: data.result as ShipperState[], loading: false }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } select() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'select', oauth_oidc.createHttpRequestHeader(true), JSON.stringify({ ddlDap: this.state.ddl.ddlDap, ddlMode1: this.state.ddl.ddlMode1, ddlMode2: this.state.ddl.ddlMode2, ddlExRollback: this.state.ddl.ddlExRollback, shipper: { shipperID: this.state.shipper.shipperID, companyName: '', phone: '', }, }), (data) => { if (data.result) { const result = data.result as ShipperState; this.setState({ shipper: { shipperID: result.shipperID, companyName: result.companyName, phone: result.phone, }, }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } insert() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'insert', oauth_oidc.createHttpRequestHeader(true), JSON.stringify({ ddlDap: this.state.ddl.ddlDap, ddlMode1: this.state.ddl.ddlMode1, ddlMode2: this.state.ddl.ddlMode2, ddlExRollback: this.state.ddl.ddlExRollback, shipper: { shipperID: '0', companyName: this.state.shipper.companyName, phone: this.state.shipper.phone, }, }), (data) => { if (data.message) { this.setState({ message: JSON.stringify(data.message) }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } update() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'update', oauth_oidc.createHttpRequestHeader(true), JSON.stringify({ ddlDap: this.state.ddl.ddlDap, ddlMode1: this.state.ddl.ddlMode1, ddlMode2: this.state.ddl.ddlMode2, ddlExRollback: this.state.ddl.ddlExRollback, shipper: { shipperID: this.state.shipper.shipperID, companyName: this.state.shipper.companyName, phone: this.state.shipper.phone, }, }), (data) => { if (data.message) { this.setState({ message: JSON.stringify(data.message) }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } delete() { this.setState({ message: '' }); common.postFetch( constants.CrudSampleRootUrl + 'delete', oauth_oidc.createHttpRequestHeader(true), JSON.stringify({ ddlDap: this.state.ddl.ddlDap, ddlMode1: this.state.ddl.ddlMode1, ddlMode2: this.state.ddl.ddlMode2, ddlExRollback: this.state.ddl.ddlExRollback, shipper: { shipperID: this.state.shipper.shipperID, companyName: '', phone: '', }, }), (data) => { if (data.message) { this.setState({ message: JSON.stringify(data.message) }); } }, (msg) => this.setState({ message: JSON.stringify(msg) }), ); } } ``` ``` import * as React from 'react'; // 型定義 interface Shipper { shipperID: string; companyName: string; phone: string; } interface OutputsProps { shippers: Shipper[]; loading: boolean; message?: string; } interface OutputsState { shippers: Shipper[]; loading: boolean; message?: string; } export class Outputs extends React.Component<OutputsProps, OutputsState> { // constructor constructor(props: OutputsProps) { super(props); this.state = { shippers: [ { shipperID: '', companyName: '', phone: '', }, ], loading: true, message: undefined, }; } // lifecycle // componentWillReceiveProps は非推奨のため getDerivedStateFromProps に移行 static getDerivedStateFromProps( newProps: OutputsProps ): OutputsState { return { loading: newProps.loading, shippers: newProps.shippers, message: newProps.message, }; } // render render() { let contents: React.ReactNode = null; if (this.state.loading) { contents = ( <p> <em>...Table...</em> </p> ); } else { contents = ( <table className="table"> <thead> <tr> <th>ShipperID</th> <th>CompanyName</th> <th>Phone</th> </tr> </thead> <tbody> {this.state.shippers.map((shipper) => ( <tr key={shipper.shipperID}> <td>{shipper.shipperID}</td> <td>{shipper.companyName}</td> <td>{shipper.phone}</td> </tr> ))} </tbody> </table> ); } return ( <div> {contents} <p>処理結果:{this.state.message}</p> </div> ); } } export default Outputs; ``` ``` import * as React from 'react'; // Propsの型定義 interface ButtonsProps { onClickButton: (actionType: string) => void; } export class Buttons extends React.Component<ButtonsProps> { constructor(props: ButtonsProps) { super(props); } render() { return ( <div> <button className='btn-primary' onClick={() => { this.onClickButton('SelectCount') }}>SelectCount</button> <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DT') }}>SelectAll_DT</button> <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DS') }}>SelectAll_DS</button> <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DR') }}>SelectAll_DR</button> <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DSQL') }}>SelectAll_DSQL</button> <button className='btn-primary' onClick={() => { this.onClickButton('Select') }}>Select</button> <button className='btn-primary' onClick={() => { this.onClickButton('Insert') }}>Insert</button> <button className='btn-primary' onClick={() => { this.onClickButton('Update') }}>Update</button> <button className='btn-primary' onClick={() => { this.onClickButton('Delete') }}>Delete</button> </div> ); } onClickButton(actionType: string): void { this.props.onClickButton(actionType); } } export default Buttons; ``` **修正内容 [#e4537aef] crudSample2Sliceは、crudSample、crudSample2で共用するため、crudSampleSliceにリネーム。 -先ずは「store」の内容 --crudSample2Slice.ts import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' interface CrudSample2SliceState { message: string; } const initialState: CrudSample2SliceState = { message: '', } const crudSample2Slice = createSlice({ name: 'crudSample2', initialState, reducers: { setMessage: (state, action: PayloadAction<string>) => { state.message = action.payload; }, clearMessage: (state) => { state.message = ''; }, }, }) export const { setMessage, clearMessage } = crudSample2Slice.actions export default crudSample2Slice.reducer --index.ts import { configureStore } from '@reduxjs/toolkit' import counterReducer from './counterSlice' import crudSample2Reducer from './crudSample2Slice' // ← 追加 export const store = configureStore({ reducer: { counter: counterReducer, crudSample2: crudSample2Reducer, // ← 追加 }, }) export type RootState = ReturnType<typeof store.getState> export type AppDispatch = typeof store.dispatch -CrudSample2.tsx --ヘッダ // Redux 関連のインポート import { connect } from 'react-redux'; import type { RootState, AppDispatch } from '../store'; import { setMessage, clearMessage } from '../store/crudSampleSlice'; // ===== 型定義 ===== // Propsの型定義 interface StateProps { message: string; } interface DispatchProps { onSetMessage: (msg: string) => void; onClearMessage: () => void; } // コンポーネント自身のProps(connect後に外から渡すものは空) type CrudSampleProps = StateProps & DispatchProps; ... interface CrudSample2State { //message: string; は Redux管理のため削除 ddl: DdlState; shipper: ShipperState; shippers: ShipperState[]; loading: boolean; } --ボディ ---クラス冒頭部 export class CrudSample2 extends React.Component<CrudSampleProps, CrudSample2State> //<Record<string, never>, CrudSample2State> { constructor(props: CrudSampleProps) {//Record<string, never>) { super(props); this.state = { // message: '', は Redux管理のため削除 ---Render部 this.props.message → this.state.message ---イベントハンドラ // ===== WebAPI イベントハンドラ ===== // this.setState({ message: ... }) → this.props.onSetMessage(...) に変更 // this.setState({ message: '' }) → this.props.onClearMessage() に変更 --フッタ // ===== Redux connect ===== const mapStateToProps = (state: RootState): StateProps => ({ message: state.crudSample.message, }); const mapDispatchToProps = (dispatch: AppDispatch): DispatchProps => ({ onSetMessage: (msg: string) => dispatch(setMessage(msg)), onClearMessage: () => dispatch(clearMessage()), }); export default connect(mapStateToProps, mapDispatchToProps)(CrudSample2); **同様にAboutを修正 [#e29b5de3] 同様に import { useSelector } from 'react-redux' import type { RootState } from '../store' export default function About() { // Reduxのstoreから値を取得 const count = useSelector((state: RootState) => state.counter.value) const message = useSelector((state: RootState) => state.crudSample.message) return <div> <h1>About</h1> <p>About Pageです。</p> {/* countを表示 */} <p>現在のカウント: {count}</p> {/* messageを表示 */} <p>現在のメッセージ: {message}</p> </div> } *参考 [#u1559c3a] -FrontendTemplates/UI/SPA/React at develop · OpenTouryoProject/FrontendTemplates~ https://github.com/OpenTouryoProject/FrontendTemplates/tree/develop/UI/SPA/React