Alright so on the application I am working on, I have a component called InputElement
, where different HTML inputs (input, textarea, select, etc) can all use this component and share the same styles and properties (I use them with react-hook-form).
type Variant =
| {
variant: 'select'
list: string[]
}
| {
variant: 'input'
list?: never
}
| {
variant: 'textarea'
list?: never
}
These are the variant props. Input and textarea are the same, but when we give InputElement
a prop of select
, it needs to have a list
of strings to render just like the html select
tag would have option
tags inside of it (will transform to option
later). So far, this works well.
type Props = Variant & {
/** Header of the InputElement */
label: string
/** Custom label to give the InputElement for form hook */
registerLabel: string
/** Input type of the field */
type?: HTMLInputTypeAttribute
}
And these are the regular props. Pretty self explanatory there.
So I have tested this, and it works, but I'm trying to use it with React.useContext
hook so I don't have to prop-drill all of these props.
createContext:
const InputContext = createContext<Props>({
label: 'Default Label',
registerLabel: 'example',
type: 'text',
variant: 'input'
})
InputElement component:
const InputElement = ({ label, registerLabel, type, ...props }: Props) => {
const { register } = useFormContext()
const { list, variant } = { ...props }
return (
<InputContext.Provider value={{ label, registerLabel, type, variant, list}}>
<div className={styles.container} {...props}>
<label>{label}</label>
{getVariant(variant)}
</div>
</InputContext.Provider>
)
}
(getVariant
just returns the variant (input, textarea, select) that the component was passed).
But typescript throws an error over the react context initial value
key with this long log that ends with Type '"select"' is not assignable to type '"textarea"'
???
I have never used conditional types before, and I love how I can apply it here, but this is killing me. Any thoughts? Your help would be greatly appreciated. Thank you in advance.
CodePudding user response:
Most likely you don't want/need the list?: never
. Try your union with the list missing for variants that dont need it, then have a function which returns the right context shape for a given variant:
import { createContext, HTMLInputTypeAttribute } from "react";
type Variant =
| {
variant: "select";
list: string[];
}
| {
variant: "input";
}
| {
variant: "textarea";
};
type Props = Variant & {
/** Header of the InputElement */
label: string;
/** Custom label to give the InputElement for form hook */
registerLabel: string;
/** Input type of the field */
type?: HTMLInputTypeAttribute;
};
const InputContext = createContext<Props>({
label: "Default Label",
registerLabel: "example",
type: "text",
variant: "input"
});
const getInputContext = (props: Props): Props => {
switch (props.variant) {
case "select":
return { ...props, variant: "select", list: props.list };
default:
return { ...props, variant: props.variant };
}
};
const InputElement = (props: Props) => {
const { register } = useFormContext();
const { label, registerLabel, type, ...otherProps } = props;
return (
<InputContext.Provider value={getInputContext(props)}>
<div className={styles.container} {...props}>
<label>{label}</label>
{getVariant(variant)}
</div>
</InputContext.Provider>
);
};
I'll stress what you are trying to achieve is a little odd. Condensing down JSX into configurable properties that just say what JSX to render, is potentially an anti pattern. What you would normally do is pass down the component into the core one, if you want some commonality. I think the abstraction you are introducing is potentially unnecessary indirection and is encouraging branching over composition (which is preferred).