<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Msotafa Mohamed</title>
    <description>The latest articles on DEV Community by Msotafa Mohamed (@mostafashadow).</description>
    <link>https://dev.to/mostafashadow</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F887374%2Fae71d42c-2a15-4829-a952-db0f05b49d04.webp</url>
      <title>DEV Community: Msotafa Mohamed</title>
      <link>https://dev.to/mostafashadow</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mostafashadow"/>
    <language>en</language>
    <item>
      <title>Your skeleton screen is lying to your users</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Tue, 27 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/mostafashadow/your-skeleton-screen-is-lying-to-your-users-26i0</link>
      <guid>https://dev.to/mostafashadow/your-skeleton-screen-is-lying-to-your-users-26i0</guid>
      <description>&lt;p&gt;Your skeleton screen is lying to your users.&lt;/p&gt;

&lt;p&gt;It was drawn to match the card six months ago. Since then the card got a badge, a second line and a bigger avatar. The skeleton didn't. So the page jumps the moment the data arrives.&lt;/p&gt;

&lt;p&gt;The usual fix is more work: keep a second layout in sync with the real one, forever. &lt;a href="https://github.com/Mostafashadow1/react-loadly" rel="noopener noreferrer"&gt;react-loadly&lt;/a&gt; 3.1 takes a different route with a new component called &lt;code&gt;Skeletonize&lt;/code&gt;. Your real component becomes its own skeleton.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Wrap the real component and give it placeholder data. While &lt;code&gt;loading&lt;/code&gt; is true, &lt;code&gt;Skeletonize&lt;/code&gt; renders it as a skeleton:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Skeletonize&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-loadly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-loadly/styles.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;placeholderUser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading role&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;avatar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Profile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;getUser&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Skeletonize&lt;/span&gt; &lt;span class="na"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UserCard&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;placeholderUser&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Skeletonize&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Text, images, icons and form controls inside &lt;code&gt;UserCard&lt;/code&gt; become skeleton blocks with the size and position of the real UI. Containers keep their background, borders and padding. If something else should become a block, add &lt;code&gt;data-skeleton&lt;/code&gt; to that element.&lt;/p&gt;

&lt;p&gt;Because the skeleton is the real component, there is no second layout to maintain. When someone adds a badge to the card, the skeleton gets the badge too, and there is no layout shift when the data arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can tune
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Skeletonize&lt;/code&gt; takes &lt;code&gt;baseColor&lt;/code&gt;, &lt;code&gt;highlightColor&lt;/code&gt; and &lt;code&gt;speed&lt;/code&gt;, and can inherit them from a &lt;code&gt;SkeletonGroupLoader&lt;/code&gt;. You can pick an animation (&lt;code&gt;shimmer&lt;/code&gt;, &lt;code&gt;wave&lt;/code&gt;, &lt;code&gt;pulse&lt;/code&gt; or &lt;code&gt;none&lt;/code&gt;) and set &lt;code&gt;loadingLabel&lt;/code&gt; for the text screen readers hear.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility
&lt;/h2&gt;

&lt;p&gt;While loading, the content is &lt;code&gt;inert&lt;/code&gt; and &lt;code&gt;aria-hidden&lt;/code&gt;. Screen readers hear one &lt;code&gt;role="status"&lt;/code&gt; message instead of your placeholder text read out loud. Animations respect &lt;code&gt;prefers-reduced-motion&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Works with the rest of your app
&lt;/h2&gt;

&lt;p&gt;Children keep their state when &lt;code&gt;loading&lt;/code&gt; toggles, so hooks and context behave normally. It works with Tailwind, SSR and the Next.js App Router, on React 17, 18 and 19 and Next.js 14, 15 and 16.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rest of the package
&lt;/h2&gt;

&lt;p&gt;react-loadly also has &lt;code&gt;PulseLoader&lt;/code&gt;, &lt;code&gt;SkeletonLoader&lt;/code&gt;, &lt;code&gt;SkeletonGroupLoader&lt;/code&gt; and &lt;code&gt;SkeletonSwitchLoader&lt;/code&gt;, which switches between loading, error, empty and content states.&lt;/p&gt;

