How to use redux-form component with typescript and custom props - reactjs

I am trying to create a functional component in a react + typescript project that is using redux-form. I have created a generic form component and I want to pass a prop to it. Everything is working except the interface definition for the component props. I can use any and it works but then whats the point. I've tried a few different combinations from other posts I've found from a google search (commented out) but nothing is working.
The code:
import React from 'react';
import {
Field,
reduxForm,
InjectedFormProps,
WrappedFieldProps,
WrappedFieldMetaProps,
} from 'redux-form';
import { Stream } from '../../constants';
interface CustomInputFieldProps {
label?: string;
}
interface CustomComponentProps extends InjectedFormProps {
onSubmit(values: any & Stream): void;
}
// type StreamFormProps = CustomComponentProps & InjectedFormProps<{}, CustomComponentProps>;
// CustomComponentProps & InjectedFormProps<{}, CustomComponentProps>
// what to replace any with?
const StreamForm = ({ handleSubmit, onSubmit }: any) => {
// values is actually typed as any o_0
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/9ce52af612e29ff0bac4317bde78d0acab29afdb/types/redux-form/v6/lib/Form.d.ts#L5
const onFormSubmit = (values: any): void => {
const { title, description } = values;
onSubmit({ title, description });
};
const renderInput = ({ input, label, meta }: WrappedFieldProps & CustomInputFieldProps): JSX.Element => {
const className = `field ${meta.error && meta.touched ? 'error' : ''}`;
return (
<div className={className}>
<label>{label}</label>
<input {...input} />
{renderError(meta)}
</div>
);
};
const renderError = ({ error, touched }: WrappedFieldMetaProps) => {
if (touched && error) {
return (
<div className="ui error message">
<div className="header">{error}</div>
</div>
);
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)} className="ui form error">
<Field
name="title"
component={renderInput}
label="Enter Title"
/>
<Field
name="description"
component={renderInput}
label="Enter Description"
/>
<button className="ui button primary">Submit</button>
</form>
);
};
const validate = (formValues: any): { title?: string; description?: string; } => {
const errors: { title?: string; description?: string; } = {};
if (!formValues.title) {
errors.title = 'You must enter a title';
}
if (!formValues.description) {
errors.description = 'You must enter a description';
}
return errors;
};
export default reduxForm({ form: 'streamForm', validate })(StreamForm);
The error I am getting:
Argument of type '({ handleSubmit, onSubmit }: StreamFormProps) => JSX.Element' is not assignable to parameter of type 'ComponentType<InjectedFormProps<any, {}, string>>'.
Type '({ handleSubmit, onSubmit }: StreamFormProps) => JSX.Element' is not assignable to type 'FunctionComponent<InjectedFormProps<any, {}, string>>'.
Types of parameters '__0' and 'props' are incompatible.
Type 'PropsWithChildren<InjectedFormProps<any, {}, string>>' is not assignable to type 'StreamFormProps'.
Property 'onSubmit' is missing in type 'InjectedFormProps<any, {}, string> & { children?: ReactNode; }' but required in type 'CustomComponentProps'.ts(2345)
StreamForm.tsx(17, 3): 'onSubmit' is declared here.

Related

TS: custom onSubmit do not match React Hook Form types

I'm geting type's problems when I try to use SubmitHandler type to typping onSubmit prop, I'm getting:
TS2345: Argument of type 'SubmitHandler<T>' is not assignable to parameter of type 'SubmitHandler<FieldValues>'. Type 'FieldValues' is not assignable to type 'T'. 'T' could be instantiated with an arbitrary type which could be unrelated to 'FieldValues'.
Implementation
import { FieldValues, useFormContext, SubmitHandler } from 'react-hook-form';
import { Pressable, Text } from 'react-native';
type ButtonProps<T extends FieldValues> = PressableProps & {
onSubmit: SubmitHandler<T>;
};
function SubmitButton<T = FieldValues>({ onSubmit, ...RestButtonProps }: ButtonProps<T>) {
const { handleSubmit } = useFormContext();
return (
<Pressable {...RestButtonProps} onPress={() => handleSubmit(onSubmit)}>
<Text>Submit</Text>
</Pressable>
);
}
type LoginFormValues = {
email: string;
password: string;
};
function LoginForm() {
const handleSubmit: SubmitHandler<LoginFormValues> = ({ email, password }) =>
console.warn(email, password);
return (
<SubmitButton<LoginFormValues> title="Sign In" onSubmit={handleSubmit} />
);
}

How to type a Form component with onSubmit?

