TS doesn't see default props in React function component - reactjs

My code
interface ButtonProps {
onClick?: () => void
}
const Button: FC<ButtonProps> = ({ onClick }) => {
const wrapClick = () => {
onClick() // TS2722: Cannot invoke an object which is possibly 'undefined'.
}
return <button onClick={wrapClick}>button</button>
}
Button.defaultProps = {
onClick: () => {},
}
In this case, everything works
const Button: FC<ButtonProps> = ({ onClick }) => {
const wrapClick = () => {
onClick && onClick()
}
return <button onClick={wrapClick}>button</button>
}
and in this
const Button: FC<ButtonProps> = ({ onClick = () => {} }) => {
const wrapClick = () => {
onClick()
}
return <button onClick={wrapClick}>button</button>
}
Is there a way to use default props to solve this problem ? Since I have large components with a lot of properties.

The onClick property in the props object is marked as optional by using a question mark, so we can't directly invoke the function.
To solve the error, use the optional chaining (?.) operator when calling the function.
import { FC } from "react";
interface ButtonProps {
onClick?: () => void;
}
const Button: FC<ButtonProps> = ({ onClick }) => {
const wrapClick = () => {
onClick?.();
};
return <button onClick={wrapClick}>button</button>;
};
Button.defaultProps = {
onClick: () => {}
};
export default Button;
Code Sandbox : DEMO

Use the Non-null assertion operator (postfix !) to let TypeScript know your default onClick(): void is not null/undefined (which it thinks it is because of IButtonProps.onClick: () => void | undefined, which is used by Button (FC<IButtonProps>, FC<T>.defaultProps is of type T & { children?: Node } if I remember correctly)).
interface IButtonProps {
onClick?: () => void; // No event args?
}
const Button: FC<IButtonProps> = (props) => {
const {
onClick = Button.defaultProps.onClick!,
} = props;
const wrapClick = (/* No event args? */) => {
onClick();
};
return <button onClick={wrapClick}>button</button>;
};
Button.defaultProps = {
onClick: () => {},
};

Related

Higher Order Component to observe Visibility: React?

I have created a higher order component as shown below:
import React from 'react';
interface IVisibility {
Component: JSX.Element;
visibilityThreshold?: number;
onVisibleCallback?: () => void;
}
const VisibilityHandler = ({
Component,
visibilityThreshold,
onVisibleCallback
}: IVisibility) => {
const ref = React.useRef(null);
React.useEffect(() => {
const componentObserver = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
onVisibleCallback ? onVisibleCallback() : null;
}
},
{
rootMargin: '0px',
threshold: visibilityThreshold ?? 0
}
);
const current = ref.current;
if (current) componentObserver.observe(current);
return () => {
componentObserver.disconnect();
};
}, [visibilityThreshold, onVisibleCallback]);
return <section ref={ref}>{Component}</section>;
};
export default VisibilityHandler;
And use it like this:
<VisibilityHandler Component={<div>Hello World</div>} />
However this wraps every component into a section which I don't want. I tried using React.Fragment but that doesn't let you pass ref to track the component. Is there a better way to re-create this HOC in order to incorporate visibility tracking without wrapping it in additional div or section?
You can use
function as a children
React.cloneElement
Function as a children
<VisibilityHandler Component={({ ref }) => <div ref={ref}>Hello world</div>} />
You have to change you HOC code
- return <section ref={ref}>{Component}</section>;
+ return Component({ ref });
React.cloneElement
Documentation
your case
- return <section ref={ref}>{Component}</section>;
+ return React.cloneElement(Component, { ref });
But I highly recommend use hook (packages) instead of HOC.
react-use: useIntersection
react-intersection-observer
I found a really neat way to do so like this:
import React from 'react';
interface IVisibility {
Component: JSX.Element;
visibilityThreshold?: number;
onVisibleCallback?: () => void;
}
const VisibilityHandler = ({
Component,
visibilityThreshold,
onVisibleCallback
}: IVisibility): JSX.Element => {
const ref = React.useRef(null);
React.useEffect(() => {
const componentObserver = new IntersectionObserver(
(entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
onVisibleCallback ? onVisibleCallback() : null;
}
},
{
rootMargin: '0px',
threshold: visibilityThreshold ?? 0
}
);
const current = ref.current;
if (current) componentObserver.observe(current);
return () => {
componentObserver.disconnect();
};
}, [visibilityThreshold, onVisibleCallback]);
return <Component.type {...Component.props} ref={ref} />;
};
export default VisibilityHandler;

