<?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: Israel Michael</title>
    <description>The latest articles on DEV Community by Israel Michael (@mikelisrael).</description>
    <link>https://dev.to/mikelisrael</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%2F1916107%2F28a62a98-2e6c-460f-9376-11fb1b967710.png</url>
      <title>DEV Community: Israel Michael</title>
      <link>https://dev.to/mikelisrael</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikelisrael"/>
    <language>en</language>
    <item>
      <title>Ilere: Building a Transparent Rental Marketplace on Expo and Supabase</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Thu, 09 Apr 2026 01:25:02 +0000</pubDate>
      <link>https://dev.to/mikelisrael/ilere-building-a-transparent-rental-marketplace-on-expo-and-supabase-2207</link>
      <guid>https://dev.to/mikelisrael/ilere-building-a-transparent-rental-marketplace-on-expo-and-supabase-2207</guid>
      <description>&lt;p&gt;Ilere is a mobile housing marketplace aimed at Nigerian cities such as Ibadan, Lagos, Abuja, and Port Harcourt. The core idea is visible in the code: tenants browse listings with explicit fee breakdowns, open a listing to competitive bids from agents, choose the best offer, and continue the conversation in-app. The real problem is not just discovery. It is making the opaque part of the rental market, especially agent pricing and coordination, visible enough to compare.&lt;/p&gt;

&lt;p&gt;The current codebase also goes beyond that loop. Tenants can save homes, contact landlord listings directly, receive in-app and push notifications, and report or block abusive users. Agents and landlords can post listings. The result is a fairly opinionated mobile product with real workflow boundaries baked into both the frontend and the database.&lt;/p&gt;

&lt;p&gt;The architecture is straightforward in a good way. &lt;code&gt;src/app/_layout.tsx&lt;/code&gt; is the actual shell of the app: it loads fonts, holds a shared &lt;code&gt;QueryClient&lt;/code&gt;, hydrates auth state through &lt;code&gt;authService.getCurrentUser()&lt;/code&gt;, subscribes to auth changes, registers notification handlers, and mounts the toast host. Navigation is file-based with Expo Router under &lt;code&gt;src/app&lt;/code&gt;, so route structure and product structure mostly line up. The route groups are meaningful: &lt;code&gt;(auth)&lt;/code&gt; for onboarding and sign-in, &lt;code&gt;(tabs)&lt;/code&gt; for the primary app shell, and detail routes like &lt;code&gt;house/[id].tsx&lt;/code&gt;, &lt;code&gt;request/[id]/bids.tsx&lt;/code&gt;, and &lt;code&gt;chat/[id].tsx&lt;/code&gt; for deeper flows.&lt;/p&gt;

&lt;p&gt;State management is intentionally split. Global client state is almost nonexistent. &lt;code&gt;src/features/auth/store.ts&lt;/code&gt; uses Zustand only for the user object plus two flags: &lt;code&gt;isLoading&lt;/code&gt; and &lt;code&gt;isResettingPassword&lt;/code&gt;. Everything else is treated as server state and lives in React Query. That is the right call for this app. Houses, requests, bids, chats, messages, notifications, and saved homes all originate in Supabase and often change underneath the client because of realtime subscriptions, so keeping them in a client store would have created unnecessary duplication.&lt;/p&gt;

