9

I've got a plain react-redux-powered form. I wish for there to be a form.container.tsx and a form.component.tsx, where form.container.tsx holds all the connections to redux state minus the Field's. I'm trying to wrap my container in react-redux's connect and then wrapping reduxForm within it to look something like TypeScript, redux-form and connect:

(ideal) form.container.tsx:

interface DummyFormContainerProps {}

export const DummyFormContainer: React.SFC<DummyFormContainerProps> = props => {
  const submitForm = (formValues: object) => {
    alert(formValues);
  };
  return (
    <DummyForm
      onSubmit={submitForm}
    />
  );
};

const mapStateToProps = (state: State) => ({});
const mapDispatchToProps = (dispatch: object) => {
  return {};
};
const mergeProps = (stateProps: State, dispatchProps: object | null, ownProps: object | void) => 
  Object.assign({}, stateProps, dispatchProps, ownProps);

const formConfiguration = {
  form: 'dummy-form',
  forceUnregisterOnUnmount: true
};

export default connect(mapStateToProps, mapDispatchToProps)(
  reduxForm(formConfiguration)(DummyFormContainer)
);

The above does not work, but if I take out the reduxForm() part, I'm left with a working container with no reduxForm Integration:

(working without reduxForm) form.container.tsx:

export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(
  DummyFormContainer
);

And I've tried different variations with reduxForms and connect, all not currently working:

(with classes) form.container.tsx:

export class DummyFormContainer extends React.Component<DummyFormContainerProps, void> {
  submitForm = (formValues: object) => {
    alert(formValues);
  }

  render() {
    return (
      <DummyForm
        onSubmit={this.submitForm}
      />
    );
  }
}

const mapStateToProps = (state: State) => ({});
const mapDispatchToProps = (dispatch: object) => {
  return {};
};
const mergeProps = (stateProps: State, dispatchProps: object | null, ownProps: object | void) => 
  Object.assign({}, stateProps, dispatchProps, ownProps);

const formConfiguration = {
  form: 'business-registration',
};

export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(
  reduxForm(formConfiguration)(DummyFormContainer) // ERROR
);

error:

./src/modules/dummy-form/dummy-form.container.tsx
(100,32): error TS2345: Argument of type 'typeof DummyFormContainer' is not assignable to parameter of type 'ComponentType<InjectedFormProps<{}, {}>>'.
  Type 'typeof DummyFormContainer' is not assignable to type 'StatelessComponent<InjectedFormProps<{}, {}>>'.
    Type 'typeof DummyFormContainer' provides no match for the signature '(props: InjectedFormProps<{}, {}> & { children?: ReactNode; }, context?: any): ReactElement<any> | null'.

(with stateless functional components) form.container.tsx:

export const DummyFormContainer: React.SFC<DummyFormContainerProps> = props => {
  const submitForm = (formValues: object) => {
    alert(formValues);
  };
  return (
    <DummyForm
      onSubmit={submitForm}
    />
  );
};

export default connect(mapStateToProps, mapDispatchToProps, mergeProps)(
  reduxForm(formConfiguration)(DummyFormContainer) // ERROR
);

error:

./src/modules/dummy-form/dummy-form.container.tsx
(100,3): error TS2345: Argument of type 'DecoratedComponentClass<{}, Partial<ConfigProps<{}, {}>>>' is not assignable to parameter of type 'ComponentType<(State & null & void) | (State & null & object) | (State & object & void) | (State ...'.
  Type 'DecoratedComponentClass<{}, Partial<ConfigProps<{}, {}>>>' is not assignable to type 'StatelessComponent<(State & null & void) | (State & null & object) | (State & object & void) | (S...'.
    Type 'DecoratedComponentClass<{}, Partial<ConfigProps<{}, {}>>>' provides no match for the signature '(props: (State & null & void & { children?: ReactNode; }) | (State & null & object & { children?: ReactNode; }) | (State & object & void & { children?: ReactNode; }) | (State & object & { children?: ReactNode; }), context?: any): ReactElement<any> | null'.

The form.component.tsx looks like this:

import * as React from 'react';
import Input from '../../components/input';

interface DummyFormProps {
  onSubmit: (formValues: object) => void
}

export const DummyForm: React.SFC<DummyFormProps> = () => {
  return (
    <div>
      <h1>DummyForm (no state)</h1>
      <form>
        <Input inputType="primary" />
      </form>
    </div>
  );
};

export default DummyForm;

And the < Input > component is a regular React component.

Does anyone know how to properly connect reduxForm and react-redux's connect()?

Kyle Truong
  • 2,545
  • 8
  • 34
  • 50

5 Answers5

10

Here's a fully typed example that allows initializing a form using initialValues and passing additional props (as IOwnProps):

sampleForm.tsx:

export interface IFormData {
  userId: string;
}

export interface IOwnProps {
  foo: string;
}

export interface IDispatchProps {
  onSubmit: (data: IFormData, dispatch: Dispatch<any>, props: IOwnProps) => void;
}

type AllSampleFormProps = IOwnProps & IDispatchProps & InjectedFormProps<IFormData, IOwnProps>;

