Annotating Custom React Hook With TypeScript - reactjs

I have a custom React Hook that watches for a click outside of a specific element. It works just fine, but I am having trouble making TypeScript happy in a few places.
App.js
import { useRef, useCallback } from "react";
import useClick from "./useClick";
export default function App() {
const asideRef = useRef(null);
const handleStuff = useCallback(() => {
console.log("a click outside of the sidebar occurred.");
}, []);
useClick(asideRef, handleStuff);
return (
<div className="App">
<aside ref={asideRef}>
<nav>
<ul></ul>
</nav>
</aside>
</div>
);
}
useClick.js
import React, { useEffect } from "react";
const useClick = (ref: React.MutableRefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: React.MouseEvent): void => {
if (ref.current && !ref.current.contains(e.target as Node)) {
cb();
}
};
document.addEventListener("click", checkClick);
return () => {
document.removeEventListener("click", checkClick);
};
}, [ref, cb]);
};
export default useClick;
The first problem area is in App.js, where the useClick hook is called. TypeScript complains about the first parameter passed to useClick and gives a message of:
Argument of type 'MutableRefObject<null>' is not assignable to parameter of type 'MutableRefObject<HTMLElement>'.
Type 'null' is not assignable to type 'HTMLElement'.ts(2345)
I know this has something to do with me setting the initial value of the ref to null, and the argument for the ref in useClick being annotated as React.MutableRefObject<HTMLElement>. I just don't know how to fix it.
The second problem area is in useClick.js, where the event listeners are added and removed. TypeScript seems to have a problem with my checkClick function. The error is so long that I have no choice but to show a photo of it below.
If anyone has any idea's on how to fix these two issues, so TypeScript will be happy, the help would be greatly appreciated.

