I am building a custom Dropdown component which uses React-Select under the hood. I'd like to add support for refs. I have declared a ref in the app using useRef and am passing this into the component like this:
import Dropdown from "./Dropdown";
import { useRef } from "react";
const App = () => {
const dropdownRef = useRef<HTMLSelectElement>(null);
return (
<div>
<Dropdown id="hello" ref={dropdownRef} />
</div>
);
};
export default App;
In the Dropdown component I'm using ForwardRef like this:
import { forwardRef } from "react";
import Select from "react-select";
export interface DropdownProps {
id: string;
}
const Dropdown = forwardRef(({ id }: DropdownProps, ref) => (
<Select id={id} ref={ref} />
));
export default Dropdown;
However I am getting a Typescript error:
Type 'MutableRefObject' is not assignable to type 'Ref<Select<unknown, false, GroupBase>> | undefined'.
I have tried swapping out the React Select for another component and I get a similar issue so I think this is a general React/Typescript issue rather than anything specific to React Select.
Code Sandbox
Any help would be much appreciated.
I'm slightly surprised, because normally when you specify the hook's initial value as null the type inference is clever enough.
I don't know react-select very well but have you tried passing a string as an initial value, and deleting the <HTMLSelectElement> from your app component? Let the package's TS definitions do the work...
You should defind forwardRef type, it's a generic function:
const Dropdown = forwardRef<HTMLSelectElement,DropdownProps>(( { id }: DropdownProps, ref) => (
<select id={id} ref={ref}>
<option>1</option>
<option>2</option>
</select>
));
You should specify the props to your forwardRef so it knows what types you are trying to forward, as such:
import { forwardRef } from "react";
export interface DropdownProps {
id: string;
}
const Dropdown = forwardRef<HTMLSelectElement, DropdownProps>(({ id }, ref) => (
<div>
<select id={id} ref={ref}>
<option>1</option>
<option>2</option>
</select>
</div>
));
export default Dropdown;
The first template argument will be helpful to determine the type of ref you are trying to forward, the second will define the arguments your component will receive.
A good cheatsheet here for reference.
Related
I basically have a button element pretty far down the component hierarchy so I'm passing a function from my App level downwards so that it can be called onClick within the button. I feel like I've correctly defined both the App function (newTodoCard()) as :()=>void and the prop of child component (onAddTodo()) as :()=>void. I would prefer to avoid defining an entire Interface for one function prop and want to understand why my approach isn't working.
App.tsx
import React from 'react';
import Header from './components/Header';
import Sidebar from './components/Sidebar';
import TodoCard from './components/TodoCard';
import { TodoCardProps } from './components/TodoCard';
import { useState } from 'react';
function App() {
const[todoCards,setTodoCards] = useState([]);
let currentTodoCard: TodoCardProps = {title:"",content:""};
const newTodoCard: ()=>void = ()=>{
currentTodoCard = {title:"NEW",content:"NEW"};
}
return (
<div className="App">
<Header/>
<div className="container">
<Sidebar {...newTodoCard}/>
<TodoCard {...currentTodoCard}/>
</div>
</div>
);
}
export default App;
This snippet from above is where the error is:
<Sidebar {...newTodoCard}/>
Sidebar.tsx
import React from 'react'
import TitleCards from './TitleCards'
import SidebarHeader from './SidebarHeader'
const Sidebar = ( onAddTodo: ()=>void) => {
return (
<div className="sidebar">
<SidebarHeader {...onAddTodo}/>
<TitleCards/>
</div>
)
}
export default Sidebar
Additionally, if I change how I pass in the prop to
<Sidebar onAddTodo={newTodoCard}/>
It seems to solve the issue, but this error Type '{ onAddTodo: () => void; }' is not assignable to type 'IntrinsicAttributes & (() => void)'.
Property 'onAddTodo' does not exist on type 'IntrinsicAttributes & (() => void)' appears (which from online research is only fixed by using {...prop} as I did originally. I appreciate any help! Thanks.
The specific error you are seeing is because you are trying to use a props spread operator ... on something that isn't an object. newTodoCard is a function. If you want to pass it to <SidebarHeader /> as a prop, you can just do <SidebarHeader onAddTodo={newTodoCard} />.
You would declare a prop in your SidebarHeader component to match the callback that is being passed.
The key thing to note here is that the props that are passed to your component is an object with a property called onAddTodo.
interface MyProps {
onAddTodo: () => void;
}
export function SidebarHeader = (props: MyProps) => {
return <div>
<Button onClick={event => props.onAddTodo()} />
</div>
}
However, your callback isn't going to work as expected the way you have written it. When it is called, it is going to set the value of currentTodoCard, but that is a local variable that has a limited scope. The next time your app re-renders, the App function is going to be called again, and you will get a new instance of currentTodoCard. In order for your callback to work as expected, it is going to need to call setTodoCards or set some other state.
I have a problem regarding MUI's MenuItem when combined with Select and rendering it in a separate component.
Here's the codesandbox
Basically, I have something like this:
import { Select } from "#material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
return (
<Select
id="user"
name="User"
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
alert(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} userId={userId} />
))}
</Select>
);
}
And this is the custom item:
import { MenuItem, Typography } from "#material-ui/core";
import React from "react";
interface CustomMenuItemProps {
userId: number;
}
const CustomMenuItem = React.forwardRef<HTMLLIElement, CustomMenuItemProps>(
(props: CustomMenuItemProps, ref) => {
const { userId, ...rest } = props;
return (
<MenuItem value={userId} {...rest} ref={ref}>
<Typography>{userId}</Typography>
</MenuItem>
);
}
);
export default CustomMenuItem;
At first, I've done this without any refs, but this gave me an error in the console (Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?), so after googling a while, I found out that I have to pass this ref. I also pass the ...rest of the props, as I understand that the MenuItem needs them.
Expected behavior: when I click on the MenuItem, it gets selected in the Select component.
Actual behavior: nothing happens.
The thing is, I made the CustomMenuItem to make it reusable. But before that, I had a simple function like: renderItem which I used both in Select.renderValue and in userIds.map and it had the same code as CustomMenuItem - it returned the same JSX tree. And it worked then, but it doesn't work now, for some reason. So if I would do:
<Select
id="user"
name="User"
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
alert(event.target.value as number);
}}
>
{userIds.map((userId) => (
<MenuItem key={userId} value={userId}>
<Typography>{userId}</Typography>
</MenuItem>
))}
</Select>
it simply works :(
Am I missing something here?
There are a few implementation details of Select that get in the way of trying to customize MenuItem in this way.
Select uses the value prop of its immediate children. The immediate children of the Select in your case are CustomMenuItem elements which only have a userId prop -- not a value prop; so Select finds undefined as the new value when you click on one of your custom menu items.
You can fix this aspect by duplicating your userId prop as a value prop:
import { Select } from "#material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} value={userId} userId={userId} />
))}
</Select>
);
}
This then successfully changes the value of the Select if you look at the console logs. The new value is not successfully displayed due to a separate problem I'll explain later.
You may think "then I can just use the value prop instead of the userId prop rather than having both", but the value prop won't actually reach your custom component. Select uses React.cloneElement to change the value prop to undefined and instead puts it in data-value to avoid a value prop being specified in the final html (which wouldn't be a valid attribute for the html element that gets rendered).
In my sandbox above, you'll notice that when you select a value, the new value is not successfully displayed as the selected value. This is because Select uses the children prop of the selected child as the display value unless you specify the renderValue prop. The children prop of the CustomMenuItem element is undefined.
You can fix this by either using the renderValue prop on the Select or by specifying the userId yet again as a child:
import { Select } from "#material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} value={userId} userId={userId}>
{userId}
</CustomMenuItem>
))}
</Select>
);
}
This works, but also removes all of the value the custom menu item component was trying to provide. I think the simplest way to achieve this (while still working well with the Material-UI Select design) is to put the reusable code in a function for rendering the menu items rather than making a custom menu item component:
import { Select } from "#material-ui/core";
import React from "react";
import { MenuItem, Typography } from "#material-ui/core";
const renderMenuItem = (value: number) => {
return (
<MenuItem key={value} value={value}>
<Typography>{value}</Typography>
</MenuItem>
);
};
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map(renderMenuItem)}
</Select>
);
}
So I'm trying to nest a TypeScript React component within another, but it complains about types. It would seem it wants me to add all of my props into the parent interface?
Is there a way of doing this without having to have all my types listed in the child component interface, but then also having to add the types to the parent component interface?
Note: I am using Styled Components in the example below
interface IField {
children: React.ReactNode
}
export function Field({ children, htmlFor, label, required, ...props }: IField) {
return (
<FormField {...props}>
<Label htmlFor={htmlFor} label={label} required={required}/>
{children}
</FormField>
)
}
interface ILabel {
htmlFor: string
label: string
required?: boolean
}
export function Label({ htmlFor, label, required }: ILabel) {
return (
<FormLabel htmlFor={htmlFor}>
{label}
{required && (
<Required />
)}
</FormLabel>
)
}
Error I get:
Type '{}' is missing the following properties from type 'ILabel': htmlFor, label
Thanks for any help in advance!
To avoid adding the properties from the child interface back to the parent component interface you can use extends (see TypeScript inheritance documentation).
After that, you can pass the props from the parent to the child component by using {...props} deconstructor on the child component.
And finally, if you are using TypeScript might as well use React.FunctionComponent typing to avoid having to manually type children.
You can check at this simplified working example:
https://stackblitz.com/edit/react-stackoverflow-60228406
I tried to adapt your snippet below...
import React from 'react';
interface IField extends ILabel {
}
export const Field: React.FunctionComponent<IField> = (props) => {
return (
<FormField>
<Label {...props} />
{props.children}
</FormField>
)
};
interface ILabel {
htmlFor: string
label: string
required?: boolean
}
export const Label: React.FunctionComponent<ILabel> = (props) => {
return (
<FormLabel htmlFor={props.htmlFor}>
{props.label}
{props.required && (<Required />)}
</FormLabel>
)
};
Is it possible to infer correct types for props from an unknown component passed also as a prop?
In case of known component (that exists in current file) I can get props:
type ButtonProps = React.ComponentProps<typeof Button>;
But if I want to create a generic component Box that accepts a component in as prop and the component's props in props prop. The component can add some default props, have some behavior, it doesn't matter. Basically its similar to higher-order components, but its dynamic.
import React from "react";
export interface BoxProps<TComponent> {
as?: TComponent;
props?: SomehowInfer<TComponent>; // is it possible?
}
export function Box({ as: Component, props }: BoxProps) {
// Note: it doesn't have to be typed within the Box (I can pass anything, I can control it)
return <Component className="box" title="This is Box!" {...props} />;
}
function MyButton(props: {onClick: () => void}) {
return <button className="my-button" {...props} />;
}
// usage:
function Example() {
// I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type.
return (
<div>
<Box
as={MyButton}
props={{
onClick: () => {
console.log("clicked");
}
}}
>
Click me.
</Box>
</div>
);
}
requirements:
must work without passing the generic type (is it possible?), because it would be used almost everywhere
must work with user-defined components (React.ComponentType<Props>)
would be great if it worked also with react html elements (a, button, link, ...they have different props), but not necessary
You can use the predefined react type ComponentProps to extract the prop types from a component type.
import React from "react";
export type BoxProps<TComponent extends React.ComponentType<any>> = {
as: TComponent;
props: React.ComponentProps<TComponent>;
}
export function Box<TComponent extends React.ComponentType<any>>({ as: Component, props }: BoxProps<TComponent>) {
return <div className="box" title="This is Box!">
<Component {...props} />;
</div>
}
function MyButton(props: {onClick: () => void}) {
return <button className="my-button" {...props} />;
}
// usage:
function Example() {
// I want here the props to be typed based on what I pass to as. Without using typeof or explicitly passing the generic type.
return (
<div>
<Box
as={MyButton}
props={{ onClick: () => { } }}
></Box>
</div>
);
}
Playground Link
Depending on you exact use case the solution might vary, but the basic idea is similar. You could for example turn the type around a little bit and take in the props as the type parameter for the BoxProps. That way you can constrain the component props to have some specific properties you can supply inside the Box component:
export type BoxProps<TProps extends {title: string}> = {
as: React.ComponentType<TProps>;
} & {
props: Omit<TProps, 'title'>;
}
export function Box<TProps extends {title: string}>({ as: Component, props }: BoxProps<TProps>) {
return <div className="box" title="This is Box!">
<Component title="Title from box" {...props as TProps} />;
</div>
}
Playground Link
If you want to take in intrinsic tags, you can also add keyof JSX.IntrinsicElements to the TComponent constraint:
export type BoxProps<TComponent extends React.ComponentType<any> | keyof JSX.IntrinsicElements> = {
as: TComponent;
props: React.ComponentProps<TComponent>;
}
export function Box<TComponent extends React.ComponentType<any>| keyof JSX.IntrinsicElements>({ as: Component, props }: BoxProps<TComponent>) {
return <div className="box" title="This is Box!">
<Component {...props} />;
</div>
}
Playground Link
I'm creating some button components and I have some custom props that I need and want them checked by flow. But as they are buttons I would also like any other props from the HTML button elements but don't want to type check them all.
Is there any way in react or maybe with an npm package to let me type check my new custom props and let the component receive any other ones? Or maybe just restricted to the HTML defined ones?
You should just be able to pass the rest of the props down without putting type annotations for it.
Example:
import React, { type Node } from 'react'
type Props = {
primary?: boolean,
children: Node
}
function Button({ primary, children, ...props }: Props) {
return (
<button
className={primary ? 'is-primary' : ''}
{...props}
>
{children}
</button>
)
}
export default Button
Usage:
function App() {
return (
<div>
<Button primary onClick={() => console.log('clicked!')}>
Click Me
</Button>
</div>
)
}
You can also check it out on flow.org/try.