How to define React component with conditional props interface? - javascript

i need to define "Field" component that renders textarea or input depends on prop multiline
I trying to do this like that:
import React from 'react';
type Props<T extends boolean = boolean> = { multiline: T } & T extends true
? React.HTMLProps<HTMLTextAreaElement>
: React.HTMLProps<HTMLInputElement>
export const Field: React.FC<Props> = ({ multiline, ...props }) => { // error here
const Element = multiline ? 'textarea' : 'input';
return <Element {...props} onInput={e => {}} />; // error here
}
// usage
const result = (
<Field onChange={e => console.log(e.target.value)} /> // error here
);
But typescript provide several errors like:
1 Property 'multiline' does not exist on type 'HTMLProps<HTMLInputElement> & { children?: ReactNode; }'.(2339)
2 [large error, more in playground]
3 Property 'value' does not exist on type 'EventTarget'.(2339)
Playground here
How can i define such component?

Problem: No T in Field
You have defined a generic type Props that depends on T but your component is not generic. It always takes Props<boolean> which resolves to the HTMLInputElement props because boolean extends true is false. The reason {multiline: boolean} is getting lost is because you need parentheses around the rest of your type.
React.HTMLProps
When using your React.HTMLProps typings I didn't get errors when assigning mismatched properties like type="number" to a textarea or rows={5} to an input. The more restrictive types are JSX.IntrinsicElements['textarea'] and JSX.IntrinsicElements['input'] (which resolve to a type like React.DetailedHTMLProps<React.TextareaHTMLAttributes<HTMLTextAreaElement>, HTMLTextAreaElement>). If you want strict enforcement then use these! This also makes the e value in the onChange callback get the correct type based on the element.
Implementation
When using a generic component with restrictive types, we now get an error in the implementation on return <Element {...props} />; I thought that breaking it into two (return multiline ? <textarea {...props} /> : <input {...props}/>;) would help but we still get errors. Conditionals are rough. You can use as assertions to fix things. I'm generally ok with making assertions in the implementation of a function when the usage of it stays strictly typed. So you can do this:
type Props<T extends boolean = boolean> = { multiline: T } & (T extends true
? JSX.IntrinsicElements['textarea']
: JSX.IntrinsicElements['input'])
export const Field = <T extends boolean>({ multiline, ...props }: Props<T>) => {
const Element = multiline ? 'textarea' : 'input';
return <Element {...props as any} />;
}
Playground #1
Union Type
We can avoid having to make assertions by typing Props as a union of two situations. This allows us to check which type in the union we have by looking at props.multiline. This gets messy though because you cannot desctructure until after you have discriminated the union, but we don't want to pass multiline through to the DOM.
This code passes all type checks, but it needs additional work to prevent passing multiline through to the DOM.
type Props = (
{ multiline: true } & JSX.IntrinsicElements['textarea'] |
{ multiline: false } & JSX.IntrinsicElements['input']
);
export const Field = ({ ...props }: Props) => {
return props.multiline ? <textarea {...props} /> : <input {...props}/>
}
Playground #2
Usage
Either way the usage is very strongly typed! Our onChange callback gets the correct type like React.ChangeEvent<HTMLTextAreaElement> and we get error if passing textarea props when multiline={false} or vice-versa.
<Field
onChange={e => console.log(e.target.value)} // e: React.ChangeEvent<HTMLTextAreaElement>
multiline={true}
rows={5} // ok
type="number" // error
/>
<Field
onChange={e => console.log(e.target.value)} // e: React.ChangeEvent<HTMLInputElement>
multiline={false}
type="number" // ok
rows={5} // error
/>

I believe you should be using an intersection of the 2 types, so it can be either/or. So for Props type use the '&' operator to cover both input types, then in the onChange event use it again for each input type event (since it can be either). Something like this:
import React from 'react';
type Props<T extends boolean = boolean> = { multiline?: T } &
React.HTMLProps<HTMLTextAreaElement> &
React.HTMLProps<HTMLInputElement>;
export const Field: React.FC<Props> = ({ multiline, ...props }) => {
const Element = multiline ? 'textarea' : 'input';
return <Element {...props} />;
}
// usage
const result = (
<Field onChange={(e: React.ChangeEvent<HTMLInputElement> & React.ChangeEvent<HTMLTextAreaElement>) => (
console.log(e.target.value)
)} />
);

Related

Typescript prop discrimination in react functional component