&lt;p&gt;Version 3.1 also fixes &lt;code&gt;require()&lt;/code&gt; for CommonJS projects and makes the package much smaller. I wrote about how those bugs happened in &lt;a href="https://dev.to/mostafashadow"&gt;another article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;react-loadly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try the &lt;a href="https://mostafashadow1.github.io/react-loadly/#skeletonize" rel="noopener noreferrer"&gt;live examples&lt;/a&gt;, and tell me where it breaks on &lt;a href="https://github.com/Mostafashadow1/react-loadly" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>webdev</category>
      <category>ux</category>
    </item>
    <item>
      <title>Three bugs my npm packages shipped, and the checks that catch them now</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/mostafashadow/three-bugs-my-npm-packages-shipped-and-the-checks-that-catch-them-now-18id</link>
      <guid>https://dev.to/mostafashadow/three-bugs-my-npm-packages-shipped-and-the-checks-that-catch-them-now-18id</guid>
      <description>&lt;p&gt;I maintain four small React libraries: react-loadly, accessly, compressly and tonely. This month I found three bugs in them that my own test suites never caught. They only showed up in other people's projects.&lt;/p&gt;

&lt;p&gt;Here they are, with the check I now run before every publish.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The missing "use client"
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The symptom.&lt;/strong&gt; Someone imports &lt;code&gt;&amp;lt;PermissionProvider&amp;gt;&lt;/code&gt; in a Next.js App Router layout and gets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: createContext is not a function
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The cause.&lt;/strong&gt; In the App Router, every file is a Server Component unless it says otherwise. My React entry used &lt;code&gt;createContext&lt;/code&gt; and hooks, but nothing in the published file marked it as client code. So Next.js ran it on the server, where those APIs don't exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; The built file has to start with the directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Putting it in your source isn't enough. Bundlers often merge modules and drop directives, and minifiers can strip them too. Add it as a banner in your bundler config (with terser, use &lt;code&gt;format.preamble&lt;/code&gt; so it survives minification), and only on the entries that really need the client. Split anything that works without React into its own entry. For accessly that became &lt;code&gt;accessly/core&lt;/code&gt;, which Server Components and Route Handlers can import safely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 20 dist/react/index.js   &lt;span class="c"&gt;# must print "use client";&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then render a component from the package in a fresh &lt;code&gt;create-next-app&lt;/code&gt; project, inside a server layout.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. require() returned an empty object
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The symptom.&lt;/strong&gt; In Jest or an older SSR setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SkeletonLoader&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-loadly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The cause.&lt;/strong&gt; The package had &lt;code&gt;"type": "module"&lt;/code&gt;, and the CommonJS build was written to &lt;code&gt;index.js&lt;/code&gt;. Node decides the format of a &lt;code&gt;.js&lt;/code&gt; file from the nearest &lt;code&gt;package.json&lt;/code&gt;, so it treated my CommonJS file as an ES module, and &lt;code&gt;require()&lt;/code&gt; got nothing useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; Give CommonJS files the &lt;code&gt;.cjs&lt;/code&gt; extension and their types &lt;code&gt;.d.cts&lt;/code&gt;, then point each condition at the right file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.js"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.d.cts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.cjs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./dist/index.cjs"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The check.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"console.log(Object.keys(require('./dist/index.cjs')))"&lt;/span&gt;
npx @arethetypeswrong/cli &lt;span class="nt"&gt;--pack&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
npx publint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first line must print your export names. The other two catch wrong types and wrong &lt;code&gt;exports&lt;/code&gt; maps across &lt;code&gt;node16&lt;/code&gt;, &lt;code&gt;bundler&lt;/code&gt; and CommonJS resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A 4 MB package for 4 kB of code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The symptom.&lt;/strong&gt; No error at all, just a package that took 4 MB on every install.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cause.&lt;/strong&gt; Without a &lt;code&gt;files&lt;/code&gt; field, npm publishes everything that isn't ignored. Mine included the docs site's images. Another package published a full copy of its type declarations in every subpath folder, which turned 32 files into 972.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix.&lt;/strong&gt; An explicit allowlist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"dist"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"!dist/types"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AGENTS.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llms.txt"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The check.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm pack &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the file list every time. accessly went from 4 MB to 128 kB, and react-loadly from 972 files to 32.&lt;/p&gt;

