6
votes

Comment taper correctement un appel de connexion Redux?

J'essaie d'utiliser un magasin d'état Redux en combinaison avec TypeScript. J'essaie d'utiliser les typages officiels de Redux et je souhaite effectuer l'appel entier sur la méthode connect (qui connecte mapStatetoProps et mapDispatchToProps avec un composant) type safe.

Je vois généralement des approches où les méthodes mapStatetoProps et mapDispatchToProps sont simplement typées de manière personnalisée et renvoient une partie des accessoires du composant, tels que les suivants :

connect<IComponentProps, any, any, IStateStore>(
  (state, ownProps) => ({
    /* some props supplied by redux state */
  }),
  dispatch => ({
    /* some more props supplied by dispatch calls */
  })
)(Component);

Ceci est typé et fonctionne, mais pas vraiment sûr car il est possible d'instancier un composant qui manque d'accessoires, car l'utilisation de l'interface Partial permet des définitions incomplètes. Cependant, l'interface partielle est requise ici car vous souhaiterez peut-être définir certains accessoires dans mapStateToProps et d'autres dans mapDispatchToProps , et pas tous dans une seule fonction. C'est pourquoi je veux éviter ce style.

Ce que j'essaie actuellement d'utiliser, c'est d'incorporer directement les fonctions dans l'appel connect et de taper l'appel de connexion avec la saisie générique fourni par redux:

function mapStateToProps(state: IStateStore, ownProps: Partial<IComponentProps>)
  : Partial<IProjectEditorProps> {}
function mapDispatchToProps (dispatch: Dispatch, ownProps: Partial<IComponentProps>)
  : Partial<IProjectEditorProps> {}

Cependant, cela génère également l'erreur que les appels mapStatetoProps et mapDispatchToProps intégrés pour ne pas définir tous Props chacun car les deux ne nécessitent qu'un sous-ensemble d'entre eux, mais définissant ensemble tous les Props.

Comment puis-je saisir correctement l'appel de connexion pour que le Les appels à mapStatetoProps et mapDispatchToProps sont vraiment sécurisés et la saisie vérifie si les valeurs combinées définies par les deux méthodes fournissent tous les accessoires requis sans l'une des méthodes requises pour définir tous les accessoires à la fois? Est-ce possible avec mon approche?


0 commentaires

3 Réponses :


8
votes

Option 1: Split IComponentProps

Le moyen le plus simple de le faire est probablement de définir simplement des interfaces séparées pour vos "accessoires dérivés d'état", vos "propres accessoires" et vos "accessoires d'expédition" et puis utilisez un type d'intersection pour les joindre pour le IComponentProps

type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps;

interface IComponentOwnProps {
  foo: string;
}

type IComponentStoreProps = ReturnType<typeof mapStateToProps>;
type IComponentDispatchProps = ReturnType<typeof mapDispatchToProps>;

class IComponent extends React.Component<IComponentProps, never> {
  //...
}


function mapStateToProps(state: IStateStore, ownProps: IComponentOwnProps) {
  return {
    bar: state.bar + ownProps.foo,
  };
}

function mapDispatchToProps(dispatch: Dispatch<IStateStore>) {
  return {
    fooAction: () => dispatch({ type: 'FOO_ACTION' })
  };
}

export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
  mapStateToProps,
  mapDispatchToProps
)(IComponent);

Nous pouvons définir les paramètres génériques de la fonction de connexion comme suit:

Option 2: laissez vos fonctions définir votre interface Props

Une autre option que j'ai vue dans la nature est pour tirer parti du ReturnType type mappé pour permettre à vos fonctions mapX2Props de définir réellement ce qu'elles contribuent à IComponentProps .

import * as React from 'react';
import { connect, Dispatch } from 'react-redux'
import { IStateStore } from '@src/reducers';


interface IComponentOwnProps {
  foo: string;
}

interface IComponentStoreProps {
  bar: string;
}

interface IComponentDispatchProps {
  fooAction: () => void;
}

