reactjs - TypeScript mengalami kesulitan dengan kontainer Redux


Saya mengalami kesulitan dalam memahami cara mengetik container Redux dengan benar.

Pertimbangkan sebuah komponen presentasional sederhana yang mungkin terlihat seperti ini:

interface MyProps {
  name: string;
  selected: boolean;
  onSelect: (name: string) => void;
}
class MyComponent extends React.Component<MyProps, {}> { }

Dari perspektif komponen ini, semua properti diperlukan.

Sekarang saya ingin menulis sebuah kontainer yang mengambil semua properti ini dari state:

function mapStateToProps(state: MyState) {
  return {
    name: state.my.name,
    selected: state.my.selected
  };
}

function mapDispatchToProps(dispatch: IDispatch) {
  return {
    onSelect(name: string) {
      dispatch(mySelectAction(name));
    }
  };
}

const MyContainer = connect(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

Ini berhasil, tetapi ada masalah pengetikan besar: fungsi pemetaan (mapStateToProps dan mapDispatchToProps) tidak memiliki perlindungan bahwa mereka menyediakan data yang benar untuk memenuhi MyProps. Ini rentan terhadap kesalahan, salah ketik, dan refaktorisasi yang buruk.

Saya bisa membuat fungsi pemetaan mengembalikan tipe MyProps:

function mapStateToProps(state: MyState): MyProps { }

function mapDispatchToProps(dispatch: IDispatch): MyProps { }

Namun ini tidak akan berhasil kecuali saya membuat semua properti MyProp menjadi opsional, sehingga setiap fungsi pemetaan hanya dapat mengembalikan bagian yang mereka pedulikan. Saya tidak ingin membuat properti tersebut opsional, karena mereka tidak opsional bagi komponen presentasional.

Opsi lain adalah membagi props untuk setiap fungsi peta dan menggabungkannya untuk props komponen:

// MyComponent
interface MyStateProps {
  name: string;
  selected: boolean;
}

interface MyDispatchProps {
  onSelect: (name: string) => void;
}

type MyProps = MyStateProps & MyDispatchProps;

class MyComponent extends React.Component<MyProps, {}> { }

// MyContainer
function mapStateToProps(state: MyState): MyStateProps { }

function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }

Oke, itu membuat saya lebih dekat dengan apa yang saya inginkan, tapi ini agak berisik dan membuat saya menulis antarmuka properti komponen presentasional saya sekitar bentuk sebuah kontainer, yang tidak saya sukai.

Dan sekarang muncul masalah kedua. Bagaimana jika saya ingin menaruh wadah di dalam komponen lain:

<MyContainer />

Ini menghasilkan kesalahan kompilasi bahwa name, selected, dan onSelect semuanya hilang... tetapi itu disengaja karena container sedang terhubung ke Redux dan menyediakan itu. Jadi ini membuat saya kembali ke membuat semua properti komponen menjadi opsional, tetapi saya tidak menyukainya karena mereka sebenarnya tidak opsional.

Keadaan menjadi lebih buruk ketika MyContainer memiliki beberapa properti sendiri yang ingin diteruskan:

<MyContainer section="somethng" />

Sekarang yang sedang saya coba lakukan adalah membuat section menjadi prop yang wajib dari MyContainer tetapi bukan prop dari MyComponent, dan name, selected, dan onSelect adalah prop yang wajib dari MyComponent tetapi opsional atau sama sekali bukan prop dari MyContainer. Saya benar-benar bingung bagaimana mengekspresikan ini.

Setiap panduan tentang hal ini akan dihargai!

Jawaban #1

Anda berada di jalur yang benar dengan contoh terakhir Anda. Yang juga perlu Anda definisikan adalah antarmuka MyOwnProps, dan memberi tipe pada fungsi connect.

Dengan typings ini: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/react-redux/react-redux.d.ts, Anda bisa melakukan sesuatu seperti ini

interface MyProps {
  section: string;
  name: string;
  selected: boolean;
  onSelect: (name: string) => void;
}

interface MyStateProps {
  name: string;
  selected: boolean;
}

interface MyDispatchProps {
  onSelect: (name: string) => void;
}

interface MyOwnProps {
  section: string;
}

class MyComponent extends React.Component<MyProps, {}> { }

function mapStateToProps(state: MyState): MyStateProps { }

function mapDispatchToProps(dispatch: IDispatch): MyDispatchProps { }

const MyContainer = connect<MyStateProps, MyDispatchProps, MyOwnProps>(
  mapStateToProps,
  mapDispatchToProps
)(MyComponent);

Ini juga memungkinkan Anda mengetik ownProps di mapStateToProps dan mapDispatchToProps, misalnya.

function mapStateToProps(state: MyState, ownProps: MyOwnProps): MyStateProps

Anda tidak perlu mendefinisikan tipe intersection untuk tipe MyProps selama Anda mendefinisikan tipe dispatch, state, dan ownProps. Dengan begitu Anda dapat menjaga MyProps di tempatnya sendiri dan membiarkan container, atau tempat di mana komponen digunakan tanpa container, menerapkan props sesuai kebutuhan mereka. Saya kira ini tergantung pada Anda dan kasus penggunaan Anda - jika Anda mendefinisikan MyProps=MyStateProps & MyDispatchProps & MyOwnProps, itu terikat pada satu container tertentu, yang kurang fleksibel (tetapi kurang verbose).

Sebagai solusi, ini cukup panjang, tetapi saya tidak berpikir ada cara untuk menghindari memberitahu TypeScript bahwa berbagai bagian dari props yang dibutuhkan akan disusun di tempat yang berbeda, dan connect akan menghubungkannya bersama.

Juga, untuk apa pun nilainya, saya biasanya menggunakan properti opsional, demi kesederhanaan, jadi saya tidak memiliki banyak pengalaman untuk dibagikan tentang menggunakan pendekatan ini.

Jawaban #2

Saya hanya menggunakan Partial<MyProps>, di mana Partial adalah tipe bawaan TypeScript yang didefinisikan sebagai:

type Partial<T> = {
    [P in keyof T]?: T[P];
}

Ini mengambil sebuah antarmuka dan membuat setiap properti di dalamnya menjadi opsional.

Berikut adalah contoh pasangan komponen presentasional/Redux-aware yang telah saya buat:

/components/ConnectionPane/ConnectionPane.tsx

export interface IConnectionPaneProps {
  connecting: boolean;
  connected: boolean;
  onConnect: (hostname: string, port: number) => void;
}

interface IState {
  hostname: string;
  port?: number;
}

export default class ConnectionPane extends React.Component<IConnectionPaneProps, IState> {
   ...
}

/kontainer/ConnectionPane/ConnectionPane.ts

import {connect} from 'react-redux';
import {connectionSelectors as selectors} from '../../../state/ducks';
import {connect as connectToTestEcho} from '../../../state/ducks/connection';
import {ConnectionPane, IConnectionPaneProps} from '../../components';

function mapStateToProps (state): Partial<IConnectionPaneProps> {
  return {
    connecting: selectors.isConnecting(state),
    connected: selectors.isConnected(state)
  };
}

function mapDispatchToProps (dispatch): Partial<IConnectionPaneProps> {
  return {
    onConnect: (hostname, port) => dispatch(connectToTestEcho(hostname, port))
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(ConnectionPane) as any;

Props komponen presentasional bersifat wajib - persis seperti yang diperlukan untuk presentasi tanpa mempengaruhi komponen "pintar" yang bersesuaian.

Sementara itu, mapStateToProps dan mapDispatchToProps akan memungkinkan saya untuk menetapkan sebagian dari props presentasi yang diperlukan di setiap fungsi, sambil menandai setiap props yang tidak didefinisikan dalam antarmuka props presentasi sebagai kesalahan.

Artikel terkait