<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Jeremy Nguyen</title>
    <link>https://jeremy.ng</link>
    <description>Personal website for Jeremy Nguyen</description>
    <language>en-US</language>
    <pubDate>Sat, 08 Aug 2026 05:12:17 GMT</pubDate>
    <category>react</category>
    <category>shell</category>
    <category>homebrew</category>
    <category>react-query</category>
    <item>
      <title>Passing callback functions as a className in Base UI</title>
      <link>https://jeremy.ng/blog/base-ui-function-classnames</link>
      <description>Use callback functions in the className and style props in Base UI</description>
      <author>Jeremy Nguyen</author>
      <category>react</category>
      <pubDate>Sat, 08 Aug 2026 04:26:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>A neat feature of Base UI is that you can style all components by passing a callback function to both its <a href="https://base-ui.com/react/handbook/styling#css-classes"><code>className</code></a> and <a href="https://base-ui.com/react/handbook/styling#style-prop"><code>style</code></a> props. This is usually overlooked in favor of styling with either <code>data-*</code> attributes or CSS variables. This allows you to style a component based on its specific <code>State</code>. For example, this is the <code>State</code> provided in a <code>&lt;Separator&gt;</code>:</p>
<pre><code class="language-typescript">// https://github.com/mui/base-ui/blob/1a2ca3c9f8a39bd8c0dda939a7a23b72da226124/packages/react/src/separator/Separator.tsx#L37-L42
export interface SeparatorState {
  /**
   * The orientation of the separator.
   */
  orientation: Orientation;
}
</code></pre>
<p>The thing about this feature is that usually, one doesn't expect a component's <code>className</code> to be of type <code>string  | undefined | ((state: State) =&gt; string | undefined)</code> or its style to be <code>React.CSSProperties | undefined | ((state: State) =&gt; React.CSSProperties | undefined)</code>. This can cause TypeScript errors or unexpected behavior if consumers of a component use a function unplanned.</p>
<p>For example, in the popular design system <a href="https://ui.shadcn.com/">shadcn/ui</a>, a common pattern is adding a utility function <code>cn</code> to merge multiple <code>className</code> values.<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup></p>
<pre><code class="language-typescript">import { clsx, type ClassValue } from &quot;clsx&quot;;
import { twMerge } from &quot;tailwind-merge&quot;;