&lt;h2&gt;
  
  
  The habit that ties it together
&lt;/h2&gt;

&lt;p&gt;All three bugs lived in the gap between my repo and a real install. My tests imported the source. My users installed a tarball. So now, before every publish, I:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;npm pack&lt;/code&gt; and install the &lt;code&gt;.tgz&lt;/code&gt; into a scratch Next.js app and a scratch CommonJS project with Jest.&lt;/li&gt;
&lt;li&gt;Render one component from a server layout, and &lt;code&gt;require()&lt;/code&gt; the package from Jest.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;publint&lt;/code&gt; and &lt;code&gt;@arethetypeswrong/cli&lt;/code&gt;.- Read the &lt;code&gt;npm pack --dry-run&lt;/code&gt; file list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It takes ten minutes, and it would have caught every one of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bonus for AI coding agents
&lt;/h2&gt;

&lt;p&gt;While fixing these, I also added an &lt;code&gt;AGENTS.md&lt;/code&gt; file to each package, with the exact API, when to use the library and the mistakes to avoid. Coding agents read it from &lt;code&gt;node_modules&lt;/code&gt;, so they stop guessing method names. If you maintain a package, it's worth an afternoon.&lt;/p&gt;

&lt;p&gt;The libraries, if you want to see the setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Mostafashadow1/react-loadly" rel="noopener noreferrer"&gt;react-loadly&lt;/a&gt;: loaders, skeletons and the new &lt;code&gt;Skeletonize&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Mostafashadow1/accessly" rel="noopener noreferrer"&gt;accessly&lt;/a&gt;: permissions for React and Next.js&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Mostafashadow1/compressly" rel="noopener noreferrer"&gt;compressly&lt;/a&gt;: image compression before upload&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Mostafashadow1/tonely" rel="noopener noreferrer"&gt;tonely&lt;/a&gt;: UI sounds without audio files&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>npm</category>
      <category>nextjs</category>
      <category>typescript</category>
    </item>
    <item>
      <title>UI sounds without a single audio file</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Sun, 25 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/mostafashadow/ui-sounds-without-a-single-audio-file-4j7m</link>
      <guid>https://dev.to/mostafashadow/ui-sounds-without-a-single-audio-file-4j7m</guid>
      <description>&lt;p&gt;Think of the apps that feel the most polished. Many of them make a small sound when a payment goes through, a message is sent or a switch flips. You don't notice the sound. You'd notice if it was missing.&lt;/p&gt;

&lt;p&gt;On the web, most of us skip it. Adding sounds usually means finding .mp3 files, hosting them, preloading them, and dealing with browsers that block audio before the user interacts with the page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Mostafashadow1/tonely" rel="noopener noreferrer"&gt;tonely&lt;/a&gt; takes a different route. It doesn't ship audio files at all. It builds each sound when it plays, from oscillators and envelopes in the Web Audio API. The whole library is under 3 kB gzip and makes no network requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basics
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tonely&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tonely&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// rises for on, falls for off&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The built-in sounds are &lt;code&gt;success&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;warning&lt;/code&gt;, &lt;code&gt;info&lt;/code&gt;, &lt;code&gt;click&lt;/code&gt;, &lt;code&gt;pop&lt;/code&gt;, &lt;code&gt;toggle&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;glass&lt;/code&gt; and &lt;code&gt;bubble&lt;/code&gt;. There are five themes (&lt;code&gt;modern&lt;/code&gt;, &lt;code&gt;glass&lt;/code&gt;, &lt;code&gt;minimal&lt;/code&gt;, &lt;code&gt;playful&lt;/code&gt; and &lt;code&gt;retro&lt;/code&gt;), and every call takes &lt;code&gt;volume&lt;/code&gt;, &lt;code&gt;pitch&lt;/code&gt;, &lt;code&gt;playbackRate&lt;/code&gt; and &lt;code&gt;theme&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;glass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;volume&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pitch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sounds that follow your API
&lt;/h2&gt;

