Skip to content

DNET_ReactThirdStep2

nishi_74322014 edited this page Sep 11, 2026 · 1 revision

Reactのサード・ステップ2

概要

引き続きRedux対応を行う。

詳細

プロンプト

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>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DT') }}>SelectAll_DT</button>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DS') }}>SelectAll_DS</button>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DR') }}>SelectAll_DR</button>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('SelectAll_DSQL') }}>SelectAll_DSQL</button>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('Select') }}>Select</button>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('Insert') }}>Insert</button>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('Update') }}>Update</button>&nbsp;
                <button className='btn-primary' onClick={() => { this.onClickButton('Delete') }}>Delete</button>
            </div>
        );
    }

    onClickButton(actionType: string): void {
        this.props.onClickButton(actionType);
    }
}

export default Buttons;

```

修正内容

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を修正

同様に

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>
}

参考

移行メモ

  • 「戻る」の並びにある「Reactのサード・ステップ2」は自ページのため、 その旨を補ってプレーン・テキストとした。
  • 元 Wiki の行頭空白によるプレフォーマット・ブロックは、 フェンス付きコードブロックにした。プロンプト中にコードブロックが 入れ子になっている箇所は、外側を 4 個のバッククォートで囲んだ。
  • 「修正内容」で参照されるファイル名は、本文では crudSample2Slice.ts のまま crudSampleSlice へのリネームが混在しているが、原文ママとした。

Tags: 移行, React, Redux, Redux Toolkit, createSlice, connect, useSelector, TypeScript, CRUD

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally