web-dev-qa-db-fra.com

TypeScript lutte avec les conteneurs Redux

J'ai du mal à comprendre comment taper correctement Les conteneurs Redux .

Considérons un composant de présentation simple qui pourrait ressembler à ceci:

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

Du point de vue de ce composant, tous les accessoires sont nécessaires.

Maintenant, je veux écrire un conteneur qui tire tous ces accessoires hors d'état:

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);

Cela fonctionne, mais il y a un gros problème de dactylographie: les fonctions de mappage (mapStateToProps et mapDispatchToProps) ne disposent pas de la protection qu'elles fournissent les bonnes données pour répondre à MyProps. Ceci est sujet aux erreurs, fautes de frappe et refactoring médiocre.

Je pourrais faire en sorte que les fonctions de mappage renvoient le type MyProps:

function mapStateToProps(state: MyState): MyProps { }

function mapDispatchToProps(dispatch: IDispatch): MyProps { }

Cependant, cela ne fonctionne que si je rends tous les accessoires MyProp facultatifs, afin que chaque fonction de mappage ne puisse renvoyer que la partie qui les intéresse. Je ne veux pas rendre les accessoires facultatifs, car ils ne sont pas facultatifs pour le composant de présentation.

Une autre option consiste à diviser les accessoires pour chaque fonction de la carte et à les combiner pour les accessoires composant:

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

Ok, ça me rapproche de ce que je veux, mais c'est plutôt bruyant et ça me fait écrire mon interface de composant de présentation prop sur la forme d'un conteneur, ce que je n'aime pas.

Et maintenant, un deuxième problème se pose. Et si je veux mettre le conteneur dans un autre composant:

<MyContainer />

Cela donne des erreurs de compilation qui name, selected et onSelect sont tous manquants ... mais cela est intentionnel car le conteneur se connecte à Redux et les fournit. Cela me pousse donc à rendre tous les accessoires de composants facultatifs, mais je n'aime pas cela parce qu'ils ne sont pas vraiment facultatifs. 

La situation empire lorsque MyContainer a certains de ses propres accessoires qu'il souhaite transmettre:

<MyContainer section="somethng" />

Maintenant, ce que j'essaie de faire, c'est d'avoir section un support requis de MyContainer mais pas un support de MyComponent, et name, selected, et onSelect sont des accessoires requis de MyComponent mais facultatifs ou ne sont pas du tout accessoires de MyContainer. Je ne sais vraiment pas comment exprimer cela.

Toute indication sur ce serait apprécié!

13
Aaron

Vous êtes sur la bonne voie avec votre dernier exemple. Vous devez également définir une interface MyOwnProps et taper la fonction connect.

Avec ces saisies: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/react-redux/react-redux.d.ts , vous pouvez faire quelque chose comme ceci

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);

Cela vous permet également de taper ownProps dans mapStateToProps et mapDispatchToProps, par ex.

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

Vous n'avez pas besoin de définir un type d'intersection pour le type MyProps tant que vous définissez les types dispatch, state et ownProps. De cette façon, vous pouvez conserver MyProps à son emplacement et laisser les conteneurs, ou les lieux où le composant est utilisé sans conteneurs, appliquer les accessoires comme il se doit. Je suppose que cela dépend de vous et de votre cas d'utilisation - si vous définissez MyProps = MyStateProps & MyDispatchProps & MyOwnProps, il est lié à un conteneur spécifique, qui est moins flexible (mais moins détaillé).

En guise de solution, c'est assez bavard, mais je ne pense pas qu'il soit possible de dire à TypeScript que les différents accessoires requis seront assemblés à différents endroits et que connect les liera.

Aussi, pour ce que cela vaut, je suis généralement allé avec des accessoires optionnels, par souci de simplicité, donc je n'ai pas beaucoup d'expérience à partager sur l'utilisation de cette approche.

16
Radio-

Je viens d'utiliser Partial<MyProps>, où Partial est un type TypeScript intégré défini comme suit:

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

Il prend une interface et rend chaque propriété en option.

Voici un exemple d'une paire de composants présentationnelle/prise en compte de Redux que j'ai écrite:

/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> {
   ...
}

/containers/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;

Les accessoires de composant de présentation ne sont pas facultatifs - exactement comme requis pour la présentation sans aucune incidence sur le composant "intelligent" correspondant.

Pendant ce temps, mapStateToProps et mapDispatchToProps me permettront d’affecter un sous-ensemble des accessoires de présentation requis dans chaque fonction, tout en signalant comme erreur tous les accessoires non définis dans l’interface des accessoires de présentation.

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

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

interface MyOwnProps {
    section: string;
}

// Intersection Types
type MyProps = MyStateProps & MyDispatchProps & MyOwnProps;


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);

Vous pouvez utiliser quelque chose appelé Types d'intersection https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types

1
Vikas Kumar