&lt;p&gt;The API layer follows a clean pattern: screens call feature hooks, hooks call service modules in &lt;code&gt;src/services/supabase/*&lt;/code&gt;, and the service modules own the actual Supabase queries. &lt;code&gt;src/services/supabase/client.ts&lt;/code&gt; sets up the typed Supabase client and persists auth in AsyncStorage:&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;supabase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Database&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;supabaseUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;supabaseAnonKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AsyncStorage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;autoRefreshToken&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="na"&gt;persistSession&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="na"&gt;detectSessionInUrl&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation pays off because most interesting behavior is not a plain fetch. The service layer uses relational selects heavily. &lt;code&gt;housesService.getHouses()&lt;/code&gt; joins &lt;code&gt;house_photos&lt;/code&gt; and the posting user in one round trip, while &lt;code&gt;src/services/supabase/chat.ts&lt;/code&gt; loads both participants plus recent messages and derives &lt;code&gt;last_message&lt;/code&gt; and &lt;code&gt;unread_count&lt;/code&gt; on the client. Supabase is acting less like a raw database client and more like a backend query surface.&lt;/p&gt;

&lt;p&gt;One small but telling implementation detail is in &lt;code&gt;authService.onAuthStateChange()&lt;/code&gt; inside &lt;code&gt;src/services/supabase/auth.ts&lt;/code&gt;. The callback intentionally stays synchronous and defers follow-up work with &lt;code&gt;setTimeout&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onAuthStateChange&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;event&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&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="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;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&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="nf"&gt;select&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&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;span class="mi"&gt;0&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;That is a pragmatic workaround for Supabase’s warning about deadlocks when auth state handlers call back into Supabase synchronously.&lt;/p&gt;

&lt;p&gt;The listing feed shows a similar level of defensive implementation. &lt;code&gt;housesService.getHouses()&lt;/code&gt; in &lt;code&gt;src/services/supabase/houses.ts&lt;/code&gt; sanitizes the free-text query before building a PostgREST &lt;code&gt;.or(...)&lt;/code&gt; filter:&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;function&lt;/span&gt; &lt;span class="nf"&gt;sanitizeSearchTerm&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="kr"&gt;string&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="nx"&gt;input&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-zA-Z0-9&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/g&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;That is paired with search debouncing in &lt;code&gt;src/app/(tabs)/index.tsx&lt;/code&gt;, where the home screen uses &lt;code&gt;useDebounce(search, 350)&lt;/code&gt; before pushing the query into filters. Combined with &lt;code&gt;useInfiniteQuery&lt;/code&gt; in &lt;code&gt;src/features/houses/hooks/useHouses.ts&lt;/code&gt;, the feed avoids hammering the backend while still feeling responsive.&lt;/p&gt;

&lt;p&gt;The most interesting data flow starts when a tenant opens a listing to bids. In &lt;code&gt;src/app/house/[id].tsx&lt;/code&gt;, the screen computes fee breakdowns locally with &lt;code&gt;housesService.getFeeBreakdown()&lt;/code&gt;, checks whether the listing is direct-contact-only for landlords, and uses &lt;code&gt;useCreateRequest()&lt;/code&gt; to open a request. That mutation lands in &lt;code&gt;requestsService.createRequest()&lt;/code&gt; in &lt;code&gt;src/services/supabase/requests.ts&lt;/code&gt;, which checks availability, rejects landlord listings for the bidding flow, guards against duplicate active requests, and tolerates a race by refetching if the database unique constraint trips.&lt;/p&gt;

&lt;p&gt;Selecting a winning bid is another good example of business logic spread across layers. &lt;code&gt;requestsService.selectBid()&lt;/code&gt; updates the request, marks the chosen bid as accepted, fetches the winning agent, and then calls &lt;code&gt;chatService.createChat(tenantId, bid.agent_id, requestId)&lt;/code&gt;. The product transition from “marketplace negotiation” to “ongoing conversation” is explicit in the code.&lt;/p&gt;

&lt;p&gt;Chat is the richest part of the frontend. &lt;code&gt;src/features/chat/hooks/useChat.ts&lt;/code&gt; combines realtime subscriptions with optimistic updates. &lt;code&gt;useSendMessage()&lt;/code&gt; inserts an optimistic message immediately, replaces it with the server-confirmed message on success, and removes it on error. The reconciliation is simple and effective:&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;optimistic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Message&lt;/span&gt; &lt;span class="o"&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="s2"&gt;`optimistic-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&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="na"&gt;chat_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;chatId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sender_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="o"&gt;!&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;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;message_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messageType&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;payload&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="na"&gt;read&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="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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;src/app/chat/[id].tsx&lt;/code&gt; pushes the conversation model further by supporting &lt;code&gt;message_type: "house_context"&lt;/code&gt;. If a chat starts from a listing or from a selected request, the screen can seed a structured property card into the first message. That gives both sides shared context without relying on free-text recall. The logic normalizes payloads, deduplicates seeded context, groups messages by date, and coordinates moderation state.&lt;/p&gt;

&lt;p&gt;On the backend side, Supabase is doing real work. &lt;code&gt;supabase/migrations/001_initial.sql&lt;/code&gt; defines the base schema, indexes, RLS policies, triggers, and realtime publication. That file alone shows the actual trust model: users can read public entities like houses and profiles, but writes are scoped tightly by &lt;code&gt;auth.uid()&lt;/code&gt;. &lt;code&gt;033_chat_moderation.sql&lt;/code&gt; adds &lt;code&gt;user_blocks&lt;/code&gt;, &lt;code&gt;user_reports&lt;/code&gt;, and a trigger that rejects message inserts for blocked pairs. Notification behavior is also database-driven. New bids and messages create &lt;code&gt;notifications&lt;/code&gt; rows through triggers, and later migrations add push fanout and city-based new-listing alerts. The Edge Function in &lt;code&gt;supabase/functions/push-notify/index.ts&lt;/code&gt; is intentionally thin; Postgres decides when something should notify, and the function just forwards payloads to Expo.&lt;/p&gt;

&lt;p&gt;There are also several practical performance and UX details worth calling out. Saved homes use optimistic cache updates in &lt;code&gt;src/features/houses/hooks/useSavedHomes.ts&lt;/code&gt;. Media uploads in &lt;code&gt;src/app/post-house/index.tsx&lt;/code&gt; are capped using &lt;code&gt;MAX_PHOTO_SIZE_BYTES&lt;/code&gt; and &lt;code&gt;MAX_VIDEO_SIZE_BYTES&lt;/code&gt; from &lt;code&gt;src/services/supabase/storage.ts&lt;/code&gt;. Notification display is route-aware in &lt;code&gt;notificationService&lt;/code&gt;, which suppresses foreground message alerts when the user is already in &lt;code&gt;/inbox&lt;/code&gt; or &lt;code&gt;/chat/...&lt;/code&gt;. Safe-area handling and keyboard avoidance are consistently applied across the app shell, tabs, posting flow, and chat UI.&lt;/p&gt;

&lt;p&gt;If I were evolving this codebase, I would not rewrite the stack. Expo Router, React Query, Zustand-for-auth-only, and Supabase are a coherent fit. The bigger opportunity is consolidation. Query key conventions are a bit thin and partially duplicated. Some business rules live in services, some in screens, and some in SQL triggers, which is workable but makes invariants harder to audit. Storage uploads currently read whole files as base64 before upload, which is simple but not memory-friendly for larger media. Tooling could also be tightened; local &lt;code&gt;expo lint&lt;/code&gt; was inconclusive here because it tried to auto-configure ESLint and fetch network metadata rather than running against a committed project config. Those are normal next-step problems for an app that already has a real product shape.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>programming</category>
      <category>fullstack</category>
    </item>
    <item>
      <title>Inside Tabs: A Practical Next.js Frontend for Multi-Business Inventory Operations</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Thu, 09 Apr 2026 01:16:34 +0000</pubDate>
      <link>https://dev.to/mikelisrael/inside-tabs-a-practical-nextjs-frontend-for-multi-business-inventory-operations-552f</link>
      <guid>https://dev.to/mikelisrael/inside-tabs-a-practical-nextjs-frontend-for-multi-business-inventory-operations-552f</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fma3g06nasrfs46kyw4uy.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.amazonaws.com%2Fuploads%2Farticles%2Fma3g06nasrfs46kyw4uy.png" alt="Tabs single business dashboard" width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tabs is a multi-tenant operations frontend built with Next.js App Router. At a product level, it is trying to keep the daily workflow of a small or midsize business in one place: products, stores, orders, transfers, customers, people, and roles all live under the same business-scoped UI. You can see that shape directly in the route tree under &lt;code&gt;app/(authenticated)/[slug]&lt;/code&gt;, where the &lt;code&gt;slug&lt;/code&gt; identifies the active business, and in &lt;code&gt;components/home/business-list.tsx&lt;/code&gt;, which acts as the switchboard for choosing which business workspace to open.&lt;/p&gt;

&lt;p&gt;That matters because the problem Tabs is solving is not just inventory tracking. It is reducing the operational overhead of hopping between separate systems for stock, staff access, customer records, and order fulfilment. The codebase reflects that ambition. It is broad, form-heavy, and biased toward shipping CRUD workflows quickly.&lt;/p&gt;

&lt;p&gt;The application shell is layered in a way that makes that scope manageable. &lt;code&gt;app/layout.tsx&lt;/code&gt; owns global metadata, fonts, and the top-level provider composition. &lt;code&gt;components/providers/providers.tsx&lt;/code&gt; then stacks &lt;code&gt;ContextProvider&lt;/code&gt;, &lt;code&gt;ThemeProvider&lt;/code&gt;, &lt;code&gt;TooltipProvider&lt;/code&gt;, &lt;code&gt;QueryProvider&lt;/code&gt;, and &lt;code&gt;KeyboardCommandsProvider&lt;/code&gt; in a single place. The authenticated shell in &lt;code&gt;app/(authenticated)/layout.tsx&lt;/code&gt; adds &lt;code&gt;TopNav&lt;/code&gt;, sidebar state, and the scroll container. Finally, &lt;code&gt;app/(authenticated)/[slug]/layout.tsx&lt;/code&gt; introduces the actual business workspace with &lt;code&gt;DesktopSidebar&lt;/code&gt;, &lt;code&gt;DashboardContent&lt;/code&gt;, and &lt;code&gt;ScrollToTop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One of the better decisions here is that business context is inferred rather than threaded manually. &lt;code&gt;components/providers/business-provider.tsx&lt;/code&gt; reads &lt;code&gt;usePathname()&lt;/code&gt;, strips out known non-business routes like &lt;code&gt;settings&lt;/code&gt; and &lt;code&gt;create-business&lt;/code&gt;, and exposes &lt;code&gt;business_slug&lt;/code&gt; through context. That means components deeper in the tree can call &lt;code&gt;useBusiness()&lt;/code&gt; and build business-scoped queries without prop-drilling the slug through every intermediate layout and card.&lt;/p&gt;

&lt;p&gt;The data access layer is similarly pragmatic. Each domain gets a thin service module in &lt;code&gt;services/*.service.ts&lt;/code&gt;, while &lt;code&gt;services/_http.service.ts&lt;/code&gt; holds the Axios instance, the &lt;code&gt;tbs-user-sid&lt;/code&gt; request interceptor, and the 401 response handling that calls &lt;code&gt;logout()&lt;/code&gt; and shows a toast. It is not a rich domain layer; it is intentionally light. Most of the orchestration lives in the client components. That tradeoff shows up everywhere: thin service modules, fat screen components.&lt;/p&gt;

&lt;p&gt;React Query is wrapped in &lt;code&gt;hooks/use-query-resource.ts&lt;/code&gt; so the app consumes a smaller API than raw TanStack Query. The hook is doing a few jobs at once: standardizing toasts, exposing &lt;code&gt;onSuccess&lt;/code&gt; and &lt;code&gt;onError&lt;/code&gt;, and invalidating related queries after mutations.&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;useModifyResource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MutationOptionsProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;invalidateKeys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mutationOptions&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&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;queryClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQueryClient&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;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mutationOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;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="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onSuccess&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;invalidateKeys&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;invalidateKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;queryKey&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;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;queryKey&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="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;p&gt;That wrapper is useful because most screens follow the same pattern: fetch with &lt;code&gt;useGetResource&lt;/code&gt;, mutate with &lt;code&gt;useModifyResource&lt;/code&gt;, and let invalidation refresh the table or detail view. The tradeoff is that the wrapper becomes part of the application architecture, so mistakes inside it spread widely. One example is &lt;code&gt;components/providers/query-provider.tsx&lt;/code&gt;, which instantiates &lt;code&gt;new QueryClient()&lt;/code&gt; directly inside the component body. In practice that risks rebuilding the cache more often than intended.&lt;/p&gt;

&lt;p&gt;The roles feature is a good example of translating UI state into API state without overengineering it. In &lt;code&gt;app/(authenticated)/[slug]/roles/_components/roles-content.tsx&lt;/code&gt;, the UI groups permissions by category such as &lt;code&gt;customers&lt;/code&gt; or &lt;code&gt;stores&lt;/code&gt;, but the backend expects normalized strings like &lt;code&gt;customer:view&lt;/code&gt; or &lt;code&gt;location:remove&lt;/code&gt;. The component handles both directions locally:&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;flattenPermissions&lt;/span&gt; &lt;span class="o"&gt;=&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="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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="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="na"&gt;list&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;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&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="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;actions&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;resource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;categoryToResource&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;action&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;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&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;resource&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;actionToPermission&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;action&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;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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&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;There is a matching &lt;code&gt;toGroupedPermissions()&lt;/code&gt; for the reverse mapping when loading role details. It is a small but honest piece of application logic: the backend permission model is cleaner than the UI model, so the frontend adapts. &lt;code&gt;RoleDialog&lt;/code&gt; and &lt;code&gt;RoleSheet&lt;/code&gt; stay mostly focused on presentation because &lt;code&gt;RolesContent&lt;/code&gt; owns the transformations and the fetch/mutate flow.&lt;/p&gt;

&lt;p&gt;The most complex implementation is probably the order creation flow, especially &lt;code&gt;app/(authenticated)/[slug]/orders/create-order/_components/step-4-product-details.tsx&lt;/code&gt;. That final step handles product selection, quantity changes, VAT toggling, derived totals, optional customer creation, and order submission in one component. It is a lot of responsibility, but it also shows the project's bias: keep the workflow close to the UI that owns it. The &lt;code&gt;handleSubmit&lt;/code&gt; path is especially telling. It first tries to reuse an existing customer, then conditionally creates one, then builds the order payload and redirects to the new order. That is not the most layered design, but it is direct and easy to trace while debugging.&lt;/p&gt;

&lt;p&gt;Forms across the app follow a consistent stack: &lt;code&gt;react-hook-form&lt;/code&gt;, &lt;code&gt;zodResolver&lt;/code&gt;, and component-local orchestration. &lt;code&gt;app/(authenticated)/[slug]/products/_components/product-form.tsx&lt;/code&gt; is representative. It combines &lt;code&gt;useForm&lt;/code&gt;, &lt;code&gt;useFieldArray&lt;/code&gt;, &lt;code&gt;useGetResource&lt;/code&gt;, &lt;code&gt;useModifyResource&lt;/code&gt;, and a preview sidebar to support both create and edit flows. The result is a fairly capable form without introducing a separate state machine library. The downside is that these large client components are becoming maintenance hotspots, because validation, loading, transformation, side effects, and UI are often sitting in the same file.&lt;/p&gt;

&lt;p&gt;The request flow is straightforward. &lt;code&gt;components/auth/login/login-form.tsx&lt;/code&gt; calls &lt;code&gt;signIn()&lt;/code&gt;, extracts &lt;code&gt;tbs-user-sid&lt;/code&gt;, and stores it with &lt;code&gt;js-cookie&lt;/code&gt;. &lt;code&gt;middleware.ts&lt;/code&gt; uses the presence of that cookie to gate authenticated routes and redirect to &lt;code&gt;/login&lt;/code&gt; when needed. After that, &lt;code&gt;services/_http.service.ts&lt;/code&gt; injects the same cookie into outgoing requests. Screen components call &lt;code&gt;useGetResource&lt;/code&gt; and &lt;code&gt;useModifyResource&lt;/code&gt;, then move the results into local state or occasionally global context. &lt;code&gt;components/dashboard/navbar/user-nav.tsx&lt;/code&gt;, for example, fetches the profile and copies it into &lt;code&gt;ContextProvider&lt;/code&gt; so hooks like &lt;code&gt;usePermissions()&lt;/code&gt; can read the active user object.&lt;/p&gt;

&lt;p&gt;State is split in a sensible, lightweight way. Server state lives mostly in React Query. Form state lives in &lt;code&gt;react-hook-form&lt;/code&gt; and &lt;code&gt;useFieldArray&lt;/code&gt;. View state such as dialog visibility or selected rows sits in &lt;code&gt;useState&lt;/code&gt;. App-global state is minimal: &lt;code&gt;components/providers/context.tsx&lt;/code&gt; stores the current user and geolocated country, and &lt;code&gt;components/providers/sidebar-provider.tsx&lt;/code&gt; stores collapsed sidebar state in &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are also a few solid cross-cutting concerns. Performance-wise, the app enables PWA support in &lt;code&gt;next.config.mjs&lt;/code&gt;, serves an offline fallback from &lt;code&gt;app/~offline/page.tsx&lt;/code&gt;, and uses &lt;code&gt;LazyMotion&lt;/code&gt; in &lt;code&gt;components/shared/animated-components.tsx&lt;/code&gt; to avoid paying for more animation runtime than needed. &lt;code&gt;components/shared/data-table/index.tsx&lt;/code&gt; centralizes table behavior and can operate in either client or server pagination mode, which is a good fit for admin-style screens.&lt;/p&gt;

&lt;p&gt;Security and accessibility are mixed in the way many real products are. On the positive side, &lt;code&gt;middleware.ts&lt;/code&gt; centralizes navigation protection, and the Axios interceptor handles expired sessions consistently. On the less comfortable side, the base API URL in &lt;code&gt;services/_http.service.ts&lt;/code&gt; is hard-coded to an ngrok endpoint, session handling is done in client JavaScript rather than server-managed cookies, and &lt;code&gt;hooks/use-permissions.ts&lt;/code&gt; currently allows all actions when no permissions are present. That last choice may be convenient during integration, but it is a dangerous default. Accessibility is stronger: the app gets a good baseline from Radix and shadcn primitives in &lt;code&gt;components/ui&lt;/code&gt;, and that shows up in labeled forms, dialogs, sheets, dropdown menus, and keyboard shortcuts.&lt;/p&gt;

&lt;p&gt;If I were continuing this codebase, I would keep the route-scoped multi-tenancy, the thin service modules, and the reusable UI primitives. They are helping the team move. I would change the infrastructure around them: stabilize the &lt;code&gt;QueryClient&lt;/code&gt;, move auth toward server-managed cookies, replace hard-coded environment values, tighten the permissions fallback, and gradually extract orchestration out of the largest client components. Tabs already has the right product surface area for an operations app. The next step is making the implementation as durable as the workflows it is trying to support.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Inside Emoney profit tracker: a pragmatic Next.js app for reseller operations</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Thu, 09 Apr 2026 00:53:24 +0000</pubDate>
      <link>https://dev.to/mikelisrael/inside-emoney-profit-tracker-a-pragmatic-nextjs-app-for-reseller-operations-4634</link>
      <guid>https://dev.to/mikelisrael/inside-emoney-profit-tracker-a-pragmatic-nextjs-app-for-reseller-operations-4634</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fk5vi1as0qi4rzdw49qpl.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.amazonaws.com%2Fuploads%2Farticles%2Fk5vi1as0qi4rzdw49qpl.png" alt="Profit tracker dashboard" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;emoney-profit-tracker&lt;/code&gt; is a small operational app for a reseller workflow. It tracks purchased products, completed sales, profit trends over time, and a lightweight "quests" system for recurring tasks. The problem it solves is familiar: once buying, listing, shipping, and bookkeeping are spread across different tools, it becomes hard to answer basic questions like "what is actually making money?" and "what still needs attention?"&lt;/p&gt;

&lt;p&gt;This codebase answers that with a single Next.js 15 App Router application. It is not trying to be a heavily server-rendered product. Most of the interesting work happens on the client: auth checks, data fetching, table filtering, form state, and mutation flows. That makes the app easy to reason about for dashboard-style work, even if it leaves some server-side capabilities unused.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture and the shape of the app
&lt;/h2&gt;

&lt;p&gt;The project is organised around App Router route groups plus feature-specific client components. &lt;code&gt;app/layout.tsx&lt;/code&gt; defines the global shell, metadata, &lt;code&gt;Providers&lt;/code&gt;, and the &lt;code&gt;Toaster&lt;/code&gt;. The authenticated area lives under &lt;code&gt;app/(aunthenticated)&lt;/code&gt;, with &lt;code&gt;app/(aunthenticated)/layout.tsx&lt;/code&gt; layering &lt;code&gt;TopNav&lt;/code&gt; and &lt;code&gt;MobileNavigation&lt;/code&gt; around the page body. Individual screens mostly render thin route files that hand off to client components like &lt;code&gt;DashboardClient&lt;/code&gt;, &lt;code&gt;ProductsClient&lt;/code&gt;, and &lt;code&gt;QuestsClient&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That split works well here. Route files stay small, while screen logic sits close to the UI that owns it.&lt;/p&gt;

&lt;p&gt;The service layer is intentionally thin. Files like &lt;code&gt;services/product.service.ts&lt;/code&gt;, &lt;code&gt;services/sales.service.ts&lt;/code&gt;, and &lt;code&gt;services/quests.service.ts&lt;/code&gt; mostly expose small functions that call a shared Axios client from &lt;code&gt;services/_http.service.ts&lt;/code&gt;. That shared client sets a fixed &lt;code&gt;baseURL&lt;/code&gt;, enables &lt;code&gt;withCredentials&lt;/code&gt;, and centralises 401 handling and network-error toasts:&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;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interceptors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&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;response&lt;/span&gt;&lt;span class="p"&gt;,&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;=&amp;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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&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;currentPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;
        &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callbackPath&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reject&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="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;That keeps auth-expiry handling out of page components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data flow and state management
&lt;/h2&gt;

&lt;p&gt;Remote state is handled with React Query, wrapped by &lt;code&gt;lib/hooks/use-query-resource.ts&lt;/code&gt;. The wrappers are simple but useful: &lt;code&gt;useGetResource&lt;/code&gt; standardises query execution and callback handling, while &lt;code&gt;useModifyResource&lt;/code&gt; and &lt;code&gt;useDeleteResource&lt;/code&gt; invalidate a provided query key after mutation.&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;return&lt;/span&gt; &lt;span class="nf"&gt;useMutation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;mutationOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mutationFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;:&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="o"&gt;=&amp;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="nx"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onSuccess&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;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateQueries&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="nx"&gt;key&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;p&gt;That pattern shows up almost everywhere. &lt;code&gt;ProductsClient&lt;/code&gt; fetches a list with &lt;code&gt;useGetResource({ fn: getProducts, key: ["products"] })&lt;/code&gt;. The product table's delete action uses &lt;code&gt;useModifyResource&lt;/code&gt; with the same key so the list refreshes without manual cache bookkeeping.&lt;/p&gt;

&lt;p&gt;Local UI state is kept local. Search input, selected tabs, dialog visibility, selected files, and transient submission state all live in component state rather than being pushed into a global store. Forms use React Hook Form and Zod, which is the right fit here: field registration stays cheap, validation is explicit, and submit handlers can sanitize outgoing payloads before mutation.&lt;/p&gt;

&lt;p&gt;Authentication is also managed on the client in &lt;code&gt;components/providers/auth-context.tsx&lt;/code&gt;. &lt;code&gt;AuthProvider&lt;/code&gt; stores the current user and an &lt;code&gt;authStatus&lt;/code&gt;, while &lt;code&gt;AuthGuard&lt;/code&gt; calls &lt;code&gt;getUser()&lt;/code&gt; once on mount, redirects unauthenticated users to &lt;code&gt;/login&lt;/code&gt;, and preserves the intended path in &lt;code&gt;sessionStorage&lt;/code&gt;. The login button in &lt;code&gt;app/login/components/login-button.tsx&lt;/code&gt; builds a Discord OAuth URL that uses that saved path as state.&lt;/p&gt;

&lt;p&gt;This is centralized and easy to follow, but it is also fully client-side. Protected routes are redirected after hydration rather than being rejected earlier by middleware or server components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interesting implementation details
&lt;/h2&gt;

&lt;p&gt;One of the better pieces of UI logic is in &lt;code&gt;app/(aunthenticated)/sales/add-sale/_components/add-sale-form.tsx&lt;/code&gt;. The form does more than collect fields. It debounces product lookup with &lt;code&gt;useDebounce&lt;/code&gt;, loads matching products through &lt;code&gt;productsLookup&lt;/code&gt;, and adjusts validation based on the selected product's stock level:&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;createDynamicSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxQuantity&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&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="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;maxQuantity&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxQuantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Quantity cannot exceed &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;maxQuantity&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;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;number&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;p&gt;When a product is selected, the form also pre-fills &lt;code&gt;sale_price&lt;/code&gt; from &lt;code&gt;purchase_price&lt;/code&gt; and clamps quantity if it exceeds stock. That is a good example of business rules living in the form layer.&lt;/p&gt;

&lt;p&gt;Another strong detail is the quest proof submission flow in &lt;code&gt;app/(aunthenticated)/quests/_components/quests-proof-submission.tsx&lt;/code&gt;. The same form renders as a &lt;code&gt;Dialog&lt;/code&gt; on desktop and a &lt;code&gt;Drawer&lt;/code&gt; on mobile using &lt;code&gt;useIsMobile(450)&lt;/code&gt;. The upload side is handled by &lt;code&gt;components/shared/file-uploader.tsx&lt;/code&gt;, which uses &lt;code&gt;react-dropzone&lt;/code&gt;, generates preview URLs with &lt;code&gt;URL.createObjectURL&lt;/code&gt;, and passes real &lt;code&gt;File&lt;/code&gt; objects into a &lt;code&gt;FormData&lt;/code&gt; payload.&lt;/p&gt;

&lt;p&gt;The shared table in &lt;code&gt;components/shared/data-table/index.tsx&lt;/code&gt; is another important building block. It standardises sorting, filtering, pagination, loading skeletons, empty states, and "add new" actions on top of TanStack Table. That keeps &lt;code&gt;SalesClient&lt;/code&gt; and &lt;code&gt;ProductsClient&lt;/code&gt; small.&lt;/p&gt;

&lt;p&gt;There are also a few rough edges worth calling out, honestly. The &lt;code&gt;QueryClient&lt;/code&gt; is instantiated inside the &lt;code&gt;Providers&lt;/code&gt; component on every render:&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;Providers&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;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;PropsWithChildren&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&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="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;queryClient&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;QueryClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;QueryClientProvider&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;queryClient&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;children&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;/QueryClientProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That risks resetting cache state if the provider re-renders. It would be safer to memoize it or create it once outside the component.&lt;/p&gt;

&lt;p&gt;There are also some endpoint mismatches that look like copy-paste drift. In the sales edit flow, &lt;code&gt;app/(aunthenticated)/sales/edit-sale/[slug]/page.tsx&lt;/code&gt; calls &lt;code&gt;getSingleProduct&lt;/code&gt;, and the sale form's edit path uses &lt;code&gt;updateProduct&lt;/code&gt;. Likewise, some delete actions for sales and recent transactions call &lt;code&gt;deleteProduct&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The admin area is another revealing detail. &lt;code&gt;app/(aunthenticated)/control/admin/_components/admin-client.tsx&lt;/code&gt; is visually fleshed out, but its metrics are currently static. Compared with the sales, products, and quests flows, it reads more like a dashboard stub than a finished screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance, security, and accessibility
&lt;/h2&gt;

&lt;p&gt;Performance work here is practical rather than exotic. The app debounces product lookup, uses React Query caching and invalidation to avoid redundant fetch bookkeeping, and leans on &lt;code&gt;next/image&lt;/code&gt; through &lt;code&gt;components/shared/image-loader.tsx&lt;/code&gt; for product imagery.&lt;/p&gt;

&lt;p&gt;Security is mostly about session handling. Axios requests are sent with credentials, 401s are intercepted centrally, and the login flow preserves the callback path through OAuth state. What is missing is stronger server-side enforcement in the frontend layer; because auth is client-guarded, a lot of route protection depends on hydration-time redirects rather than server rejection.&lt;/p&gt;

&lt;p&gt;Accessibility is mixed but generally thoughtful. Many controls come from Radix-based UI primitives, which is a strong baseline. The app uses labels, tooltips, dialog primitives, and &lt;code&gt;sr-only&lt;/code&gt; text in menu triggers. A few custom pieces would benefit from more attention, especially image fallbacks and drag-and-drop affordances.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would change next
&lt;/h2&gt;

&lt;p&gt;If I were continuing this codebase, I would keep the general architecture but tighten the boundaries. First, I would stabilize the React Query provider and formalize query keys. Second, I would separate product and sale service contracts more aggressively so endpoint mix-ups become harder to write. Third, I would move at least part of auth enforcement into middleware or server-side route handling.&lt;/p&gt;

&lt;p&gt;The larger lesson is that this app gets a lot of value from a modest amount of structure. Thin services, reusable query hooks, shared table infrastructure, and form-driven business rules go a long way in an internal dashboard. The tradeoff is that once those abstractions exist, they need naming discipline and a few guardrails. &lt;code&gt;emoney-profit-tracker&lt;/code&gt; is a good example of a pragmatic frontend that solves a real operational problem while making its next engineering steps fairly obvious.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>programming</category>
      <category>react</category>
    </item>
    <item>
      <title>UICS Connect: A Technical Walkthrough of the Alumni Partnership System</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Thu, 09 Apr 2026 00:36:11 +0000</pubDate>
      <link>https://dev.to/mikelisrael/uics-connect-a-technical-walkthrough-of-the-alumni-partnership-system-loo</link>
      <guid>https://dev.to/mikelisrael/uics-connect-a-technical-walkthrough-of-the-alumni-partnership-system-loo</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fez9ynqopapqbos9tshkx.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.amazonaws.com%2Fuploads%2Farticles%2Fez9ynqopapqbos9tshkx.png" alt="Login screen" width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;alumni-partnership-system&lt;/code&gt; is a networking platform for the University of Ibadan Computer Science community. It connects students and alumni through profiles, posts, communities, jobs, direct messaging, notifications, and connection requests. It is a Next.js 14 App Router application that pushes a lot of behaviour to the client, uses Supabase as both database and auth boundary, and relies on React Query to keep the UI coherent while data changes quickly.&lt;/p&gt;

&lt;p&gt;A school community needs more than a directory. It needs identity, lightweight publishing, private conversation, and a way to move between social and career features without building separate systems. UICS Connect treats the authenticated user as the centre of every workflow: sign in, hydrate user state, fetch domain data, then keep the UI current with optimistic updates and realtime subscriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture and Boundaries
&lt;/h2&gt;

&lt;p&gt;The repository has a clean split between routing, domain logic, and UI composition. &lt;code&gt;app/&lt;/code&gt; owns layouts and pages. &lt;code&gt;components/&lt;/code&gt; contains shared UI, feature components, and provider setup. &lt;code&gt;services/&lt;/code&gt; is the domain layer for Supabase queries, mutations, file uploads, and permission checks. &lt;code&gt;hooks/&lt;/code&gt; wraps those services with React Query and turns them into UI-friendly primitives.&lt;/p&gt;

&lt;p&gt;Authentication is handled in two places. &lt;code&gt;middleware.ts&lt;/code&gt; delegates to &lt;a href="//./../lib/supabase/middleware.ts"&gt;&lt;code&gt;lib/supabase/middleware.ts&lt;/code&gt;&lt;/a&gt;, which refreshes the session and redirects users based on route type. It also sanitizes the login callback so the app only redirects internally:&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;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;user&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isPublicRoute&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextUrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/login&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="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;callbackUrl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pathname&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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;That is paired with server-side auth actions in &lt;code&gt;services/auth.service.ts&lt;/code&gt;, where &lt;code&gt;login()&lt;/code&gt; calls &lt;code&gt;redirect()&lt;/code&gt; only after checking &lt;code&gt;callbackUrl.startsWith("/")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At the application shell level, &lt;code&gt;components/providers/providers.tsx&lt;/code&gt; creates one &lt;code&gt;QueryClient&lt;/code&gt; and wraps the tree with theme, push notification, keyboard command, and context providers. &lt;code&gt;components/providers/context.tsx&lt;/code&gt; currently exposes an empty context object, which tells you what this codebase is &lt;em&gt;not&lt;/em&gt; doing. Remote state lives in React Query. Local UI state stays local.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Flow and State Management
&lt;/h2&gt;

&lt;p&gt;The dominant data flow looks like this: a component fires an event, a custom hook triggers a React Query mutation, the service module talks to Supabase, and the cache is updated optimistically or invalidated for refetch. If the feature needs live updates, a Supabase realtime channel patches the cache when rows change.&lt;/p&gt;

&lt;p&gt;The post flow is the clearest example. &lt;code&gt;app/(logged-in)/_components/post-composer.tsx&lt;/code&gt; collects content with TipTap, validates images, then calls &lt;code&gt;useCreatePost()&lt;/code&gt;. That hook lives in &lt;a href="//./../hooks/use-posts.ts"&gt;&lt;code&gt;hooks/use-posts.ts&lt;/code&gt;&lt;/a&gt; and delegates to &lt;code&gt;createPost()&lt;/code&gt; in &lt;a href="//./../services/posts.service.ts"&gt;&lt;code&gt;services/posts.service.ts&lt;/code&gt;&lt;/a&gt;. The same file also contains the more interesting mutations, especially optimistic updates for edits:&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;queryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setQueriesData&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;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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;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;old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;pages&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;old&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;...&lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Post&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;postId&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;p&lt;/span&gt;&lt;span class="p"&gt;,&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="na"&gt;_optimistic&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="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That pattern shows up all over the app. &lt;code&gt;hooks/use-chats.ts&lt;/code&gt; uses the same approach for messages, including temporary blob URLs for attachment previews and rollback on failure. The upside is responsiveness. The cost is cache bookkeeping when a mutation affects both an infinite list and a detail view.&lt;/p&gt;

&lt;p&gt;Remote state is mostly React Query state. Local state is reserved for ephemeral interaction details. &lt;code&gt;app/(logged-in)/chat/components/chat-zone.tsx&lt;/code&gt; keeps &lt;code&gt;replyingTo&lt;/code&gt;, &lt;code&gt;editingMessage&lt;/code&gt;, &lt;code&gt;stickyDate&lt;/code&gt;, and scroll state in component memory because those values are view-specific and do not belong in shared cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interesting Implementation Details
&lt;/h2&gt;

&lt;p&gt;The feed and comment system carries some of the best engineering work in the repo. &lt;code&gt;getPostComments()&lt;/code&gt; in &lt;code&gt;services/posts.service.ts&lt;/code&gt; builds a nested tree in O(n) time using &lt;code&gt;Map&lt;/code&gt;, then applies a ranking model that prioritizes the current user, then the post author, then engagement, then recency:&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;commentMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Comment&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;repliesMap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Comment&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;topLevelComments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Comment&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

&lt;span class="nx"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;comment&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="na"&gt;commentWithLike&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Comment&lt;/span&gt; &lt;span class="o"&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;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;user_liked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;likedCommentIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&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;replies&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;commentMap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;comment&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="nx"&gt;commentWithLike&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a better choice than recursive insertion during fetch because it keeps the construction cost predictable as the thread grows. The companion realtime hook, &lt;code&gt;usePostCommentsSubscription()&lt;/code&gt;, then merges inserts and deletes back into the cached tree instead of forcing a full reload.&lt;/p&gt;

&lt;p&gt;Chat is the other area where the implementation gets interesting. &lt;code&gt;services/chats.service.ts&lt;/code&gt; is more imperative than the post service because messaging needs attachment handling, unread counts, and reply references. &lt;code&gt;useSendMessage()&lt;/code&gt; in &lt;code&gt;hooks/use-chats.ts&lt;/code&gt; creates an optimistic message first, including local attachment previews, then replaces it when the insert completes. &lt;code&gt;useChatSubscription()&lt;/code&gt; listens to &lt;code&gt;postgres_changes&lt;/code&gt; for the current chat and merges new rows into &lt;code&gt;["chats", chatId, "messages"]&lt;/code&gt;. &lt;code&gt;chat-zone.tsx&lt;/code&gt; then groups messages by day and collapses adjacent messages from the same sender in the same minute.&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.amazonaws.com%2Fuploads%2Farticles%2Fk1kw32ac612lttv307ij.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.amazonaws.com%2Fuploads%2Farticles%2Fk1kw32ac612lttv307ij.png" alt="Chat screen" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Communities and connections follow the same architectural style with less cache complexity. &lt;code&gt;services/communities.service.ts&lt;/code&gt; handles membership joins, image uploads to the &lt;code&gt;community-images&lt;/code&gt; storage bucket, search via &lt;code&gt;textSearch("search_vector", filter.search)&lt;/code&gt;, and role-aware membership state. &lt;code&gt;services/connection.service.ts&lt;/code&gt; is careful about ownership, especially in &lt;code&gt;acceptConnectionRequest()&lt;/code&gt;, &lt;code&gt;rejectConnectionRequest()&lt;/code&gt;, and &lt;code&gt;removeConnection()&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Integration, Performance, Security, and Accessibility
&lt;/h2&gt;

&lt;p&gt;Most integration work happens from the browser through the Supabase client in &lt;code&gt;services/*&lt;/code&gt;. That keeps feature code easy to follow, but it also means many services start with &lt;code&gt;supabase.auth.getUser()&lt;/code&gt;. The repeated check adds chatter and keeps a lot of data access in the client. The main server-side exception is auth in &lt;code&gt;services/auth.service.ts&lt;/code&gt;, plus &lt;code&gt;app/api/link-preview/route.ts&lt;/code&gt;, which fetches arbitrary URLs and parses metadata with &lt;code&gt;cheerio&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Performance-wise, the project makes several strong choices. The feed uses &lt;code&gt;useInfiniteQuery()&lt;/code&gt; and &lt;code&gt;useInView()&lt;/code&gt; for incremental loading in &lt;code&gt;app/(logged-in)/_components/feed.tsx&lt;/code&gt;. Comments use linear-time tree construction. Posts, comments, and chat all use optimistic writes. Realtime subscriptions avoid waiting for manual refresh. &lt;code&gt;next.config.mjs&lt;/code&gt; also enables PWA behavior through &lt;code&gt;@ducanh2912/next-pwa&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There are tradeoffs. One global &lt;code&gt;QueryClient&lt;/code&gt; in &lt;code&gt;components/providers/providers.tsx&lt;/code&gt; is simple, but cache policy is mostly default behavior. Chat and community fetches still show some N+1 tendencies, particularly where last messages, participants, unread counts, and reply targets are fetched in follow-up queries. There is also no visible automated test suite, which makes optimistic and realtime paths riskier to change.&lt;/p&gt;

&lt;p&gt;On security, the foundations are reasonable. Middleware protects private routes. Auth actions sanitize redirects. Service methods often check ownership before mutation. Still, the design assumes Supabase Row Level Security is doing a lot of the final enforcement. The link preview route deserves extra scrutiny because fetching arbitrary external URLs is exactly where SSRF concerns start.&lt;/p&gt;

&lt;p&gt;Accessibility is mixed in the way many product-driven apps are mixed. The baseline is good because the app leans on Radix primitives like &lt;a href="//./../components/ui/dialog.tsx"&gt;&lt;code&gt;components/ui/dialog.tsx&lt;/code&gt;&lt;/a&gt;, which already carry focus management and semantics. Forms and buttons are generally explicit. But there is no dedicated accessibility layer or test strategy, so quality will depend on each custom component.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Keep and What I Would Change
&lt;/h2&gt;

&lt;p&gt;The service-plus-hook split is the right core decision here. It keeps Supabase logic out of the view layer, makes React Query reusable, and gives the app room to add richer UX such as optimistic comments and live chat without turning components into query scripts.&lt;/p&gt;

&lt;p&gt;What I would change is mostly about tightening boundaries. I would consolidate repeated auth and query helpers, move more high-value writes behind server-side boundaries, add tests around optimistic cache transitions, and review the N+1 paths in chat and community fetches. The current architecture works, but it is at the point where more features will increase coordination cost unless the data access patterns become more deliberate.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
    <item>
      <title>React vs Next.js vs TanStack Start: Deciding on the Right Tool</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Sun, 08 Feb 2026 16:32:51 +0000</pubDate>
      <link>https://dev.to/mikelisrael/react-vs-nextjs-vs-tanstack-start-deciding-on-the-right-tool-lib</link>
      <guid>https://dev.to/mikelisrael/react-vs-nextjs-vs-tanstack-start-deciding-on-the-right-tool-lib</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2F0pxo7ifpx74bu2jwzxc9.jpg" 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.amazonaws.com%2Fuploads%2Farticles%2F0pxo7ifpx74bu2jwzxc9.jpg" alt="what tool is better to use for frontend development" width="800" height="1195"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  React vs Next.js vs TanStack Start: Deciding on the Right Tool
&lt;/h2&gt;

&lt;p&gt;Choosing the right tool for a frontend project is more than just picking something familiar. React, Next.js, and the new TanStack Start framework each solve similar problems in different ways, and understanding their design goals can help you make the best choice for your needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  React: The Core UI Library
&lt;/h3&gt;

&lt;p&gt;React is the foundational UI library for building interactive interfaces. It doesn’t come with routing, data fetching, or server rendering by default, but it gives you the pure building blocks — components, state, hooks, and rendering. React is flexible, battle-tested, and supported by a massive ecosystem of tools and libraries.&lt;/p&gt;

&lt;p&gt;When to use React alone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app is heavily interactive, and you don’t need server rendering.&lt;/li&gt;
&lt;li&gt;You want total control over your stack and are comfortable choosing your own routing and data libraries.&lt;/li&gt;
&lt;li&gt;You’re building dashboards, internal tools, or SPAs where SEO isn’t a priority.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;React is essentially the baseline for everything that follows here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js: The Production Ready Framework
&lt;/h3&gt;

&lt;p&gt;Next.js is built on top of React and adds routing, server rendering, static site generation, and automatic optimizations. It’s ideal for production applications where performance and SEO matter.&lt;/p&gt;

&lt;p&gt;Next.js supports features like SSR, SSG, and incremental static regeneration out of the box, and its App Router makes server components the default. This can simplify building content-rich sites, but it also brings complexity and conventions you have to learn.&lt;/p&gt;

&lt;p&gt;Strengths of Next.js:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Excellent SEO and performance with server rendering&lt;/li&gt;
&lt;li&gt;Built-in file-based routing and tooling that gets you shipping quickly&lt;/li&gt;
&lt;li&gt;Mature ecosystem and widespread adoption&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic caching and server component behavior can feel opaque to debug&lt;/li&gt;
&lt;li&gt;It has heavier defaults and tight integration with the Vercel platform, though it can be deployed elsewhere&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next.js remains the established choice for content sites, e-commerce platforms, and applications where SEO or SSR is critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  TanStack Start: A New Framework with Strong Dev Ergonomics
&lt;/h3&gt;

&lt;p&gt;TanStack Start is a new full-stack React framework powered by TanStack Router and Vite. It aims to unify modern frontend needs with clearer control and a simpler mental model than larger frameworks, while still offering SSR, streaming, server functions, and deployment flexibility.&lt;/p&gt;

&lt;p&gt;TanStack Start takes a different approach than Next.js. It uses traditional client-side React components that are server-rendered by default, giving you SSR benefits with full client-side interactivity. Server Components are planned but not yet supported.&lt;/p&gt;

&lt;p&gt;TanStack Start is currently in release candidate and near v1, meaning it’s feature-complete and considered production-ready by many early adopters for real projects, even though it hasn’t reached a stable v1 release yet.&lt;/p&gt;

&lt;p&gt;Strengths of TanStack Start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deep TypeScript support and type-safe routing&lt;/li&gt;
&lt;li&gt;SSR and streaming through explicit primitives&lt;/li&gt;
&lt;li&gt;Flexible deployment anywhere JavaScript runs&lt;/li&gt;
&lt;li&gt;Developer experience powered by Vite with fast startup and HMR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Still newer than Next.js with a smaller ecosystem&lt;/li&gt;
&lt;li&gt;Doesn’t support React Server Components yet, though integration is planned&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many developers report that Start feels closer to the React way of thinking, without as much framework magic, which leads to predictable behavior and easier debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which One Should You Choose?
&lt;/h3&gt;

&lt;p&gt;React alone is a great core library for flexible UI work and complex SPAs.&lt;br&gt;
Next.js shines when you need SEO, SSR, and a mature framework with established conventions.&lt;br&gt;
TanStack Start is a promising alternative when you want performance, type safety, explicit control, and flexibility without the implicit complexity of larger frameworks.&lt;/p&gt;

&lt;p&gt;These tools are not mutually exclusive. For example, you can use TanStack libraries like TanStack Query and Router within Next.js. Understanding the philosophy and current landscape helps you choose with confidence.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>nextjs</category>
      <category>programming</category>
    </item>
    <item>
      <title>Mastering the art of frontend performance optimization</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:48:56 +0000</pubDate>
      <link>https://dev.to/mikelisrael/mastering-the-art-of-frontend-performance-optimization-284b</link>
      <guid>https://dev.to/mikelisrael/mastering-the-art-of-frontend-performance-optimization-284b</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fd64wx72qemwy7iavz8hw.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.amazonaws.com%2Fuploads%2Farticles%2Fd64wx72qemwy7iavz8hw.png" alt=" " width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a Frontend Software Engineer, I’ve worked on a range of challenging projects, and one area I focus on constantly is frontend performance. Why? Because a fast, responsive application directly improves user experience, retention, and overall satisfaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Don’t Ignore Performance Issues&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It’s easy to overlook slow load times or laggy interfaces, but ignoring them is not just lazy programming—it can drive users away and hurt your product’s reputation. A smooth, efficient user experience is our responsibility as developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Performance Matters&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User experience:&lt;/strong&gt; Fast websites keep users happy; slow pages frustrate them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO benefits:&lt;/strong&gt; Search engines favor speed, boosting your site’s ranking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conversion rates:&lt;/strong&gt; Quicker, responsive sites encourage users to stay and engage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Effective Strategies I’ve Used&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Refactoring:&lt;/strong&gt; Optimizing and cleaning up code improves load times and maintainability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Responsive design:&lt;/strong&gt; Using React and Next.js, I ensure applications work seamlessly across desktops, tablets, and smartphones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advanced analytics:&lt;/strong&gt; Tracking user interactions helps identify performance bottlenecks and improve UX based on real feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security measures:&lt;/strong&gt; Secure APIs and strong authentication are essential for reliable, safe applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;A Call to Action&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s take pride in our work and prioritize performance from the start. Fast, reliable applications are not just a technical goal—they shape the user’s experience and trust.&lt;/p&gt;

&lt;p&gt;What are your go-to strategies for frontend performance optimization? Have you faced challenges in this area? Let’s share experiences and learn from each other.&lt;/p&gt;

&lt;p&gt;Reach out if you’d like to discuss frontend development, ask questions, or collaborate on exciting projects. Stay curious and keep coding! 💻✨&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webperf</category>
      <category>performance</category>
    </item>
    <item>
      <title>The Principles of Good Design</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:35:04 +0000</pubDate>
      <link>https://dev.to/mikelisrael/the-principles-of-good-design-321h</link>
      <guid>https://dev.to/mikelisrael/the-principles-of-good-design-321h</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Folp3davkewpund3uz6lg.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.amazonaws.com%2Fuploads%2Farticles%2Folp3davkewpund3uz6lg.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ve all experienced it—scrolling through social media and stopping at a design that just grabs your attention. It looks polished, engaging, and effortless. Then there are those designs that feel… off. They lack cohesion, confuse the viewer, or just don’t resonate.&lt;/p&gt;

&lt;p&gt;Good design isn’t magic, but it can feel like it. A well-crafted graphic or interface can inform, entertain, and evoke emotion. The secret is understanding a few core principles that make design work.&lt;/p&gt;

&lt;p&gt;Here are four key principles to level up your designs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Contrast&lt;/strong&gt;&lt;br&gt;
Contrast adds visual interest by using opposing elements such as light and dark, big and small, or bold and subtle. Without it, a design feels flat and unengaging. Contrast helps guide the viewer’s eye through the composition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Balance&lt;/strong&gt;&lt;br&gt;
Balance makes a design feel stable and harmonious. This doesn’t always mean symmetry. By distributing visual weight thoughtfully, you create a sense of order and cohesiveness that feels natural to the viewer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Hierarchy&lt;/strong&gt;&lt;br&gt;
Not all information is equally important. Hierarchy uses size, colour, and placement to prioritize elements, guiding attention to the most crucial parts first. It ensures your message is clear and easily digestible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Simplicity&lt;/strong&gt;&lt;br&gt;
Simplicity focuses on what matters most and removes distractions. A clean design communicates effectively and leaves a stronger impression. As Steve Jobs said, achieving simplicity requires effort, but the result allows your work to move mountains.&lt;/p&gt;

&lt;p&gt;Mastering these principles allows you to create designs that captivate, inform, and leave a lasting impression.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>design</category>
      <category>ui</category>
      <category>ux</category>
    </item>
    <item>
      <title>Syncing Data Across Browser Tabs in Next.js: A Frontend 'Cron Job' Solution for Timed Fetching</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:26:54 +0000</pubDate>
      <link>https://dev.to/mikelisrael/syncing-data-across-browsers-in-nextjs-a-frontend-cron-job-solution-for-timed-fetching-18m2</link>
      <guid>https://dev.to/mikelisrael/syncing-data-across-browsers-in-nextjs-a-frontend-cron-job-solution-for-timed-fetching-18m2</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fyynf1c3kgresbducpfx6.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.amazonaws.com%2Fuploads%2Farticles%2Fyynf1c3kgresbducpfx6.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Imagine this. You are building a dynamic Next.js app where data needs to refresh every two minutes. Sounds simple. Drop a setInterval inside useEffect and move on.&lt;/p&gt;

&lt;p&gt;That was my assumption too, until I opened multiple browser tabs.&lt;/p&gt;

&lt;p&gt;Each tab was fetching data on its own schedule. Nothing was aligned. One tab refreshed, another lagged behind, and the experience felt messy. That was not acceptable.&lt;/p&gt;

&lt;p&gt;This post documents how I solved that problem by building a frontend “cron job” that synchronizes data fetching across all open tabs. I will walk through the initial approach, the issue it caused, and the solution that finally made everything click.&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting out: the timer-based approach
&lt;/h3&gt;

&lt;p&gt;I began with a straightforward setup. Fetch on mount, then fetch every two minutes.&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;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&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&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;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&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;setData&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;null&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;fetchData&lt;/span&gt; &lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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/getData&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;newData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&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;fetchData&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;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120000&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&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="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&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;data&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading...&lt;/span&gt;&lt;span class="dl"&gt;"&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;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this works perfectly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem: tabs falling out of sync
&lt;/h3&gt;

&lt;p&gt;The issue appeared as soon as I opened a second tab.&lt;/p&gt;

&lt;p&gt;Each tab runs its own instance of setInterval, which means timing depends on when the tab was opened.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open tab A at 12:01. It fetches at 12:03, 12:05, and so on.&lt;/li&gt;
&lt;li&gt;Open tab B at 12:02. It fetches at 12:04, 12:06, and so on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same app, same user, different data states. That inconsistency was the real problem.&lt;/p&gt;

&lt;p&gt;What I actually needed was this:&lt;/p&gt;

&lt;p&gt;All tabs should fetch at the same exact moments. Ideally at fixed two minute boundaries like 12:00, 12:02, 12:04, no matter when the tab was opened.&lt;/p&gt;

&lt;h3&gt;
  
  
  The idea: align all fetches to real time
&lt;/h3&gt;

&lt;p&gt;Instead of starting a timer immediately, I decided to align fetching to the clock.&lt;/p&gt;

&lt;p&gt;The plan was simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Calculate how long until the next even two minute mark.&lt;/li&gt;
&lt;li&gt;Wait until that moment using setTimeout.&lt;/li&gt;
&lt;li&gt;From there, run a steady two minute interval using setInterval.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This way, every tab locks onto the same schedule.&lt;/p&gt;

&lt;h3&gt;
  
  
  The final implementation
&lt;/h3&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;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useCallback&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&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;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Page&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;setData&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;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Fetch function wrapped in useCallback to prevent unnecessary re-renders&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&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="k"&gt;try&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;response&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/getData&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;newData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&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;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Error fetching data:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&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="p"&gt;[]);&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="c1"&gt;// Fetch immediately on mount so users don't wait up to 2 minutes&lt;/span&gt;
    &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Get current time&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&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;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Calculate milliseconds until the next even 2-minute mark (12:00, 12:02, 12:04, etc.)&lt;/span&gt;
    &lt;span class="c1"&gt;// Formula: 120000ms (2 min) - (current position within 2-min cycle)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;millisecondsUntilNextEvenMinute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="mi"&gt;120000&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; 
      &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMinutes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="c1"&gt;// Minutes component&lt;/span&gt;
       &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSeconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;          &lt;span class="c1"&gt;// Seconds component&lt;/span&gt;
       &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMilliseconds&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;            &lt;span class="c1"&gt;// Milliseconds component for precision&lt;/span&gt;

    &lt;span class="c1"&gt;// Variable to store the interval ID so we can clear it on cleanup&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;intervalId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Wait until the next even 2-minute boundary&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timeoutId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="c1"&gt;// Fetch at the exact 2-minute mark&lt;/span&gt;
      &lt;span class="nf"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// Now that we're synced, fetch every 2 minutes going forward&lt;/span&gt;
      &lt;span class="nx"&gt;intervalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;120000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;millisecondsUntilNextEvenMinute&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Cleanup function: clear both timeout and interval to prevent memory leaks&lt;/span&gt;
    &lt;span class="k"&gt;return &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;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutId&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;intervalId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;intervalId&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fetchData&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// Include fetchData in dependencies&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&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;data&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading...&lt;/span&gt;&lt;span class="dl"&gt;"&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;/div&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What is happening here
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Initial delay calculation&lt;/strong&gt;&lt;br&gt;
I calculate how many milliseconds remain until the next even two minute mark.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Delayed first fetch&lt;/strong&gt;&lt;br&gt;
setTimeout ensures the first fetch happens exactly on that boundary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stable interval after sync&lt;/strong&gt;&lt;br&gt;
Once aligned, setInterval keeps all tabs fetching together every two minutes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The result
&lt;/h3&gt;

&lt;p&gt;Now every tab behaves the same way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open a tab at 12:01:29. It waits until 12:02:00, then fetches every two minutes.&lt;/li&gt;
&lt;li&gt;Open another tab at 12:01:58. It waits two seconds, then fetches at 12:02:00 alongside the first tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Perfect sync, every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Final thoughts
&lt;/h3&gt;

&lt;p&gt;This change looks small, but it dramatically improved consistency across tabs. The pattern behaves like a frontend cron job, anchored to real time rather than tab lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  When this approach makes sense
&lt;/h3&gt;

&lt;p&gt;This is useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data needs to stay consistent across multiple open tabs.&lt;/li&gt;
&lt;li&gt;Timing matters more than “fetch every X minutes from mount”.&lt;/li&gt;
&lt;li&gt;A plain setInterval introduces drift you cannot afford.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the difference between working code and reliable code is simply aligning with time instead of starting from zero.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Is "use client" Bad for SEO?</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:18:35 +0000</pubDate>
      <link>https://dev.to/mikelisrael/is-use-client-bad-for-seo-10gp</link>
      <guid>https://dev.to/mikelisrael/is-use-client-bad-for-seo-10gp</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fu89ldn7cm64xatk3fhnp.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.amazonaws.com%2Fuploads%2Farticles%2Fu89ldn7cm64xatk3fhnp.png" alt="Use client on white background" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've been working with Next.js lately, you've probably seen the &lt;code&gt;use client&lt;/code&gt; directive pop up in components. It's just one line at the top of a file, but it raises an important question: does it affect SEO?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer: No.&lt;/strong&gt; Client components don't hurt SEO. Let me explain why.&lt;/p&gt;

&lt;h2&gt;
  
  
  What use client actually does
&lt;/h2&gt;

&lt;p&gt;In Next.js 13 and above, components are server components by default. When you add &lt;code&gt;use client&lt;/code&gt; to a component, you're telling Next.js this component needs client-side features like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State (useState, useEffect)&lt;/li&gt;
&lt;li&gt;Event handlers&lt;/li&gt;
&lt;li&gt;Browser-only APIs (like window or localStorage)&lt;/li&gt;
&lt;li&gt;Animations or heavy interactivity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Here's the key thing:&lt;/strong&gt; Client components are still pre-rendered on the server and included in the initial HTML. Search engines see them immediately, exactly the same as server components.&lt;/p&gt;

&lt;p&gt;The difference is that client components are also bundled and sent to the browser for hydration—making them interactive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why people think use client hurts SEO (and why they're wrong)
&lt;/h2&gt;

&lt;p&gt;There's a common misconception that client components aren't server-rendered. This isn't true.&lt;/p&gt;

&lt;p&gt;Both server components and client components appear in the initial HTML that gets sent to the browser. From a crawler's perspective, there's no difference. Google sees the content immediately either way.&lt;/p&gt;

&lt;p&gt;So putting an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;, product description, or blog content in a client component is totally fine for SEO. The content is there in the HTML from the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually matters for SEO
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;use client&lt;/code&gt; itself doesn't hurt SEO, there are two things that DO matter:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Don't hide content behind interactivity
&lt;/h3&gt;

&lt;p&gt;This is the real SEO risk. If content only appears after a user clicks a button, opens a tab, or submits a form, crawlers won't see it.&lt;/p&gt;

&lt;p&gt;Crawlers load the page and run JavaScript, but they don't interact with it. They don't click buttons or switch tabs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad for SEO:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product details hidden behind a "Read More" button&lt;/li&gt;
&lt;li&gt;Pricing shown only after selecting a tab&lt;/li&gt;
&lt;li&gt;Content that requires form submission to view&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fine for SEO:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Interactive components that render content immediately&lt;/li&gt;
&lt;li&gt;Client components with state that shows content on load&lt;/li&gt;
&lt;li&gt;Animations that don't hide initial content&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Be mindful of performance
&lt;/h3&gt;

&lt;p&gt;Client components increase your JavaScript bundle size. If you're overusing &lt;code&gt;use client&lt;/code&gt; across your entire app, you can end up with a bloated bundle that slows down your site.&lt;/p&gt;

&lt;p&gt;Performance affects SEO indirectly through Core Web Vitals and page speed rankings. But this is a holistic concern about your app's architecture, not a per-component decision.&lt;/p&gt;

&lt;p&gt;You shouldn't avoid putting an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; in a client component just because it's an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;. If that component needs interactivity, use &lt;code&gt;use client&lt;/code&gt;. The performance cost comes from the component being in the bundle, not from what content it contains.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual decision tree
&lt;/h2&gt;

&lt;p&gt;When deciding between server and client components, ask yourself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this component need client-side features?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;State management?&lt;/li&gt;
&lt;li&gt;Event handlers?&lt;/li&gt;
&lt;li&gt;Browser APIs?&lt;/li&gt;
&lt;li&gt;Interactivity after page load?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Yes?&lt;/strong&gt; → Use &lt;code&gt;use client&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;No?&lt;/strong&gt; → Keep it as a server component&lt;/p&gt;

&lt;p&gt;That's it. It's about functionality, not SEO.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use use client correctly
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DO use &lt;code&gt;use client&lt;/code&gt; for:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forms with validation&lt;/li&gt;
&lt;li&gt;Interactive widgets&lt;/li&gt;
&lt;li&gt;Modals and dialogs&lt;/li&gt;
&lt;li&gt;Tabs (as long as content is visible on initial render)&lt;/li&gt;
&lt;li&gt;Animations&lt;/li&gt;
&lt;li&gt;Any component that needs state or browser APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DON'T worry about:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether the component contains an &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; or important text&lt;/li&gt;
&lt;li&gt;Search engine indexing—crawlers see client components just fine&lt;/li&gt;
&lt;li&gt;Per-component performance decisions based on content type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DO worry about:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hiding content behind clicks, tabs, or interactions&lt;/li&gt;
&lt;li&gt;Overall bundle size if you're using &lt;code&gt;use client&lt;/code&gt; everywhere&lt;/li&gt;
&lt;li&gt;Whether you actually need client features or just defaulted to &lt;code&gt;use client&lt;/code&gt; out of habit&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bottom line
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use client&lt;/code&gt; doesn't hurt SEO. Both client and server components appear in the initial HTML that crawlers see.&lt;/p&gt;

&lt;p&gt;The choice between them should be based on whether you need client-side features, not whether the content is important for SEO.&lt;/p&gt;

&lt;p&gt;The real SEO concerns are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hiding content behind interactivity&lt;/strong&gt; that requires user action&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overall app performance&lt;/strong&gt; from excessive JavaScript bundles&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Get those right, and you can use client components wherever you need them without worrying about SEO.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; An earlier version of this post incorrectly stated that client components aren't server-rendered and recommended against putting SEO-important content in them. That was wrong. Thanks to the Vercel and Next.js team for the corrections.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>performance</category>
    </item>
    <item>
      <title>Stop Using Redux for Request-Response Management – It’s Making Your Life Harder</title>
      <dc:creator>Israel Michael</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:11:35 +0000</pubDate>
      <link>https://dev.to/mikelisrael/stop-using-redux-for-request-response-management-its-making-your-life-harder-18a0</link>
      <guid>https://dev.to/mikelisrael/stop-using-redux-for-request-response-management-its-making-your-life-harder-18a0</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2F6itsejhsind520qpgsq8.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.amazonaws.com%2Fuploads%2Farticles%2F6itsejhsind520qpgsq8.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let me be blunt: if you’re still using Redux to manage API requests, you’re making your life harder than it needs to be.&lt;/p&gt;

&lt;p&gt;Redux has had a legendary run. For years, it was &lt;em&gt;the&lt;/em&gt; default state management choice for React apps. Many of us learned it early, shipped production apps with it, and defended it in code reviews. But here’s the part that often gets ignored: Redux was never designed to manage server state.&lt;/p&gt;

&lt;p&gt;Yet somehow, it became the go-to tool for handling API requests, responses, loading states, caching, retries, and everything in between. And if you’ve ever felt like that setup is exhausting, frustrating, or unnecessarily complex, you’re not imagining things. It really is that bad.&lt;/p&gt;

&lt;p&gt;The good news is that it doesn’t have to be this way. There’s a tool that actually understands server state, and that tool is React Query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Redux Falls Apart With API Calls
&lt;/h3&gt;

&lt;p&gt;Using Redux for request-response data feels a bit like forcing a tool to do a job it was never built for. Yes, it can work, but the friction is constant.&lt;/p&gt;

&lt;p&gt;First, there’s the boilerplate. For a single API call, you define request, success, and failure action types. Then you write action creators. Then reducers. Then middleware like thunk or saga just to make async logic possible. After all that, you still have to manually track loading states, error states, caching behavior, and refetch logic in your UI.&lt;/p&gt;

&lt;p&gt;All of this effort just to fetch data and display it.&lt;/p&gt;

&lt;p&gt;Second, Redux has no concept of stale data. Server data changes over time, but Redux doesn’t know that. If you want fresh data, you have to explicitly tell it when to refetch. That means more logic for pagination, refetch-on-focus, refetch-on-mount, background updates, and avoiding duplicate requests. Miss one edge case, and suddenly your users are looking at outdated data.&lt;/p&gt;

&lt;p&gt;Then there’s the bigger design issue: not all API data belongs in global state. Many API responses are temporary, screen-specific, or tied to a single user flow. Redux forces everything into a global store, which makes debugging harder and bloats your state with data that doesn’t need to live there long-term.&lt;/p&gt;

&lt;p&gt;At some point, you step back and realize you’re fighting the tool instead of using it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where React Query Changes Everything
&lt;/h3&gt;

&lt;p&gt;React Query flips the entire approach. Instead of treating server data like client state, it treats it as what it really is: data that comes from the server and needs to stay in sync with it.&lt;/p&gt;

&lt;p&gt;There’s no boilerplate circus. You write a query, get your data, loading state, and errors from a hook, and move on. No actions. No reducers. No middleware. Just data.&lt;/p&gt;

&lt;p&gt;Caching is automatic. React Query knows when data is fresh, when it’s stale, and when it should refetch. It updates data when users revisit a page, when the browser regains focus, or when a mutation changes something on the server. You don’t write extra logic for this. It just happens.&lt;/p&gt;

&lt;p&gt;Mutations are where the difference really shows. Things like optimistic updates, retries, and error handling that are painful in Redux become straightforward. You describe what should happen, and React Query handles the mechanics.&lt;/p&gt;

&lt;p&gt;The result is code that’s easier to read, easier to reason about, and far less fragile.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does This Mean Redux Is Dead?
&lt;/h3&gt;

&lt;p&gt;Not at all. Redux is still useful for client state. UI state, authentication state, feature flags, theme settings, and other global concerns still fit nicely there.&lt;/p&gt;

&lt;p&gt;But server state is a different problem space. Treating it like client state is the mistake. Once you separate the two, things click into place very quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;If you’re using Redux for API requests because “that’s how it’s always been done,” it might be time to reassess. There’s no prize for maintaining 500 lines of boilerplate just to fetch data.&lt;/p&gt;

&lt;p&gt;React Query exists because this problem needed a better solution. Less code. Fewer bugs. Better performance. Saner defaults.&lt;/p&gt;

&lt;p&gt;Your future self will appreciate the switch.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