&lt;p&gt;Most UI feedback is really "did that request work?". tonely can answer that from the HTTP status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 2xx success, 3xx info, 4xx and 5xx error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or wrap &lt;code&gt;fetch&lt;/code&gt; once and forget about it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchWithSound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wrapFetch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchWithSound&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/checkout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already have a promise, &lt;code&gt;tonely.track(promise)&lt;/code&gt; plays a sound when it settles and returns the same promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  React and Next.js
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useTonely&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tonely/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PayButton&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTonely&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;glass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
          &lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      Pay
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server, every call is a safe no-op, so importing tonely in shared code won't break SSR. Version 0.1.1 also ships &lt;code&gt;tonely/react&lt;/code&gt; with a &lt;code&gt;"use client"&lt;/code&gt; directive, so it works in App Router files without extra setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Toasts with sound
&lt;/h2&gt;

&lt;p&gt;If you use Sonner or shadcn/ui toasts, a small wrapper gives every toast a matching sound:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toast&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sonner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tonely&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tonely&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tonely&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use sound with care
&lt;/h2&gt;

&lt;p&gt;Sound is easy to overdo. A few rules I follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Play sounds for outcomes (saved, paid, failed), not for every hover or keystroke.&lt;/li&gt;
&lt;li&gt;Always offer a mute switch. &lt;code&gt;tonely.setMuted(true)&lt;/code&gt; silences everything, so it's one line to wire up to a setting.&lt;/li&gt;
&lt;li&gt;Never use sound as the only feedback. Someone with the sound off still needs to see that it worked.&lt;/li&gt;
&lt;li&gt;Browsers only allow audio after the user has interacted with the page, so trigger sounds from event handlers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;tonely
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can hear every sound and theme on the &lt;a href="https://tone-ly.vercel.app/" rel="noopener noreferrer"&gt;demo site&lt;/a&gt;. It works with React, Next.js, Vue, Svelte or plain JavaScript. Ideas for new sounds are welcome on &lt;a href="https://github.com/Mostafashadow1/tonely" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>ux</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Compress images in the browser before you upload them</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/mostafashadow/compress-images-in-the-browser-before-you-upload-them-2cla</link>
      <guid>https://dev.to/mostafashadow/compress-images-in-the-browser-before-you-upload-them-2cla</guid>
      <description>&lt;p&gt;A modern phone photo is 4000 pixels wide and several megabytes. Your avatar slot is 128 pixels. Your product gallery shows images at 1600 pixels at most.&lt;/p&gt;

&lt;p&gt;Most apps upload the full photo anyway, then resize it on the server. On a slow mobile connection, the user waits for megabytes they will never see, and you pay for the bandwidth and storage.&lt;/p&gt;

&lt;p&gt;The browser can do the resizing itself before the upload starts. That's what &lt;a href="https://github.com/Mostafashadow1/compressly" rel="noopener noreferrer"&gt;compressly&lt;/a&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The basic case
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compressImage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compressly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;small&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compressImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetSizeMB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/webp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;photo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;small&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/upload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;compressImage&lt;/code&gt; returns a regular &lt;code&gt;File&lt;/code&gt;, so the rest of your upload code doesn't change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that usually go wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sideways photos.&lt;/strong&gt; Phones often store the camera orientation in EXIF data instead of rotating the pixels. Draw that image on a canvas naively and it comes out sideways. compressly reads the orientation and rotates the image first (&lt;code&gt;autoRotate&lt;/code&gt;, on by default).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A frozen page.&lt;/strong&gt; Decoding and re-encoding a large image takes time. compressly uses a Web Worker with OffscreenCanvas where the browser supports it, so the page keeps scrolling and the progress bar keeps moving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hitting a size limit.&lt;/strong&gt; If your storage or API has a hard limit, pass &lt;code&gt;targetSizeMB&lt;/code&gt; and compressly makes up to four passes, lowering the quality and then the dimensions, to get under it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small files.&lt;/strong&gt; Files already under &lt;code&gt;maxSizeMB&lt;/code&gt; (1 MB by default) and within the max dimensions are returned as they are, so you don't lose quality for nothing. Pass &lt;code&gt;forceResize: true&lt;/code&gt; if you always want the max dimensions applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Showing the user what happened
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compressImageWithDetails&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compressly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compressImageWithDetails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;compressedSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reductionPercentage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also get &lt;code&gt;timeTakenMs&lt;/code&gt; and the final &lt;code&gt;dimensions&lt;/code&gt;, which makes a nice "saved 87%" label.&lt;/p&gt;

