<?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>Forem: Chao Fang</title>
    <description>The latest articles on Forem by Chao Fang (@hitsu91).</description>
    <link>https://forem.com/hitsu91</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3526395%2F7f9f41ea-b73e-4d1c-a6a2-511521a28a73.jpg</url>
      <title>Forem: Chao Fang</title>
      <link>https://forem.com/hitsu91</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/hitsu91"/>
    <language>en</language>
    <item>
      <title>Improving Data Fetching in Next.js: Lessons from Moving Beyond useEffect</title>
      <dc:creator>Chao Fang</dc:creator>
      <pubDate>Mon, 16 Mar 2026 16:19:02 +0000</pubDate>
      <link>https://forem.com/subito/improving-data-fetching-in-nextjs-lessons-from-moving-beyond-useeffect-4a2i</link>
      <guid>https://forem.com/subito/improving-data-fetching-in-nextjs-lessons-from-moving-beyond-useeffect-4a2i</guid>
      <description>&lt;p&gt;At &lt;strong&gt;Subito&lt;/strong&gt; (Italy's leading classifieds platform), we never paid much attention to how we made API calls in our &lt;a href="https://dev.to/subito/from-independent-microsites-to-context-driven-architecture-5166"&gt;web frontend microsites&lt;/a&gt;, all built on &lt;a href="https://nextjs.org/" rel="noopener noreferrer"&gt;Next.js&lt;/a&gt;. We had a simple library based on &lt;a href="https://axios-http.com/" rel="noopener noreferrer"&gt;Axios&lt;/a&gt; that handled everything: making requests, modeling data, and managing errors.&lt;/p&gt;

&lt;p&gt;On the server side, it was a simple call; on the client side, we used &lt;code&gt;useEffect&lt;/code&gt; with state management for data and errors. It worked, until we started asking: &lt;strong&gt;is this still the right approach?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wake-Up Call: The Cloudflare Incident
&lt;/h2&gt;

&lt;p&gt;The turning point was reading about the &lt;a href="https://blog.cloudflare.com/deep-dive-into-cloudflares-sept-12-dashboard-and-api-outage/" rel="noopener noreferrer"&gt;Cloudflare outage&lt;/a&gt; caused by excessive API calls triggered by &lt;code&gt;useEffect&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It made us reflect on our own stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are we over-fetching?&lt;/li&gt;
&lt;li&gt;Are we accidentally triggering duplicate requests?&lt;/li&gt;
&lt;li&gt;Could we overload our backend without realizing it?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Mission
&lt;/h2&gt;

&lt;p&gt;Our primary goal was simple: &lt;strong&gt;embrace the standard&lt;/strong&gt;! We wanted to align perfectly with the official recommendations from React and Next.js.&lt;/p&gt;

&lt;p&gt;Since our architecture relies on the Next.js App Router to consume REST APIs, we needed a clean, well-defined data-fetching strategy that clearly distinguishes between React Server Components and Client Components.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Server Side: embracing the standard
&lt;/h2&gt;

&lt;p&gt;For Server Components, the decision was surprisingly easy: Next.js extends the native &lt;code&gt;fetch&lt;/code&gt; API, adding powerful caching and revalidation features out of the box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we did:&lt;/strong&gt; we replaced all Axios calls in our Server Components with the native &lt;a href="https://nextjs.org/docs/app/api-reference/functions/fetch" rel="noopener noreferrer"&gt;fetch API extended by Next.js&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This "simple" switch allowed us to remove Axios entirely from our code, a library that, while historic, adds ~13kb to the bundle and has had &lt;strong&gt;multiple security advisories over the years&lt;/strong&gt; (&lt;em&gt;Axios served us well for years and played a huge role in making HTTP requests easier across the JavaScript ecosystem; but it was time to move on&lt;/em&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Client Side: the great debate (React Query vs. SWR)
&lt;/h2&gt;

&lt;p&gt;While the server side was easy, the client side required a comparison between two libraries: &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;TanStack Query (React Query)&lt;/a&gt; and &lt;a href="https://swr.vercel.app/" rel="noopener noreferrer"&gt;SWR&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The React Query Experiment
&lt;/h3&gt;

&lt;p&gt;We started with &lt;a href="https://tanstack.com/query/latest" rel="noopener noreferrer"&gt;React Query&lt;/a&gt;: it has gained massive popularity in the React ecosystem, and for good reason!&lt;br&gt;
It's incredibly powerful, a true "server state" manager; we had to try it!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What we found:&lt;/strong&gt; for our use case it felt like overkill: it requires a &lt;code&gt;QueryClient&lt;/code&gt;, a &lt;code&gt;Provider&lt;/code&gt;, and careful &lt;code&gt;queryKey&lt;/code&gt; management.&lt;br&gt;
The learning curve was steeper than expected, and integrating it into our existing microsites wasn't trivial. The mental shift and boilerplate needed slowed down adoption.&lt;br&gt;
On top of that, we don't really need mutations or cache invalidation on the client since our caching is handled server-side by our backend REST APIs.&lt;/p&gt;
&lt;h3&gt;
  
  
  The SWR "Toy" that Could
&lt;/h3&gt;

&lt;p&gt;Then we tried &lt;strong&gt;&lt;a href="https://swr.vercel.app/" rel="noopener noreferrer"&gt;SWR&lt;/a&gt;&lt;/strong&gt; (Stale-While-Revalidate) by Vercel.&lt;br&gt;
One of our colleagues jokingly called it a "giocattolino" ("a little toy" in italian) because of how simple it is. But for our use case, it was perfect!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero Boilerplate:&lt;/strong&gt; No mandatory providers. Just a hook: &lt;a href="https://swr.vercel.app/docs/getting-started" rel="noopener noreferrer"&gt;&lt;code&gt;useSWR(key, fetcher)&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low Learning Curve:&lt;/strong&gt; It felt like a natural extension of the platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-only by design:&lt;/strong&gt; SWR is focused solely on Client Components, exactly what we needed, since we already solved server-side fetching with native fetch&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The Migration
&lt;/h2&gt;

&lt;p&gt;The move from &lt;code&gt;useEffect&lt;/code&gt; to &lt;code&gt;SWR&lt;/code&gt; felt like cleaning a cluttered room. Here's a real example from our codebase:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (useEffect):&lt;/strong&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setItems&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AdItem&lt;/span&gt;&lt;span class="o"&gt;&amp;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="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="nx"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&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;// load the Ads&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&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;getRecommendedItems&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setItems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setIsLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="nx"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After (SWR):&lt;/strong&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&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;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;SWR_KEYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recommender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="nx"&gt;fetchRecommendedItems&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We deleted two &lt;code&gt;useState&lt;/code&gt; declarations and replaced a bulky &lt;code&gt;useEffect&lt;/code&gt; block with a single, declarative hook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Centralized Key Management
&lt;/h3&gt;

&lt;p&gt;To avoid "magic strings" and ensure consistent caching, we centralized our SWR keys:&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;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SWR_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;recommender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;vertical&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recommender&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="s1"&gt;items&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vertical&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;userId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;Simple, type-safe, and predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Without the Headache
&lt;/h3&gt;

&lt;p&gt;Since SWR caches globally, we developed a utility to ensure our unit tests stay isolated:&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;render&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;@testing-library/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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SWRConfig&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;swr&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;renderWithSWR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ui&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactElement&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SWRConfig&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="na"&gt;dedupingInterval&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="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&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;new&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;()&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;ui&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/SWRConfig&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Usage: renderWithSWR(&amp;lt;RecommenderWidget /&amp;gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By moving away from manual &lt;code&gt;useEffect&lt;/code&gt; fetching and adopting the &lt;strong&gt;SWR + Native Fetch&lt;/strong&gt; combo, we've achieved cleaner code and a much more React-standard way to handle data fetching.&lt;/p&gt;

&lt;p&gt;Sometimes the simplest tool is the most effective one. Our use case was clear: few mutations on the client side, no shared cache needed between server and client, and no client-side caching required since we're a pure frontend team calling REST services that handle their own caching.&lt;/p&gt;

&lt;p&gt;For this scenario, SWR turned out to be the most immediate, simple, and fitting solution. No over-engineering, just the right tool for the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The big takeaway:&lt;/strong&gt; don't just follow the hype. React Query is an amazing library, if you need complex cache invalidation, optimistic updates, or tight server-client state synchronization, it's probably the right choice. For us, it wasn't. &lt;strong&gt;Understand your actual requirements first, then pick the tool that fits, not the other way around&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>swr</category>
      <category>react</category>
    </item>
    <item>
      <title>Evolving Our UI Library: From Custom Components to a Hybrid Radix Approach</title>
      <dc:creator>Chao Fang</dc:creator>
      <pubDate>Tue, 07 Oct 2025 13:37:00 +0000</pubDate>
      <link>https://forem.com/subito/evolving-our-ui-library-from-custom-components-to-a-hybrid-radix-approach-448f</link>
      <guid>https://forem.com/subito/evolving-our-ui-library-from-custom-components-to-a-hybrid-radix-approach-448f</guid>
      <description>&lt;p&gt;&lt;em&gt;How &lt;a href="https://www.subito.it/" rel="noopener noreferrer"&gt;subito.it&lt;/a&gt;, Italy’s leading online classifieds platform, navigated the complexities of UI component libraries, from building everything in-house, to embracing open-source solutions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Building and maintaining an internal UI component library is one of those challenges that seems straightforward until you're deep into it. &lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.subito.it/" rel="noopener noreferrer"&gt;subito&lt;/a&gt;, we've experienced this journey firsthand, evolving from building custom components in-house, to experimenting with external &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwindcss&lt;/a&gt;  (often simply referred to as Tailwind) based libraries like &lt;a href="https://github.com/leboncoin/spark-web" rel="noopener noreferrer"&gt;Spark&lt;/a&gt;, and ultimately adopting a hybrid approach centered around &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;This is the story of our lessons learned and the pragmatic decisions that shaped our current approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Our Own: The Custom Component Challenge
&lt;/h2&gt;

&lt;p&gt;Initially, we wanted to build our own components starting from native HTML elements as a foundation. &lt;/p&gt;

&lt;p&gt;We had specific design requirements and wanted full control over our component behavior while building on top of standard browser elements like &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;However, we quickly discovered that building truly accessible and cross-browser compatible components is far more complex than it appears on the surface.&lt;/p&gt;

&lt;p&gt;The main challenges we encountered included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Browser inconsistencies&lt;/strong&gt;: What works perfectly in Chrome might behave differently in Safari or Firefox&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accessibility requirements&lt;/strong&gt;: Implementing proper ARIA attributes, keyboard navigation, and screen reader support requires deep expertise&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance optimization&lt;/strong&gt;: Ensuring components perform well across different devices and scenarios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance overhead&lt;/strong&gt;: Every custom component becomes a long-term commitment that needs updates, bug fixes, and feature enhancements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We realized that maintaining these internal components was becoming expensive both in terms of development time and ongoing support.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tailwind-Based Library Experiment
&lt;/h2&gt;

&lt;p&gt;Recognizing the need for a more sustainable approach, we began looking for external solutions. &lt;/p&gt;

&lt;p&gt;Our first attempt involved adopting a Tailwind-based library called &lt;a href="https://github.com/leboncoin/spark-web" rel="noopener noreferrer"&gt;Spark&lt;/a&gt;, developed by our colleagues at &lt;a href="https://www.leboncoin.fr/" rel="noopener noreferrer"&gt;leboncoin&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This seemed like a natural choice given that &lt;a href="https://www.leboncoin.fr/" rel="noopener noreferrer"&gt;leboncoin&lt;/a&gt; faced similar business requirements and design challenges, plus, the library offered a solid technical foundation and a comprehensive component set.&lt;/p&gt;

&lt;p&gt;Integrating this Tailwind-based library into our existing ecosystem proved more challenging than anticipated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application-Specific Design&lt;/strong&gt;: It was designed primarily for complete applications rather than as a foundational library for building other UI libraries, as in our case, where we maintain a UI library &lt;a href="https://dev.to/subito/from-independent-microsites-to-context-driven-architecture-5166"&gt;used across many microsites&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tailwind Adoption Challenge&lt;/strong&gt;: Adopting the Tailwind-based library forced us to integrate &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; into our web application, even though we hadn't officially approved it as a development tool.&lt;/p&gt;

&lt;p&gt;We were implicitly opening the doors to its usage throughout the codebase.&lt;br&gt;
This generated significant confusion among developers who were left wondering: "&lt;em&gt;Can I now use &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; utilities throughout the codebase, or should I continue writing CSS the traditional way?&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;Additionally, integrating &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; forced us to modify our existing CSS reset and baseline styles to make them coexist with Tailwindcss's own reset styles, adding another layer of complexity to our styling architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Path to Radix: Finding the Right Foundation
&lt;/h2&gt;

&lt;p&gt;After analyzing the complexities introduced by the Tailwind-based library approach, we decided to step back and reconsider our strategy. This led us to &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; as our core solution.&lt;/p&gt;

&lt;p&gt;It proved to be the ideal foundation for several reasons:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maturity and Reliability&lt;/strong&gt;: is an incredibly mature library with a strong track record of stability and performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility First&lt;/strong&gt;: Built with accessibility as a core principle, it handles all the complex ARIA attributes, keyboard navigation, and screen reader support that we struggled with in our custom components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community and Contribution&lt;/strong&gt;: The open-source nature allows us to contribute back to the project and benefit from a large community of developers and maintainers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-Friendly Development&lt;/strong&gt;: As a widely adopted library, it has excellent AI tooling support. Its popularity means that AI agents and coding assistants have extensive knowledge of &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; patterns, making it incredibly easy to build solutions on top of it with AI assistance.&lt;/p&gt;

&lt;p&gt;However, we noticed that &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; and our UX needs are not always perfectly aligned, so we decided to adopt a hybrid strategy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Hybrid Strategy: Radix + Selective Integration
&lt;/h2&gt;

&lt;p&gt;Thanks to &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; modular nature, we are not forced to adopt the entire library but can include only the components we truly need.  &lt;/p&gt;

&lt;p&gt;This gives us the flexibility to rely on &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; where it provides the most value, while integrating other specialized libraries whenever a specific component offers a better fit for our expectations.  &lt;/p&gt;

&lt;p&gt;In this way, we embrace a hybrid strategy that combines the robustness of &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; with the freedom to choose the most suitable solution for each case.&lt;/p&gt;

&lt;p&gt;For example, since &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; did not provide a dropdown that fully met our needs, we chose to integrate &lt;a href="https://react-select.com/" rel="noopener noreferrer"&gt;React Select&lt;/a&gt; instead of building our own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Our journey in building a UI library highlighted some key lessons:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating truly accessible components in-house is costly and time consuming, so leveraging open-source solutions is often the smarter move.
&lt;/li&gt;
&lt;li&gt;If you’re not ready to fully commit to &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind CSS&lt;/a&gt;, avoid libraries tightly coupled with it, mixing approaches usually adds complexity instead of value.
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.radix-ui.com/" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; proved to be a fantastic foundation: it offers mature, accessible, and behavior-driven components without dictating styling. &lt;em&gt;We read about the &lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix&lt;/a&gt; maintainers moving on, but we agree with what the author of Shadcn pointed out &lt;a href="https://x.com/shadcn/status/1936082723904565435" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Mixing and matching the best tools from different open-source libraries, helped us strike a balance between control, quality, and maintainability.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Beyond technology, this journey also sparked a &lt;strong&gt;process shift&lt;/strong&gt;: while our UX/design team used to propose brand-new components, the focus is now on identifying the most suitable ones already available in the open-source ecosystem, contributing when possible, and adapting them to our design system.&lt;/p&gt;

&lt;p&gt;This change transforms the relationship between design and engineering:  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;before&lt;/strong&gt;, the model was focused on creation,
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;now&lt;/strong&gt;, it’s increasingly about curation, research, and adaptation.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of the day, the goal is simple: &lt;strong&gt;deliver great user experiences without reinventing every wheel along the way.&lt;/strong&gt;  &lt;/p&gt;

</description>
      <category>radix</category>
      <category>tailwindcss</category>
      <category>css</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
