How to write a Typescript React Component Wrapper - reactjs

A custom ReactJS wrapper component should be used to wrap code and only render it, if the user has the required permission. Therefore, it needs to accept two parameters, the children that shall be rendered as well as the permission it needs to check for.
The idea is to use it like this:
const AdminTools = () => {
return (
<RequirePermission require_permission="ui:see_admin_menu">
<>
<h1>Sudo mode</h1>
<Link href="/intern/users/">
<a>
<ActionCard link>User management</ActionCard>
</a>
</Link>
</>
</RequirePermission>
);
};
What I came up with so far is the following code:
const RequirePermission = (children: React.FC<{}>, required_permission: string) => {
const user = useContext(AuthContext);
let hasPermission = false;
if (user.state == AuthState.Authorized) {
user.user?.realm_access?.roles.forEach(role => {
if (permissions[role].includes(required_permission)) {
hasPermission = true;
}
});
}
if (hasPermission) {
return children;
} else {
return <div />;
}
};
export default RequirePermission;
When using the code snippet as described above, the following error is thrown:
Type '{ children: Element; require_permission: string; }' is not assignable to type 'IntrinsicAttributes & FC<{}>'.
Property 'children' does not exist on type 'IntrinsicAttributes & FC<{}>'.ts(2322)
'RequirePermission' cannot be used as a JSX component.
Its return type 'FC<{}> | Element' is not a valid JSX element.
Type 'FunctionComponent<{}>' is missing the following properties from type 'Element': type, props, keyts(2786)
I don't really understand the error message to be frank. Any help would be much appreciated.
//Edit:
Error messages given by proposed code:
This JSX tag's 'children' prop expects a single child of type 'ReactChildren', but multiple children were provided.ts(2746)
and
'RequirePermission' cannot be used as a JSX component.
Its return type 'Element | ReactChildren' is not a valid JSX element.
Type 'ReactChildren' is missing the following properties from type 'Element': type, props, key

Try this, importing ReactChildren from react.
I wish i could explain this better, but i know typescript is expecting a JSX/TSX element, so we could use ReactElement for the return type.
For a better explanation on the children type
https://stackoverflow.com/a/58123882/7174241
import React, { ReactChildren, ReactElement, ReactNode } from "react";
interface RequireType {
children: ReactChildren | ReactNode | ReactElement;
required_permission: string;
}
const RequirePermission = ({ children, required_permission }: RequireType):ReactElement => {
const user = useContext(AuthContext);
let hasPermission = false;
if (user.state == AuthState.Authorized) {
user.user?.realm_access?.roles.forEach((role) => {
if (permissions[role].includes(required_permission)) {
hasPermission = true;
}
});
}
return <>{hasPermission ? children : null}</>
};
export default RequirePermission;

Related

What's the correct way to use useRef and forwardRef together?