&lt;h2&gt;
  
  
  Many files at once
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compressImages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compressly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compressImages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onBatchProgress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;percent&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep &lt;code&gt;concurrency&lt;/code&gt; around 2 to 4 on phones, where memory is the limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  React
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useImageCompressor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compressly/react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AvatarInput&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onReady&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;onReady&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;File&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isCompressing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useImageCompressor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;
        &lt;span class="na"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"image/*"&lt;/span&gt;
        &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isCompressing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onReady&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isCompressing&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;progress&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;progress&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Version 0.1.1 ships the React entry with a &lt;code&gt;"use client"&lt;/code&gt; directive, so it works in Next.js App Router files without extra setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  One thing it doesn't do
&lt;/h2&gt;

&lt;p&gt;Client-side compression saves bandwidth and makes uploads faster. It isn't a security check, because anyone can skip your JavaScript and upload directly. Keep validating file type and size on the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;compressly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has zero dependencies and is about 2 kB gzip. Try it with your own photos on the &lt;a href="https://compress-ly.vercel.app/" rel="noopener noreferrer"&gt;demo site&lt;/a&gt;, and open an issue on &lt;a href="https://github.com/Mostafashadow1/compressly" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; if an image comes out wrong.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>webperf</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Permissions in React that explain themselves</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Fri, 23 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/mostafashadow/permissions-in-react-that-explain-themselves-9g</link>
      <guid>https://dev.to/mostafashadow/permissions-in-react-that-explain-themselves-9g</guid>
      <description>&lt;p&gt;Every app with roles ends up with the same support ticket: "I can't see the Refund button."&lt;/p&gt;

&lt;p&gt;Then someone opens the code and starts reading. Is it the role? A missing permission? A feature flag? Is the permission list still loading? The UI just shows nothing, and nothing tells you why.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/Mostafashadow1/accessly" rel="noopener noreferrer"&gt;accessly&lt;/a&gt; to make that question take seconds. It's a small permissions layer for React and Next.js, and every check returns a decision with a reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  A check that answers "why"
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;checkPermission&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatDecision&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accessly/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;u_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;support&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tickets.view&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rolePermissions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;support&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tickets.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users.view&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;checkPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payments.refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rolePermissions&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allowed: false
Reason: missing_permission
Requested: payments.refund
Missing: payments.refund
Checked from: none
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when a check passes, it tells you which rule let it through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Allowed: true
Reason: allowed
Requested: tickets.close
Matched: tickets.*
Checked from: role
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is one of &lt;code&gt;allowed&lt;/code&gt;, &lt;code&gt;missing_permission&lt;/code&gt;, &lt;code&gt;missing_flag&lt;/code&gt;, &lt;code&gt;unknown_permission&lt;/code&gt;, &lt;code&gt;not_ready&lt;/code&gt; or &lt;code&gt;invalid_request&lt;/code&gt;. That last pair matters more than it looks. While permissions are still loading, a check returns &lt;code&gt;not_ready&lt;/code&gt; instead of a plain &lt;code&gt;false&lt;/code&gt;, so you can show a spinner instead of flashing an "Access denied" page.&lt;/p&gt;

&lt;h2&gt;
  
  
  The React side
&lt;/h2&gt;