I've got my own Form component which I want to keep separated from the Parent Component.
const Form: React.FC<any> = ({ children, handleFormSubmit }) => (
<form onSubmit={handleFormSubmit}>{children}</form>
);
Parent Component
const ParentComponent = () => {
...
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
publishReview(review).then(() => setReview(initialReviewState));
};
return (
<Form onSubmit={handleFormSubmit}>
...
</Form>
)}
I thought handleFormSubmit would get the types from its declaration const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>).
But it doesn't
I've tried to build an interface and even with an any type:
interface FormProps {
handleFormSubmit: any
}
const Form: React.FC<FormProps>= ({ children, handleFormSubmit }) => (
<form onSubmit={handleFormSubmit}>{children}</form>
);
But it gave the below error:
const Form: React.FunctionComponent<FormProps>
Type '{ children: Element; onSubmit: (event: FormEvent<HTMLFormElement>) => void; }' is not assignable to type 'IntrinsicAttributes & FormProps & { children?: ReactNode; }'.
Property 'onSubmit' does not exist on type 'IntrinsicAttributes & FormProps & { children?: ReactNode; }'.
This worked for me. Just guessing based on your guess. Thanks!
handlePostForm = (e: React.FormEvent) => {
You would need to specify the type of the form elements. Let's say for example in your form you have an input with id yourInputName:
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="yourInputName">Username:</label>
<input id="yourInputName" type="text" />
</div>
<button type="submit">Submit</button>
</form>
In your handleSubmit, you would need to specify the type of the event.currentTarget. A form tag has a type of HTMLFormElement with a property elements, which are a type of HTMLFormControlsCollection, but we can override that to tell TS what our input is. Create an interface which extends the HTMLFormControlsCollection, specify the type of your field and then use the custom interface to override the elements property in the HTMLFormElement.
interface FormElements extends HTMLFormControlsCollection {
yourInputName: HTMLInputElement
}
interface YourFormElement extends HTMLFormElement {
readonly elements: FormElements
}
Then you pass that new type to your handling function
const handleFormSubmit = (e: React.FormEvent<YourFormElement>) => {}
You can now access the value of your field and typescript will know what it is
const handleFormSubmit = (e: React.FormEvent<YourFormElement>) => {
e.preventDefault();
console.log(e.currentTarget.elements.yourInputName.value)
}
If you hover on value, TS should show you a message that the data type is HTMLInputElement.value: string, just as you have specified in your FormElements interface.
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
if you have uncontrolled form in react( i used MUI field) :
<form onSubmit={handleSubmit}>
<TextField
error={isError}
helperText={isError ? 'Incorrect entry.' : ''}
margin="normal"
color="success"
label="Name and surname"
id="nameSurname"
size="small"
/></form>
you can make this:
const handleSubmit = (e: SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
nameSurname: { value: string };
//password: { value: string };
};
const nameSurname = target.nameSurname.value;
console.log(`nameSurname`, nameSurname);
};
inspired from here
You've got a mistake in your prop names. Do you see the mismatch?
const Form: React.FC<any> = ({ children, handleFormSubmit }) => (
<Form onSubmit={handleFormSubmit}>
Your Form component takes a prop named handleFormSubmit, but you are calling it with a prop named onSubmit instead.
You need to change the name in one of those places so that they both match. Either call <Form handleFormSubmit={handleFormSubmit}> or change the props of the Form component so that the prop is named onSubmit.
Here's how it should look if you rename the prop to onSubmit and give it a proper type instead of any.
interface FormProps {
onSubmit: React.FormEventHandler;
}
const Form: React.FC<FormProps> = ({ children, onSubmit }) => (
<form onSubmit={onSubmit}>{children}</form>
);
You do not need to make any changes in your ParentComponent if you define Form like this.
As Ivo wrote, but in case you want to properly type it with type:
type FormElements = Readonly<
{
yourInputName: HTMLInputElement
} & HTMLFormControlsCollection
>;
type YourFormElement = Readonly<
{
elements: FormElements;
} & HTMLFormElement
>;

Typescript & React Component that accepts onChange for both TextArea and Input

I'm new to typescript and I am trying to create an input component that, if it receives type="text" it renders an input and if it receives type="textarea" it renders, you got it, a textarea. The problem is that typescript is complaining when I use the component on my code together with a onChange, it seems it wont allow me to use two types on the same event?
It shows me:
Type '(e: ChangeEvent<HTMLInputElement>) => void' is not assignable to type '(e?: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement> | undefined) => void'.
Types of parameters 'e' and 'e' are incompatible.
Type 'ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement> | undefined' is not assignable to type 'ChangeEvent<HTMLInputElement>'.
Type 'undefined' is not assignable to type 'ChangeEvent<HTMLInputElement>'.
input.js
interface InputProps {
className?: string;
type?: string;
placeholder?: string;
onChange?: (e?: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLTextAreaElement>) => void;
}
const Input: FunctionComponent<InputProps> = ({ type = 'text', ...props }) => {
if (type === 'textarea') {
return <textarea {...props} />;
}
return <input type={type} {...props} />;
};
usage:
class Usage extends React.Component<State> {
state: State;
onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ input: e.target.value });
};
render() {
return (
<Input placeholder="Write an something..." onChange={this.onInputChange} />
);
}
}
How can I fix it?
UPDATE
The way I solved it for the moment is by saying that event can be of type any, but that's kind of a hack
type CommonProps = {
className?: string;
placeholder?: string;
type?: string;
onChange?: (e: any) => void;
};
You can use a discriminated union to pass in two types of arguments, one for text and the other for textarea. This has the added bonus of ensuring the handler and the type are in sync.
type InputProps = { // The common Part
className?: string;
placeholder?: string;
} & ({ // The discriminated union
type?: "text";
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
} | {
type: "textarea";
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
})
const Input: FunctionComponent<InputProps> = (props: InputProps) => {
if (props.type === 'textarea') {
return <textarea {...props} />;
}
return <input {...props} />;
};
class Usage extends React.Component<State> {
state: State;
onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ input: e.target.value });
};
render() {
return (
<Input placeholder="Write an something..." onChange={this.onInputChange} />
);
}
}
You need to create a class incase you are using typescript.
The normal function does not allows the | in the typescript prop types.
This should be your Input.js file:
export interface IInputProps {
className?: string;
type?: string;
placeholder?: string;
onChange?: (e?: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLTextAreaElement>) => void;
}
export class Input extends React.Component<IInputProps, {}> {
constructor(props: IInputProps) {
super(props);
}
render() {
const { props, props: { type } } = this;
if (type === 'textarea') {
return <textarea {...props} />;
}
return <input type={type} {...props} />;
}
}
and here is how it can be used:
class Usage extends React.Component<State> {
state: State;
onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ input: e.target.value });
};
render() {
return (
<Input placeholder="Write an something..." onChange={this.onInputChange} />
);
}
}
This is how it will evaluate if it is a Input or a TextArea:

Can't handle typescript with react and redux

I am new at typescript and really struggling. I can't understand where to begin and where to end . Yes , there are many recourses on internet , but i couldn't manage to get that information and use in my project . Hope to meet some help here. I have also done some typechecking and if you find something that i could have done better , please just tell me to improve it.
So now i am struggling with redux mapStateToProps and mapDispatchToProps . I have tried many variants , but every time i have got some kind of errors.
I will post my code which represents my dashboard component which is connected to state .
import * as React from 'react';
import { connect } from 'react-redux';
import SearchIcon from '../SvgIcons';
import MapComponent from '../Map';
import { getEventInfo, getUserInfo } from '../../actions';
interface StateProps {
userReducer: {
accessToken: string
},
eventReducer: {
events: object[]
}
}
interface DispatchProps {
dispatchUserInfo: () => void;
dispatchEventInfo: (accessToken: string, query: string) => void;
}
interface OwnProps {
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void,
accessToken: string,
events: object[]
}
type Props = StateProps & DispatchProps & OwnProps;
class DashboardPage extends React.Component<Props, {}> {
componentDidMount() {
const { dispatchUserInfo } = this.props;
dispatchUserInfo();
}
handleEventSearch = e => {
e.preventDefault();
const { dispatchEventInfo, accessToken } = this.props;
const query: string = e.target.children[0].value;
dispatchEventInfo(accessToken, query);
}
render() {
const { events } = this.props;
return (
<div className="dashboard-container">
<div className="search-event">
<form className="search-event__form" onSubmit={this.handleEventSearch}>
<input
autoComplete="off"
type="text"
name="search-event"
placeholder="Search an event"
className="search-event__input"
aria-label="Enter search text"
/>
<button type="submit" className="search-event__button">
<SearchIcon />
Search
</button>
</form>
<p className="sign-out">
Sign out
</p>
</div>
<div className="google-map">
<MapComponent events={events} />
</div>
</div>
);
}
}
const mapStateToProps = (state: StateProps) => {
const accessToken = state.userReducer.accessToken || '';
const events = state.eventReducer || [];
return {
accessToken,
events
};
};
const mapDispatchToProps = (dispatch: DispatchProps) => ({
dispatchEventInfo(query: string, token: string) {
dispatch(getEventInfo(query, token));
},
dispatchUserInfo() {
dispatch(getUserInfo());
}
});
export default connect(mapStateToProps, mapDispatchToProps)(DashboardPage);
These are typescript errors
1) Refers tohandleEventSearch`method
[ts] Parameter 'e' implicitly has an 'any' type.
2) refers to mapDispatchToProps
[ts] Cannot invoke an expression whose type lacks a call signature. Type 'DispatchProps' has no compatible call signatures.
3) refers to mapDispatchToProps in connect HOC
Argument of type '(dispatch: DispatchProps) => { dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }' is not assignable to parameter of type 'MapDispatchToPropsParam<{ dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }, {}>'.
Type '(dispatch: DispatchProps) => { dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }' is not assignable to type 'MapDispatchToPropsFunction<{ dispatchEventInfo(query: string, token: string): void; dispatchUserInfo(): void; }, {}>'.
Types of parameters 'dispatch' and 'dispatch' are incompatible.
Type 'Dispatch>' is not assignable to type 'DispatchProps'.
Property 'dispatchUserInfo' is missing in type 'Dispatch>'.
Also if you can provide me very good sources to learn about react and redux with typescript , so i could easily write my code.
You need to supply event type explicitly, any or
React.SyntheticEvent<...something>
Dispatch parameter should be Dispatch type from Redux
Also a little tip, you can define your props like:
ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps> & OwnProps
and remove unnecessary interfaces

How to pass rest of props to react component while also having required props defined in an interface

So is this the same issue, or am I just missing something?
import * as React from 'react';
interface Props {
value: string;
}
const MyComponent = (props: Props) => {
const { value, ...rest } = props;
return (
<input {...rest} type="text" value={value} />
);
}
interface ParentState {
searchText: string;
}
class ParentComponent extends React.Component<{}, ParentState> {
state: ParentState = {
searchText: ''
};
onSearchTextChanged = (e: React.FormEvent<HTMLInputElement>) => {
this.setState({
searchText: e.currentTarget.value
});
}
render() {
const { searchText } = this.state;
return (
<div>
<h2>Some Text</h2>
<MyComponent value={searchText} onChange={this.onSearchTextChanged} className="search-input" placeholder="Enter text"/>
// Errors here
</div>
)
}
}
export default ParentComponent
When I have my props for MyComponent defined in an interface, I get the following error:
error TS2339: Property 'onChange' does not exist on type 'IntrinsicAttributes & Props'.
However if I change the props type to any, it compiles just fine.
const MyComponent = (props: any) => {
Is it possible to define the props in an interface so that there are specific props that are required, but also allow additional props to be passed in so I don't have to explicitly add them into the interface?
I'm using TypeScript 2.3.4 and React 15.5.4.
You can avoid excess property/attribute checks by adding a string index signature to your interface:
interface Props {
value: string;
// This is a string index signature.
// This means that all properties in 'Props' are assignable to
// the empty object type ({})
[propName: string]: {};
}
You could also write [propName: string]: any but that generally makes it less safe to use in MyComponent itself.
If you want to forward all properties of the input element inside MyComponent to the props of MyComponent you can lookup what props input (in VSCode for example, use go to definition while on the input tag).
interface IntrinsicElements {
....
input: React.DetailedHTMLProps<React.InputHTMLAttributes<>, HTMLInputElement>;
....
}
We could use React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> as the base type for our props and we will get all properties of input. Since DetailedHTMLProps just adds ref to React.InputHTMLAttributes<HTMLInputElement> we can use just React.InputHTMLAttributes<HTMLInputElement> as the base interface to get all input properties except the ref:
import * as React from 'react'
interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
value: string;
}
const MyComponent = (props: Props) => {
const { value, ...rest } = props;
return (
<input {...rest} type="text" value={value} />
);
}
// Usage
interface ParentState { searchText :string }
class ParentComponent extends React.Component<{}, ParentState> {
state: ParentState = {
searchText: ''
};
onSearchTextChanged = (e: React.FormEvent<HTMLInputElement>) => {
this.setState({
searchText: e.currentTarget.value
});
}
render() {
const { searchText } = this.state;
return (
<div>
<h2>Some Text</h2>
<MyComponent value={searchText} onChange={this.onSearchTextChanged} className="search-input" placeholder="Enter text"/>
</div>
)
}
}
export default ParentComponent

Resources