Best Practice
Destructuring
Some of the React Hooks in foxact will return an object of memoized values. You can use destructuring to extract the values you need, e.g:
import { useClipboard } from 'foxact/use-clipboard';
export default function App() {
const { copied, copy } = useClipboard();
const handleCopyButtonClick = useCallback(() => copy('Hello World'), [copy]);
return (
<div>
<button onClick={handleCopyButtonClick}>{copied ? 'Copied' : 'Copy Hello World'}</button>
</div>
);
}Here, the copy function returned by useClipboard is memoized, so it’s safe to use it as a dependency in the useCallback hook.
Other more re-usable utilities and hooks from foxact will return an array of memoized values, so you can give them a more descriptive name by using array destructuring , e.g:
import { createContextState } from 'foxact/context-state';
const [TokenProvider, useToken, useSetToken] = createContextState<string | null>(null);
const [SidebarActiveProvider, useSidebarActive, useSetSidebarActive] = createContextState(false);
const Provider = ({ children }: React.PropsWithChildren) => (
<TokenProvider>
<SidebarActiveProvider>
{children}
</SidebarActiveProvider>
</TokenProvider>
);
export {
Provider,
useToken, useSetToken,
useSidebarActive, useSetSidebarActive
};import { useIntersection } from 'foxact/use-intersection';
const [setAvatarIntersection, isAvatarIntersected, resetAvatarIsIntersected] = useIntersection<HTMLImageElement>();
const [setThumbnailIntersection, isThumbnailIntersected, resetThumbnailIsIntersected] = useIntersection<HTMLImageElement>();
const setToken = useSetToken();And returned functions (like setAvatarIntersection, resetAvatarIsIntersected, setThumbnailIntersection, resetThumbnailIsIntersected, and setToken) are always memoized, so it’s safe to add them to any dependencies array.
Why / Why not
Global state management library
You might not need a global state management library, most of the time.
Why not useIsMounted
It is common to use useIsMounted to check if the component is mounted before calling setState during useEffect:
// ONLY FOR DEMONSTRATION, NEVER DO THIS IN REAL WORD REACT PROJECT
const isMountedRef = useIsMountedRef();
useEffect(() => {
someAsyncStuff().then(data => {
if (isMountedRef.current) {
// trying to avoid set state on unmounted component
setData(data);
// BUT NO, DO NOT TO THIS. THIS IS WRONG
}
});
});The real problem is that you are trying to avoid is not updating the state after the component has been unmounted, but what you really should do is to avoid updating the state after the current effect has been unsubscribed / cleanup-ed.
Imagine this common race condition:
interface ExampleComponentProps {
dataKey: 'data1' | 'data2'
}
const ExampleComponent = ({ dataKey }: ExampleComponentProps) => {
const [data, setData] = useState(null);
// ONLY FOR DEMONSTRATION, NEVER DO THIS IN REAL WORD REACT PROJECT
const isMountedRef = useIsMountedRef();
useEffect(() => {
someAsyncStuff(dataKey).then(data => {
if (isMountedRef.current) {
// trying to avoid set state on unmounted component
setData(data);
// BUT NO, DO NOT TO THIS. THIS IS WRONG
}
});
}, [dataKey]);
return (
<div>{data}</div>
);
};│ Request data 1 ────────────────────────────────────────► data1 response (setData(data1)) │
│ Request data 2 ────► data2 response (setData(data2)) │Here, although the request for data1 happened before data2, the response for data2 is received before data1. And useIsMountedRef doesn’t help with that.
To properly avoid setData(data1) from being called, the correct pattern is described below.
interface ExampleComponentProps {
dataKey: 'data1' | 'data2'
}
const ExampleComponent = ({ dataKey }: ExampleComponentProps) => {
const [data, setData] = useState(null);
useEffect(() => {
let isCancelled = false;
someAsyncStuff().then(data => {
if (!isCancelled) {
setData(data);
}
});
return () => {
isCancelled = true;
};
}, [dataKey]);
}│ Request data 1 ───────────────────────────────────────────────────────► data1 response │
| isCancelled: false | isCancelled: true | isCancelled: true, no setData(data1)
│ Request data 2 ────► data2 response (setData(data2)) │
| isCancelled: false | isCancelled: false, setData(data2)You can also use useAbortableEffect to achieve the same thing with less boilerplate code:
interface ExampleComponentProps {
dataKey: 'data1' | 'data2'
}
const ExampleComponent = ({ dataKey }: ExampleComponentProps) => {
const [data, setData] = useState(null);
// Do notice that useAbortableEffect requires AbortController support
useAbortableEffect((signal) => {
someAsyncStuff().then(data => {
if (signal.aborted) return
setData(data);
});
}, [dataKey]);
}│ Request data 1 ───────────────────────────────────────────────────────► data1 response │
| aborted: false | aborted | aborted, no setData(data1)
│ Request data 2 ────► data2 response (setData(data2)) │
| aborted: false | aborted: false, setData(data2)