Best Practice

Destructuring

Some of the React Hooks in foxact will return an object of memoized values. You can use destructuring (opens in a new tab) 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 (opens in a new tab), e.g:

src/context/provider.tsx
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
};
src/App.tsx
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 vs useContext + useState

Global state management libraries (like Redux (opens in a new tab) or Zustand (opens in a new tab)) usually store the state outside of React, then use useSyncExternalStore to sync the state into the React.

useSyncExternalStore is a new React built-in hook introduced in React 18. It is a trade-off between React Concurrent Rendering and consistency (without tearing). Currently (React 18.2), when React encounters useSyncExternalStore during the concurrent rendering, React will "de-optimize" and fall back to the synchronous rendering (slower and less responsive) to make sure there is no inconsistency (tearing). Only states stored inside of React (using useState and useReducer) are concurrent-rendering-friendly.

Global state management libraries are indeed very powerful and great for React application that has complex logic and complicated data flow. But most of the time, a simple state shared across the application is good enough.

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)