typescript + redux: exclude redux props in parent

2019-02-07 14:18发布

问题:

I am using redux and typescript for my current webapp.

What is the best practice to define the props of a component which receives redux-actions via @connect, but also props from parent? Example:

// mychild.tsx
export namespace MyChildComponent {

  export interface IProps {
    propertyFromParent: string;
    propertyFromRedux: string;  // !!!! -> This is the problem
    actionsPropertyFromRedux: typeof MyReduxActions;  // !!!! -> And this     
  }
}

@connect(mapStateToProps, mapDispatchToProps)
export class MyChildComponent extends React.Component <MyChildComponent.IProps, any> {

... react stuff

}

function mapStateToProps(state: RootState) {
  return {
    propertyFromRedux: state.propertyFromRedux
  };
}
function mapDispatchToProps(dispatch) {
  return {
    actionsPropertyFromRedux: bindActionCreators(MyReduxActions as any, dispatch)
  };
}




// myparent.tsx
export class MyParentComponent extends React.Component <MyParentComponent.IProps, any> {

... react stuff

    render(){
        // typescript complains, because I am not passing `propertyFromRedux`! 
        return <div><MyChildComponent propertyFromParent="yay" /></div>;
    }

}

As I see it I got 2 solutions.

  1. Just pass the actions & state down through my whole app. But that would mean that my whole app gets re-rendered even when just some small child component would have to change. Or is it the redux way to listen in my top level component on all store changes? Then I would have to write a lot of logic inside shouldComponentUpdate for props which are no flat objects.

  2. Set the param in IProps of MyChildComponent optional like this:

-

// mychild.tsx
export namespace MyChildComponent {

  export interface IProps {
    propertyFromParent: string;
    propertyFromRedux?: typeof MyAction;  // This is the problem
  }
}

Is there another way? Both of the above ways seem too messy in my eyes.

回答1:

You need to split up your props - you'll need a DispatchProps, StateProps, and an OwnProps type. You'll also have to use TypeScript's generics with connect

  • DispatchProps are your action creators.
  • StateProps are your state props (duh) - these come from mapStateToProps - the return type of that function should match this type.
  • OwnProps are props which are accepted (and perhaps expected) by your component. Optional props should be marked as optional in the interface.

The way I do it (without decorators, but i'm sure it applies here) is

interface ComponentDispatchProps {
    doSomeAction: typeof someAction;
}

interface ComponentStateProps {
    somethingFromState: any;
}

interface ComponentOwnProps {
    somethingWhichIsRequiredInProps: any;
    somethingWhichIsNotRequiredInProps?: any;
}

// not necessary to combine them into another type, but it cleans up the next line
type ComponentProps = ComponentStateProps & ComponentDispatchProps & ComponentOwnProps;

class Component extends React.Component<ComponentProps, {}> {...}

function mapStateToProps(state, props) { 
    return { somethingFromState };
}

export default connect<ComponentStateProps, ComponentDispatchProps, ComponentOwnProps>(
    mapStateToProps,
    mapDispatchToProps
)(Component);

I think you have to use @connect<StateProps, DispatchProps, OwnProps> which will decorate and return a class which accepts OwnProps.

If you look at connects implementation in TS

export declare function connect<TStateProps, TDispatchProps, TOwnProps>(...): ComponentDecorator<TStateProps & TDispatchProps, TOwnProps>

interface ComponentDecorator<TOriginalProps, TOwnProps> {
    (component: ComponentClass<TOriginalProps> | StatelessComponent<TOriginalProps>): ComponentClass<TOwnProps>;
}

connect<...> returns a ComponentDecorator, which, when passed the component (in your case this is done transparently when transpiling the decorator out), regardless of StateProps, and DispatchProps returns a component which expects OwnProps.

connect (non-generic) returns InferableComponentDecorator

export interface InferableComponentDecorator {
    <P, TComponentConstruct extends (ComponentClass<P> | StatelessComponent<P>)>(component: TComponentConstruct): TComponentConstruct;
}

which attempts to infer the props based on the props supplied to the component, which in your case is the combination of all props (OwnProps becomes ComponentProps from above).