&lt;p&gt;Wrap the app once with the current user's access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PermissionProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accessly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Providers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;PermissionProvider&lt;/span&gt;
      &lt;span class="na"&gt;access&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;rolePermissions&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;admin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts.*&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;PermissionProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then gate UI with components or hooks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Can&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;usePermissions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useAccessDecision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;formatDecision&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accessly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Can&lt;/span&gt; &lt;span class="na"&gt;permission&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"posts.delete"&lt;/span&gt; &lt;span class="na"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Delete&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Can&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;canEdit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canPublish&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePermissions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;canEdit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts.edit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;canPublish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts.edit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;posts.publish&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refund&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAccessDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payments.refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;formatDecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refund&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permissions are dot-separated strings. Wildcards work per segment: &lt;code&gt;users.*&lt;/code&gt; matches &lt;code&gt;users.create&lt;/code&gt; but not &lt;code&gt;users.profile.edit&lt;/code&gt;, and &lt;code&gt;*&lt;/code&gt; matches everything. A check can be a single permission, &lt;code&gt;{ any: [...] }&lt;/code&gt;, &lt;code&gt;{ all: [...] }&lt;/code&gt; or &lt;code&gt;{ flag: "..." }&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  New in 0.2: the same rules on the server
&lt;/h2&gt;

&lt;p&gt;The main &lt;code&gt;accessly&lt;/code&gt; entry is React. Until 0.2, that meant you couldn't reuse your rules in a Server Component or a Route Handler. Now there is &lt;code&gt;accessly/core&lt;/code&gt;, with no React import at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/refunds/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createAccessChecker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;accessly/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAccessChecker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;rolePermissions&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;access&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;can&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payments.refund&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also has &lt;code&gt;filterNavigation&lt;/code&gt;, which takes your menu items and returns only the ones the user can open. That way the sidebar and the route guard use the same rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;accessly controls what your UI shows. It doesn't replace authorization on your API, so keep enforcing permissions on the server too. With &lt;code&gt;accessly/core&lt;/code&gt; you can at least use the same rule definitions in both places. It also isn't a policy engine for conditions like "only the author can edit their own post". For that, use CASL or a dedicated policy engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;accessly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has zero dependencies and is about 4 kB gzip. The &lt;a href="https://accessly-website.vercel.app/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; cover adapters for different backend shapes, &lt;code&gt;ProtectedRoute&lt;/code&gt;, and debugging with &lt;code&gt;inspectAccess&lt;/code&gt;. Issues and ideas are welcome on &lt;a href="https://github.com/Mostafashadow1/accessly" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Writing Clunky if/else Checks: Scalable Access Control (RBAC) in React &amp; Next.js</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Sun, 11 Oct 2026 17:05:00 +0000</pubDate>
      <link>https://dev.to/mostafashadow/stop-writing-clunky-ifelse-checks-scalable-access-control-rbac-in-react-nextjs-3h43</link>
      <guid>https://dev.to/mostafashadow/stop-writing-clunky-ifelse-checks-scalable-access-control-rbac-in-react-nextjs-3h43</guid>
      <description>&lt;p&gt;Building enterprise web applications or multi-tenant SaaS dashboards almost always leads to a complex challenge: Role-Based Access Control (RBAC).&lt;/p&gt;

&lt;p&gt;As applications grow, embedding raw conditional statements like if (user.role === 'admin') directly inside UI components rapidly creates technical debt. It scatters authorization logic across your codebase, making audits and policy changes a maintenance nightmare.&lt;/p&gt;

&lt;p&gt;To solve this, I built Accessly — a lightweight, zero-dependency access-control library built for React and Next.js applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Architecture Highlights:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvvvur43w2gukwgy6xyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvvvur43w2gukwgy6xyv.png" alt=" " width="800" height="339"&gt;&lt;/a&gt;&lt;br&gt;
Declarative Control: Clean UI components ( / ) to conditionally render elements based on permissions.&lt;/p&gt;

&lt;p&gt;Wildcard Matching: Support for granular permissions like reports:* or users:delete without manual mapping.&lt;/p&gt;

&lt;p&gt;Explainable Authorization: Built-in .explain() decisions so developers can instantly debug why a user was allowed or denied access during runtime.&lt;/p&gt;