I'm creating a dynamic Widget, which can be either empty or populated. It should be by default empty. When populated={true}, I'd like the totalGross and totalNet values to be required. Otherwise, those 2 props shouldn't be allowed.
For that, I'm trying to use a discriminating type (WidgetBodyProps). However, TypeScript complains because totalNet & totalGross are required props in <WidgetBodyPopulated />
type WidgetBodyProps =
| { isEmpty?: false; totalNet: string; totalGross: string }
| { isEmpty: true; totalNet?: never; totalGross?: never };
type WidgetProps = WidgetHeaderProps & WidgetBodyProps;
const Widget = (props: WidgetProps) => {
const { title = "", isEmpty = true, totalNet, totalGross } = props;
return (
<Card>
<Box>
<WidgetHeader title={title} />
<Divider my="4" />
{isEmpty ? (
<WidgetBodyEmpty flex="1" />
) : (
<WidgetBodyPopulated totalNet={totalNet} totalGross={totalGross} />
)}
</Box>
</Card>
);
};
How can I fix this Typescript Error?
Once you destructure the props, Typescript forgets they were ever related. So a test on one variable, will not discriminate the type of another. But at the time that you destructure props you haven't tested it's type, so all variables are typed with all possibilities.
However, if you avoid destructuring, then Typescript can keep tabs on the type of props. This way testing a preperty of props narrows the type of props are you are expecting.
{props.isEmpty ? (
<WidgetBodyEmpty flex="1" />
) : (
<WidgetBodyPopulated
totalNet={props.totalNet}
totalGross={props.totalGross}
/>
)}
Then you can even simplify those props to:
type WidgetBodyProps =
| { isEmpty?: false; totalNet: string; totalGross: string }
| { isEmpty: true };
Because you never access .totalNet without checking .isEmpty first.
Working example
Usually this error occurs because Typescript doesn't trust you that something isn't null. Use a non-null assertion operator to tell Typescript that it can trust that you know what you're doing and you won't feed it a null. The other option would be to ensure the variables in question are properly set to be empty strings.

React Typescripted Higher order component loses generic prop type of the passed component