Type errors when extending component more than one level using forwardRef and useImperativeHandle

I'm experimenting with extending components in React. I'm trying to extend Handsontable using forwardRef and useImperativeHandle. First I wrap Handsontable in my own BaseTable component, adding some methods. Then I extend the BaseTable in a CustomersTable component in the same way to add even more methods and behavior. Everything seems to work well until I try to consume the CustomersTable in CustomersTableConsumer where I get some type errors. The component works just fine, it's just Typescript that isn't happy.
BaseTable:
export type BaseTableProps = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export const BaseTable = forwardRef<BaseTableProps, HotTableProps>(
(props, ref) => {
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableProps)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
/>
</div>
);
}
);
CustomersTable:
export type CustomerTableProps = HotTable & {
customerTableFunc: () => void;
};
export const CustomersTable = forwardRef<CustomerTableProps, BaseTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableProps>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableProps)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableProps>(null);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
customersTableRef?.current?.findReplace("x", "y"); };
useEffect(() => {
init();
}, []);
// can't access extended props from handsontable on CustomersTable
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
Here is a Codesandbox example.
How do I need to update my typings to satisfy Typescript in this scenario?
You need to specify the type of the ref for forwardRef. This type is used then later in useRef<>().
It's confusing, because HotTable is used in useRef<HotTable>(), but BaseTable can't be used the same way, as it is a functional component and because forwardRef was used in BaseTable. So, basically, for forwardRef we define a new type and then later use that in useRef<>(). Note the distinction between BaseTableRef and BaseTableProps.
Simplified example
export type MyTableRef = {
findReplace: (v: string, rv: string) => void;
};
export type MyTableProps = { width: number; height: number };
export const MyTable = forwardRef<MyTableRef, MyTableProps>(...);
// then use it in useRef
const myTableRef = useRef<MyTableRef>(null);
<MyTable width={10} height={20} ref={myTableRef} />
Final solution
https://codesandbox.io/s/hopeful-shape-h5lvw7?file=/src/BaseTable.tsx
BaseTable:
import HotTable, { HotTableProps } from "#handsontable/react";
import { registerAllModules } from "handsontable/registry";
import { forwardRef, useImperativeHandle, useRef } from "react";
import Handsontable from "handsontable";
export type BaseTableRef = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export type BaseTableProps = HotTableProps;
export const BaseTable = forwardRef<BaseTableRef, BaseTableProps>(
(props, ref) => {
registerAllModules();
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableRef)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
licenseKey="non-commercial-and-evaluation"
/>
</div>
);
}
);
CustomersTable:
import Handsontable from "handsontable";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from "react";
import { BaseTable, BaseTableRef, BaseTableProps } from "./BaseTable";
export type CustomerTableRef = {
customerTableFunc: () => void;
} & BaseTableRef;
export type CustomerTableProps = BaseTableProps;
export const CustomersTable = forwardRef<CustomerTableRef, CustomerTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableRef>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableRef)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
import { useEffect, useRef, useState } from "react";
import { CustomersTable, CustomerTableRef } from "./CustomerTable";
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableRef>(null);
// Check console and seee that customerTableFunc from customersTable,
// findReplace from BaseTable and __hotInstance from Handsontable is available
console.log(customersTableRef?.current);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
};
useEffect(() => {
init();
}, []);
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
In your sandbox example it's almost correct, just fix the props type for CustomersTable. I would recommend though to not use Props suffix for ref types, as it is very confusing.
https://codesandbox.io/s/unruffled-framework-1xmltj?file=/src/CustomerTable.tsx
export const CustomersTable = forwardRef<CustomerTableProps, HotTableProps>(...)

What is the type of function received in props?

