foxact

repository·master·Indexed 19 days ago

https://github.com/sukkaw/foxact

A collection of React Hooks and utilities optimized for modern React features including Concurrent Rendering, SSR, and React Server Components (RSC). Compatible with Next.js, Waku, Gatsby, Remix, and Shopify Hydrogen. Features include type-safe utilities, a colocation-teleport breadcrumb system, ComposeContextProvider for flattening provider nesting, and createContextReducer for shared state management.

Tokens
60.8K
Snippets
175
Records
213
Agent score
66%

What's inside foxact

  1. Overview of foxact

    master

    foxact is a library of React Hooks and Utilities designed for modern React environments, including Browser, Server-Side Rendering (SSR), and React Server Components (RSC). It is built to be resilient with React 18+ Concurrent Rendering features like <Suspense />, startTransition, and <Offscreen /> by strictly following React best practices (e.g., avoiding reading/writing refs during the render phase).

    Key characteristics:

    • SSR Friendly: Compatible with Next.js (Pages and App Router), Waku, Gatsby, Remix, and Shopify Hydrogen.
    • Type Safe: Written in TypeScript (requires TypeScript 4.8+).
    • Lightweight & Tree Shakable: Every hook and utility is isolated and side-effect free, allowing for efficient tree shaking to minimize client bundle sizes.
  2. Overview of foxact features and compatibility

    master

    foxact is a library of React Hooks and Utilities designed for modern React environments. Key characteristics include:

    • React 18+ Safe: Resilient to Concurrent Rendering. It strictly follows React best practices, such as never reading or writing refs during the render phase, making it compatible with <Suspense />, startTransition, and <OffScreen />.
    • SSR Friendly: Works with Server-Side Rendering (SSR), Incremental Static Generation (ISG), and Static Site Generation (SSG). It supports frameworks like Next.js (Pages and App Router), Waku, Gatsby, Remix, and Shopify Hydrogen.
    • Type Safe: Written in TypeScript, requiring TypeScript 4.8+ to unlock full typing benefits.
    • Lightweight & Tree Shakable: The library is designed to be side-effect free and highly modular. Every hook and utility is isolated, allowing bundlers to tree-shake unused code effectively to minimize client bundle size.
  3. What is useErrorBoundary and when to use it

    master

    React's built-in Error Boundaries only catch errors thrown during the rendering phase. They cannot catch errors occurring inside event handlers or useEffect hooks.

    useErrorBoundary bridges this gap by allowing you to manually trigger an existing Error Boundary from non-rendering contexts (like event handlers or asynchronous callbacks), enabling you to reuse your existing error handling UI and logic for all types of errors.

  4. Handle Server-side Rendering with useLocalStorage

    master

    Because localStorage is a browser-only API, useLocalStorage behaves differently during Server-Side Rendering (SSR) depending on whether an initialValue is provided:

    With an initial value

    If you provide an initialValue (the second argument), React uses it to render the HTML on the server.

    1. Server: Generates HTML using the initialValue.
    2. Client Hydration: React hydrates using the initialValue.
    3. Client Re-render: React immediately re-renders with the actual value from localStorage. Result: The user sees the initialValue briefly before it switches to the stored value.

    Without an initial value

    If the second argument is omitted, the hook relies on React's <Suspense> mechanism.

    1. Server: React finds the nearest <Suspense> boundary and renders its fallback UI into the HTML.
    2. Client Hydration: React attempts to render the component on the client, reading the actual value from localStorage. Result: The user sees the fallback UI first, then the actual stored value after hydration.

    Note: To use the second approach, you must wrap the component using the hook in a <Suspense> boundary.

    // Approach 1: Using initial value
    const [value, setValue] = useLocalStorage('local-storage-key', 'server');
    
    // Approach 2: Using Suspense (no initial value)
    const Comp = () => {
      const [value, setValue] = useLocalStorage('local-storage-key');
      return <div>{value}</div>;
    };
    
    const App = () => (
      <Suspense fallback={<div>fallback</div>}>
        <Comp />
      </Suspense>
    );
  5. Handle Server-side Rendering with useMediaQuery

    master

    Because window.matchMedia() is only available in the browser, useMediaQuery requires specific strategies for Server-side Rendering (SSR) to avoid hydration mismatches:

    Option 1: Providing an initial value

    If you provide a second argument (the initial value), React will use this value to render the UI and generate HTML on the server.

    • Server/Initial Hydration: The user sees the UI based on the initial value.
    • Client: After hydration, React immediately re-renders the component with the actual value read from the browser's matchMedia() API.

    Option 2: Using Suspense

    If you do not provide a second argument, the hook will trigger a Suspense boundary.

    • Server: React will find the closest <Suspense> boundary and render its fallback UI into the generated server HTML.
    • Client: The user sees the fallback UI first. During hydration, React reads the actual value from the browser and renders the component with the correct state once hydration is complete.
  6. Caveats and performance pitfalls of `useStateWithDeps`

    master

    To maintain the performance benefits of useStateWithDeps, follow these rules:

    • Use Plain Objects: It is designed for plain objects with a mostly fixed shape.
    • Permanent Dependencies: Once a property is read via the snapshot, it remains a rendering dependency for the lifetime of the component, even if it is no longer read in subsequent conditional branches.
    • NEVER Spread the Snapshot: Do not use { ...snapshot }. Spreading reads every property at once, which forces every property to become a rendering dependency and causes a re-render on every state change.
    • Destructuring is Safe: Destructuring specific properties (e.g., const { a, b } = snapshot) is encouraged as it only tracks the properties you explicitly name.
  7. Handle Server-side Rendering with createLocalStorageState

    master

    When using createLocalStorageState in a Server-side Rendering (SSR) environment, the behavior depends on whether an initialValue is provided:

    1. With initialValue: React will use the provided initial value to render the UI and generate HTML on the server. This prevents hydration mismatches.
    2. Without initialValue: React cannot access localStorage on the server. It will find the closest <Suspense> boundary and render its fallback UI into the generated server HTML. The actual state will be hydrated on the client.
  8. Produce derived state from existing states

    master

    When you need to derive state from other states, you can avoid a global state library by using standard React patterns depending on the complexity of the computation:

    1. Synchronous & Cheap Computation: Compute the value on the fly within your component. Use useMemo to prevent unnecessary re-computations and re-renders when the source states haven't changed.
    2. Asynchronous Computation: If the derivation requires an async source, use useSWR. Perform the computation inside the fetcher function. The result will be cached and only re-computed during the initial fetch or subsequent cache invalidations.
    3. Singleton-based Async Computation: For complex async sources (like IndexedDB), combine foxact/use-singleton with useSWRImmutable. useSingleton ensures the initializer (e.g., a database connection) runs only once and maintains the same instance across re-renders.
    // Example: Synchronous derived state with useMemo
    export function useIsPanelActive() {
      const sidebarActive = useSidebarActive();
      const globalDrawer = useGlobalDrawer();
      return useMemo(() => sidebarActive && globalDrawer, [sidebarActive, globalDrawer]);
    }
    
    // Example: Asynchronous derived state with useSWR
    const useFinalizedServerConfig = () => useSWR(
      { url: '/api/server-config', method: 'GET' },
      async ({ url, method }) => {
        const r = await fetch(url, { method });
        const rawConfig = await r.json();
        return await produceFinalConfig(rawConfig);
      }
    );
    
    // Example: Using foxact's useSingleton for async sources
    const useLocalConfigFromIndexedDB = (key) => {
      const idbkvInstance = useSingleton(() => new IDBKeyVal('my-db', 'my-store'));
    
      return useSWRImmutable(
        { key },
        async ({ key }) => {
          const localData = await idbkvInstance.current.get(key);
          return makeTransformation(await deserialize(localData));
        }
      );
    }
  9. Compare `useDebouncedState` with `useDeferredValue` and `useDebouncedValue`

    master

    useDebouncedState vs useDeferredValue (React 18+)

    • Access to State: useDeferredValue requires direct access to the original state value. useDebouncedState is used when you do not have access to the original state.
    • Network Requests: useDeferredValue cannot reduce network requests; the re-render will always trigger the request. useDebouncedState can reduce network requests by delaying the state update that triggers the request.
    • Performance: useDeferredValue is interruptible and integrates with React's Concurrent Rendering to adapt to device speed. useDebouncedState is not interruptible.

    useDebouncedState vs useDebouncedValue (foxact)

    • Component Type: useDebouncedState is for uncontrolled components (using defaultValue and onChange). useDebouncedValue is designed for controlled components (using the value prop).
  10. How Context Reducer works

    master

    The Context Reducer pattern provides a way to lift state up and pass it deeply into a React application using React Context without the overhead of a global state management library like Redux or Zustand.

    It works by combining React.createContext with the useReducer hook. The createContextReducer utility automates the creation of:

    1. A Provider component that holds the reducer state.
    2. A State hook (e.g., useCounter) to consume the current state.
    3. A Dispatch hook (e.g., useCounterDispatch) to trigger state updates via actions.

    This pattern is ideal for shared state that is scoped to a specific part of the component tree rather than the entire application.

  11. How foxact breadcrumbs work

    master

    The foxact/breadcrumbs library uses a 'colocation - teleport' pattern to solve the problem of breadcrumbs in React. Instead of a centralized configuration or global state, breadcrumbs are declared as components directly within the UI tree where they belong.

    The mechanism:

    1. Top-Down Accumulation: As the component tree is traversed, BreadcrumbSegment components use React Context to accumulate a chain of breadcrumb items. Each segment reads the parent's chain, appends its own { title, href }, and provides the extended chain to its children.
    2. Teleportation: At the leaf node (the page), BreadcrumbCurrent reads the final accumulated chain and uses Magic Portal to 'teleport' the rendered breadcrumb UI up to a BreadcrumbTarget located in a high-level layout (like the root layout).

    This approach ensures breadcrumbs are declarative, co-located with the UI they describe, and avoid extra renders or complex state management.

    /* Conceptual flow: */
    <BreadcrumbProvider>
      <BreadcrumbTarget /> {/* UI teleports here */}
      <BreadcrumbSegment title="A" href="/a">
        <BreadcrumbSegment title="B" href="/b">
          <BreadcrumbCurrent title="C">
            {(items) => <RenderUI items={items} />}
          </BreadcrumbCurrent>
        </BreadcrumbSegment>
      </BreadcrumbSegment>
    </BreadcrumbProvider>
  12. How Magic Portal works

    master

    Magic Portal implements a pattern where child components can decide what a parent component should render. This is useful for complex UIs where a layout (parent) needs to display information (like a page title or breadcrumbs) that is logically owned by a specific page (child).

    Unlike traditional methods that might break React's top-down data flow or cause double-renders, Magic Portal uses a provider/target/content model. It leverages createContextState and React's createPortal to capture a DOM node from a <PortalTarget /> and render <PortalContent /> into it. This allows for excellent code co-location: you can define the UI for a header inside the page component itself, even though it physically renders in the global layout.

    import { createMagicPortal } from 'foxact/magic-portal';
    
    // Returns [Provider, Target, Content]
    export const [Provider, Target, Content] = createMagicPortal('MyPortalName');