I have an input component that accepts a name property that is generic that extends a string type (I don't recall how this type is called, but it results in a type of that string value)
A simple example would be
type SuperType<T extends string> = T;
const value: SuperType<'test'> = 'test';
This results in value being of type 'test'.
Now when I wrap my component into a higher-order function like in the code below, I lose the strict type, and the generic fall back to a type of string.
The code example has comments that showcase the issue. If I call the component by itself onClick function gets a property of type 'Input' but the input wrapped inside a Higher-order component gives a property of type string.
import * as React from "react";
import "./styles.css";
const Input = <T extends string>(props: {
name: T;
onClick: (name: T) => void;
}) => {
return (
<input
name={props.name}
onClick={() => {
props.onClick(props.name);
}}
/>
);
};
const inputHoC = <T extends object>(Component: React.FunctionComponent<T>) => (
props: T
) => {
return <Component {...props} />;
};
const InputWithHoC = inputHoC(Input);
export default function App() {
return (
<div className="App">
<Input
name="Input"
onClick={inputName => {
//** here inputName is of type Input */
}}
/>
<InputWithHoC
name="Input"
onClick={inputName => {
//** here input is of type string */
}}
/>
</div>
);
}
Sandbox link:
https://codesandbox.io/s/stoic-chatterjee-p52ct?file=/src/App.tsx
My question is why does this happen and how to retain the strict type.
const InputWithHoC = inputHoC(Input);
What's happening is that the type for InputWithHoc is evaluated once at this line and it becomes:
const InputWithHoC: (props: {
name: string;
onClick: (name: string) => void;
}) => JSX.Element
The InputWithHoc is not generic. It is the function inputHoC that is generic.
We want to switch that around. The inputHoC function should not be generic, but it should return a generic component.
I'm kind of blanking right now on how to do this is a more generalized way, if it's even possible, but here's how you do it for your input:
// interface for props which depends on T
interface InputProps<T> {
name: T;
onClick: (name: T) => void;
}
// type for a generic input
type GenericInput = <T extends string>(props: InputProps<T>) => JSX.Element;
// HOC takes a generic function component and returns a generic function component
const inputHoC = (Component: GenericInput): GenericInput => <T extends string>(
props: InputProps<T>
) => {
return <Component {...props} />;
};
Now your onClick callback knows that the type of inputName is "Input".

Get containing component from nested component

I am writing a ControlledInput component, and in order to have access to the state of the component using ControlledInput, I have a binder prop in ControlledInput.
I'm having a slight issue when using the component:
render() {
const CI = props => <ControlledInput binder={this} {...props} />;
return (
<div style={styles.container}>
<h1>NEW RECIPE</h1>
<ControlledInput binder={this} label={"Title"} />
</div>
);
}
The implementation above works completely fine. However, note the const CI I've defined. I tried to use this so I could just write <CI label={"Title"}/> without the binder since the binder will be the same on all the ControlledInput components I use in a given render method.
The problem with using <CI label={"Title"}/> is that when I type into the input, the input "blurs" and I have to reselect it. This appears to be because the render method creates the CI on every render.
I hope I've explained that clearly, because my head hurts.
Anyway, it makes sense to me why this happens. And I know that one solution is to put const CI = props => <ControlledInput binder={this} {...props} />; outside of the render function. But then I'd have to call it as <this.CI> and that starts to defeat the purpose.
And I can't put CI in global scope because then I don't have access to this.
Is there a way to solve this?
Update
Here is the current (very much in progress) code for ControlledInput:
// #flow
import React, { Component } from "react";
type Props = {
containerStyle?: Object,
label: string,
propName?: string,
binder: Component<Object, Object>,
onChange?: Object => void
};
class ControlledInput extends Component<Props> {
render() {
const props = this.props;
const propName = props.propName || props.label.toLowerCase();
return (
<div style={props.containerStyle}>
<p>{props.label}</p>
<input
type="text"
label={props.label}
onChange={
this.props.onChange ||
(e => {
props.binder.setState({ [propName]: e.target.value });
})
}
value={props.binder.state[propName]}
></input>
</div>
);
}
}
The point of this whole endeavor is to simplify creating a form with controlled components, avoiding having to add value={this.state.whatever} and onChange={e=>this.setState({whatever: e})} to each one, which is not DRY in my opinion.
And then I want get a little more DRY by not passing binder={this} to every component and that's why I'm doing const CI = props => <ControlledInput binder={this} {...props} />;, which, again, has to be inside the class to access this and inside the render function to be called as CI rather than this.CI.
So that first explanation why you need to pass this, although I suppose I could also have props like setState={this.setState} parentState={this.state}, and in that case it does indeed start to make sense to combine those into something like {...propsToSend} as #John Ruddell suggested.
Note that I've provided a possibility to override onChange, and plan on doing so for most or all of the other props (e.g, value={this.props.value || binder.state[propName]}. If one were to override a lot of these (especially value and onChange) it would indeed make the component much less reusable, but the main use case is for quickly creating multiple inputs that don't have special input handling.
So, again, my ideal would be to call <ControlledInput label="Title"/> and have the component code take care of binding state and setState correctly. If this is possible. And then the second option would be to have a place to define the necessary context props in a place that makes it simple when it's time to actually use the component multiple times, like so:
<ControlledInput label={"title"} {...contextProps}/>
<ControlledInput label={"author"} {...contextProps}/>
<ControlledInput label={"email"} {...contextProps}/>
<ControlledInput label={"content"} textArea={true} {...contextProps}/> // textarea prop not implemented yet, fyi
etc
I hear that accessing the parent state/context may be an anti-pattern, but there must be some way to do what I'm trying to do without using an anti-pattern, isn't there?
If you want the state of the parent, handle the state there and pass down the value to your input - ControlledInput won't have to know anything except how to handle data in and out. Something like this, and note that I jacked up the names a little so you can see which component is handling what:
import React, { useState } from "react"
const Parent = () => {
const [title, setTitle] = useState("")
const handleChangeInParent = (newTitle) => {
setTitle((oldValue) => newTitle)
}
return(<div style={styles.container}>
<h1>NEW RECIPE</h1>
<ControlledInput handleChange={handleChangeInParent} label={title} />
</div>)
}
const ControlledInput = ({handleChange, label}) => {
return (
<input onChange={handleChange} type="text" value={label} />
)
}
If ControlledComponent needs to handle its own state, then pass it a default value and then have the Parent read the value when saving (or whatever):
import React, { useState } from "react"
const Parent = () => {
const handleSaveInParent = (newTitle) => {
console.log("got the new title!")
}
return (
<div style={styles.container}>
<h1>NEW RECIPE</h1>
<ControlledInput handleSave={handleSaveInParent} initialLabel="Title" />
</div>
)
}
const ControlledInput = ({ handleSave, initialLabel }) => {
const [title, setTitle] = useState(initialLabel)
const handleChange = (ev) => {
const value = ev.target.value
setTitle((oldValue) => value)
}
const handleSubmit = (ev) => {
ev.preventDefault()
handleSave(title)
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} type="text" value={title} />
</form>
)
}
You shouldn't be sending this through - just send values and/or functions to handle values.
With Updated Implementation
(okay, John you win!)
Not positive if this is technically an "answer", but I've rewritten the component to take a state and (updated) a setterFn prop:
component
// #flow
import React, { Component } from "react";
type Props = {
containerStyle?: Object,
labelStyle?: Object,
label: string,
propName?: string,
state: Object,
onChange?: Object => void,
textArea?: boolean,
setterFn: (key: string, value: mixed) => void
};
class ControlledInput extends Component<Props> {
render() {
const props = this.props;
const propertyName = props.propName || props.label.toLowerCase();
const TagType = props.textArea ? "textarea" : "input";
// only pass valid props to DOM element (remove any problematic custom props)
const { setterFn, propName, textArea, ...domProps } = props;
return (
<div style={props.containerStyle}>
<p style={props.labelStyle}>{props.label}</p>
<TagType
{...domProps}
label={props.label} // actually could get passed automatically, but it's important so I'm leaving it in the code
onChange={
this.props.onChange ||
(setterFn ? e => setterFn(propertyName, e.target.value) : null)
}
value={props.state[propertyName] || ""}
></TagType>
</div>
);
}
}
export default ControlledInput;
in use (somehow less code than before!)
class Wrapper extends Component<Object, Object> {
state = {};
render() {
const setterFn = (k, v) => this.setState({ [k]: v });
const p = { state: this.state, setterFn: setterFn.bind(this) };
return <ControlledInput {...p} {...this.props.inputProps} />
}
}
I guess this is more appropriate. It still takes up a lot more space than binder={this}.
It doesn't actually the questions of:
How to access the parent's state from the component. Though from comments it seems like this is an anti-pattern, which I do understand from the theory of React.
How to set these repeating props elsewhere so that I can just call `. I guess the only solution is to do something like this:
render() {
const props = {state: this.state, setState: this.setState}
<ControlledInput {...props} label="Title"/>
}
Which certainly isn't such a bad solution. Especially if I shorten that name to, say, a single character.
Much thanks to #John Ruddell for setting me on the right path.

Input Attribute onChange only if Props is not Undefined

I'm using React with TypeScript, and I've created a Input Component which will take lots of Props, and many of them are optional. So far I've this.
interface InputProps {
labelClassName?: string;
labelName?: string;
inputType: string;
inputName?: string;
inputValue: any;
inputOnChange?: (e: any) => void;
readOnly?: boolean;
}
Now, in the Input Component I want to render an label tag with an input inside. This input will inherit basically all the props, so I will have something like this.
export class Input extends React.Component<InputProps, {}> {
render() {
console.log(this.props);
return (
<label className={this.props.labelClassName}>
{this.props.labelName}
<input
type={this.props.inputType}
name={this.props.inputName}
value={this.props.inputValue || ""}
readOnly={this.props.readOnly}
onChange={(e) => this.props.inputOnChange(e)} />
</label>)
;
}
}
Now, the problem is that I can't write the line with OnChange because TypeScript tell me "Object is possibly 'undefined'", which is totally true, since this.props.inputOnChange it's an optional props.
So, what I'd like to write it's something like "if this.props.inputOnChange(e) != undefined, then add onChange in the input", but... I don't know how do this.
I've tried the conditional rendering:
{this.props.inputOnChange &&
onChange = {(e) => this.props.inputOnChange(e)} />
But it does not work
EDIT: One solution I did found it's to write something like this.
let inputOnChange = (this.props.inputOnChange != undefined)
? this.props.inputOnChange
: undefined;
...
<input
...
onChange={inputOnchange} />
But I honestly don't know if it's ok to pass an undefined to onChange
Generally you will want to handle onChange by providing your own function, and then conditionally propagating if the prop exists. Something like this:
export class Input extends React.Component<InputProps, {}> {
handleOnChange = (e) => {
if (this.props.inputOnChange) {
this.props.inputOnChange(e);
}
}
render() {
console.log(this.props);
const { labelClassName, labelName, inputType, inputName,
inputValue, readOnly } = this.props;
return (
<label className={labelClassName}>
{labelName}
<input
type={inputType}
name={inputName}
value={inputValue || ""}
readOnly={readOnly}
onChange={this.handleOnChange} />
</label>
);
}
}
I'll give you one extra piece of advice, don't ever create an arrow function inside your render method if you can help it. For example:
<input onChange={(e) => something} />
The above creates a new function each time the render method is called. This will cause react to re-render the entire sub component tree because the function reference changed. In this case, it might not be a big deal, but if you had a large subtree of components you can run into performance issues relatively quickly.
You can include conditional props like this.
<input
{...this.props.inputOnChange && { onChange: this.props.inputOnChange }}
// or
{...this.props.inputOnChange && { onChange: (e) => this.props.inputOnChange(e, 'foo') }}
/>
Repeat for as many props as you need. This is just using the Spread Attributes syntax and you are not limited to just one spread or just one property in the object.
Meaning, this is all ok:
<input
{...props} // you already know this spread syntax
{...props && props} // spreads if defined
{...a && { a }} // shorthand property name
{...c && { c: (e) => c(e) }}
{...x && { x, y:2, z:3 }} // multiple properties
/>
Shorthand property names (ES2015)
You can define conditionalProps object
let conditionalProps = {}
if (this.props.inputOnChange)
conditionalProps["onChange"] = this.props.inputOnChange
// or with custom fn
if (this.props.inputOnChange)
conditionalProps["onChange"] = (ev) => this.props.inputOnChange(ev, otherArgs)
return <input { ...conditionalProps} />
Alternatively
return (
<XyzSomeOtherJSXMarkup>
{this.props.inputOnChange
? <input onChange={this.props.inputOnchange} />
: <input />}
</XyzSomeOtherJSXMarkup>
)

Redux Form: Input stays with 'touched: false'

Wanted to validate my inputs and change the CSS depending of the user interaction.
Starting with a required validation method I wrap all my inputs component with a <Field> and pass to validate an array of func. Just required for now.
But for all my fields the value stay the same touched: false and error: "Required". If I touch or add stuff in the input, those values stay the same.
Validation
export const required = value => (value ? undefined : 'Required')
NameInput
import React from 'react';
import { Field } from 'redux-form'
import InputItem from 'Components/InputsUtils/InputItem';
import { required } from 'Components/InputsUtils/Validation';
const NameInput = () => (
<Field
name={item.spec.inputName}
type={item.spec.type}
component={InputItem}
validate={[required]}
props={item}
/>
);
export default NameInput;
InputItem
import React from 'react';
const InputItem = ({ spec, meta: { touched, error } }) => {
const { type, placeholder } = spec;
return (
<input
className="input"
type={type}
placeholder={placeholder}
/>
);
};
export default InputItem;
There are 2 solutions to solve the "touched is always false" issue.
1) Ensure that input.onBlur is called in your component
For an input:
const { input } = this.props
<input {...input} />
For custom form elements without native onBlur:
const { input: { value, onChange, onBlur } } = this.props
const className = 'checkbox' + (value ? ' checked' : '')
<div
className={className}
onClick={() => {
onChange(!value)
onBlur()
}}
/>
2) Declare your form with touchOnChange
const ReduxFormContainer = reduxForm({
form: 'myForm',
touchOnChange: true,
})(MyForm)
The redux-form controls its own props within your <input /> element as long as you use the spread operator to pass those props into your input.
For example, where you are doing const InputItem = ({ spec, meta: { touched, error } }) => ...
Try destructing the input from the Component: const InputItem = ({ input, spec, meta: { touched, error } }) => ...
And where you have your <input ... />, try doing the following:
<input
{...input}
className="input"
type={type}
placeholder={placeholder}
/>
The redux-form captures any onBlur and onChange events and uses its own methods to change the touched state. You just need to pass those along as shown above.
These are what you need: https://redux-form.com/7.1.2/docs/api/field.md/#input-props
Another point to consider:
I passed {...props} to my custom component, however, touched still remained false.
That is because although props contained the input object, my component couldn't
deduce onBlur from it. When explicitly stating <CustomComponent {...this.props} onBlur={this.props.input.onBlur}, it worked as expected.

Categories

Resources