type IComponentProps = IComponentOwnProps & IComponentStoreProps & IComponentDispatchProps

class IComponent extends React.Component<IComponentProps, never> {
  public render() {
    return (
      <div>
        foo: {this.props.foo}
        bar: {this.props.bar}
        <button onClick={this.props.fooAction}>Do Foo</button>
      </div>
    );
  }
}

export default connect<IComponentStoreProps, IComponentDispatchProps, IComponentOwnProps, IStateStore>(
  (state, ownProps): IComponentStoreProps => {
    return {
      bar: state.bar + ownProps.foo
    };
  },
  (dispatch: Dispatch<IStateStore>): IComponentDispatchProps => (
    {
      fooAction: () => dispatch({type:'FOO_ACTION'})
    }
  )
)(IComponent);

Le gros avantage ici est qu'il réduit un peu la plaque chauffante et vous permet de ne disposer que d'un seul endroit pour mettre à jour lorsque vous ajoutez un nouvel accessoire mappé.

Je me suis toujours éloigné de ReturnType , simplifiez-vous car il se sent à l'envers de laisser votre implémentation définir votre interface de programmation "contrats" (IMO). Il devient presque trop facile de modifier vos IComponentProps d’une manière que vous n’avez pas prévue.

Cependant, comme tout ici est assez autonome, il est probablement un cas d'utilisation acceptable.


4 commentaires

Donc, l'appel connect lui-même ne prend pas en charge un seul état props qui vérifie à la fois les props d'expédition et de stockage? Cela serait-il généralement possible avec un typage personnalisé, en vérifiant deux ensembles distincts s'ils sont disjoints et définissant également tous les accessoires requis, ou n'est-ce pas possible avec TypeScript?


En théorie, il semble que vous devriez être en mesure de le faire. Cependant, j'obtiens une erreur lorsque j'essaye de le configurer. J'ai soumis un problème: github.com/Microsoft/TypeScript/issues/29399


Merci, votre approche devrait fonctionner comme ça. Marquer cette réponse comme acceptée comme le code de votre réponse semble être la meilleure approche actuellement possible.


@LukasBach merci! En fait, je viens d'ajouter une autre option que j'ai vue dans le passé. Ce n'est pas personnellement la voie que j'emprunte, mais vous l'aimerez peut-être mieux.



2
votes

Une solution consiste à diviser les propriétés de vos composants en propriétés d'état, de répartition et peut-être propres:

import React from "react";
import { connect } from "react-redux";

import { deleteItem } from "./action";
import { getItemById } from "./selectors";

interface StateProps {
  title: string;
}

interface DispatchProps {
  onDelete: () => any;
}

interface OwnProps {
  id: string;
}

export type SampleItemProps = StateProps & DispatchProps & OwnProps;

export const SampleItem: React.SFC<SampleItemProps> = props => (
  <div>
    <div>{props.title}</div>
    <button onClick={props.onDelete}>Delete</button>
  </div>
);

// You can either use an explicit mapStateToProps...
const mapStateToProps = (state: RootState, ownProps: OwnProps) : StateProps => ({
  title: getItemById(state, ownProps.id)
});

// Ommitted mapDispatchToProps...

// ... and infer the types from connects arguments ...
export default connect(mapStateToProps, mapDispatchToProps)(SampleItem);

// ... or explicitly type connect and "inline" map*To*.
export default connect<StateProps, DispatchProps, OwnProps, RootState>(
  (state, ownProps) => ({
    title: getItemById(state, ownProps.id)
  }),
  (dispatch, ownProps) => ({
    onDelete: () => dispatch(deleteItem(ownProps.id))
  })
)(SampleItem);


0 commentaires

1
votes

Vraiment comme l'approche de fractionnement de @ NSjonas, mais j'emprunterais également quelque chose à sa deuxième approche pour avoir un équilibre entre l'aspect pratique, ne pas laisser l'implémentation définir complètement votre interface et ne pas être extrêmement verbeux dans la saisie de vos actions de répartition; XXX


0 commentaires