Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript errors when using Redux-Form with React-Redux connect

I was falling in love with TypeScript until I found some very discouraging incompatibilities between Redux-Form with React-Redux.

My goal is wrap a reduxForm decorated component with the react-redux connect decorator—this pattern has always worked for me in babel configurations and seems to follow the HOC methodology. Here's an example:

import * as React from 'react';
import { Dispatch } from 'redux';
import { connect } from 'react-redux';
import { reduxForm, Field, InjectedFormProps } from 'redux-form';

interface SampleFormData {
  username: string;
}

interface SampleFormProps {
  saveData: (data: SampleFormData) => void;
}

type AllSampleFormProps = SampleFormProps & InjectedFormProps<SampleFormData>;

const SampleForm: React.SFC<AllSampleFormProps> = (props) => (
  <form onSubmit={props.handleSubmit(props.saveData)}>
    <Field name="username" component="input" />
  </form>
);

const DecoratedSampleForm = reduxForm<SampleFormData>({ form: "sampleForm" })(SampleForm);

export default connect(
  () => ({}),
  (dispatch) => ({
    saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
  })
)(DecoratedSampleForm);

Here's the errors TypeScript is throwing:

> Argument of type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' is not assignable to
> parameter of type 'ComponentType<{ saveData: (data: SampleFormData) =>
> { type: string; data: SampleFormData; }; }>'.
> 
> Type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' is not assignable to type
> 'StatelessComponent<{ saveData: (data: SampleFormData) => { type:
> string; data: SampleFormData; };...'.
> 
> Type 'DecoratedComponentClass<SampleFormData,
> Partial<ConfigProps<SampleFormData, {}>>>' provides no match for the
> signature '(props: { saveData: (data: SampleFormData) => { type:
> string; data: SampleFormData; }; } & { children?: ReactNode; },
> context?: any): ReactElement<any>'.

Has anyone found a solution to make react-redux accept the DecoratedComponentClass type? I found a suggestion to use a "middle" component but I haven't managed to get this to work with thunk actions. Plus I've found that this creates more problems than it solves in terms of typing the form's props.

like image 912
Tom McKinney Avatar asked Sep 28 '17 20:09

Tom McKinney


People also ask

Does TypeScript work with Redux?

TypeScript is a typed superset of JavaScript that provides compile-time checking of source code. When used with Redux, TypeScript can help provide: Type safety for reducers, state and action creators, and UI components.

Can I use Redux connect with React hooks?

React Redux recently released version 7.1, which includes long awaited support for React Hooks. This means that you can now ditch the connect higher-order component and use Redux with Hooks in your function components.

Should form data be in Redux?

We specifically recommend that most form state probably shouldn't be kept in Redux. However, it's your app, so you should evaluate the specific use cases you need to deal with to determine the right approach.


2 Answers

To anyone who comes across this, I found that I was able to dismiss the error by providing the connect statement with empty TStateProps and TDispatchProps objects.

interface SampleFormData {
  username: string;
}

interface SampleFormProps {
  saveData: (data: SampleFormData) => void;
}

type AllSampleFormProps = SampleFormProps & InjectedFormProps<SampleFormData>;

const SampleForm: React.SFC<AllSampleFormProps> = (props) => (
  <form onSubmit={props.handleSubmit(props.saveData)}>
    <Field name="username" component="input" />
  </form>
);

const DecoratedSampleForm = reduxForm<SampleFormData>({ form: "sampleForm" })(SampleForm);

export default connect<{},{}>(
  () => ({}),
  (dispatch) => ({
    saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
  })
)(DecoratedSampleForm);

The one downside to this is that it forces us to blindly supply connect props but I felt that this was a more elegant solution than writing an override @types declaration.

To address this shortcoming, I was able to validate the types by providing connect with the correct interfaces versus empty objects; however, this method can only be done temporarily to check the bindings as it doesn't resolve the DecoratedComponentClass error.

export default connect<{}, SampleFormProps, InjectedFormProps<SampleFormData>>(
  () => ({}),
  (dispatch) => ({
    saveData: (data: SampleFormData) => dispatch({ type: "SAVE_DATA", data })
  })
)(DecoratedSampleForm);
like image 111
Tom McKinney Avatar answered Oct 03 '22 01:10

Tom McKinney


A higher Component Interface declaration does the trick that wraps up connects Type with component state and props Type using a decorator.

connect.ts

import * as React from "react";
import {
  connect as originalConnect,
  MapStateToPropsParam,
  MergeProps,
  Options
} from "react-redux";
import { IState } from "./index";

export interface IDisPatchProps {
  [key: string]: () => void;
}

export type InferableComponentEnhancerWithProps<TInjectedProps, TNeedsProps> = <
  TComponent extends React.ComponentType<TInjectedProps & TNeedsProps>
>(
  component: TComponent
) => TComponent;

export interface IConnectProps {
  <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
    mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, IState>,
    mapDispatchToProps?: IDisPatchProps
  ): InferableComponentEnhancerWithProps<
    TStateProps & TDispatchProps,
    TOwnProps
  >;

  <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, TMergedProps = {}>(
    mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, IState>,
    mapDispatchToProps?: IDisPatchProps,
    mergeProps?: MergeProps<
      TStateProps,
      TDispatchProps,
      TOwnProps,
      TMergedProps
    >,
    options?: Options<TStateProps, TOwnProps, TMergedProps>
  ): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>;
}

declare module "react-redux" {
  // tslint:disable-next-line
  interface Connect extends IConnectProps {}
}

export const connect = originalConnect as IConnectProps;


 ***ClassFile***

 @connect(
  (state: IState): IStateProps => ({
    count: state.counter.count,
    isLoading: state.counter.isLoading
  }),
  {
    decrement,
    increment
  }
)
export default class MyApp

Link: https://github.com/TomasHubelbauer/react-redux-typescript-connect-decorator-demo/blob/master/my-app/src/connect.ts

credit goes to: TomasHubelbauer https://github.com/TomasHubelbauer

like image 34
Mukundhan Avatar answered Oct 03 '22 00:10

Mukundhan