I would like to create a ConditionalWrapper component to being more declarative, in my app.
My idea is to use it as following
<ConditionalWrapper condition={whatever} element={<a href="my-link" />}>
...other children
</ConditionalWrapper>
I got this so far, but obviously it is not working, and I really cannot see where I am wrong.
interface ConditionalWrapperProps {
condition: boolean
children?: React.ReactNode
element: React.ReactElement
defaultElement?: React.ReactElement
}
const ConditionalWrapper = ({
condition,
children,
element,
defaultElement
}: ConditionalWrapperProps): JSX.Element => {
const Element = (Wrapper): JSX.Element => <Wrapper>{children}</Wrapper>
return condition ? (
<Element Wrapper={element}>{children}</Element>
) : (
<Element Wrapper={defaultElement || Fragment}>{children}</Element>
)
}
The error I get at the moment is Uncaught Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
It's clear that my types and logic are wrong, but I also tried different variations without success. Any suggestions?
You need to do several things. First of all your Element function isn't actually a valid React Function Component.
Then you need to accept parameters which are function components, and not quite elements yet.
I have separated the Element into its own scope called ElementWrapper, just for sake of understanding how the parameters were incorrect. You can of course move this back into the ConditionalWrapper.
You'll also have to move the fragment logic elsewhere, since Fragment is not a FunctionComponent
interface ConditionalWrapperProps {
condition: boolean;
children?: React.ReactNode;
element: React.FunctionComponent; //These need to be FunctionComponents
defaultElement?: React.FunctionComponent;
}
//Here you can see you forgot to have a children property
const ElementWrapper = (props: {
Wrapper: React.FunctionComponent;
children: React.ReactNode;
}): JSX.Element => <props.Wrapper>{props.children}</props.Wrapper>;
const ConditionalWrapper = ({
condition,
children,
element,
defaultElement,
}: ConditionalWrapperProps): JSX.Element => {
return condition ? (
<ElementWrapper wrapper={element>{children}</ElementWrapper>
) : DefaultElement ? (
<ElementWrapper Wrapper={defaultElement}>{children}</ElementWrapper>
) : (
<>{children}</>
);
);
};
Personally I don't think you even need the ElementWrapper class function, just call the functionComponents directly in ConditionalWrapper, like so. The properties are renamed to follow the guidelines that React Elements should have capitalized names.
const ConditionalWrapper = ({
condition,
children,
WrapperElement,
DefaultElement,
}: ConditionalWrapperProps): JSX.Element => {
return condition ? (
<WrapperElement>{children}</WrapperElement>
) : DefaultElement ? (
<DefaultElement>{children}</DefaultElement>
) : (
<>{children}</>
);
};
If you define your function as const Element = (Wrapper):...-->Wrapper refers to the Element properties, not the Wrapper property, changing to const Element = ({Wrapper}):... will work.
I think you can make you code shorter and save a few lines by doing this:
const ConditionalWrapper = ({
condition,
children,
element,
defaultElement
}: ConditionalWrapperProps): JSX.Element => {
const Wrapper = condition ? element : defaultElement || Fragment;
return (<Wrapper>{children}</Wrapper>)
}
Related
export type ComponentPropsWithoutChildren<
Component extends ElementType,
Props = {}
> = Props & Omit<ComponentPropsWithoutRef<Component>, keyof Props>;
How to omit children prop from element type?
If I have something like the following:
export const Input = ({
className,
children,
href,
...props
}: ComponentPropsWithoutChildren<"input", InputProps>) => {
return (
...
);
};
Placeholder is okay, but href gets an error like:
Property 'href' does not exist on type 'Omit<Pick<DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "key" | keyof InputHTMLAttributes<...>>, never>'.ts(2339)
I'd like children to get a Typescript error as well
I have a question. I am currently learning react with Typescript using the - fullstack react with typescript textbook. I am now on the first app - the trello clone.
I hit a bug I am finding difficult to resolve.
I get this error from the AppStateContext.
Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes'
This is the block where it is defined: AppStateContext.tsx
export const AppStateProvider: FC = ({ children }: React.PropsWithChildren<{}>) => {
const [state, dispatch] = useImmerReducer(appStateReducer, appData)
const { lists } = state
const getTasksByListId = (id: string) => {
return lists.find((list) => list.id === id)?.tasks || []
}
return (
<AppStateContext.Provider value={{ lists, getTasksByListId, dispatch }}>
{children}
</AppStateContext.Provider>
)
}
This is the block where it is called: index.tsx
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<AppStateProvider>
<App />
</AppStateProvider>
);
The error is coming from where I call the in the index.tsx file.
I really appreciate the help in advance. React version is 18.2.0
You need to get rid of explicit FC type for AppStateProvider.
See this simplified example:
interface FunctionComponent<P = {}> {
(props: P, context?: any): ReactElement<any, any> | null;
}
As you might have noticed, FunctionComponent is a simplified version (removed props which are not related to your question) of FC.
Generic P represents props argument. If P generic don't have children property, you are not allowed to use it. In fact you are allowed to use only that properties which are declared in P generic.
Please keep in mind, that you have used just FC, it means that P generic is replaced with {}. Hence, you are not allowed to provide any property to react component.
See this:
type Fn<P = 'no properties'> = (props: P) => void
const fn: Fn = (props) => {
props // no properties
}
props has 'no properties' type, and you are not allowed to override this with another type:
// errpr
const fn: Fn = (props:number) => {
props // no properties
}
YOur example is a bit different, because typescript type {} is a tricky type. Since everything in javascript is object, any (not sure for 100%) type is assignable to {}. This is why you don't have an error using React.PropsWithChildren<{}> for props. However, using React.PropsWithChildren<{}> does not affect AppStateProvider type, because when you call it, it will expect only {} as a properties.
Just as a rule of thumb, you are allowed to use either explicit type of whole function and avoid using argument type or just use type for argument.
Either this:
type Fn<P = 'no properties'> = (props: P) => void
const fn: Fn = (props) => {
props // no properties
}
OR
const fn = (props:'no properties') => {
props // no properties
}
P.S. In react 17.*, FC had children type, see this article
The error is basically telling you that the type of children (which is ELements) has no overlap with your typescript defined type (React.PropsWithChildren<{}>). The easy way to fix the issue is to change the AppStateContext.tsx to something like this:
export const AppStateProvider: FC = ({ children }: {children:Elements) => {
...
However, if you really want to practice the clean code, and use React.PropsWithChildren, here is a repository with a sample use case:
https://github.com/KishorNaik/Sol_PropsWithChildren_React
HTH
I've got a component that wrap any children that I give him. This component has a useState to give an editMode status and setter. So inside this component, to pass this state, I've done it like this :
import ShowMoreWrapper from 'components/ui/showmore/ShowMoreWrapper'
type PropsTypes = {
children: JSX.Element
}
const TargetCard = ({
children,
}: PropsTypes): JSX.Element => {
const [editMode, setEditMode] = useState(false)
return (
<ShowMoreWrapper initialHeight={initialHeight} maxHeight={maxHeight}>
{cloneElement(children, {
editMode,
setEditMode,
})}
</ShowMoreWrapper>
)
}
Then
<TargetCard>
<ProfileDetails />
</TargetCard>
And inside my children component, I received it like this :
type ProfileDetailsTypes = {
editMode?: boolean
setEditMode?: React.Dispatch<React.SetStateAction<boolean>>
}
const ProfileDetails = ({
editMode,
setEditMode,
}: ProfileDetailsTypes): JSX.Element => {
Inside this component, I use editMode as a boolean to display/hide things and setEditMode as a setter, like this example :
{editMode && <div>Hello world</div>}
<div onClick={() => setEditMode(false)}>Cancel edit mode</div>
But on render I've this double error :
React does not recognize the `editMode` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `editmode` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
React does not recognize the `setEditMode` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `seteditmode` instead. If you accidentally passed it from a parent component, remove it from the DOM element.
In another child component I don't have any problem to deal with them, but on this one, I'm stuck... Can you see something obvious in my code ? Thanks.
I have a Control flow component with React that
renders its children when the condition is true,
renders null or a fallback if the condition is false
The component
interface Props {
when: boolean
fallback?: () => JSX.Element
children: Children
}
export const Show = ({
when,
fallback,
children
}: Props) => {
if (!when)
return <>{fallback?.() || null}</>
return <>{children}</>
}
Without the component
if I do not use this component and use a simple binary operator, Typescript works great:
interface Props {
value: {nested: string} | null
}
const SomeComponent =({value}:Props)=>(
<div>
{value && (
<div>
{value.nested}
value is inferred the type "{nested: string}"
</div>
)}
</div>
With the component
If I use the control flow component the type is not inferred and typescript gives an error:
interface Props {
value: {nested: string} | null
}
const SomeComponent =({value}:Props)=>(
<div>
<Show when={!!value}>
<div>
{value?.nested}
typeof value remains "{nested: string} | null",
therefore I need some conditional chaining
</div>
</Show>
</div>
Any idea on how to make type inference work?
You can't do it in this way, because the code inside <Show></Show> is actually executed before Show gets a chance to prevent the div from rendering. Think what happens when JSX is transpiled to javascript:
React.createElement(
Show,
{when: !!value},
React.createElement(
'div',
{},
`${value?.nested} typeof value remains "{nested: string} | null", therefore I need some conditional chaining`
)
)
See the problem? If we rewrite it a bit:
const textContent = `${value?.nested} typeof value remains "{nested: string} | null", therefore I need some conditional chaining`
React.createElement(
Show,
{when: !!value},
React.createElement(
'div',
{},
textContent
)
)
These snippets are exactly the same, and in both of them value?.nested is evaluated before Show even renders. I can't recommend you any good solution here, maybe something like this:
interface Props<T> {
value: T
fallback?: () => JSX.Element
children: (value: T) => ReactNode
}
export const CheckExists = <T extends any>({
value,
fallback,
children
}: Props<T>) => {
if (!value)
return fallback ? fallback() : null
return <>{children(value)}</>
}
<CheckExists value={value}>
{existingValue => (
<div>{existingValue.nested}</div>
)}
</CheckExists>
But this is different interface and may not be suitable for you
I have a functional component (written in Typescript) that needs to pass a handler function down to a child component. Here is a scaled down version of the parent function:
type Props = { handleLocationChange(): void };
const Sidebar: React.FC<Props> = (props) => {
const handleLocationChange = () => {
console.log('Location Changed.');
};
return (
<>
<Search handleLocationChange={handleLocationChange} />
</>
)
}
In VS Code the search component shows an error:
Type '{ handleLocationChange: () => void; }' is not assignable to type 'IntrinsicAttributes & { children?: ReactNode; }'.
Property 'handleLocationChange' does not exist on type 'IntrinsicAttributes & { children?: ReactNode; }'.ts(2322)
Any help would be much appreciated. I am sure I am missing something small.
You need to declare the prop type in Search Component and declare type to parameter too:
//use this type to both components (children and parent)
interface FuncProps {
//here you can declare the return type (here is void)
handleLocationChange: (values: any) => void;
}
//children start
// here are the tip, define the type in the React.FC and in the FC's parameters itself
const Search: React.FC<FuncProps> = (props: FuncProps) => {
... your component behavior and view ...
return (
{/*↓↓↓↓ use the prop like this ↓↓↓↓*/}
<input onClick={props.handleLocationChange('something if you need')}/>
)
};
//children end
// parent start
const Sidebar: React.FC<Props> = (props: FuncProps) => {
return (
<>
<Search handleLocationChange={props.handleLocationChange} />
</>
)
}
//parent end
I hope this answer can help who wants to use typescript and want to easy your own life passing functions through components (I don't recomend pass functions through many levels).
You need to declare handleLocationChange as a prop on the Search component
Code your handler like a function, this way
function handleLocationChange(){
console.log('Location Changed.');
};
Then it should work