I have two components: App and Child. I use useRef hook to get the DOM element in Child component and do some work with it.
I also pass ref from App component to Child component using React.forwardRef API.
App.tsx:
import { useRef } from "react";
import Child from "./Child";
export default function App() {
const ref = useRef(null);
console.log("App ref: ", ref);
return (
<div className="App">
<Child ref={ref} />
</div>
);
}
Child.ts:
import React, { useEffect, useRef } from "react";
export default React.forwardRef((props, ref) => {
const innerRef = useRef<HTMLDivElement>(null);
const divElement = ref || innerRef; // TSC throw error here.
useEffect(() => {
if (divElement.current) {
console.log("clientWidth: ", divElement.current.clientWidth);
// do something with divElement.current
}
}, []);
return <div ref={divElement}></div>;
});
I am not sure it's a correct way to use innerRef and forwarded ref together like above. At least, the TSC throw an error which means it does NOT allow use it like this.
Type '((instance: unknown) => void) | MutableRefObject' is not assignable to type 'string | ((instance: HTMLDivElement | null) => void) | RefObject | null | undefined'.
Type 'MutableRefObject' is not assignable to type 'string | ((instance: HTMLDivElement | null) => void) | RefObject | null | undefined'.
Type 'MutableRefObject' is not assignable to type 'RefObject'.
Types of property 'current' are incompatible.
Type 'unknown' is not assignable to type 'HTMLDivElement | null'.
Type 'unknown' is not assignable to type 'HTMLDivElement'.ts(2322)
index.d.ts(143, 9): The expected type comes from property 'ref' which is declared here on type 'DetailedHTMLProps<HTMLAttributes, HTMLDivElement>'
I want to get the correct ref in App component and use this ref in Child component as well. How can I do this?
codesandbox
You can largely ignore the TS error because you didn't provide types for the forwardRef arguments so it doesn't know that ref/innerRef are equivalently typed.
const divElement = ref || innerRef; <div ref={divElement}></div>
I don't understand this bit - do you want it to have a reference to the div element directly regardless of if the is given a ref or not?
Anyway, I think if you want to get it working like this, it's best you use the useImperativeHandle hook like useImperativeHandle(ref, () => innerRef.current);
So, the Child component itself manages the ref with the innerRef but if someone does pass a ref to the Child component, it will yield that.
export default React.forwardRef((props, ref) => {
const innerRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => innerRef.current);
useEffect(() => {
if (innerRef.current) {
console.log("clientWidth: ", innerRef.current.clientWidth);
}
}, []);
return <div ref={innerRef}></div>;
});

Getting error when using type "React.ReactNode" to children. Which type should I use?

Getting error message when using function LoaderIndicator(), which renders children, when loading = false, see code below. The error appeared when I used React.ReactNode instead of type any, but normally I use React.ReactNode as type for children. This is the error I get:
'LoaderIndicator' cannot be used as a JSX component. Its return type '{}' is not a valid JSX element. Type '{}' is missing the following properties from type 'Element': type, props, key. ts(2786)
I don't know how to fix this error, I have tried almost everything, is anyone able to help? :)
export type LoaderIndicatorProps = {
loading: boolean;
children?: React.ReactNode;
};
export function LoaderIndicator(props: LoaderIndicatorProps) {
const { loading, children } = props;
if (loading) {
return (
<div>
<Container>
<Icon />
<h3 style={{ color: "#ffffff" }}>Wait for a moment...</h3>
</Container>
</div>
);
} else {
return children;
}
}
React components should return JSX.Element. As you can see, your children are of type React.ReactNode, so straight returning them will not work. You need to wrap them inside an element or React.Fragment:
type TestComponentProps = {
children?: React.ReactNode;
}
function TestComponent(props: TestComponentProps) {
return <React.Fragment>{props.children}</React.Fragment>;
}

React typescript and children

I try to dynamically check in typescript the types of react children components.
The following code is working pretty well, however seems typescript does not want me to destructure children.
I get the Typescript error :
TS2339: Property 'type' does not exist on type 'ReactNode'.
What can i do t get rid of the typescript error instead of using // #ts-ignore.
import * as React from 'react';
export interface AuxProps {
children: React.ReactNode[]
}
export const Message: React.FC<AuxProps> = ({
children,
}: AuxProps) => {
const test = children.filter(({ type }) => type === Test);
return (
<div>
{test}
<div/>
);
};
You can't read type from ReactChild, because ReactChild is a union and not every member of that union has a property called type.
In fact, only ReactElements do.
The solution is to check for two things inside your predicate function:
Is this child a ReactElement?
If yes, then is that element of the desired type?
Code:
const test = children.filter(child => React.isValidElement(child) && child.type === 'Test');
This is because the default ReactNode does not have the field type.
You can simply add that key by using the & functionality:
export interface AuxProps {
children: (React.ReactNode & {type: string})[]
}
This will add the type to the elements.

TypeScript error when passing untyped string to React component with typed props?

