React control flow component with Typescript - javascript

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

Related

Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes' - React 18.2.0

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

Create react component to conditionally wrapping children

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>)
}

How to type any param with React Typescript in Functional Component

I have an existing javascript component:
const RenderState = ({ state, else: elseChild = undefined, ...states }) => {
if (state && states[state]) return states[state]
if (elseChild) return elseChild
return null
}
export default RenderState
I am trying to convert it to typscript and have so far:
import React from 'react'
interface Props {
state: string
else?: JSX.Element
any: JSX.Element
}
const RenderState: React.FC<Props> = ({ state, else: elseChild = undefined, ...states }) => {
if (state && states[state]) return states[state]
if (elseChild) return elseChild
return null
}
export default RenderState
The problem I am having is of course that any: JSX.Element is looking for a prop literally called 'any' when instead I want to allow any prop as a JSX element, such as custom in the following example
Here is an example use case of this component:
import React from 'react'
import RenderState from './RenderState'
const MyComp = () => {
const [state, setState] = React.useState<string | null>(null)
<>
<Button onClick={()=>{setState(null)}}>Default</Button>
<Button onClick={()=>{setState('custom')}}>Custom</Button>
<RenderState
state={state}
custom={(<p>Some custom content here...</p>)}
else={(<p>Some default content here...</p>)}
/>
</>
}
Sonds like you need index signature on your type.
A naive approach would be
interface Props {
state: string
else?: JSX.Element
[key: string]: JSX.Element
}
However, this won't compile - because index signature on an interface requires that all named properties (state and else) are compatible with it.
Instead you can rewrite it in a form of type intersection:
type Props = {
state: string
else?: JSX.Element
} & Record<string, JSX.Element>
This still won't compile for the same, but gives us a clue. What if instead of string for keys we have a well-known set of possible values?
Next iteration would be
type Key = "custom" | "another";
type Props = {
state: Key | null
else?: JSX.Element
} & Record<Key, JSX.Element>
This looks better. But how do we make it reusable, so that the same component could be used with different lists of possible Keys? Let's make it generic!
So the final code:
type Props<Key extends string> = {
state: Key | null
else?: JSX.Element
} & Record<Key, JSX.Element>
You need to make the component a generic function as well:
function RenderState<Key>(props: Props<Key>) { ... }
And use it in your app:
const [state, setState] = React.useState<"custom" | null>(null);
<RenderState
state={state}
custom={(<p>Some custom content here...</p>)}
else={(<p>Some default content here...</p>)}
/>
Edit 1
This approach allows to write props[props.state] inside RenderState (the result is inferred to JSX.Element).
However, it fails to compile with rest destructuring, as in the original code snippet:
const {state, else: elseEl, ...rest} = props;
rest[props] // TS error here
It reveals a flaw in our type definition. We need to make sure state value doesn't include "state" and "else" literals, to avoid type conflict.
Updated type would be
type Props<Key extends string> = {
state: Exclude<Key, "state" | "else"> | null
else?: JSX.Element
} & Record<Key, JSX.Element>
Now this should compile well
const {state, else: elseEl, ...rest} = props;
rest[props]

Forwarding refs by multiple levels [Typescript / React]

I'm trying to follow atomic design principles, but I'm not sure if I'm doing it correctly.
I have created three components: Form, ExpandableInput and Input. Input is HTML input wrapper, Expandable input is Input with dropdown and Form is just form. I want to access current input value from Form component so I'm doing ref forwarding by multiple levels. Everything works fine in Form component, but the problem is that I have to use ref.current.value in ExpandableInput component, but I'm getting following error.
Property 'current' does not exist on type '((instance:
HTMLInputElement | null) => void) | MutableRefObject<HTMLInputElement
| null>'. Property 'current' does not exist on type '(instance:
HTMLInputElement | null) => void'.
I don't know how to tell typescript that ref variable in ExpandableInput has type of Ref and is not null. I don't know if ref forwarding by multiple levels is good practice, maybe there is a better way to get input value.
Example code:
AddForm
const Form = () => {
// Refs
const caRef = useRef<HTMLInputElement>(null);
return (
<FormTemplate >
<ExpandableInput
option={CompetenceAreaOption}
ref={caRef}
/>
</FormTemplate>
);
}
export default AddForm;
ExpandableInput
const ExpandableInput = React.forwardRef<HTMLInputElement, IProps>((
props,
ref
): JSX.Element => {
const pickInputValue = (value: string): void => {
console.log(ref.current) // Error
console.log(ref) // Input reference
}
}
return (
<div>
<div>
<Input
ref={ref}
/>
</div>
</div>
);
})
export default ExpandableInput;
Input
const Input = React.forwardRef<HTMLInputElement, IProps>(({
props,
ref
): JSX.Element => {
return (
<input
ref={ref}
/>
);
})
export default Input;
There are actually three possible values for the ref received by forwardRef.
type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;
It can be null. It can be what you are expecting, which is a MutableRefObject created by useRef. And the third possibility is that it can be a callback function. (Note: legacy string refs are not supported by forwardRef).
Any of these three are fine to pass down to the next level. But if we want to access ref.current then we need to make sure that what we have is a MutableRefObject. We do this with a type guard: if ( ref && "current" in ref). That makes it ok to access ref.current.
ref.current might be null (when the ref hasn't been attached to the DOM yet). So we also need to check that current is valid either with an if or with the optional chaining operator ?.
Short version:
if ( ref && "current" in ref) {
ref.current?.focus();
}
Long version:
// make sure that ref is a MutableRefObject
if (ref && "current" in ref) {
// input has type HTMLInputElement | null
const input = ref.current;
// exlude null current value
if (input) {
// now we know that we have an input
input.focus();
}
}
Typescript Playground Link

Passing a function as a prop to a Typescript React Functional Component

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

Categories