&lt;p&gt;Framework Agnostic Foundations: Seamlessly integrates with SSR, Next.js Middleware, and App Router.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/accessly" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/accessly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://accessly-website.vercel.app/" rel="noopener noreferrer"&gt;https://accessly-website.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  javascript, #react, #webdev, #opensource, #architecture
&lt;/h1&gt;

</description>
      <category>architecture</category>
      <category>nextjs</category>
      <category>react</category>
      <category>security</category>
    </item>
    <item>
      <title>How Web Workers &amp; OffscreenCanvas Made Client-Side Image Compression 10x Faster</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Sun, 11 Oct 2026 07:04:25 +0000</pubDate>
      <link>https://dev.to/mostafashadow/how-web-workers-offscreencanvas-made-client-side-image-compression-10x-faster-3gbf</link>
      <guid>https://dev.to/mostafashadow/how-web-workers-offscreencanvas-made-client-side-image-compression-10x-faster-3gbf</guid>
      <description>&lt;p&gt;Allowing users to upload uncompressed 15MB smartphone photos directly to your cloud storage destroys bandwidth, increases server costs, and delivers a sluggish User Experience (UX).&lt;/p&gt;

&lt;p&gt;Traditional client-side compression libraries execute heavy pixel manipulations on the Main Thread, freezing the DOM and causing noticeable UI lag.&lt;/p&gt;

&lt;p&gt;To address this performance bottleneck, I created Compressly — a zero-dependency, micro-sized (&amp;lt;3KB) image compression library that offloads processing away from the main thread.&lt;/p&gt;

&lt;p&gt;How It Works Under the Hood:&lt;/p&gt;

&lt;p&gt;Web Worker Offloading: Offloads heavy mathematical image transformations to a background Web Worker thread, keeping the main thread running smoothly at 60 FPS.&lt;/p&gt;

&lt;p&gt;OffscreenCanvas API: Utilizes hardware-accelerated canvas rendering directly inside worker threads without DOM dependencies.&lt;/p&gt;

&lt;p&gt;Smart Formatting: Auto-converts heavy JPEGs/PNGs into optimized WebP formats while preserving EXIF orientations.&lt;/p&gt;

&lt;p&gt;Performance Impact:&lt;/p&gt;

&lt;p&gt;File Reduction: Converts a 14.2 MB raw smartphone photo down to ~380 KB in under 800ms.&lt;/p&gt;

&lt;p&gt;Main Thread Blocking: 0ms (completely asynchronous).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
&lt;span class="nx"&gt;TypeScript&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;compressImage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;compressly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;optimizedFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;compressImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1920&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;convertFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Offload compression to the client without sacrificing frame rates.&lt;/p&gt;

&lt;p&gt;🔗 Try Compressly on npm: npmjs.com/package/compressly&lt;/p&gt;

&lt;h1&gt;
  
  
  javascript, #react, #webdev, #opensource, #architecture
&lt;/h1&gt;

</description>
      <category>react</category>
      <category>vue</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Eliminating Layout Shifts: Modern Skeleton Loaders and Deferred UI States in React</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Sun, 11 Oct 2026 07:00:27 +0000</pubDate>
      <link>https://dev.to/mostafashadow/eliminating-layout-shifts-modern-skeleton-loaders-and-deferred-ui-states-in-react-863</link>
      <guid>https://dev.to/mostafashadow/eliminating-layout-shifts-modern-skeleton-loaders-and-deferred-ui-states-in-react-863</guid>
      <description>&lt;p&gt;A poor loading experience directly harms user retention. Replacing sudden layout shifts or generic full-screen spinners with subtle, skeleton placeholders improves perceived application performance by up to 40%.&lt;/p&gt;

&lt;p&gt;However, implementing skeleton loaders often introduces subtle bugs, such as UI flickering when API responses resolve too fast (e.g., under 100ms).&lt;/p&gt;

&lt;p&gt;I built React Loadly to provide a production-ready toolkit for managing complex loading states, skeleton screens, and fluid transitions in React.&lt;/p&gt;