export const cn = (...inputs: ClassValue[]) =&gt; {
  return twMerge(clsx(inputs));
};
</code></pre>
<p>Suppose you had an <code>&lt;Input&gt;</code> component like the following (similar to shadcn/ui's <a href="https://ui.shadcn.com/docs/components/base/input"><code>&lt;Input&gt;</code></a> component but shortened for brevity):</p>
<pre><code class="language-tsx">import { Input as InputPrimitive } from &quot;@base-ui/react/input&quot;;
import type { ComponentProps } from &quot;react&quot;;
import { cn } from &quot;../utils/cn&quot;;

export const Input = ({
  className,
  ...props
}: ComponentProps&lt;typeof InputPrimitive&gt;) =&gt; {
  return (
    &lt;InputPrimitive
      {...props}
      className={cn(
        &quot;flex h-9 w-full appearance-none rounded border border-gray-300 bg-white py-1 text-start dark:border-gray-700 dark:bg-gray-950&quot;,
        className,
      )}
    /&gt;
  );
};
</code></pre>
<p>And then you attempted to use the aforementioned feature by passing a callback function to its <code>className</code> prop:</p>
<pre><code class="language-tsx">&lt;Input className={(state) =&gt; (state.dirty ? &quot;border-red-600&quot; : undefined)} /&gt;
</code></pre>
<p>...you would probably notice two strange things:</p>
<ol>
<li>The function is completely ignored</li>
<li>There is no TypeScript error despite passing a function for a <code>className</code></li>
</ol>
<h2>An aside on TypeScript</h2>
<p>This is how <code>ClassValue</code> from <code>clsx</code> is defined in TypeScript:</p>
<pre><code class="language-typescript">// https://github.com/lukeed/clsx/blob/925494cf31bcd97d3337aacd34e659e80cae7fe2/clsx.d.mts#L1-L3
export type ClassValue =
  | ClassArray
  | ClassDictionary
  | string
  | number
  | bigint
  | null
  | boolean
  | undefined;
export type ClassDictionary = Record&lt;string, any&gt;;
export type ClassArray = ClassValue[];
</code></pre>
<p>The most curious option in <code>ClassValue</code> is the <code>ClassDictionary</code> type, which is <code>Record&lt;string, any&gt;</code>.</p>
<p>In TypeScript, <code>Record&lt;string, any&gt;</code> behaves differently compared to other types with an <a href="https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures">index signature</a>. Typically, if you attempt to assign a function to something with an index signature, you'll get an error like this:</p>
<pre><code class="language-typescript">// Type '() =&gt; void' is not assignable to type 'Record&lt;string, unknown&gt;'.
//   Index signature for type 'string' is missing in type '() =&gt; void'.
const foo: Record&lt;string, unknown&gt; = () =&gt; {};
</code></pre>
<p>However, for <code>Record&lt;string, any&gt;</code>, this is not checked, so effectively, the type accepts any object, including functions.</p>
<p>This issue was raised in the TypeScript GitHub (<a href="https://github.com/microsoft/TypeScript/issues/41746">microsoft/TypeScript#41746</a>), where <a href="https://github.com/RyanCavanaugh">@RyanCavanaugh</a> confirms this is intentional behavior.<sup><a href="#user-content-fn-2" id="user-content-fnref-2" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup></p>
<h2>An aside on <code>clsx</code></h2>
<p>To make a long story short, on all inputs (including the elements of arrays), <code>clsx</code> returns a string value using <a href="https://github.com/lukeed/clsx/blob/925494cf31bcd97d3337aacd34e659e80cae7fe2/src/index.js#L1-L28"><code>toVal</code></a>. Since <code>typeof className === &quot;function&quot;</code>, it simply returns an empty string.</p>
<h2>Solution</h2>
<p>While this is all fine for <code>shadcn/ui</code> users, there are other times when this type can become problematic. For example, if you use <code>tailwind-variants</code>, you'll get a TypeScript error when passing <code>className</code> directly:</p>
<pre><code class="language-tsx">import { Input as InputPrimitive } from &quot;@base-ui/react/input&quot;;
import type { ComponentProps } from &quot;react&quot;;
import { tv } from &quot;tailwind-variants&quot;;

const inputVariants = tv({
  base: &quot;&quot;,
});

export const Input = ({
  className,
  ...props
}: ComponentProps&lt;typeof InputPrimitive&gt;) =&gt; {
  // Type 'string | ((state: InputState) =&gt; string | undefined) | undefined' is not assignable to type 'ClassNameValue'.
  //   Type '(state: InputState) =&gt; string | undefined' is not assignable to type 'ClassNameValue'.ts(2322)
  return &lt;InputPrimitive {...props} className={inputVariants({ className })} /&gt;;
};
</code></pre>
<h3><code>composeRenderProps</code></h3>
<p>In another React UI library, <a href="https://react-aria.adobe.com/">React Aria</a> by Adobe, this concern was solved with the helper function <code>composeRenderProps</code>. I'll be referring to it by that name for consistency; although, given Base UI conventions, it probably should be called something like <code>composeState</code> or <code>composeBaseUiState</code>.</p>
<pre><code class="language-typescript">// https://github.com/adobe/react-spectrum/blob/d038f46152341b0afb15b191f34a4b60a074d7a8/packages/react-aria-components/src/utils.tsx#L284-L294
/**
 * A helper function that accepts a user-provided render prop value (either a static value or a
 * function), and combines it with another value to create a final result.
 */
export function composeRenderProps&lt;T, U, V extends T&gt;(
  // https://stackoverflow.com/questions/60898079/typescript-type-t-or-function-t-usage
  value: T extends any ? T | ((renderProps: U) =&gt; V) : never,
  wrap: (prevValue: T, renderProps: U) =&gt; V,
): (renderProps: U) =&gt; V {
  return (renderProps) =&gt;
    wrap(typeof value === &quot;function&quot; ? value(renderProps) : value, renderProps);
}
</code></pre>
<p>By using this function, we can correctly handle function <code>className</code> values and avoid TypeScript errors:</p>
<pre><code class="language-tsx">&lt;InputPrimitive
  {...props}
  className={composeRenderProps(props.className, (className) =&gt;
    inputVariants({ size, className }),
  )}
/&gt;
</code></pre>
<h2>Closing thoughts</h2>
<p>I honestly find it a bit strange that, as far as I know, this bug hasn't been noticed by any <code>shadcn/ui</code> users despite the library being so popular. The closest documented issue I have found on this is here: <a href="https://github.com/shadcn-ui/ui/issues/11303">shadcn-ui/ui#11303</a>.</p>
<p>While there is a <code>mergeProps</code> helper in Base UI, per documentation,<sup><a href="#user-content-fn-3" id="user-content-fnref-3" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup> it only works with string <code>className</code> values. You can see this is true in <a href="https://github.com/mui/base-ui/blob/b34551d644f2e58ebf8fc1050d949f6654ceca6c/packages/react/src/merge-props/mergeProps.ts#L278-L292">the source code</a>. I think it would be very useful if Base UI were to either add a function similar to <code>composeRenderProps</code> or somehow add that functionality to <code>mergeProps</code>.</p>
<p>Using <code>composeRenderProps</code> honestly often makes the code very verbose, but it also opens up some possibilities in terms of styling. For example, one can treat the <code>State</code> as a variant, and style it accordingly with <a href="https://www.tailwind-variants.org/"><code>tailwind-variants</code></a>:</p>
<pre><code class="language-tsx">import type { ComponentProps } from &quot;react&quot;;
import { Separator as SeparatorPrimitive } from &quot;@base-ui/react/separator&quot;;
import { tv } from &quot;tailwind-variants&quot;;

import { composeRenderProps } from &quot;../utils/composeRenderProps&quot;;

const separatorVariants = tv({
  base: null,
  variants: {
    orientation: {
      horizontal: &quot;h-px border-t&quot;,
      vertical: &quot;w-px border-s&quot;,
    },
  },
  defaultVariants: { orientation: &quot;horizontal&quot; },
});

export const Separator = ({
  variant,
  size,
  ...props
}: ComponentProps&lt;typeof SeparatorPrimitive&gt;) =&gt; {
  return (
    &lt;SeparatorPrimitive
      {...props}
      className={composeRenderProps(props.className, (className, state) =&gt;
        separatorVariants({ className, ...state }),
      )}
    /&gt;
  );
};
</code></pre>
<p>There are, of course, other use cases, but that is the most obvious to me.</p>
<p>Another minor caveat is that if you are using React Server Components, a function on the server that isn't a server action cannot be passed as a prop to a client component. However, if you accidentally do that, React will <a href="https://github.com/react/react/blob/3a717e42438afac81020cdec297dadb5613a4304/scripts/error-codes/codes.json#L364">notify you with an error</a>, so I don't expect this to be a real concern.</p>
<p>Furthermore, some people like to destructure <code>className</code> from their props. While you can do this without runtime errors, it does result in a bit of a codesmell since there are now two variables named <code>className</code> (the destructured prop and the one provided in the callback). Since the callback <code>className</code> has higher priority within its scope, this won't result in any actual errors in runtime, but it's probably best practice to simply pass it as <code>props.className</code> for clarity.</p>
&lt;!-- Footnotes --&gt;
<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p><a href="https://ui.shadcn.com/docs/installation/manual#add-a-cn-helper">https://ui.shadcn.com/docs/installation/manual#add-a-cn-helper</a> <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-2">
<p><a href="https://github.com/microsoft/TypeScript/issues/41746#issuecomment-737361754">https://github.com/microsoft/TypeScript/issues/41746#issuecomment-737361754</a> <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-3">
<p><a href="https://base-ui.com/react/utils/use-render#merging-props">https://base-ui.com/react/utils/use-render#merging-props</a> <a href="#user-content-fnref-3" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]>
      </content:encoded>
    </item>
    <item>
      <title>Enabling `--require-sha` on all checksum-verifiable Homebrew casks</title>
      <link>https://jeremy.ng/blog/require-sha-homebrew-cask</link>
      <description>Require SHA-256 hash on Homebrew casks when it's available</description>
      <author>Jeremy Nguyen</author>
      <category>shell</category>
      <category>homebrew</category>
      <pubDate>Sat, 08 Aug 2026 04:26:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[&lt;!-- Originally posted on https://gist.github.com/jeremy-code/4274adce9f400db8580d857472e07bbd --&gt;
<p>You may want to set <code>--require-sha</code> as a default option in your Homebrew casks for extra security, either with the environment variable <code>HOMEBREW_CASK_OPTS=&quot;--require-sha&quot;</code><sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup> or by adding <code>cask_args require_sha: true</code> to your <code>Brewfile</code>.<sup><a href="#user-content-fn-2" id="user-content-fnref-2" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup></p>
<p>As Homebrew itself has noted, casks, which use prebuilt binaries from an upstream source, have a different security model compared to formulae built by Homebrew. At the very least, enabling <code>--require-sha</code> will guarantee that the downloaded cask has not changed since it was last reviewed by a Homebrew maintainer.<sup><a href="#user-content-fn-3" id="user-content-fnref-3" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup></p>
<p>The thing is: many Homebrew casks do not have a SHA-256 checksum because their download link is not versionable.<sup><a href="#user-content-fn-4" id="user-content-fnref-4" data-footnote-ref="" aria-describedby="footnote-label">4</a></sup> The top ten casks in <a href="https://github.com/Homebrew/homebrew-cask"><code>Homebrew/homebrew-cask</code></a> that have &quot;no_check&quot; set for their SHA-256 according to their JSON analytics data as of August 7, 2026 are the following:<sup><a href="#user-content-fn-5" id="user-content-fnref-5" data-footnote-ref="" aria-describedby="footnote-label">5</a></sup></p>
&lt;!-- prettier-ignore-start --&gt;
<table>
<thead>
<tr>
<th>Cask</th>
<th>Place</th>
<th>Install Events (365 days)</th>
<th>%</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/g/google-chrome.rb#L3">google-chrome</a></td>
<td>#6</td>
<td>449,427</td>
<td>1.81%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/s/spotify.rb">spotify</a></td>
<td>#48</td>
<td>97,716</td>
<td>0.39%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/c/chromium.rb#L5">chromium</a></td>
<td>#83</td>
<td>57,790</td>
<td>0.23%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/s/steam.rb#L3">steam</a></td>
<td>#106</td>
<td>46,666</td>
<td>0.19%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/g/google-drive.rb#L3">google-drive</a></td>
<td>#119</td>
<td>39,690</td>
<td>0.16%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/l/logi-options%2B.rb#L37">logi-options+</a></td>
<td>#120</td>
<td>39,532</td>
<td>0.16%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/font/font-s/font-source-code-pro.rb#L3">font-source-code-pro</a></td>
<td>#187</td>
<td>19,784</td>
<td>0.08%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/a/anydesk.rb#L3">anydesk</a></td>
<td>#200</td>
<td>18,714</td>
<td>0.08%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/t/termius.rb#L5">termius</a></td>
<td>#233</td>
<td>15,240</td>
<td>0.06%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/o/onyx.rb#L2">onyx</a></td>
<td>#240</td>
<td>14,807</td>
<td>0.06%</td>
</tr>
<tr>
<td><a href="https://github.com/Homebrew/homebrew-cask/blob/aa45e6dcc972cce894dcd31b009f7f930f106676/Casks/g/google-gemini.rb#L3">google-gemini</a></td>
<td>#276</td>
<td>11,895</td>
<td>0.05%</td>
</tr>
</tbody>
</table>
&lt;!-- prettier-ignore-end --&gt;
&lt;!--
The data for the table above was generated with the following script:

```shell
#!/bin/zsh

# Run `wget https://formulae.brew.sh/api/analytics/cask-install/365d.json`
# beforehand to download the file

input_file=&quot;365d.json&quot;

# Remove casks from outside taps (i.e. anything containing a forward slash)
casks=(&quot;${(@f)$(jq --raw-output '.items[].cask | select(.|test(&quot;\/&quot;)|not)' &quot;$input_file&quot;)}&quot;)

# Limit to first 300 casks
input=$(brew info --quiet --cask --json=v2 &quot;${casks[@]:0:300}&quot;)

jq --raw-output '
  .casks[]
  | select(.sha256 == &quot;no_check&quot;)
  | .token
' &lt;&lt;&lt; &quot;$input&quot;
```
--&gt;
<p>For more information on this concern, see the following issue on GitHub: <a href="https://github.com/Homebrew/homebrew-cask/issues/147305">Homebrew/homebrew-cask#147305</a>. Hence, enabling <code>--require-sha</code> with any of the above casks installed will lead to this error:<sup><a href="#user-content-fn-6" id="user-content-fnref-6" data-footnote-ref="" aria-describedby="footnote-label">6</a></sup></p>
<pre><code class="language-text">...
==&gt; Verifying checksum for '89947a18e10d5bbda8e2b5d15a60b6823353f019ea1a4b74e3d9c4f91248776e--chromium.rb'
==&gt; Checking cask has checksum
Error: Cask 'chromium' does not have a sha256 checksum defined and was not installed.
This means you have the --require-sha option set, perhaps in your HOMEBREW_CASK_OPTS.
/opt/homebrew/Library/Homebrew/cask/installer.rb:177:in `verify_has_sha'
...
</code></pre>
<p>Hopefully, in the future, a feature like pnpm's <a href="https://pnpm.io/settings#trustpolicy"><code>no-downgrade</code></a> could be added, where a cask cannot be installed if it now lacks a SHA-256 checksum when it had one previously. A feature such as the aforementioned request for a per-cask option could also prevent this issue.</p>
<p>For now, to quickly enable SHA-256 checksum verification for the casks in your Brewfile that have one available, run the following command:</p>
<pre><code class="language-shell">brew info --cask --quiet --json=v2 $(brew bundle --global list --cask --quiet) | jq --raw-output '
  .casks[]
  | if .sha256 == &quot;no_check&quot; then
      &quot;cask \&quot;\(.token)\&quot; # See Homebrew/homebrew-cask#147305&quot;
    else
      &quot;cask \&quot;\(.token)\&quot;, args: { require_sha: true }&quot;
    end
'
</code></pre>
<p>This will output the casks portion of the Brewfile (minus pinning, extra arguments, etc.) into <code>stdout</code> and enable <code>require_sha</code> only on the casks that have a SHA-256 checksum.</p>
&lt;!-- Footnotes --&gt;
<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p><a href="https://docs.brew.sh/Manpage#:~:text=HOMEBREW%5FCASK%5FOPTS">https://docs.brew.sh/Manpage#:~:text=HOMEBREW%5FCASK%5FOPTS</a> <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-2">
<p><a href="https://docs.brew.sh/Brew-Bundle-and-Brewfile#advanced-brewfiles">https://docs.brew.sh/Brew-Bundle-and-Brewfile#advanced-brewfiles</a> <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-3">
<p><a href="https://docs.brew.sh/Homebrew-Security-and-Supply-Chain#casks-have-a-different-trust-model">https://docs.brew.sh/Homebrew-Security-and-Supply-Chain#casks-have-a-different-trust-model</a> <a href="#user-content-fnref-3" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-4">
<p><a href="https://github.com/Homebrew/homebrew-cask/issues/147305#issuecomment-1550490475">https://github.com/Homebrew/homebrew-cask/issues/147305#issuecomment-1550490475</a> <a href="#user-content-fnref-4" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-5">
<p><a href="https://formulae.brew.sh/analytics/cask-install/365d/">https://formulae.brew.sh/analytics/cask-install/365d/</a> <a href="#user-content-fnref-5" data-footnote-backref="" aria-label="Back to reference 5" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-6">
<p><a href="https://github.com/Homebrew/homebrew-cask/issues/147305">https://github.com/Homebrew/homebrew-cask/issues/147305</a> <a href="#user-content-fnref-6" data-footnote-backref="" aria-label="Back to reference 6" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]>
      </content:encoded>
    </item>
    <item>
      <title>Working with files/blobs in TanStack React Query</title>
      <link>https://jeremy.ng/blog/tanstack-react-query-hash-files-blobs</link>
      <description>Hashing files/blobs for proper caching in React Query</description>
      <author>Jeremy Nguyen</author>
      <category>react</category>
      <category>react-query</category>
      <pubDate>Sat, 08 Aug 2026 04:26:13 GMT</pubDate>
      <content:encoded>
        <![CDATA[&lt;!-- Originally posted on https://gist.github.com/jeremy-code/8ca2001db0b30c5935fa303727c06fe5 --&gt;
<p>If you're working with a <code>File</code> or <code>Blob</code> object in JavaScript, you can't really do much with them besides read their size and type unless you use one of its methods (e.g. <code>bytes</code>, <code>arrayBuffer</code>, <code>slice</code>, <code>stream</code>, <code>text</code>), all of which (besides <code>stream</code>) return a <code>Promise</code>.<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup></p>
<p>For me, in React, if I'm expected to handle a <code>Promise</code>, my mind gravitates to either <a href="https://github.com/tanstack/query">React Query</a> or <a href="https://github.com/vercel/swr">SWR</a>. My go-to reaction would be to write something like the following:</p>
<pre><code class="language-tsx">import { useQuery } from &quot;@tanstack/react-query&quot;;

const Component = ({ file }: { file: File }) =&gt; {
  const { data: arrayBuffer } = useQuery({
    queryKey: [&quot;Component&quot;, file],
    queryFn: () =&gt; file.arrayBuffer(),
  });
  // ...
};
</code></pre>
<p>The thing is, the part of React Query that is usually the most meaningful for queries is the ability to cache queries, which is done via a <code>queryKey</code> array.<sup><a href="#user-content-fn-2" id="user-content-fnref-2" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup> However, since query keys in React Query are serialized into strings using <code>JSON.stringify</code> by default,<sup><a href="#user-content-fn-2" id="user-content-fnref-2-2" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup> your <code>queryKey</code> would end up looking like this: <code>[&quot;Component&quot;,{}]</code>.</p>
<p>The most straightforward &quot;solution&quot; that usually ensures better caching behavior is to simply use the properties available as a <code>queryKey</code>.</p>
<pre><code class="language-tsx">import { useQuery } from &quot;@tanstack/react-query&quot;;

const serializeFile = (file: File) =&gt; {
  return {
    lastModified: file.lastModified,
    name: file.name,
    webkitRelativePath: file.webkitRelativePath,
    size: file.size,
    type: file.type,
  };
};

const Component = ({ file }: { file: File }) =&gt; {
  const { data: arrayBuffer } = useQuery({
    queryKey: [&quot;Component&quot;, serializeFile(file)],
    queryFn: () =&gt; file.arrayBuffer(),
  });
  // ...
};
</code></pre>
<p>However, this really isn't ideal for a couple of reasons. For one, in <code>Blob</code>s, only the <code>size</code> and <code>type</code> properties are available, which really aren't that unique of an identifier. <code>lastModified</code> in <code>File</code>s also just returns the current time by default, so that also may be too unique of an identifier. Furthermore, there are doubtless a number of possible collisions that can occur with this kind of setup.</p>
<p>The solution, it seems, would be to hash the <code>File</code>, somehow, and use that as the <code>queryKey</code>. An algorithm like SHA-256 would be collision-resistant and help prevent doing extra work that was already cached.</p>
<p>While <code>queryKeyHashFn</code> does exist as a property, it seems to only accept synchronous functions.<sup><a href="#user-content-fn-3" id="user-content-fnref-3" data-footnote-ref="" aria-describedby="footnote-label">3</a></sup> The vast majority of hashing functions seem to be asynchronous, including the web default <a href="https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest"><code>SubtleCrypto.digest</code></a>, so I suspect this is a non-starter.</p>
<p>My first thought was to simply use <code>useEffect</code>, which compares directly by reference.<sup><a href="#user-content-fn-4" id="user-content-fnref-4" data-footnote-ref="" aria-describedby="footnote-label">4</a></sup> Below is a more simplified version of <a href="https://github.com/jeremy-code/exifi/blob/bd8398e3ddac172ccee1564e32de86ee317b6e24/apps/client/src/hooks/useFileHash.tsx">this file</a>.</p>
<pre><code class="language-tsx">import { useEffect, useState } from &quot;react&quot;;

import { sha256 } from &quot;hash-wasm&quot;;

type UseFileHashResult = {
  fileHash: string | null;
  isPending: boolean;
  error: Error | null;
};

export const useFileHash = (
  file: File | undefined | null,
): UseFileHashResult =&gt; {
  const [fileHashState, setFileHashState] = useState&lt;UseFileHashResult&gt;(() =&gt;
    !file ?
      { fileHash: null, isPending: false, error: null }
    : { fileHash: null, isPending: true, error: null },
  );

  useEffect(() =&gt; {
    if (!file) {
      return;
    }

    const abortController = new AbortController();

    const computeHash = async () =&gt; {
      setFileHashState({ fileHash: null, isPending: true, error: null });
      try {
        const fileInBytes = await file.bytes();
        const fileHash = await sha256(fileInBytes);
        if (abortController.signal.aborted) {
          return;
        }
        setFileHashState({ fileHash, isPending: false, error: null });
      } catch (error) {
        if (abortController.signal.aborted) {
          return;
        }
        setFileHashState({
          fileHash: null,
          isPending: false,
          error: error instanceof Error ? error : new Error(String(error)),
        });
      }
    };

    void computeHash();
    return () =&gt; abortController.abort();
  }, [file]);

  return fileHashState;
};
</code></pre>
<p>In which case, you would use it like this:</p>
<pre><code class="language-tsx">import { useQuery } from &quot;@tanstack/react-query&quot;;

import { useFileHash } from &quot;./useFileHash&quot;;

const Component = ({ file }: { file: File }) =&gt; {
  const { fileHash } = useFileHash(file);
  const { data: arrayBuffer } = useQuery({
    queryKey: [&quot;Component&quot;, fileHash],
    queryFn: () =&gt; file.arrayBuffer(),
    enabled: !!fileHash,
  });

  return &lt;div&gt;{arrayBuffer ? &quot;Loaded&quot; : &quot;Loading...&quot;}&lt;/div&gt;;
};
</code></pre>
<p>It works... but I'm not really a fan. I have been mostly using React Query's <code>useSuspsenseQuery</code> where setting <code>enabled</code> is not possible.<sup><a href="#user-content-fn-5" id="user-content-fnref-5" data-footnote-ref="" aria-describedby="footnote-label">5</a></sup> Furthermore, the code is a bit too verbose and overengineered for something that should be fairly simple.</p>
<p>Alternatively, I tried using React 19's <code>use</code> hook. Since I wasn't using React Server Components, this meant I had to pass the promise from a parent component.</p>
<pre><code class="language-tsx">import { Suspense, use } from &quot;react&quot;;

import { useQuery } from &quot;@tanstack/react-query&quot;;
import { sha256 } from &quot;hash-wasm&quot;;

const Parent = ({ file }: { file: File }) =&gt; {
  const fileHashPromise = file
    .bytes()
    .then((fileInBytes) =&gt; sha256(fileInBytes));

  return (
    &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
      &lt;Child file={file} fileHashPromise={fileHashPromise} /&gt;
    &lt;/Suspense&gt;
  );
};

const Child = ({
  file,
  fileHashPromise,
}: {
  file: File;
  fileHashPromise: Promise&lt;string&gt;;
}) =&gt; {
  const fileHash = use(fileHashPromise);
  const { data: arrayBuffer } = useSuspenseQuery({
    queryKey: [&quot;Child&quot;, fileHash],
    queryFn: () =&gt; file.arrayBuffer(),
  });
  // ...
};
</code></pre>
<p>Since I already have <code>Suspense</code> around my components due to using <code>useSuspenseQuery</code>, I thought this approach was better and less intrusive. Still, it is a bit frustrating to have the strange nested structure, especially since it's only dependent on the <code>file</code> prop.</p>
<p>As a quick aside, it does raise the question: why doesn't this work?</p>
<pre><code class="language-tsx">const Child = ({ file }: { file: File }) =&gt; {
  const fileHash = use(file.bytes().then((fileInBytes) =&gt; sha256(fileInBytes)));
  // ...
};
</code></pre>
<p>If you were to do something like this, the component would infinitely re-render. More specifically, in the React documentation, it notes that &quot;Promises created in Client Components are recreated on every render.<sup><a href="#user-content-fn-6" id="user-content-fnref-6" data-footnote-ref="" aria-describedby="footnote-label">6</a></sup>&quot;</p>
<p>So, what if we were to make sure the <code>Promise</code> was stable?</p>
<p>The React documentation references creating a promise cache as a useful option for library authors,<sup><a href="#user-content-fn-7" id="user-content-fnref-7" data-footnote-ref="" aria-describedby="footnote-label">7</a></sup> so it seems like a viable pattern.</p>
<p>One concern is that using a regular <code>Map</code> would result in the File not being garbage collected even after you are done with it. However, we can instead use a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap">WeakMap</a>, which avoids this specific problem.<sup><a href="#user-content-fn-8" id="user-content-fnref-8" data-footnote-ref="" aria-describedby="footnote-label">8</a></sup></p>
<pre><code class="language-tsx">import { use } from &quot;react&quot;;

import { useSuspenseQuery } from &quot;@tanstack/react-query&quot;;
import { sha256 } from &quot;hash-wasm&quot;;

const blobHashPromiseCache = new WeakMap&lt;Blob, Promise&lt;string&gt;&gt;();

const getBlobHashPromise = (blob: Blob) =&gt; {
  let blobHashPromise = blobHashPromiseCache.get(blob);
  if (blobHashPromise === undefined) {
    blobHashPromise = blob.bytes().then((blobInBytes) =&gt; sha256(blobInBytes));
    blobHashPromiseCache.set(blob, blobHashPromise);
  }
  return blobHashPromise;
};

const Child = ({ file }: { file: File }) =&gt; {
  const fileHash = use(getBlobHashPromise(file));
  const { data: arrayBuffer } = useSuspenseQuery({
    queryKey: [&quot;Child&quot;, fileHash],
    queryFn: () =&gt; file.arrayBuffer(),
  });
  // ...
};
</code></pre>
<p>This does seem to work, and it has been what I have been using for my application. Here is my complete <a href="https://github.com/jeremy-code/exifi/blob/main/apps/client/src/hooks/useFileHash.tsx">useFileHash.tsx</a> (and accompanying <a href="https://github.com/jeremy-code/exifi/blob/main/apps/client/src/hooks/useFileHash.test.tsx">unit tests</a>) at the time of writing, if you are curious.</p>
<p>One minor caveat is that you may also want to add the properties <code>.name</code> and <code>.lastModified</code> to your <code>queryKey</code> if those are important to your processing, as this only generates a SHA-256 hash of the file's contents.</p>
<p>I can also foresee other concerns that may be meaningful. For one, <code>getBlobHashPromise</code> is not a pure function, which may lead to unpredictable behavior or bugs. Furthermore, the cache being stored at the module level may have implications or consequences. Nonetheless, for my use case, it has been working well.</p>
&lt;!-- Footnotes --&gt;
<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob#instance_methods">https://developer.mozilla.org/en-US/docs/Web/API/Blob#instance_methods</a> <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-2">
<p><a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-keys">https://tanstack.com/query/latest/docs/framework/react/guides/query-keys</a> <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a> <a href="#user-content-fnref-2-2" data-footnote-backref="" aria-label="Back to reference 2-2" class="data-footnote-backref">↩<sup>2</sup></a></p>
</li>
<li id="user-content-fn-3">
<p><a href="https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#:~:text=collection-,queryKeyHashFn">https://tanstack.com/query/latest/docs/framework/react/reference/useQuery#:~:text=collection-,queryKeyHashFn</a> <a href="#user-content-fnref-3" data-footnote-backref="" aria-label="Back to reference 3" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-4">
<p><a href="https://react.dev/reference/react/useEffect#:~:text=React%20will%20compare%20each%20dependency%20with%20its%20previous%20value%20using%20the%20Object%2Eis%20comparison%2E">https://react.dev/reference/react/useEffect#:~:text=React%20will%20compare%20each%20dependency%20with%20its%20previous%20value%20using%20the%20Object%2Eis%20comparison%2E</a> <a href="#user-content-fnref-4" data-footnote-backref="" aria-label="Back to reference 4" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-5">
<p><a href="https://tanstack.com/query/latest/docs/framework/react/guides/suspense#:~:text=you%20therefore%20can%27t%20conditionally%20enable%20%2F%20disable%20the%20Query">https://tanstack.com/query/latest/docs/framework/react/guides/suspense#:~:text=you%20therefore%20can%27t%20conditionally%20enable%20%2F%20disable%20the%20Query</a> <a href="#user-content-fnref-5" data-footnote-backref="" aria-label="Back to reference 5" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-6">
<p><a href="https://react.dev/reference/react/use#promises-must-cached">https://react.dev/reference/react/use#promises-must-cached</a> <a href="#user-content-fnref-6" data-footnote-backref="" aria-label="Back to reference 6" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-7">
<p><a href="https://react.dev/reference/react/use#how-to-implement-a-promise-cache">https://react.dev/reference/react/use#how-to-implement-a-promise-cache</a> <a href="#user-content-fnref-7" data-footnote-backref="" aria-label="Back to reference 7" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-8">
<p><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap">https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakMap</a> <a href="#user-content-fnref-8" data-footnote-backref="" aria-label="Back to reference 8" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
