How can I create component dynamically using React with TypeScript?
Assuming that I have some RandomComponent and I pass it as props to renderInput, how could I return <RandomComponent> from it?
TypeScript does not seem to understand something like this:
export const renderInput = (props) => {
const Field: JSX.Element = props.component;
return (
<Field {...props} />
)
}
Edit:
Made it work, but am wondering what if I wanted to add this ComponentType type. It works without it, but when I add it and the code looks like this:
const Field: ComponentType = component;
return (
<Field name={name} label={label} value={value} onChange={onChange} {...props} />
);
};
I get:
TS2322: Type '{ name: any; label: any; value: any; onChange: any; }' is not assignable to type 'IntrinsicAttributes'. Property 'name' does not exist on type 'IntrinsicAttributes'
You can use generic type
export const renderInput = <T,>(props:T & {component: React.reactNode }) => {
const Field = props.component;
return (
<Field {...props}/>
)
}
Alternatively if only need to send a component you can send it as children
export const renderInput = (props: { children: React.reactNode }) => {
const { children } = props;
return children;
};
Related
I'm developing a complex solution for building forms from a JSON contract using Formik.
By design, inputs can have tooltips. Or they could not.
Trying to make some parts of the code reusable to avoid repeating myself. Created TextFieldSSWrapper for inputs with server-side validations, forwarded a ref as docs tells. Now, this component could be wrapped with Tooltip, if there is one.
The problem is the following: now, when using this middleware component, input loses focus after each onChange call. Also I'm getting a warning on each onChange call:
The anchorEl prop provided to the component is invalid.
If drop wrapper component and do simple copy-paste, everything works fine.
What am I doing wrong? Thanks a lot.
import {useField, Field} from 'formik';
import TextField from '#mui/material/TextField';
import {asyncValidationCheckers} from '../services/validation.service';
import {Tooltip} from '#mui/material';
interface ITextItem {
id: string;
type?: string;
name: string;
value: string | number;
label: string;
placeholder: string;
tooltip?: string;
variant: 'standard' | 'outlined' | 'filled';
size: 'small' | 'medium',
serverValidation: string;
}
interface IConfigFormControl {
error?: boolean;
errorText?: string;
}
interface ITextFieldSSWrapper {
form: any;
ref: any;
}
export const TextItem = (
{
id,
type,
name,
value,
label,
placeholder,
tooltip,
variant,
size,
serverValidation
}: ITextItem
) => {
const [field, meta] = useField(name);
const configFormControl: IConfigFormControl = {
error: false,
errorText: ''
};
if (meta && meta.touched && meta.error) {
configFormControl.error = true;
configFormControl.errorText = meta.error;
}
const helperText = configFormControl.errorText;
const props = {
fullWidth: true,
id,
type,
label,
placeholder,
helperText,
variant,
size,
error: configFormControl.error,
...field
};
const TextFieldSSWrapper = React.forwardRef<HTMLDivElement, ITextFieldSSWrapper>(({ form, ...rest }, ref) => (
<TextField
{...props}
{...rest}
ref={ref}
onChange={(e) => {
const value = e.target.value;
asyncValidationCheckers[serverValidation].setInputValid();
// it the line above we remove server-side validation error
// in order to be able to run server-side validation again on blur
// because it won't run if input has an error from previous sever-side validation cycle
form.setFieldTouched(id, true);
form.setFieldValue(id, value);
}}
onBlur={(e) => {
const value = e.target.value;
if (!form.errors[id]) {
setTimeout(() => {
asyncValidationCheckers[serverValidation].validateInput(value, () => {
form.setFieldTouched(id, true);
});
}, 0);
}
}}
/>
));
const ref = React.createRef<HTMLDivElement>();
return (
serverValidation
? (
<Field
name={id}
>
{
({form} : any) => (
tooltip
? (
<Tooltip title={tooltip} placement='bottom'>
<TextFieldSSWrapper ref={ref} form={form} />
</Tooltip>
)
: (
<TextFieldSSWrapper ref={ref} form={form} />
)
)
}
</Field>
)
: (
tooltip
? (
<Tooltip title={tooltip} placement='bottom'>
<TextField
{...props}
/>
</Tooltip>
)
: (
<TextField
{...props}
/>
)
)
);
};
I am using React + Typescript. I am working on a component that can return dynamic HTML element depends on props:
interface Props {
label?: string;
}
const DynamicComponent = forwardRef<
HTMLButtonElement | HTMLLabelElement,
Props
>((props, ref) => {
if (props.label) {
return (
<label ref={ref as React.ForwardedRef<HTMLLabelElement>}>
{props.label}
</label>
);
}
return (
<button ref={ref as React.ForwardedRef<HTMLButtonElement>}>BUTTON</button>
);
});
Is it possible to type ref's interface in a way that will depend on the label prop?
export default function App() {
const btnRef = useRef<HTMLButtonElement>(null); // Allowed
const labelRef = useRef<HTMLLabelElement>(null); // Allowed
// const labelRef = useRef<HTMLButtonElement>(null); // Not allowed, because `label` prop was not provided
return (
<>
<DynamicComponent ref={btnRef} />
<DynamicComponent ref={labelRef} label="my label" />
</>
);
}
Sandbox link
In order to do that we need to use function overloading, with higher order function pattern and typeguards:
FIXED
import React, { forwardRef, useRef, Ref } from 'react'
interface Props {
label?: string;
}
// typeguard
const isLabelRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLLabelElement> => {
return true // ! NON IMPLEMENTED
}
// typeguard
const isButtonRef = (props: Props, ref: React.ForwardedRef<any>): ref is React.ForwardedRef<HTMLButtonElement> => {
return true // ! NON IMPLEMENTED
}
// Higher order COmponent with overloads
function DynamicComponent<T extends HTMLButtonElement>(reference: Ref<T>): any
function DynamicComponent<T extends HTMLLabelElement>(reference: Ref<T>, props: Props): any
function DynamicComponent<T extends HTMLElement>(reference: Ref<T> | undefined, props?: Props) {
const WithRef = forwardRef<HTMLElement, Props>((_, ref) => {
if (props && isLabelRef(props, ref)) {
return (
<label ref={ref}>
{props.label}
</label>
);
}
if (props && isButtonRef(props, ref)) {
return (
<button ref={ref}>BUTTON</button>
);
}
return null
});
return <WithRef ref={reference} />
}
export default function App() {
const btnRef = useRef<HTMLButtonElement>(null); // Allowed
const labelRef = useRef<HTMLLabelElement>(null); // Allowed
return (
<>
{DynamicComponent(btnRef)}
{DynamicComponent(labelRef, { label: 'sdf' })}
</>
);
}
Playground
As you might have noticed, I use only props from DynamicComponent.
Also T generic parameter serves for narrowing the ref type
I left isButtonRef and isLabelRef unimplemented
UPDATE
Seems that my previous example is useless. Sorry for that.
My bad. I have already fixed it.
As an alternative solution, you can override built in forwardRef function.
interface Props {
label: string;
}
declare module "react" {
function forwardRef<T extends HTMLButtonElement, P>(
render: ForwardRefRenderFunction<HTMLButtonElement, never>
): ForwardRefExoticComponent<
PropsWithRef<{ some: number }> & RefAttributes<HTMLButtonElement>
>;
function forwardRef<T extends HTMLLabelElement, P extends { label: string }>(
render: ForwardRefRenderFunction<HTMLLabelElement, { label: string }>
): ForwardRefExoticComponent<
PropsWithRef<{ label: string }> & RefAttributes<HTMLLabelElement>
>;
}
const WithLabelRef = forwardRef<HTMLLabelElement, Props>((props, ref) => (
<label ref={ref}>{props.label}</label>
));
const WithButtonRef = forwardRef<HTMLButtonElement>((props, ref) => (
<button ref={ref}>{props}</button>
));
function App() {
const btnRef = useRef<HTMLButtonElement>(null);
const labelRef = useRef<HTMLLabelElement>(null);
const divRef = useRef<HTMLDivElement>(null);
return (
<>
<WithButtonRef ref={btnRef} />
<WithLabelRef ref={labelRef} label="my label" />
<WithLabelRef ref={divRef} label="my label" /> //expected error
</>
);
}
I have this Select component:
type SelectProps = {
title: string;
name: string;
items: string[];
onChange: (e: ChangeEvent<HTMLSelectElement>) => void;
defValue: string;
};
const Select: FC<SelectProps> = ({ title, name, items, onChange, defValue }) => {
return (
<>
<label htmlFor={name}>
{title}:
</label>
<select name={name} onChange={onChange} defaultValue={defValue}>
{items.map((item) => (
<option value={item} key={item}>
{item}
</option>
))}
</select>
</>
);
};
and I'm handling onChange with this function:
const onThemeChange = (e: ChangeEvent<HTMLSelectElement>) => {
const theme = e.target.value;
setTheme(theme)
};
...
<Select
title='Theme'
defValue={props.theme}
name='theme'
items={['light', 'dark']}
onChange={onThemeChange}
/>
My setTheme action creator accepts argument with type 'light' | 'dark', so I'm getting an error:
Argument of type 'string' is not assignable to parameter of type '"light" | "dark"'
What is the best way to solve this issue?
There is away, but it requires a little trick.
First, let's recognize the relationships between types in your SelectProps:
the items are string literals
in onChange, the event will have a target.value equal to one of your items
the defValue should also be one of the items
To express these constraints, we need to use a generic interface.
type SelectProps<T extends string> = {
title: string;
name: string;
items: T[];
onChange: (e: ChangeEvent<HTMLSelectElement> & { target: { value: T }}) => void;
defValue: DeferTypeInference<T>;
};
const Select = function<T extends string>({ title, name, items, onChange, defValue }: SelectProps<T>) {
return (
<>
<label htmlFor={name}>
{title}:
</label>
<select name={name} onChange={onChange} defaultValue={defValue}>
{items.map((item) => (
<option value={item} key={item}>
{item}
</option>
))}
</select>
</>
);
};
We have achieved everything.
<Select
title='Theme'
defValue="light" // only "light" or "dark" are accepted
name='theme'
items={['light', 'dark']}
onChange={event => event.target.value} // event.target.value is "light" or "dark"
/>
Note the use of a type called DeferTypeInference<T>. If you're curious why it's there, check out this answer.
The quickest way of doing so would be to do type assertions.
Assuming that this is how you initialised the state,
type ThemeState = 'light' | 'dark';
const [theme, useTheme] = useState<ThemeState>('light');
And then, on your onThemeChange method, you will assert the value as ThemeState
const theme = e.target.value as ThemeState;
I'm having this Typescript error when using react-autosuggest with a custom input styled-component.
Types of property 'onChange' are incompatible. Type '(event: FormEvent, params: ChangeEvent) => void' is not assignable to type '(event: ChangeEvent) => void'.
Code:
(note that it's not complete, just relevant portions)
// styled.ts
export const Input = styled.input`
// styles
`;
// index.tsx
function renderInput(
inputProps: Autosuggest.InputProps<SuggestionSearch>,
placeholder: string
) {
// --> error here
return <Input {...inputProps} placeholder={placeholder} />;
}
const MyComponent = () => {
const autosuggestRef = React.useRef<
Autosuggest<SuggestionSearch, SuggestionSearch>
>(null);
const onChange = (event: React.FormEvent, { newValue }: ChangeEvent) =>
setValue(newValue);
const inputProps = {
value,
onChange,
onKeyPress: onEnterPress,
};
return (
<Autosuggest
ref={autosuggestRef}
renderInputComponent={props => renderInput(props, placeholder)}
inputProps={inputProps}
// other props
/>
)
}
Not sure how to fix this, as Autosuggest onChange function overrides the base input onChange prop.
I spent about all night on this problem myself. It seems that this is a bug. Here is the signature of the InputProps:
interface InputProps<TSuggestion>
extends Omit<React.InputHTMLAttributes<any>, 'onChange' | 'onBlur'> {
onChange(event: React.FormEvent<any>, params: ChangeEvent): void;
onBlur?(event: React.FocusEvent<any>, params?: BlurEvent<TSuggestion>): void;
value: string;
}
It seems we'll need to create our own Input component that take in this type of onChange signature or wrap this onChange in the standard onChange.
So, this is my approach:
const renderInputComponent = (inputProps: InputProps<TSuggestion>): React.ReactNode => {
const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>): void => {
inputProps.onChange(event, { newValue: event.target.value, method: 'type' });
};
return <Input {...inputProps} onChange={onChangeHandler} />;
}
Two potential bugs:
Notice that the event returned is React.ChangeEvent and not the React.FormEvent. Minor difference but it can be a problem in runtime if you actually use it and particular enough about the event.
The returned params: ChangeEvent only account for type event method. If you want others, like click, esc, etc..., you must supplement your own (via onKeyUp for example, and then call `onChange(...{method: 'esc'})
According to doc:
Note: When using renderInputComponent, you still need to specify the usual inputProps. Autosuggest will merge the inputProps that you provide with other props that are needed for accessibility (e.g. 'aria-activedescendant'), and will pass the merged inputProps to renderInputComponent.
you don't have to do this:
renderInputComponent={props => renderInput(props, placeholder)}
Simply pass the placeholder directly into your inputProps and that would be given right back to you in the renderInputComponent.
const inputProps = {
value,
onChange,
onKeyPress: onEnterPress,
placeholder: 'something!`
};
My problem is the onChange() on the renderInputComponent does not trigger the onSuggestionsFetchRequested.
If I do not use custom input (via renderInputComponent), then everything works fine - onSuggestionsFetchRequested got triggered and suggestion list showing.
Main component:
const MySearchBox: FC<MySearchBoxProps> = (props) => {
// Autosuggest will pass through all these props to the input.
const inputProps = {
placeholder: props.placeholder,
value: props.value,
onChange: props.onChange
};
return (
<Autosuggest
suggestions={props.suggestions}
onSuggestionsFetchRequested={props.onSuggestionsFetechRequested}
onSuggestionsClearRequested={props.onSuggestionsClearRequested}
getSuggestionValue={props.getSuggestionValue}
shouldRenderSuggestions={(value) => value.trim().length > 2}
renderSuggestion={(item: ISuggestion) => {
<div className={classes.suggestionsList}>{`${item.text} (${item.id})`}</div>;
}}
renderInputComponent={(ip: InputProps<ISuggestion>) => {
const params: InputProps<ISuggestion> = {
...ip,
onChange: props.onChange
};
return renderInput(params);
}}
inputProps={inputProps}
/>
);
};
export { MySearchBox };
Custom Input Component:
import React, { FC, ChangeEvent } from 'react';
import SearchIcon from '#material-ui/icons/Search';
import useStyles from './searchInput.styles';
import { Input } from '#material-ui/core';
type SearchInputBoxProps = {
loading?: boolean;
searchIconName: string;
minWidth?: number;
placeHolder?: string;
onChange?: (e: ChangeEvent, params: { newValue: string; method: string }) => void;
};
const SearchInputBox: FC<SearchInputBoxProps> = (props) => {
const classes = useStyles();
return (
<div className={classes.root}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<Input
placeholder={props.placeHolder}
classes={{
root: classes.inputContainer,
input: classes.inputBox
}}
inputProps={{ 'aria-label': 'search' }}
onChange={(e) => {
const params: { newValue: string; method: string } = {
newValue: e.target.value,
method: 'type'
};
console.log(`e: ${JSON.stringify(params)}`);
if (props.onChange) props.onChange(e, params);
}}
//onMouseOut={(e) => alert(e.currentTarget.innerText)}
/>
</div>
);
};
export { SearchInputBox };
Render Function:
const renderInput = (inputProps: InputProps<ISuggestion>): React.ReactNode => {
return <SearchInputBox loading={false} onChange={inputProps.onChange} placeHolder={inputProps.placeholder} searchIconName={'search'} {...inputProps.others} />;
};
I had same problem.
I just had to cast HTMLElement into HTMLInputElement
Like below
const target = e.currentTarget as HTMLInputElement
Then, it works fine when you do
target.value
I have a JS Component in which I have defined some normal jsx functions and states and now I want to use them as props in a tsx file, but since I'm a beginner, I'm confused. this is the js version of what I want to implement in tsx:
export class FormUserDetails extends Component {
continue = e => {
e.preventDefault();
this.props.nextStep();
};
render() {
const { values, handleChange } = this.props;
return (
...
<AppBar title="Enter User Details" />
<TextField
placeholder="Enter Your First Name"
label="First Name"
onChange={handleChange('firstName')}
defaultValue={values.firstName}
margin="normal"
fullWidth="true"
/>
...
How to implement this in React TypeScript with:
export class MainInfo extends Component<Props,{}>
or similar?
Sandbox:it has my tsx file that I want to implement like my example in question and the JS file defining my props
https://codesandbox.io/s/tsx-rj694
hi you can make types for your class component this way
interface IProps {
nextStep: () => void;
values: {
firstName: string
};
handleChange: (value: string) => void
}
export class FormUserDetails extends Component<IProps> {
continue = e => {
e.preventDefault();
this.props.nextStep();
};
render() {
const { values, handleChange } = this.props;
return (
...
<AppBar title="Enter User Details" />
<TextField
placeholder="Enter Your First Name"
label="First Name"
onChange={handleChange('firstName')}
defaultValue={values.firstName}
margin="normal"
fullWidth="true"
/>
...
You will need to provide an interface for your component's props. For the case of FormUserDetails, you will need to define an interface or type alias that contains values and handleChange.
interface FormUserDetailsProps {
values: {
name: string; // include other required properties
};
handleChange: (value: string) => void;
}
export class FormUserDetails extends Component<FormUserDetailsProps> {
// do the rest here
}
but you can also use function component because it's much cleaner, nicer and more readable specially when using typescript:
interface IProps {
nextStep: () => void;
handleChange: (value: string) => void
values: {
firstName: string
};
}
const FormUserDetails = (props: IProps) => {
const { values, handleChange, nextStep } = props;
const continue = (e: any) => {
e.preventDefault();
nextStep();
};
return (
<AppBar title="Enter User Details" />
<TextField
placeholder="Enter Your First Name"
label="First Name"
onChange={handleChange('firstName')}
defaultValue={values.firstName}
margin="normal"
fullWidth="true"
/>
)}