&lt;p&gt;Core Features:&lt;/p&gt;

&lt;p&gt;Zero-Flicker Hooks: useDeferredLoading delays skeleton displays until an API request exceeds a specified threshold (e.g., 200ms), eliminating jarring UI flashes on fast connections.&lt;/p&gt;

&lt;p&gt;Accessible Skeletons: Built-in ARIA support (aria-busy, aria-live) to ensure accessibility for screen readers.&lt;/p&gt;

&lt;p&gt;Adaptive Theme Support: Automatic dark/light mode adaptations with customizable pulse and wave animations.&lt;/p&gt;

&lt;p&gt;TypeScript&lt;/p&gt;

&lt;p&gt;import { Skeleton, useDeferredLoading } from 'react-loadly';&lt;/p&gt;

&lt;p&gt;function UserProfile({ isLoading, data }) {&lt;br&gt;
  const showSkeleton = useDeferredLoading(isLoading, { delay: 200 });&lt;/p&gt;

&lt;p&gt;if (showSkeleton) return ;&lt;br&gt;
  return ;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Upgrade your application's loading experience effortlessly.&lt;/p&gt;

&lt;p&gt;🔗 Explore React Loadly on npm: npmjs.com/package/react-loadly&lt;/p&gt;

&lt;h1&gt;
  
  
  javascript, #react, #webdev, #opensource, #architecture
&lt;/h1&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>nextjs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Synthesizing Procedural UI Micro-Interactions via the Web Audio API</title>
      <dc:creator>Msotafa Mohamed</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:59:21 +0000</pubDate>
      <link>https://dev.to/mostafashadow/synthesizing-procedural-ui-micro-interactions-via-the-web-audio-api-3hfd</link>
      <guid>https://dev.to/mostafashadow/synthesizing-procedural-ui-micro-interactions-via-the-web-audio-api-3hfd</guid>
      <description>&lt;p&gt;Tactile, auditory feedback significantly elevates web user interactions—think of the subtle "click" of a button, the crisp notification pop, or a success chime on task completion.&lt;/p&gt;

&lt;p&gt;However, loading external audio assets (MP3s/WAVs) introduces extra HTTP requests, network latency, and browser autoplay restriction headaches.&lt;/p&gt;

&lt;p&gt;I developed Tonely to synthesize UI micro-interaction sound effects procedurally in real-time directly inside the browser using the native Web Audio API—requiring zero external audio files.&lt;/p&gt;

&lt;p&gt;Why Procedural Audio?&lt;/p&gt;

&lt;p&gt;Zero Asset Overhead: No MP3 downloads, no asset hosting, and zero network latency.&lt;/p&gt;

&lt;p&gt;Instantaneous Playback: Synthesized instantly via Web Audio Oscillators for 0ms trigger latency.&lt;/p&gt;

&lt;p&gt;Microscopic Footprint: Adds virtually no bundle size to your application.&lt;/p&gt;

&lt;p&gt;Global Mute &amp;amp; Pitch Controls: Easily configurable volume and pitch controls to respect user preference settings.&lt;/p&gt;

&lt;p&gt;TypeScript&lt;/p&gt;

&lt;p&gt;import { tonely } from 'tonely';&lt;/p&gt;

&lt;p&gt;// Trigger subtle UI feedback on button click&lt;br&gt;
const handleButtonClick = () =&amp;gt; {&lt;br&gt;
  tonely.play('pop');&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;// Trigger procedural success chime&lt;br&gt;
const handleFormSubmit = () =&amp;gt; {&lt;br&gt;
  tonely.play('success');&lt;br&gt;
};&lt;/p&gt;

&lt;p&gt;Add tactile polish to your web applications without fetching a single audio file.&lt;/p&gt;

&lt;p&gt;🔗 Discover Tonely on npm: npmjs.com/package/tonely&lt;/p&gt;

&lt;h1&gt;
  
  
  javascript, #react, #webdev, #opensource, #architecture
&lt;/h1&gt;

</description>
      <category>react</category>
      <category>programming</category>
      <category>architecture</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