interface IProps {
handleCloseModal: React.MouseEventHandler<HTMLButtonElement>
returnFunction: () => void;
}
export default function Modal({
children,
returnFunction,
handleCloseModal,
}: React.PropsWithChildren<IProps>) {
const ref = React.useRef<HTMLDivElement>(null);
const handleToggle = (e: MouseEvent) => {
const target = e.target as HTMLDivElement;
if (!ref.current?.contains(target)) {
handleCloseModal();
}
};
const handleCloseAndExcuteFn = () => {
handleCloseModal();
returnFunction();
};
return (
<section>
<Button close onClick={handleCloseModal}>
no
</Button>
<Button onClick={handleCloseAndExcuteFn}>yes</Button>
</section>
)
};
If you look at "handleCloseModal" in the code above,
Since it is used in 'onclick', I gave the type as 'React.MouseEventHandler'
But 'handleCloseAndExcuteFn'
Because it is also used here, a type error occurs.
What is the right type?
In addition
I'm curious about the 'returnFunction' type.
This function is
const returnFunction = () => { logout().then(() => dispatch(setUserId(''))); };
Same as above code, change 'redux state' after axios request.
Move the event type to the actual function you are passing to onClick which is
handleCloseAndExcuteFn
interface IProps {
handleCloseModal: React.MouseEventHandler<HTMLButtonElement>
returnFunction: () => void;
}
const handleCloseAndExcuteFn: React.MouseEventHandler<HTMLButtonElement> = () => {
handleCloseModal();
returnFunction();
};

React - child.ref.current is null in componentdidmount

Here's many similar questions but I still couldn't solve this problem.
Child's ref is null in Listner.
I really don't understand what this is.
The code is below.
react 17.0.1
// Parent.tsx
const Parent: React.FC<{id: string}> = (props) => {
const [id] = useState(props.id)
const modalRef = createRef<ModalRef>();
// If I registerd the Listner here, modalRef is not null but,
// multiple Listner has registered.
useEffect(() => {
listner.on('MODAL_POPUP', (o:{param:string}) => {
modalRef.current?.pop(o.param); // <--- modalRef.current is null
});
return() => {};
}, []);
return (
<Modal ref={modalRef} id={id}>
<div>contents</div>
</Modal>
);
};
// Modal.tsx
export interface ModalProps {
id: string;
}
export interface ModalRef {
pop: () => void;
}
const Modal = React.forwardRef<ModalRef, ModalProps>((props, ref) => {
const [id] = useState(props.id);
useImperativeHandle(ref, () => ({
pop() {
console.log('popup modal');
},
}));
return createPotal(
<div>contents..</div>,
document.getElementById('modal-root') as HTMLElement,
);
});
Any advice for me?
Thanks.
You need to use useRef for creating the ref in React Function Components, so change it to this:
const Parent: React.FC<{id: string}> = (props) => {
const [id] = useState(props.id)
const modalRef = useRef<ModalRef>(); // <== here
useEffect(() => {
listner.on('MODAL_POPUP', (o:{param:string}) => {
modalRef.current?.pop(o.param);
});
return() => {
listner.off('MODAL_POPUP', ()=>{});
};
}, []);
return (
<Modal ref={modalRef} id={id}>
<div>contents</div>
</Modal>
);
};

Strange React hooks behavior, can't access new state from a function

I use the library react-use-modal, and
I'm trying to read the updated value of confirmLoading when inside the handleClick function.
handleClick does read the first value of confirmLoading defined when doing const [ confirmLoading, setConfirmLoading ] = useState(false), but never updates when I setConfirmLoading inside handleOk.
I don't understand what I'm doing wrong
import { Button, Modal as ModalAntd } from 'antd'
import { useModal } from 'react-use-modal'
export interface ModalFormProps {
form: React.ReactElement
}
export const ModalForm: React.FC = () => {
const [ confirmLoading, setConfirmLoading ] = useState(false)
const { showModal, closeModal } = useModal()
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
}
const handleCancel = () => {
closeModal()
}
const handleClick = () => {
showModal(({ show }) => (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
))
}
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}
This is happening because of closures. The component that you pass to showModal remembers confirmLoading and when you call function setConfirmLoading your component renders again and function handleClick is recreated. 'Old' handleClick and 'old' component in showModal know nothing about the new value in confirmLoading.
Try to do this:
export const ModalForm: React.FC = () => {
const { showModal, closeModal } = useModal();
const handleClick = () => {
showModal(({ show }) => {
const [ confirmLoading, setConfirmLoading ] = useState(false);
const handleOk = () => {
setConfirmLoading(true)
setTimeout(() => {
setConfirmLoading(false)
closeModal()
}, 1000)
};
const handleCancel = () => {
closeModal()
};
return (
<ModalAntd
onCancel={handleCancel}
onOk={handleOk}
title='Title'
visible={show}
>
// the value of confirmLoading is always the one defined
// with useState at the beginning of the file.
<p>{confirmLoading ? 'yes' : 'no'}</p>
</ModalAntd>
);
})
};
return (
<div>
<Button onClick={handleClick}>
Open Modal
</Button>
</div>
)
}

Resources