Hi Dan: I'll start with what needs to change, then explain why below. (I also renamed your TypeScript file extensions to reflect their content.)
useClick.ts
Before:
// ...
const useClick = (ref: React.MutableRefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: React.MouseEvent): void => {
// ...
After:
// ...
const useClick = (ref: React.RefObject<HTMLElement>, cb: () => void) => {
useEffect(() => {
const checkClick = (e: MouseEvent): void => {
// ...
App.tsx
Before:
export default function App() {
const asideRef = useRef(null);
After:
export default function App() {
const asideRef = useRef<HTMLElement>(null);
First, let's address the checkClick function: This function signature should be assignable to the EventListener type in lib.dom.d.ts:
interface EventListener {
(evt: Event): void;
}
Because your parameter is React.MouseEvent, which does not extend the native Event, type, it is incompatible. Just use the native MouseEvent type instead, since you're adding the listener to the document (which has nothing to do with React anyway.)
Next, let's look at the function signature for useClick:
You might want to reference the type definitions for useRef while reading this part:
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5149827c1f97541dd69d950039b83ace68e119e6/types/react/index.d.ts#L1025-L1063
The first parameter is the only one that needs to change, and it needs to change from a MutableRefObject to a RefObject. The difference between these can be subtle, but there are some great resources you can read to learn more about them. Here's one, for example:
https://dev.to/wojciechmatuszewski/mutable-and-immutable-useref-semantics-with-react-typescript-30c9
When creating a ref that you pass to React to use for an element reference, you should provide null as the initial argument to useRef (which you did) and also provide a generic type annotation for the type of value it should hold (in this case it's HTMLElement). When using a ref for a DOM element, React expects that you will never mutate this ref since it's being controlled by React, so the type that is returned by useRef in this case is simply a RefObject. Therefore, that's what you should use for the first parameter of useClick.

Related

Typescript React: generic-based callback event handler function as prop to component

I'm having trouble creating a generic-based callback event handler function that I want to pass as prop down to my component.
My goal: Allow user to pass a custom callback function that:
always takes in the same argument
an event (not a react/dom event handler event, its coming from a library)
can return different return type
for my use cases, this component is used in different context's so in one place a user will return a certain value over another
What I have Attempted
I have my onNodeClick function defined using generics & when i use it in isolation, it works.
// ✅ Simple example of calling a generic function
const onNodeClick = <T,> (event:any) => {
return null as unknown as T;
}
const string_result = onNodeClick<string>(event_from_somewhere)
However, when I try to pass this method as a prop to my component, I am getting errors. I'm unsure of how to resolve it
Live Typescript Code Playground
import React from 'react';
type NodeComponentProps = {
onNodeClick: <T>(event: any) => T;
};
export const NodeComponent = ({onNodeClick}: NodeComponentProps) => {
return null;
}
const Homepage = () => {
const handleNodeClick = <T,>(event: any): T => {
return null as unknown as T;
};
return (
<NodeComponent
onNodeClick={(event): string => {
const string_result = handleNodeClick<string>(event); // ✅ correct type
return string_result; // ❌ onNodeClick is throwing type error; see error message a few lines below
}}
/>
)
}
/*
Type '<T>(event: any) => string' is not assignable to type '<T>(event: any) => T'.
Type 'string' is not assignable to type 'T'.
'T' could be instantiated with an arbitrary type which could be unrelated to 'string
*/
I think you want the type to be generic, not the function. If the function is generic, then it should be callable with angle brackets and work correctly. For example, the identity function works with any type, so it should pass your generic function definition:
const Homepage = () => {
const onNodeClick = <T>(x: T): T => {
return x;
};
return (
<NodeComponent
onNodeClick={onNodeClick}
/>
)
}
However, the function you used will always return a string. If you tried to call it with <number>, it would fail. Therefore, not a generic function.
By contrast, if you make the type generic, you should be able to specify you are using it with a string:
Here is a Playground link
type NodeComponentProps<T> = {
onNodeClick: (event: any) => T;
};
export const NodeComponent = <T,>({onNodeClick}: NodeComponentProps<T>) => {
return null;
}

How to type custom `preload` method for React.lazy components?

When trying to implement a preload() method for React.lazy() components, a typical pattern looks something like,
const ReactLazyPreload = (importStatement) => {
const Component = React.lazy(importStatement);
Component.preload = importStatement; // Property 'preload' does not exist on type 'LazyExoticComponent<T>'.
return Component;
};
which can later be used, eg,
const MyComponent = ReactLazyPreload(() => import("./MyComponent.tsx");
const onHover = () => { MyComponent.preload() };
However, the assignment on the 3rd line of the first snippet causes a TS error,
Property 'preload' does not exist on type 'LazyExoticComponent<T>'.
I've been playing around with declare, but have been unsuccessful in removing the error. What type should be used for the preload() method?
// extend lazy component with `preload` property
interface LazyPreload<Props>
extends React.LazyExoticComponent<React.ComponentType<Props>> {
preload: () => {};
}
function ReactLazyPreload<Props>(
importStatement: () => Promise<{ default: React.ComponentType<Props> }>
) {
// use Object.assign to set preload
// otherwise it will complain that Component doesn't have preload
const Component: LazyPreload<Props> = Object.assign(
React.lazy(importStatement),
{
preload: importStatement,
}
);
return Component;
}
TypeScript is trying to prevent you from making a mistake here.
Just because other people follow a convention doesn't make it a good one: in this case, it's not a safe one. As a general rule, it's never safe to mutate things you don't own.
While I can't find anything in the React codebase at the current version tag (17.0.2) that would seem to cause an issue with assigning something to the preload property of a lazy component, that doesn't mean that the React maintainers won't use this property in a subsequent release. If that happens, and you overwrite that property, then unpredictable behavior would arise.
Instead of mutating the component, just return the preload function alongside it:
TS Playground link
import {default as React, lazy} from 'react';
import type {ComponentType, LazyExoticComponent} from 'react';
export type ReactLazyFactory<T = any> = () => Promise<{default: ComponentType<T>}>;
export type ComponentPreloadTuple<T = any> = [
component: LazyExoticComponent<ComponentType<T>>,
preloadFn: () => void,
];
export function getLazyComponentWithPreload <T = any>(componentPath: string): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload <T = any>(factory: ReactLazyFactory<T>): ComponentPreloadTuple<T>;
export function getLazyComponentWithPreload <T = any>(input: string | ReactLazyFactory<T>): ComponentPreloadTuple<T> {
const factory = () => typeof input === 'string' ? import(input) : input();
return [lazy(factory), factory];
}
// ----------
// Example.tsx
export type ExampleProps = {
text: string;
};
export default function ExampleComponent ({text}: ExampleProps) {
return <div>{text}</div>;
}
// ----------
// AnotherComponent.tsx
// use with path to component:
const [Example1, preloadExample1] = getLazyComponentWithPreload<ExampleProps>('./Example');
// use with factory function:
const [Example2, preloadExample2] = getLazyComponentWithPreload<ExampleProps>(() => import('./Example'));

How to use TypeScript with a custom hook for reactjs useContext?

I'm trying to refactor my context to use a custom hook in TypeScript and I've got an error I don't know how to fix.
This is the error:
This expression is not callable. Not all constituents of type
'boolean | Dispatch<SetStateAction>' are callable.
Type 'false' has no call signatures. TS2349
My custom hook is defined like this:
export const useOfflineContext = () => {
const isOffline = React.useContext(OfflineContext);
const setIsOffline = React.useContext(OfflineSetContext);
if (!setIsOffline) {
throw new Error('The OfflineProvider is missing.');
}
return [isOffline, setIsOffline];
};
And I'm calling it like this:
const [setIsOffline] = useOfflineContext();
// Check if we are online or offline.
useEffect(() => {
Network.addListener('networkStatusChange', (networkStatus) => {
// console.log('Network status changed', networkStatus);
if (!networkStatus.connected) {
setIsOffline(true);
This causes the error above.
Here's my full component OfflineContextProvider.tsx:
import React, { useState } from 'react';
const OfflineContext = React.createContext(false);
const OfflineSetContext = React.createContext({} as React.Dispatch<React.SetStateAction<boolean>>);
interface MyProps {
children: JSX.Element,
}
export const OfflineContextProvider: React.VFC<MyProps> = ({ children }: MyProps) => {
const [isOffline, setIsOffline] = useState<boolean>(false);
return (
<OfflineContext.Provider value={isOffline}>
<OfflineSetContext.Provider value={setIsOffline}>
{children}
</OfflineSetContext.Provider>
</OfflineContext.Provider>
);
};
export const useOfflineContext = () => {
const isOffline = React.useContext(OfflineContext);
const setIsOffline = React.useContext(OfflineSetContext);
if (!setIsOffline) {
throw new Error('The OfflineProvider is missing.');
}
return [isOffline, setIsOffline];
};
I don't understand why, when I specifically ask for const [setIsOffline] = useOfflineContext();, TypeScript thinks that I might want isOffline instead. (Because the TypeScript warning mentions the boolean value (isOffline).)
So my question is: How can I properly type useState when used in a custom hook to return a context?
Currently using TypeScript 4.3.
You're using the first element of the returned array, which is isOffline (a boolean), but you're calling it setIsOffline. However, it is still a boolean.
What you probably want is to get the second element of the array:
const [ , setIsOffline ] = useOfflineContext();

function argument types when get a value from an HTML element on react

So Im just making a function that gets a value from an HTML element (LI tag). And this function will be called on an onClick event as well.
const getContents = (e: any) => {
const data = e.target.textContent.split('some-separator') //.. and so on
// other stuff
}
return (
<li onClick={(e) => getContents(e)}>{some date}</li>
)
for the meantime, I'm putting any as I don't know and can't narrow the type for now. What would be the correct type for this, and when I try to put a fancy type - typescript would scream
(property) Node.textContent: string | null - Object is possible 'null'
import {SyntheticEvent} from 'react'
const getContents = (e: SyntheticEvent) => {
// ...
}
// or more specific
const getContents = (e: SyntheticEvent<HTMLInputElement>) => {
e.currentTarget.value
}

Typescript: how to declare a type that includes all types extending a common type?

TLDR: Is there a way in Typescript to declare a type that encompasses all types that extend a given interface?
My specific problem
I am writing a custom React hook that encapsulates logic for deciding whether or not an element is moused over. It is modelled roughly after this hook. It exposes a ref that should be able to take any HTMLElement:
const ref = useRef<HTMLElement>(null);
The problem is, if I try to use this ref on any specific React element, I get an error telling me that this specific element is not quite HTMLElement. For example, if I use it with HTMLDivElement, I get this error: argument of type HTMLElement is not assignable to parameter of type HTMLDivElement.
Here's a simple repro case of the problem above in Typescript playground
Obviously, I wouldn't want to list types of all html elements in my hook. Given that HTMLDivElement extends the HTMLElement type, is there a way of declaring that the type that I am actually after is not strictly HTMLElement, but whatever extends HTMLElement?
React code example
source code of the hook
import { useRef, useState, useEffect } from 'react';
type UseHoverType = [React.RefObject<HTMLElement>, boolean];
export default function useHover(): UseHoverType {
const [isHovering, setIsHovering] = useState(false);
let isTouched = false;
const ref = useRef<HTMLElement>(null); // <-- What should the type be here?
const handleMouseEnter = () => {
if (!isTouched) {
setIsHovering(true);
}
isTouched = false;
};
const handleMouseLeave = () => {
setIsHovering(false);
};
const handleTouch = () => {
isTouched = true;
};
useEffect(() => {
const element = ref.current;
if (element) {
element.addEventListener('mouseenter', handleMouseEnter);
element.addEventListener('mouseleave', handleMouseLeave);
element.addEventListener('touchstart', handleTouch);
return () => {
element.removeEventListener('mouseenter', handleMouseEnter);
element.removeEventListener('mouseleave', handleMouseLeave);
element.removeEventListener('touchend', handleTouch);
};
}
}, [ref.current]);
return [ref, isHovering];
}
which produces type error if used like this:
import useHover from 'path-to-useHover';
const testFunction = () => {
const [hoverRef, isHovered] = useHover();
return (
<div
ref={hoverRef}
>
Stuff
</div>
);
}
Type error in example above will be:
Type 'RefObject<HTMLElement>' is not assignable to type 'string | RefObject<HTMLDivElement> | ((instance: HTMLDivElement | null) => void) | null | undefined'.
Type 'RefObject<HTMLElement>' is not assignable to type 'RefObject<HTMLDivElement>'.
Property 'align' is missing in type 'HTMLElement' but required in type 'HTMLDivElement'.
I think you are mistaken about the direction of the assignment that fails. If you have an interface A, then the type that matches all subclasses of A is just called A. This way, HTMLElement (i.e. is assignable from) any HTML element, e.g. HTMLDivElement.
This means that if you have a bunch of functions, one of them accepts HTMLDivElement, another accepts HTMLLinkElement etc, then there is no real type that you can pass to all of them. It would mean you expect to have an element that is both a div and a link and more.
Edited based on your edits of the question:
If the code you have works fine, and your only problem is that it doesn't compile, then just make your useHover generic, like this:
type UseHoverType<T extends HTMLElement> = [React.RefObject<T>, boolean];
function useHover<T extends HTMLElement>(): UseHoverType<T> {
const ref = useRef<T>(null); // <-- What should the type be here?
...
And then:
const testFunction = () => {
const [hoverRef, isHovered] = useHover<HTMLDivElement>();
Something like this will make your code compile fine, without changing its runtime behaviour. I'm unable to tell if the runtime behaviour right now is as desired.
It works as expected, since HTMLDivElement extends HTMLElement. In your typescirpt playground you mixed it up. I updated it by switching x and y in this playground. You want the function to extend HTMLElement and pass y, which is and HTMLDivElement into it. And that works.

Resources