I have an Icon component:
type Props = {
icon:
| "one"
| "two";
}
const Icon: React.FC<Props> = ({ icon }) => {
// generate icon
}
If I use it in another component with a hard coded value everything is fine:
const MyComponent = () => {
return(
<div>
<Icon icon="one" />
</div>
)
}
However when it's set dynamically from props I get a TypeScript error, even when the icon is displayed correctly:
type Props = {
icon?: string;
};
const MyComponent: React.FC<Props> = ({ icon }) => {
return(
<div>
{
icon && <Icon icon={icon} />
}
</div>
)
}
TS2322: Type 'string' is not assignable to type '"one" | "two".
index.tsx(8, 3): The expected type comes from property 'icon' which is declared here on type 'IntrinsicAttributes & Props & { children?: ReactNode; }'
Is this because TypeScript doesn't know that the string value passed will be either "one" or "two"?
If so then the only solution I can think of would be to export "one" and "two" as an enum. However this would make the component annoying to use as you'd always have to import this enum, rather than just pass the string that you want. Is there a more elegant solution?
Props in component have union values only that is one or two but where you are using the component its data type is defined by optional string variable so it's a mismatch
type Props = {
icon:
| "one"
| "two";
}
type Props = {
icon?: string;
};
To fix it you need to make both of these consistent
type Props = {
icon?: "one"|"two";
};

Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>'

I'm learning Typescript-react and I'm stuck in this error Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>' and I am lost on this.
Complete error:
Type '({ items }: PropsWithChildren<TodoProps>) => Element[]' is not assignable to type 'FunctionComponent<TodoProps>'.
Type 'Element[]' is missing the following properties from type 'ReactElement<any, string | ((props: any) => ReactElement<any, string | ... | (new (props: any) => Component<any, any, any>)> | null) | (new (props: any) => Component<any, any, any>)>': type, props, key
Link for code: sandbox repo.
Error happens on the declaration of TodoList function within the TodoList.tsx file.
Any help is appreciated. Cheers!
Code:
import React from "react";
interface Todo {
id: number;
content: string;
completed: boolean;
}
interface TodoProps {
items: Todo[];
}
// v------v here is error
const TodoList: React.FC<TodoProps> = ({ items }) => {
return items.map((item: Todo) => <div key={item.id}>{item.id}</div>);
};
export default TodoList;
Yeah, the error may sound a bit confusing - in essence it says that you can only return a single ReactElement or its equivalent JSX.Element in the function component definition, enforced by React.FC type.
React Fragments solve this limitation, so you can write TodoList in the following manner:
interface TodoProps {
items: Todo[];
}
const TodoList: React.FC<TodoProps> = ({ items }) => (
<React.Fragment>
{items.map((item: Todo) => (
<div key={item.id}>{item.id}</div>
))}
</React.Fragment>
);
Short form:
const TodoList: React.FC<TodoProps> = ({ items }) => (
<>
{items.map(({ id }) => <div key={id}>{id}</div>)}
</>
);
By the way: With pure JS, both class and function components can return multiple elements in an array as render output. Currently, TS has a type incompatibility for returned arrays in function components, so Fragments provide a viable workaround here (in addition to type assertions).
I've encountered a similar error. Eventually I noticed that I'd renamed the file incorrectly from .js to .ts instead of to .tsx when converting a component to a FunctionComponent with TypeScript.
I also got this error when I was trying to return children props from my Loading component like below.
const { loading, children } = props;
return loading ? <p>Loading ... </p> : children;
Then i realize that React is expecting only one return value(1 parent component) from its render method. Therefore I wrapped children props with React.Fragment which is denoted by <></> and that solves my problem. Below is my Loadingcomponent sample, hope that helps someone else.
import { FunctionComponent } from "react";
interface ILoadingProps {
loading: boolean;
}
export const Loading: FunctionComponent<ILoadingProps> = (props) => {
const { loading, children } = props;
return loading ? <p>Loading ... </p> : <>{children}</>;
};
My problem was that I had allowed a "TypeScript Quick Fix" in VSCode to add async to the main functional component interface.
const Welcome: React.FC<TProps> = async (props) => {
After removing it,
const Welcome: React.FC<TProps> = (props) => {
things were back to normal.

Resources