const SampleForm: React.SFC<AllSampleFormProps> = (props) => (
  <form onSubmit={props.handleSubmit(props.onSubmit)}>
    <div>foo={props.foo}</div>
    <Field name="userId" component="input" />
    <button type="submit">Submit</button>
  </form>
);

export const DecoratedSampleForm = reduxForm<IFormData, IOwnProps>({})(SampleForm);

sampleForm.ts:

The trick here is to specify proper return type for mapStateToProps, otherwise compiler will be complaining like other authors pointed out.

function mapStateToProps(state: AppState, props: IOwnProps): ConfigProps<IFormData, IOwnProps> {
  return {
    form: "sampleForm", // Form will be handled by Redux Form using this key
    initialValues: {
      userId: state.somethere.userId // Can also be calculated using props
    }
  }
}

function mapDispatchToProps(dispatch: Dispatch<any>): IDispatchProps {
  return {
    onSubmit: (formData: IFormData, dispatch: Dispatch<any>, props: IOwnProps) => {
      console.log(formData);
      console.log(props);
    }
  }
}

export default connect<ConfigProps<IFormData, IOwnProps>>(
  mapStateToProps,
  mapDispatchToProps
)(DecoratedSampleForm);

Now this form can be mounted like this:

<FormContainer foo="bar"/>
  • How do you handle the case where you want to add more props then form/initialValues. Let's say i want the caller to add the initial values of a drop down. How would you do that? – darewreck Apr 22 '19 at 23:26
  • Ex. say that your form's drop down gets populated with [a,b,c,d]. This wouldn't go into initialValues since it's not part of the form's selected values vs the actual selected value of the form. If it's not part of initial value, how is that configured in the mapStateToProps – darewreck Apr 23 '19 at 00:12
3

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);
Tom McKinney
  • 253
  • 2
  • 9
2

I also ran into this issue trying to initialise my form from redux state, as per the example in https://redux-form.com/7.0.4/examples/initializefromstate/

I ended up getting around it by connecting the component at a higher level, eg:

component.tsx:

interface DummyFormComponentProps {} extends InjectedFormProps

const DummyFormComponent: React.SFC<DummyFormComponentProps> = props => {
  return (
    <form onSubmit={props.handleSubmit}>
      // Fields go here
    </form>
  )
}

export const DummyForm = reduxForm({
  form: "dummy-form"
})(DummyFormComponent)

// Trying to connect here also gave errors with DecoratedComponentClass

container.tsx:

interface DummyFormContainerProps {} extends Pick<InjectedFormProps,
  "initialValues"
>

const submitForm = (formValues: object) => {
  alert(formValues);
};

const DummyFormContainer: React.SFC<DummyFormContainerProps> = props => {  
  return (
    <DummyForm 
      initialValues={props.initialValues}
      onSubmit={submitForm}
    />
  )
}

const mapStateToProps = (state: State) => ({
  initialValues: {}
});
const mapDispatchToProps = (dispatch: object) => {
  return {};
};
export default connect(mapStateToProps, mapDispatchToProps)(DummyFormContainer)
bel
  • 36
  • 2
0

What we ended up doing was to close our eyes and override the default types with a type declaration file:

redux-forms.d.ts:

declare module 'redux-form' {
  type anyProps = { [key: string]: {} }
  function Field(): React.Component<anyProps>;
  function reduxForm({}): <T>(c: T) => T
  function reducer(): object
  interface SubmissionError {
    new(error?: {}) : Error;
  }
  function getFormValues(formName: string): (formName: {}) => {}
  function stopSubmit(formName: string, errorObject?: {}): any
  function isSubmitting(formName: string): any
  function setSubmitFailed(formName: string): any
  function setSubmitSucceeded(formName: string): any
  function touch(formName: string): any
  function clearSubmitErrors(formName: string): any
  function getFormMeta(formName: string, ...fields: string[]): (state: {}) => {}
  function getFormSyncErrors(formName: string): (state: {}) => {}
  function getFormSubmitErrors(formName: string): (state: {}) => {}
  function getFormNames(): any
}
Kyle Truong
  • 2,545
  • 8
  • 34
  • 50
  • where to put this type file either on project root level or in node_modules folder – Rahul Mangal Oct 17 '17 at 10:09
  • We just put it in src/typings/redux-forms.d.ts. We used create-react-app with Microsoft's Typescript scripts. Didn't modify the package.json to do anything so I believe TypeScript is just picking things within the src/ folder and reading the 'declare' keyword. – Kyle Truong Oct 17 '17 at 13:21
  • when i do as you say. i faced this below error. D:\repositories\tsproj\reactjs-ts-form-demo\node_modules\@types\redux-form\lib\selectors.d.ts (1,10): error TS2305: Module ''redux-form'' has no exported member 'FormErrors'. my dependencies are: https://jsfiddle.net/mglrahul/9qrn6gbn/1/ – Rahul Mangal Oct 17 '17 at 14:07
  • it sounds like the TypeScript declare is working then, and you need to define the type for 'FormErrors' in the declaration – Kyle Truong Oct 17 '17 at 15:34
-4

I had the same problem and found it was caused by "@types/react-redux", remove this types definition file and everything works the way you would expect it to without any other side effects/type errors caused by not having that type-def-file.

TimArsen
  • 1
  • 1