<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Let's Code]]></title><description><![CDATA[Welcome to "Let's Code" a blog dedicated to web development, where I share insights, tutorials, and experiences to help you enhance your skills and stay updated with the latest industry trends.]]></description><link>https://letscode.adelpro.us.kg</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1738511663995/2c08da91-f976-4fd3-98b7-ddc024854f8c.png</url><title>Let&apos;s Code</title><link>https://letscode.adelpro.us.kg</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 11 Apr 2026 09:47:35 GMT</lastBuildDate><atom:link href="https://letscode.adelpro.us.kg/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Quran Search Engine v0.3.x-(athar) Released: What's New and How to Migrate from v0.1.5]]></title><description><![CDATA[Building search functionalities for Arabic texts—especially the Quran—comes with unique challenges, from morphology and lemmatization to phonetic matching and performance bottlenecks. The quran-search]]></description><link>https://letscode.adelpro.us.kg/quran-search-engine-v0-3-x-athar-released-what-s-new-and-how-to-migrate-from-v0-1-5</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/quran-search-engine-v0-3-x-athar-released-what-s-new-and-how-to-migrate-from-v0-1-5</guid><category><![CDATA[quran-search-engine]]></category><category><![CDATA[Quran]]></category><category><![CDATA[Quran search]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Sun, 05 Apr 2026 16:01:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/624c1b304b4dde0dc0bec7d4/fa757973-e532-48cc-8eac-5ed268403aa8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building search functionalities for Arabic texts—especially the Quran—comes with unique challenges, from morphology and lemmatization to phonetic matching and performance bottlenecks. The <a href="https://github.com/adelpro/quran-search-engine"><strong><strong>quran-search-engine</strong></strong></a> library was built to solve this by providing a stateless, UI-agnostic, and purely TypeScript-driven search engine.</p>
<p>With the release of <strong><strong>v0.3.x-(athar)</strong></strong>, the library has undergone a massive architectural shift to prioritize speed, scalability, and developer experience. If you are currently using <strong><strong>v0.1.5</strong></strong> (or v0.2.x), you will notice significant breaking changes designed to make your application much faster.</p>
<p>In this article, we will break down what's new in version 0.3.x-(athar) and provide a comprehensive, step-by-step guide on how to migrate your codebase from v0.1.5 to v0.3.x-(athar).</p>
<hr />
<h2>What is <code>quran-search-engine</code>?</h2>
<p>For those new to the library, <code>quran-search-engine</code> is a deterministic, client/server-side search engine for the Quran. Unlike traditional solutions that tie you to a specific UI or require a dedicated backend server, this library runs anywhere JavaScript runs (vanilla JavaScript, React, Vue, React Native, Node.js ... etc).</p>
<p>It features:</p>
<ul>
<li><em><strong>Advanced Linguistic Search</strong></em>*: Lemma and root matching.</li>
<li><em><strong>Semantic Search</strong></em>*: Concept-based mapping (e.g., searching for English concepts to find Arabic verses).</li>
<li><em><strong>Boolean Operators</strong></em>*: <code>AND(+)</code>, <code>OR(|)</code>, <code>NOT(-)</code>, and grouped queries.</li>
<li><em><strong>Regex Search</strong></em>*: Pattern-based queries with built-in ReDoS safety validation.</li>
<li><em><strong>Range Queries</strong></em>*: Verse-coordinate lookups like <code>2:255</code> or <code>1:1-7</code>.</li>
<li><em><strong>Phonetic Search (Pronunciation)</strong></em>*: Latin-to-Arabic transliteration for non-Arabic queries (e.g., searching "Alhamdulillah" to find "الحمد لله").</li>
<li><em><strong>Cross-Language Search</strong></em>*: English-to-Arabic search out of the box, which can be easily extended or replaced with other languages (e.g., French, Urdu).</li>
<li><em><strong>Fuzzy Search</strong></em>*: Fallback matching for misspelled words.</li>
<li><em><strong>UI-Agnostic Highlighting</strong></em>*: Returns precise match offsets (<code>HighlightRanges</code>) instead of raw HTML, pushing the display responsibility to your framework (React, Next.js, React Native, Terminals, etc.).</li>
<li><em><strong>Stateless Architecture</strong></em>*: You control the data loading, caching, and state management.</li>
</ul>
<h3>The New Layered Search Architecture</h3>
<p>To ensure the engine remains highly performant, maintainable, and extensible, the search logic has been refactored into a <strong>Layered Search Pipeline</strong>. The orchestrator (<code>search()</code>) routes each query through a strict priority cascade, where each layer is an isolated, independently testable module located in <code>core/layers/</code>:</p>
<ol>
<li><strong>Range Layer</strong>: Verse-coordinate queries (<code>2:255</code>) short-circuit all linguistic processing.</li>
<li><strong>Boolean Layer</strong>: Parses complex logic (<code>AND</code>, <code>OR</code>, <code>NOT</code>, <code>()</code>) into an AST.</li>
<li><strong>Regex Layer</strong>: Executes pattern queries in isolation (skipping linguistic layers).</li>
<li><strong>Simple Layer</strong>: Exact token matching in normalized Arabic text.</li>
<li><strong>Linguistic Layer</strong>: Lemma and root matching via morphology data.</li>
<li><strong>Fuse Layer</strong>: Fuzzy fallback matching (Fuse.js) for unmatched tokens.</li>
<li><strong>Semantic Layer</strong>: Concept expansion and cross-language mappings.</li>
<li><strong>Phonetic Layer</strong>: Latin→Arabic transliteration processing.</li>
</ol>
<p>This modular structure means that <strong>managing, adding, or updating new search features</strong> is now as simple as inserting a new isolated layer into the pipeline without breaking existing functionality or risking monolithic complexity.</p>
<hr />
<p><strong>## What’s New in Version 0.3.x(athar)?</strong></p>
<p>The jump from v0.1.5 to v0.3.x-(athar) brings game-changing performance optimizations. Here are the major highlights:</p>
<p><strong>### 1. Architectural Shift: Arrays to Maps for O(1) Performance</strong></p>
<p>In previous versions, the Quran data was stored and searched as an Array (<code>QuranText[]</code>). As queries grew more complex, array iterations became a bottleneck. In v0.3.x, data structures have been migrated to <strong><strong>Maps</strong></strong> (<code>Map&lt;number, QuranText&gt;</code>). This shift enables <strong><strong>O(1) access time</strong></strong>, drastically reducing the latency of verse lookups and filtering.</p>
<p><strong>### 2. The Context Object Pattern</strong></p>
<p>The <code>search()</code> function signature has been revamped. Instead of passing an endless list of positional arguments, v0.3.x-(athar) introduces a clean <strong><strong>Search Context</strong></strong> object. This makes your code more readable and future-proof.</p>
<p><strong>### 3. Mandatory Inverted Index</strong></p>
<p>To achieve lightning-fast lookups for morphological and semantic searches, v0.3.x-(athar) introduces a mandatory <strong><strong>Inverted Index</strong></strong>. Instead of computing search paths on the fly, you now build an inverted index once during initialization (<code>buildInvertedIndex</code>) and pass it into the search context.</p>
<p><strong>### 4. Enhanced Web Worker Support &amp; Error Handling</strong></p>
<p>Moving search operations off the main thread is crucial for a smooth UI. Version 0.3.x-(athar) simplifies worker initialization and introduces the <code>WorkerError</code> class. This provides structured, type-safe error codes when dealing with web workers.</p>
<p><strong>### 5. Deprecations and Cleanups</strong></p>
<p>To keep the library lightweight and focused, several legacy features and data files from previous versions have been removed:</p>
<ul>
<li><em><strong>Pre-built Index Files</strong></em>*: Static JSON files (e.g., <code>lemma-index.json</code>, <code>root-index.json</code>, <code>word-index.json</code>) are no longer distributed. Instead, these indices are now built dynamically in-memory via <code>buildInvertedIndex()</code>, reducing bundle size and ensuring data consistency.</li>
</ul>
<hr />
<h2>Data Modularity</h2>
<p>One of the most powerful aspects of <code>quran-search-engine</code> is how it handles data ingestion. The library does not force heavy data files into your bundle; instead, it empowers you with dynamic loading and strict validation.</p>
<h3>1. Dynamic Data Loading</h3>
<p>All core datasets—including the Quran text, morphology maps, semantic words, and phonetic transliterations—can be dynamically loaded from the package on demand. This ensures your initial application bundle size remains incredibly small. You only load the datasets (like English semantic data or Latin transliteration data) when the user actually requests those features.</p>
<h3>2. "Bring Your Own Language"</h3>
<p>You are not locked into our default English semantic indices. The engine allows you to easily load another language by replacing the default English semantic map with your own (e.g., French, Urdu, or Turkish mappings). This gives you complete flexibility to build multi-lingual Quran applications without being bound to English as the only bridge for semantic search.</p>
<h3>3. Custom Data Validation</h3>
<p>Beyond just semantics, you can also inject custom datasets. The library exposes robust data validation functions to ensure your custom payloads are correctly structured.</p>
<p>As long as your custom dataset passes the engine's rigorous validation schema, the library guarantees the search engine won't crash when running advanced queries on them.</p>
<hr />
<h2>Highly Typed (TypeScript First)</h2>
<p>The library is written entirely in TypeScript and is strictly typed from the ground up. Every data model, search context, configuration object, and search response (like <code>HighlightRanges</code>) is backed by explicit interfaces.</p>
<p>This robust typing provides:</p>
<ul>
<li><strong>Excellent IDE autocomplete</strong>: Discover available search options and response fields seamlessly.</li>
<li><strong>Runtime safety</strong>: Prevents errors during data hydration by enforcing strict contracts on custom datasets.</li>
<li><strong>Absolute confidence</strong>: Know exactly what properties exist on a verse or search result when manipulating them in your UI.</li>
</ul>
<hr />
<h2>Production Ready &amp; Heavily Tested</h2>
<p><code>quran-search-engine</code> is not just an experimental library; it is heavily tested in production environments. It powers the search infrastructure behind <strong><a href="https://github.com/adelpro/open-mushaf-native">Open Mushaf Native</a></strong>, a modern, fully-featured offline Quran application built with React Native and Expo. The library handles complex morphological queries seamlessly on mobile devices without relying on any backend API.</p>
<h2>Included Playground &amp; Examples</h2>
<p>To help you get started quickly, the repository ships with a comprehensive playground featuring multiple real-world integration examples. These examples demonstrate how to architect stateless searching across different frameworks:</p>
<ul>
<li><strong>Vanilla TypeScript</strong> (<code>examples/vanilla-ts</code>): Pure browser implementation without any VDOM overhead.</li>
<li><strong>React + Vite</strong> (<code>examples/vite-react</code>): A feature-rich web app showcasing a full search UI with highlighted components.</li>
<li><strong>Node.js</strong> (<code>examples/nodejs</code>): Bare-metal server-side searching via CLI.</li>
<li><strong>Angular</strong> (<code>examples/angular</code>): Dedicated Angular component highlighting implementation.</li>
</ul>
<p>You can easily spin up these examples locally using the provided yarn scripts (e.g., <code>yarn playground:react</code> or <code>yarn playground:node</code>).</p>
<h2>Offline &amp; AI-Friendly Documentation</h2>
<p>We understand that modern development workflows often involve AI pair programmers. That is why <code>quran-search-engine</code> ships with a comprehensive <code>docs/</code> folder directly inside the package.</p>
<p>This offline documentation is split into practical <strong>Guides</strong> and deep-dive <strong>References</strong> covering everything from search syntax to the core API. Whether you are reading it locally in your IDE or providing it as context to an AI agent (like GitHub Copilot or Trae), the markdown files are structured to be easily digestible and highly accessible without needing an internet connection.</p>
<h2>Powered by the ITQAN Community</h2>
<p>The architectural leap in version <code>0.3.x-(athar)</code> was heavily inspired and shaped by the brilliant developers in the <strong><a href="https://community.itqan.dev">ITQAN Community</a></strong>.</p>
<p>Their participation in the <strong>Athar Initiative</strong> (a collaborative effort to build advanced, open-source Quranic technologies) directly influenced the strict typings, the dynamic data loading approach, and the robust layered search architecture you see today. The name of this release, <code>athar</code> (meaning "impact" or "trace"), is dedicated to their ongoing contributions to the Quran technology ecosystem.</p>
<p>You can read more about the initial ideas and discussions that led to this release in the official <strong><a href="https://community.itqan.dev/d/254/15">Athar Initiative article on the ITQAN Community Forum</a></strong>.</p>
<hr />
<h2>Step-by-Step Migration Guide: From v0.1.5 to v0.3.x-(athar)</h2>
<p>Because v0.3.x introduces breaking changes, your v0.1.5 code will not work out of the box. Follow these steps to migrate your application safely.</p>
<h3>Step 1: Update Your State Definitions</h3>
<p>Since <code>quranData</code> is now a Map, and you need to store the new Inverted Index and Semantic Map, update your state hooks (if using React).</p>
<ul>
<li><em><strong>Before (v0.1.5):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
const [quranData, setQuranData] = useState&lt;QuranText[]&gt;([]);
</code></pre>
<ul>
<li><em><strong>After (v0.3.x):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
import { InvertedIndex, QuranText } from 'quran-search-engine';

const [quranData, setQuranData] = useState&lt;Map&lt;number, QuranText&gt; | null&gt;(null);

const [invertedIndex, setInvertedIndex] = useState&lt;InvertedIndex | null&gt;(null);

const [semanticMap, setSemanticMap] = useState&lt;Map&lt;string, string[]&gt; | null&gt;(null);

const [phoneticMap, setPhoneticMap] = useState&lt;Map&lt;string, string[]&gt; | null&gt;(null);
</code></pre>
<h3>Step 2: Load Data and Build the Inverted Index</h3>
<p>You must now load the semantic data and explicitly build the inverted index before allowing users to search.</p>
<ul>
<li><em><strong>Before (v0.1.5):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
const [data, morphology, dictionary] = await Promise.all([

loadQuranData(),

loadMorphology(),

loadWordMap(),

]);
</code></pre>
<ul>
<li><em><strong>After (v0.3.x-(athar)):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
import { loadQuranData, loadMorphology, loadWordMap, loadSemanticData, loadPhoneticData, buildInvertedIndex } from 'quran-search-engine';

const [data, morphology, dictionary, semantic, phonetic] = await Promise.all([

loadQuranData(),

loadMorphology(),

loadWordMap(),

loadSemanticData(), // New requirement for v0.3.x

loadPhoneticData(), // New requirement for phonetic search

]);

setQuranData(data);

setMorphologyMap(morphology);

setWordMap(dictionary);

setSemanticMap(semantic);

setPhoneticMap(phonetic);

// Build the inverted index synchronously

const index = buildInvertedIndex(morphology, data, semantic);

setInvertedIndex(index);
</code></pre>
<h3>Step 3: Refactor the Search Function Call</h3>
<p>The <code>search()</code> function no longer accepts <code>quranData</code>, <code>morphologyMap</code>, and <code>wordMap</code> as separate positional arguments. They must be grouped into a context object alongside the <code>invertedIndex</code>.</p>
<ul>
<li><em><strong>Before (v0.1.5):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
const response = search(

query,

quranData,

morphologyMap,

wordMap,

options,

{ page: 1, limit: 10 },

undefined,

searchCache

);
</code></pre>
<ul>
<li><em><strong>After (v0.3.x-(athar)):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
const response = search(
  query,
  {
    quranData,
    morphologyMap,
    wordMap,
    invertedIndex,
    semanticMap, // Used for semantic search (cross-language)
    transliterationMap, // Used for phonetic search (pronunciation)
  },
  {
    ...options,
    isRegex: false, // New regex layer option
    isBoolean: false, // New boolean parsing option
    phonetic: false, // New phonetic matching option
  },
  { page: 1, limit: 10 },
  undefined,
  searchCache
);
</code></pre>
<h3>Step 4: Update Web Worker Initialization (If Applicable)</h3>
<p>If you were offloading search to a Web Worker, the initialization path has been simplified using Vite/Webpack dynamic URL imports.</p>
<ul>
<li><em><strong>Before (v0.1.5):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
const client = createSearchWorker({

workerUrl: new URL('quran-search-engine/worker', import.meta.url),

});

await client.initData();
</code></pre>
<ul>
<li><em><strong>After (v0.3.x):</strong></em>*</li>
</ul>
<pre><code class="language-typescript">
// Dynamically import the worker URL

const mod = await import('quran-search-engine/worker?url');

const client = createSearchWorker({ workerUrl: mod.default });

await client.initData();
</code></pre>
<p>You can now also gracefully catch worker errors:</p>
<pre><code class="language-typescript">
import { WorkerError } from 'quran-search-engine';

try {

await client.runSearch(query, options, pagination);

} catch (error) {

if (error instanceof WorkerError) {

console.error(`Worker failed with code: ${error.code}`);

}

}
</code></pre>
<hr />
<h2>Complete Before &amp; After Comparison</h2>
<p>To see the big picture, here is how a standard React component initialization looks before and after the migration.</p>
<h3>❌ The Old Way (v0.1.5)</h3>
<pre><code class="language-typescript">
useEffect(() =&gt; {

async function init() {

const [data, morphology, dictionary] = await Promise.all([

loadQuranData(),

loadMorphology(),

loadWordMap(),

]);

setQuranData(data);

setMorphologyMap(morphology);

setWordMap(dictionary);

}

init();

}, []);

// Later in the search handler...

search(query, quranData, morphologyMap, wordMap, options, pagination);
</code></pre>
<h3>✅ The New Way (v0.3.x-(athar))</h3>
<pre><code class="language-typescript">
useEffect(() =&gt; {

async function init() {

const [data, morphology, dictionary, semantic, phonetic] = await Promise.all([

loadQuranData(),

loadMorphology(),

loadWordMap(),

loadSemanticData(),

loadPhoneticData(),

]);

setQuranData(data);

setMorphologyMap(morphology);

setWordMap(dictionary);

setSemanticMap(semantic);

setPhoneticMap(phonetic);

// Build the inverted index once

const index = buildInvertedIndex(morphology, data, semantic);

setInvertedIndex(index);

}

init();

}, []);

// Later in the search handler...

search(query, { quranData, morphologyMap, wordMap, invertedIndex, semanticMap, phoneticMap }, options, pagination);
</code></pre>
<hr />
<h2>Conclusion</h2>
<p>The release of <strong><strong>v0.3.x</strong></strong> marks a maturity milestone for the <code>quran-search-engine</code>. By shifting from Arrays to Maps and enforcing an Inverted Index, the library now guarantees <strong><strong>O(1) access times</strong></strong>, allowing for instant, complex searches across the entire Quran text without freezing the main thread.</p>
<p>While migrating from v0.1.5 requires adjusting your data loading logic and function signatures, the performance gains are well worth the effort.</p>
<p>Ready to upgrade? Run <code>yarn add quran-search-engine@latest</code> today! If you encounter any issues during your migration, feel free to check out the <a href="https://github.com/adelpro/quran-search-engine">official repository</a> and open an issue. Happy coding!</p>
]]></content:encoded></item><item><title><![CDATA[Use Lubb AI Writer for Free with OpenRouter]]></title><description><![CDATA[Lubb AI Writer is designed to be provider-agnostic. That means you’re not locked into paid APIs. With OpenRouter, you can run it completely free using community models.
Here’s the exact setup.
1) Clon]]></description><link>https://letscode.adelpro.us.kg/use-lubb-ai-writer-for-free-with-openrouter</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/use-lubb-ai-writer-for-free-with-openrouter</guid><category><![CDATA[extension]]></category><category><![CDATA[fireofx]]></category><category><![CDATA[chrome extension]]></category><category><![CDATA[firefox addon]]></category><category><![CDATA[Edge Addons]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Wed, 01 Apr 2026 15:20:04 GMT</pubDate><content:encoded><![CDATA[<p><img src="https://raw.githubusercontent.com/adelpro/lubb-writer/main/screenshots/modal.png" alt="Lubb API Screen" />
Lubb AI Writer is designed to be provider-agnostic. That means you’re not locked into paid APIs. With OpenRouter, you can run it completely free using community models.</p>
<p>Here’s the exact setup.</p>
<h3>1) Clone the project</h3>
<pre><code class="language-Bash">git clone https://github.com/adelpro/lubb-writer.git
cd lubb-writer
</code></pre>
<h3>2) Configure environment</h3>
<p>Rename the example file:</p>
<pre><code>mv .env.example .env
</code></pre>
<p>Generating Your Lubb API Token</p>
<p>The API_TOKEN authenticates requests to the Lubb backend. You can generate it locally or via a key generator, no registration required.</p>
<pre><code>node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
</code></pre>
<p>This will output a 64-character token. Copy it into your .env:</p>
<pre><code>API_TOKEN=generated-token-from-node
</code></pre>
<p>Edit .env and add your OpenRouter config:</p>
<pre><code>CUSTOM_PROVIDER_2_API_KEY=your-openrouter-key
CUSTOM_PROVIDER_2_BASE_URL=https://openrouter.ai/api/v1
CUSTOM_PROVIDER_2_MODELS=qwen/qwen3.6-plus-preview:free
</code></pre>
<p><strong>Models:</strong>
Search OpenRouter for :free models
You can chain multiple models:
CUSTOM_PROVIDER_2_MODELS=qwen/qwen3.6-plus-preview:free,mistralai/mistral-7b-instruct:free</p>
<p>This gives fallback + flexibility. Better UX, more resilience.</p>
<h3>3) Start the API</h3>
<p>Run the backend container:</p>
<pre><code>docker compose up -d api
</code></pre>
<p>Or if you're running locally without Docker:</p>
<pre><code>npm install
npm run dev
</code></pre>
<h3>4) Install the extension</h3>
<p><img src="https://raw.githubusercontent.com/adelpro/lubb-writer/main/screenshots/popup.png" alt="Lubb API Screen" /></p>
<p>Install Lubb AI Writer from:</p>
<ul>
<li>Chrome Web Store: <a href="https://chromewebstore.google.com/detail/lubb-ai-writer/hbdghnimecpnpoomobaeeeeekleanjjo">https://chromewebstore.google.com/detail/lubb-ai-writer/hbdghnimecpnpoomobaeeeeekleanjjo</a></li>
<li>Edge Add-ons: <a href="https://microsoftedge.microsoft.com/addons/detail/lubb-ai-writer/faoekdjnfhnkkjimlbdecgoffpdekpbb">https://microsoftedge.microsoft.com/addons/detail/lubb-ai-writer/faoekdjnfhnkkjimlbdecgoffpdekpbb</a></li>
<li>Firefox Add-ons: <a href="https://addons.mozilla.org/en-US/firefox/addon/lubb-ai-writer/">https://addons.mozilla.org/en-US/firefox/addon/lubb-ai-writer/</a></li>
<li>Or use your built version if running locally</li>
</ul>
<h3>5) Connect the extension</h3>
<p><img src="https://raw.githubusercontent.com/adelpro/lubb-writer/main/screenshots/api-screen.png" alt="Lubb API Screen" /></p>
<p>Inside the extension settings:</p>
<p>Set API URL → your local API (e.g. <a href="http://localhost:3001">http://localhost:3001</a>)
Set API Key → same key used in .env</p>
<h3>6) Done</h3>
<p>You now have:</p>
<ul>
<li>Free AI writing</li>
<li>Works on any input field</li>
<li>Multiple model fallback</li>
<li>Zero vendor lock-in</li>
</ul>
<p>For more information: <a href="https://lubb-writer.adelpro.us.kg/">https://lubb-writer.adelpro.us.kg/</a></p>
]]></content:encoded></item><item><title><![CDATA[I Built an AI Writing Tool That Works With Any Provider — Here's How]]></title><description><![CDATA[I Built an AI Writing Tool That Works With Any Provider — Here's How
I was tired of paying for multiple AI subscriptions just to handle different writing tasks. So I built Lubb Writer — an open-source]]></description><link>https://letscode.adelpro.us.kg/i-built-an-ai-writing-tool-that-works-with-any-provider-here-s-how</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/i-built-an-ai-writing-tool-that-works-with-any-provider-here-s-how</guid><category><![CDATA[AI]]></category><category><![CDATA[writing]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[chrome extension]]></category><category><![CDATA[Plasmo]]></category><category><![CDATA[firefox addon]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Mon, 23 Mar 2026 07:54:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/624c1b304b4dde0dc0bec7d4/1253280f-7791-4b34-b056-f9c06bec6721.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>I Built an AI Writing Tool That Works With Any Provider — Here's How</h1>
<p>I was tired of paying for multiple AI subscriptions just to handle different writing tasks. So I built <strong>Lubb Writer</strong> — an open-source tool that gives you 13 different text enhancement modes through a single interface.</p>
<p><strong>Home page:</strong> <a href="https://lubb-writer.adelpro.us.kg/">https://lubb-writer.adelpro.us.kg/</a></p>
<h2>The Problem</h2>
<p>Most AI writing tools are:</p>
<ul>
<li><p>Expensive ($20-50/month)</p>
</li>
<li><p>Single-provider (you're locked in)</p>
</li>
<li><p>Desktop-only or require copy-pasting</p>
</li>
</ul>
<p>I wanted something that:</p>
<ul>
<li><p>Works with any AI provider</p>
</li>
<li><p>Integrates into my browser</p>
</li>
<li><p>Costs nothing if I already have API keys</p>
</li>
<li><p>Can be self-hosted</p>
</li>
</ul>
<h2>The Solution</h2>
<p><strong>Lubb Writer</strong> — a full-stack project with:</p>
<pre><code class="language-plaintext">lubb-writer/
├── api/           # Express + TypeScript REST API
├── extension/     # Plasmo framework (Chrome, Firefox, Edge)
└── docker-compose.yml
</code></pre>
<h2>Key Technical Features</h2>
<h3>Multi-Provider Abstraction</h3>
<pre><code class="language-typescript">// provider-adapter.ts (simplified)
interface AIProvider {
  enhance(text: string, mode: WritingMode): Promise&lt;EnhancedText&gt;;
}

class ProviderFactory {
  static create(provider: ProviderType): AIProvider {
    switch (provider) {
      case 'openai': return new OpenAIAdapter();
      case 'anthropic': return new ClaudeAdapter();
      case 'gemini': return new GeminiAdapter();
      case 'custom': return new CustomAdapter();
    }
  }
}
</code></pre>
<p>Each adapter handles:</p>
<ul>
<li><p>API authentication</p>
</li>
<li><p>Request/response transformation</p>
</li>
<li><p>Error handling</p>
</li>
<li><p>Token counting</p>
</li>
</ul>
<h3>Browser Extension with Plasmo</h3>
<p><a href="https://plasmo.com/">Plasmo</a> makes cross-browser extensions painless:</p>
<pre><code class="language-typescript">// popup.tsx
function Popup() {
  const [text, setText] = useState('');
  
  return (
    &lt;div className="w-80 p-4"&gt;
      &lt;textarea 
        value={text}
        onChange={(e) =&gt; setText(e.target.value)}
        placeholder="Enter text to enhance..."
      /&gt;
      &lt;ModeSelector onSelect={handleMode} /&gt;
      &lt;EnhanceButton onClick={() =&gt; enhance(text)} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3>One-Command Deployment</h3>
<pre><code class="language-bash"># Deploy the API
git clone https://github.com/YOUR_USERNAME/lubb-writer.git
cd lubb-writer/api
cp .env.example .env
# Add your API keys
docker compose up -d

# Install the extension
# Chrome: Load from build/chrome directory
# Firefox: Load from build/firefox directory
</code></pre>
<h2>13 Writing Modes</h2>
<table>
<thead>
<tr>
<th>Mode</th>
<th>Use Case</th>
</tr>
</thead>
<tbody><tr>
<td>rewrite</td>
<td>Paraphrase while keeping meaning</td>
</tr>
<tr>
<td>summarize</td>
<td>Condense text</td>
</tr>
<tr>
<td>humanize</td>
<td>Make AI text sound natural</td>
</tr>
<tr>
<td>grammar</td>
<td>Fix errors</td>
</tr>
<tr>
<td>formal</td>
<td>Professional tone</td>
</tr>
<tr>
<td>casual</td>
<td>Conversational tone</td>
</tr>
<tr>
<td>academic</td>
<td>Scholarly writing</td>
</tr>
<tr>
<td>SEO</td>
<td>Optimize for search</td>
</tr>
<tr>
<td>persuasive</td>
<td>Marketing copy</td>
</tr>
<tr>
<td>creative</td>
<td>Creative writing</td>
</tr>
<tr>
<td>Twitter</td>
<td>Character-limited posts</td>
</tr>
<tr>
<td>LinkedIn</td>
<td>Professional content</td>
</tr>
<tr>
<td>story</td>
<td>Narrative content</td>
</tr>
</tbody></table>
<h2>What I Learned</h2>
<ol>
<li><p><strong>Provider abstraction is 60% of the work</strong> — each AI has different quirks</p>
</li>
<li><p><strong>Keyboard shortcuts &gt; click paths</strong> — Ctrl+Shift+Y is much faster than navigating menus</p>
</li>
<li><p><strong>Docker is non-negotiable</strong> for easy self-hosting</p>
</li>
<li><p><strong>Plasmo &gt; vanilla extension APIs</strong> — cross-browser support without the headache</p>
</li>
</ol>
<h2>Try It</h2>
<p><strong>GitHub</strong>: <a href="https://github.com/adelpro/lubb-writer">https://github.com/adelpro/lubb-writer</a></p>
<p>Clone it, run it, break it, improve it. PRs welcome!</p>
<h2>What's Next?</h2>
<ul>
<li><p>[ ] Streaming responses for longer texts</p>
</li>
<li><p>[ ] Team collaboration features</p>
</li>
<li><p>[ ] API key management dashboard</p>
</li>
<li><p>[ ] More writing modes</p>
</li>
</ul>
<p>What features would you want to see? Drop a comment!</p>
]]></content:encoded></item><item><title><![CDATA[I Built a 48-Skill Arsenal for My AI Agent — Here's What's Inside]]></title><description><![CDATA[The Problem with Generic AI Agents
Most AI agents are great at being chatty. Not so great at being useful.
Out of the box, an AI agent can answer questions. What it cannot do is:

Review a pull reques]]></description><link>https://letscode.adelpro.us.kg/i-built-a-48-skill-arsenal-for-my-ai-agent-here-s-what-s-inside</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/i-built-a-48-skill-arsenal-for-my-ai-agent-here-s-what-s-inside</guid><category><![CDATA[openclaw]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[automation]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Sat, 21 Mar 2026 15:45:07 GMT</pubDate><content:encoded><![CDATA[<h2>The Problem with Generic AI Agents</h2>
<p>Most AI agents are great at being chatty. Not so great at being useful.</p>
<p>Out of the box, an AI agent can answer questions. What it cannot do is:</p>
<ul>
<li><p>Review a pull request and comment with specific line-level feedback</p>
</li>
<li><p>Run a semantic search across your entire codebase</p>
</li>
<li><p>Set up a private search engine for your homelab in 10 minutes</p>
</li>
<li><p>Wake up each morning and brief you on what happened overnight</p>
</li>
<li><p>Improve itself after every failure</p>
</li>
</ul>
<p>I needed an agent that could actually do work — not just talk about work.</p>
<p>That is what <strong>openclaw-arsenals</strong> is.</p>
<hr />
<h2>What Is It?</h2>
<p>A production-ready skill and agent collection for OpenClaw, built over 6 months of real use.</p>
<p><strong>48 skills</strong>, <strong>13 named agents</strong>, <strong>16 infrastructure guides</strong>, and <strong>10 automation workflows</strong> — all sanitized, documented, and tested.</p>
<p>Clone it:</p>
<pre><code class="language-plaintext">git clone https://github.com/adelpro/openclaw-arsenals.git
cp -r * ~/.openclaw/skills/
</code></pre>
<hr />
<h2>Skills</h2>
<h3>Write and Research</h3>
<ul>
<li><p><strong>humanizer</strong> — strips AI patterns from generated text. Makes outputs sound human, not robotic</p>
</li>
<li><p><strong>purple-cow-content</strong> — applies the Seth Godin / MrBeast virality lens to any content task</p>
</li>
<li><p><strong>reddit-readonly</strong> — curated Reddit research without API keys</p>
</li>
<li><p><strong>search-cluster</strong> — Google, Wikipedia, Reddit, and RSS in one query</p>
</li>
<li><p><strong>answeroverflow</strong> — searches Discord community discussions that never make it to Google</p>
</li>
</ul>
<h3>Code Quality</h3>
<ul>
<li><p><strong>pr-review</strong> — automated PR review with blast-radius analysis</p>
</li>
<li><p><strong>skill-engineer</strong> — builds skills with Designer / Reviewer / Tester quality gates</p>
</li>
<li><p><strong>git-workflow</strong> — conventional commits, branch strategy, PR etiquette</p>
</li>
<li><p><strong>github-ops</strong> — full repo lifecycle: create → push → release</p>
</li>
<li><p><strong>github-actions</strong> — CI/CD workflow generation with security hardening</p>
</li>
</ul>
<h3>Agent Orchestration</h3>
<ul>
<li><p><strong>council-of-wisdom</strong> — 3+ agents deliberate before a decision is made</p>
</li>
<li><p><strong>agent-orchestrator</strong> — decomposes a task, spawns sub-agents, consolidates results</p>
</li>
<li><p><strong>agent-team-orchestration</strong> — multi-agent teams with defined roles and handoff protocols</p>
</li>
</ul>
<h3>Self-Improvement (The Compounding Loop)</h3>
<p><code>foundry</code> is a self-writing agent. It observes failures, crystallizes patterns into permanent hooks, and evolves its own tools. Run it every night and your agent wakes up smarter than when it went to sleep.</p>
<p>The loop:</p>
<ul>
<li><p>Work → Foundry records outcome</p>
</li>
<li><p>Failure repeated 5x → Hook crystallized (permanent fix)</p>
</li>
<li><p>Success → Pattern stored for next time</p>
</li>
<li><p>Nightly → compound-engineering reviews sessions, updates memory</p>
</li>
</ul>
<h3>Infrastructure</h3>
<ul>
<li><p><strong>private-web-search-searchxng</strong> — self-hosted search, zero tracking</p>
</li>
<li><p><strong>qdrant-ollama</strong> — semantic code search with local embeddings</p>
</li>
<li><p><strong>context7</strong> — documentation with citations for 100+ libraries</p>
</li>
<li><p><strong>guardian</strong> — AI-powered penetration testing automation</p>
</li>
<li><p><strong>docker-essentials</strong> — container management, debugging, compose patterns</p>
</li>
</ul>
<hr />
<h2>Agents</h2>
<p>Named agents with defined roles:</p>
<ul>
<li><p><strong>ORION</strong> — task orchestration and routing</p>
</li>
<li><p><strong>ATLAS</strong> — operations, health checks, morning briefings</p>
</li>
<li><p><strong>LENS</strong> — code quality, PR reviews, blast-radius analysis</p>
</li>
<li><p><strong>PROBE</strong> — API endpoint health and uptime alerts</p>
</li>
<li><p><strong>TRACE</strong> — bug analysis and regression detection</p>
</li>
<li><p><strong>ECHO</strong> — content, README, blog posts</p>
</li>
<li><p><strong>RANK</strong> — SEO audits and traffic analysis</p>
</li>
<li><p><strong>SCROLL</strong> — Reddit and community research</p>
</li>
<li><p><strong>NEWSLETTER</strong> — weekly digest curation</p>
</li>
<li><p><strong>VULN-SCANNER</strong> — dependency audits, CVE checks, secret scanning</p>
</li>
</ul>
<hr />
<h2>Infrastructure Guides</h2>
<p>Step-by-step setup guides for:</p>
<ul>
<li><p>Karakeep (bookmark storage + AI retrieval)</p>
</li>
<li><p>SearXNG (private search)</p>
</li>
<li><p>Qdrant + Ollama (semantic search)</p>
</li>
<li><p>Cloudflare Tunnel (expose homelab securely)</p>
</li>
<li><p>Portainer (container management UI)</p>
</li>
<li><p>GitHub MCP (GitHub as MCP server)</p>
</li>
<li><p>SocratiCode (semantic code search)</p>
</li>
<li><p>Secrets management (zero secrets in workspace)</p>
</li>
<li><p>Heartbeat monitoring (lightweight agent health checks)</p>
</li>
<li><p>Documents parsing (PDF → Markdown for RAG pipelines)</p>
</li>
</ul>
<hr />
<h2>Workflows</h2>
<p>Ten end-to-end pipelines:</p>
<ul>
<li><p><strong>second-brain-karakeep</strong> — save link → auto-tag → summarise → store</p>
</li>
<li><p><strong>pr-review-pipeline</strong> — PR → security scan → blast-radius → comment</p>
</li>
<li><p><strong>daily-digest</strong> — GitHub + weather + news → morning briefing</p>
</li>
<li><p><strong>code-to-release</strong> — branch → CI → merge → release → announce</p>
</li>
<li><p><strong>semantic-code-search</strong> — natural language → file references</p>
</li>
<li><p><strong>content-publishing-pipeline</strong> — research → write → humanize → visual → publish</p>
</li>
<li><p><strong>skill-creation-pipeline</strong> — idea → spec → build → review → publish</p>
</li>
<li><p><strong>homelab-health-check</strong> — Docker health → alert → auto-restart</p>
</li>
<li><p><strong>multi-agent-task-pipeline</strong> — ORION decompose → parallel agents → consolidate</p>
</li>
<li><p><strong>seo-audit-workflow</strong> — crawl → fix → rank → report</p>
</li>
</ul>
<hr />
<h2>Security</h2>
<p>Every skill in this repo has been sanitized. No API keys. No internal hostnames. No personal URLs.</p>
<p>Workspace files reference key names only — never the actual values. Secrets live in systemd environment variables, not in markdown files.</p>
<p>Before any commit, this runs:</p>
<pre><code class="language-plaintext">grep -rE "ak2_|ghp_|eyJ|sk-|=[a-zA-Z0-9]{20,}"   --include="*.md" . 2&gt;/dev/null
</code></pre>
<hr />
<h2>The Compounding Effect</h2>
<p>Most automation breaks down over time. You build it, it works, then the world changes and it silently stops working.</p>
<p>openclaw-arsenals is designed to be self-maintaining. The foundry loop means failures generate permanent fixes. The memory stack means context compounds. The heartbeat monitoring means infrastructure drift is caught automatically.</p>
<p>After 6 months of use, the agent is measurably better at its job than it was on day one.</p>
<hr />
<p><strong>Next steps:</strong></p>
<ul>
<li><p>Star the repo: <a href="https://github.com/adelpro/openclaw-arsenals">https://github.com/adelpro/openclaw-arsenals</a></p>
</li>
<li><p>Clone it and try one workflow</p>
</li>
<li><p>Open an issue with a skill request</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[I built an MCP Server to stop AI from hallucinating Quranic verses – Introducing quran-search-engine-mcp]]></title><description><![CDATA[​I wanted to share a tool I’ve been working on to solve a major issue when using LLMs for religious study: Hallucinations.
We’ve all seen AIs confidently misquote verses or mix up chapters. To fix this, I developed quran-search-engine-mcp. It’s a Mod...]]></description><link>https://letscode.adelpro.us.kg/i-built-an-mcp-server-to-stop-ai-from-hallucinating-quranic-verses-introducing-quran-search-engine-mcp</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/i-built-an-mcp-server-to-stop-ai-from-hallucinating-quranic-verses-introducing-quran-search-engine-mcp</guid><category><![CDATA[AI]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[Quran]]></category><category><![CDATA[Arabic ]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Tue, 20 Jan 2026 22:37:49 GMT</pubDate><content:encoded><![CDATA[<p>​I wanted to share a tool I’ve been working on to solve a major issue when using LLMs for religious study: Hallucinations.</p>
<p>We’ve all seen AIs confidently misquote verses or mix up chapters. To fix this, I developed quran-search-engine-mcp. It’s a Model Context Protocol (MCP) server that connects any MCP-compatible AI (like Claude Desktop) directly to a reliable Quranic database.</p>
<p>​How it works:<br />​Instead of letting the AI guess or "recall" a verse from its training data, the AI sends the search intent to the MCP server. The server fetches the literal, accurate text, and the AI then uses that verified data for its response.</p>
<p>​Features:<br />​Zero Hallucinations: The AI only interprets the search results; the text itself is pulled directly from the source.<br />​Semantic/Contextual Search: It doesn't just look for exact words. For example, if you search for "Prophet Yunus and all his synonyms," the engine understands the context and retrieves verses mentioning "Yunus," "Dhul-Nun," and "The Companion of the Whale."<br />​Plug &amp; Play: If you use Claude Desktop, you can add it instantly.</p>
<p>​Quick Setup for Claude Desktop:<br />​Add this to your claude_desktop_config.json:</p>
<pre><code class="lang-plaintext">{
  "mcpServers": {
    "quran-search-engine-mcp": {
      "command": "npx",
      "args": ["-y", "quran-search-engine-mcp"]
    }
  }
}
</code></pre>
<p>Why I built this:​This opens up possibilities for building highly accurate educational tools, research assistants, or any project that requires verified Quranic data without the risk of AI-generated errors.<br />​I’d love to get your feedback on the implementation!</p>
<p>www.github.com/adelpro/quran-search-engine-mcp</p>
]]></content:encoded></item><item><title><![CDATA[How I Successfully Implemented quran-search-engine in open-mushaf-native]]></title><description><![CDATA[When I started building advanced search for open-mushaf-native, I wanted three things:

powerful search (text, lemma, root, fuzzy)
good performance, with pagination and infinite scroll
code that stays simple and maintainable, not a mess of custom sea...]]></description><link>https://letscode.adelpro.us.kg/how-i-successfully-implemented-quran-search-engine-in-open-mushaf-native</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/how-i-successfully-implemented-quran-search-engine-in-open-mushaf-native</guid><category><![CDATA[Quran]]></category><category><![CDATA[quran insight]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[npm]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Sun, 18 Jan 2026 16:48:35 GMT</pubDate><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1768754700607/f3937da6-cf31-4136-8bf7-0c13a0f60322.png" alt class="image--center mx-auto" /></p>
<p>When I started building advanced search for <a target="_blank" href="https://github.com/adelpro/open-mushaf-native"><strong>open-mushaf-native</strong></a>, I wanted three things:</p>
<ul>
<li>powerful search (text, lemma, root, fuzzy)</li>
<li>good performance, with pagination and infinite scroll</li>
<li>code that stays simple and maintainable, not a mess of custom search logic</li>
</ul>
<p>The <a target="_blank" href="https://github.com/adelpro/quran-search-engine"><code>quran-search-engine</code></a> package is what made that possible. Instead of reinventing search from scratch, I delegated the heavy logic to a dedicated library and focused my app code on UI and UX.</p>
<p>This article explains how I integrated <code>quran-search-engine</code> into my app, what new features it unlocked (like pagination), and how it helped me shorten the codebase, reduce files, and still add more features and highlights.</p>
<hr />
<h2 id="heading-why-i-chose-quran-search-engine">Why I Chose <code>quran-search-engine</code></h2>
<p>Before using the package, implementing Quran search meant handling all of this myself:</p>
<ul>
<li>parsing and normalizing Arabic text</li>
<li>wiring in morphology and root/lemma data</li>
<li>writing custom matching logic for different search modes</li>
<li>trying to keep it fast and memory friendly</li>
</ul>
<p>That approach quickly becomes complex and hard to maintain.</p>
<p><code>quran-search-engine</code> solved that by providing:</p>
<ul>
<li>a single <code>search</code> function that takes the Quran text, morphology data, and a word map</li>
<li>support for multiple modes: exact text, lemma, root, and fuzzy</li>
<li>pagination built in, so I can fetch search results page by page</li>
<li>metadata such as matched tokens and token types that I can use for highlighting in the UI</li>
</ul>
<p>So instead of spreading search logic across multiple files, I only need to:</p>
<ul>
<li>prepare the data</li>
<li>call <code>search(...)</code> with the right parameters</li>
<li>render the results</li>
</ul>
<hr />
<h2 id="heading-was-it-easy-to-implement">Was It Easy to Implement?</h2>
<p>Yes – the integration was straightforward because the package has a clean, focused API.</p>
<p>At a high level, my implementation was:</p>
<ol>
<li>Install and import the package:<ul>
<li><code>import { search, type QuranText, type MorphologyAya, type WordMap } from 'quran-search-engine';</code></li>
</ul>
</li>
<li>Load the data I already had:<ul>
<li>Quran text (array of verses)</li>
<li>morphology JSON</li>
<li>word map JSON</li>
</ul>
</li>
<li>Wrap the <code>search</code> call in a custom React hook (<code>useQuranSearch</code>) that:<ul>
<li>accepts the current query, options, and pagination info</li>
<li>calls <code>search</code></li>
<li>exposes <code>results</code>, <code>counts</code>, and helper methods for highlighting</li>
</ul>
</li>
<li>Connect that hook to a search screen with a <code>FlatList</code> and infinite scroll.</li>
</ol>
<p>Because the package does the heavy lifting, the React Native side stays relatively small:</p>
<ul>
<li>one hook for search logic</li>
<li>one screen for UI and infinite scroll</li>
<li>one result item component for highlighting and navigation</li>
</ul>
<p>The hard parts (morphology, token-level matching, fuzzy search) all live inside <code>quran-search-engine</code>, not scattered across the app.</p>
<hr />
<h2 id="heading-pagination-the-new-feature-that-changed-the-ux">Pagination: The New Feature That Changed the UX</h2>
<p>One of the biggest wins from the package is native support for pagination.</p>
<p>The <code>search</code> function accepts <code>page</code> and <code>limit</code> options. Instead of loading all matches at once, I can ask only for:</p>
<ul>
<li>page 1, 50 items</li>
<li>page 2, 50 items</li>
<li>and so on</li>
</ul>
<p>In the app, I use:</p>
<ul>
<li><code>page</code>: React state that tracks the current page</li>
<li><code>PAGE_SIZE</code>: a constant, for example 50</li>
<li><code>FlatList.onEndReached</code>: to detect when the user scrolls near the end and then increment <code>page</code></li>
</ul>
<p>Every time <code>page</code> changes, my <code>useQuranSearch</code> hook calls:</p>
<pre><code class="lang-ts">search(
  normalizedQuery,
  quranData,
  morphologyMap,
  wordMap,
  {
    lemma: advancedOptions.lemma,
    root: advancedOptions.root,
    fuzzy: advancedOptions.fuzzy,
  },
  {
    page,
    limit: PAGE_SIZE,
  },
);
</code></pre>
<p>This gave me infinite scroll through <code>FlatList</code>:</p>
<ul>
<li>the package handles skipping and limiting results</li>
<li>the app just renders and appends each page</li>
<li>I don’t need to manually calculate offsets or slice arrays</li>
</ul>
<p>From a user perspective, it feels like a smooth endless list of search results. From a developer perspective, it’s just a <code>page</code> number and a <code>FlatList</code>.</p>
<hr />
<h2 id="heading-code-shorter-cleaner-and-with-less-files">Code Shorter, Cleaner, and With Less Files</h2>
<p>Before leaning on <code>quran-search-engine</code>, a search feature tends to produce:</p>
<ul>
<li>multiple utility files for normalization and matching</li>
<li>custom indexing logic spread across the project</li>
<li>a mix of UI and search logic inside the same components</li>
</ul>
<p>With the package, I was able to centralize and simplify:</p>
<ul>
<li>I keep search logic in a single hook (<code>useQuranSearch</code>).</li>
<li>The search screen handles UI, state, and infinite scroll.</li>
<li>The result item handles only presentation and highlighting.</li>
</ul>
<p>That means:</p>
<ul>
<li>fewer files: no separate search engine implementation in my own codebase</li>
<li>shorter files: each file has a clear responsibility (hook vs screen vs item)</li>
<li>clear boundaries:<ul>
<li><code>quran-search-engine</code> handles how to search the Quran</li>
<li>my app handles how to display the results nicely</li>
</ul>
</li>
</ul>
<p>The result is a much cleaner architecture:</p>
<ul>
<li>easier to read</li>
<li>easier to debug</li>
<li>easier to extend later</li>
</ul>
<hr />
<h2 id="heading-more-features-and-better-highlighting">More Features and Better Highlighting</h2>
<p>The package doesn’t just return verses; it returns rich metadata.</p>
<p>For each verse, it includes:</p>
<ul>
<li>a list of matched tokens</li>
<li>a mapping of token to type (<code>exact</code>, <code>lemma</code>, <code>root</code>, <code>fuzzy</code>)</li>
</ul>
<p>I use that data to build:</p>
<ul>
<li>color‑coded highlighting:<ul>
<li>one color for exact text matches</li>
<li>another for lemma/root matches</li>
<li>another for fuzzy matches</li>
</ul>
</li>
<li>a legend component that explains the colors to the user</li>
</ul>
<p>Because the engine already tells me which tokens matched and how, adding more features is straightforward:</p>
<ul>
<li>I can tweak colors without touching search logic</li>
<li>I can show counts like:<ul>
<li>“X exact matches”</li>
<li>“Y lemma matches”</li>
<li>“Z root matches”</li>
<li>“W fuzzy matches”</li>
</ul>
</li>
<li>I can experiment with different UI layouts (cards, compact list, etc.) without changing how the search works</li>
</ul>
<p>In other words, <code>quran-search-engine</code> lets me focus on UX, not parsing and matching.</p>
<hr />
<h2 id="heading-how-much-code-did-i-really-need">How Much Code Did I Really Need?</h2>
<p>If you look at the actual integration, the core parts are surprisingly small:</p>
<ul>
<li>a single hook that:<ul>
<li>sanitizes the query (Arabic only, trimming, etc.)</li>
<li>calls <code>search(...)</code> with options and pagination</li>
<li>holds <code>pageResults</code> and <code>counts</code> in React state</li>
<li>exposes a <code>getPositiveTokens</code> helper to classify matched tokens</li>
</ul>
</li>
<li>a screen that:<ul>
<li>debounces the query</li>
<li>manages <code>page</code>, <code>results</code>, <code>hasMore</code>, and <code>isLoadingMore</code></li>
<li>wires up a <code>FlatList</code> with <code>onEndReached</code></li>
</ul>
</li>
<li>a result item component that:<ul>
<li>receives a verse and matched tokens</li>
<li>passes them to a <code>HighlightText</code> component with colors</li>
<li>handles navigation to the specific page and aya</li>
</ul>
</li>
</ul>
<p>Compared to building a full search engine from scratch, the amount of app code is very reasonable. Most of my time went into UI design and UX details, not low‑level search algorithms.</p>
<hr />
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Using <code>quran-search-engine</code> in <strong>open-mushaf-native</strong> gave me:</p>
<ul>
<li>a powerful, production‑ready Quran search engine</li>
<li>built‑in support for pagination (enabling infinite scroll)</li>
<li>cleaner and shorter code in my own app (fewer files, clearer responsibilities)</li>
<li>richer features like lemma/root/fuzzy modes and color‑coded highlighting</li>
</ul>
<p>Instead of being “the search engine project”, my app stays what it should be: a beautiful, focused Quran reading experience, powered under the hood by a dedicated search package.</p>
<p>If you are building your own Quran app, my advice is simple: let <code>quran-search-engine</code> do the heavy lifting and spend your energy on the user experience.</p>
<hr />
<h2 id="heading-links">Links</h2>
<ul>
<li>Open Mushaf Native repo: <a target="_blank" href="https://github.com/adelpro/open-mushaf-native">https://github.com/adelpro/open-mushaf-native</a></li>
<li>Quran Search Engine repo: <a target="_blank" href="https://github.com/adelpro/quran-search-engine">https://github.com/adelpro/quran-search-engine</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Progressive Web Apps (PWA): The Hybrid Web Revolution]]></title><description><![CDATA[1. Introduction
Progressive Web Apps (PWAs) combine the reach of the web with the capabilities of native applications.
They load instantly, work offline, send notifications, and can be installed on any device — all using standard web technologies: HT...]]></description><link>https://letscode.adelpro.us.kg/progressive-web-apps-pwa-the-hybrid-web-revolution</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/progressive-web-apps-pwa-the-hybrid-web-revolution</guid><category><![CDATA[PWA]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Thu, 09 Oct 2025 15:39:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760024178920/983d44c6-d0c5-4b34-b2d0-6785cb4f64bc.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-1-introduction"><strong>1. Introduction</strong></h2>
<p>Progressive Web Apps (PWAs) combine the reach of the web with the capabilities of native applications.</p>
<p>They load instantly, work offline, send notifications, and can be installed on any device — all using standard web technologies: <strong>HTML, CSS, and JavaScript</strong>.</p>
<p>In 2025, PWAs are no longer an experiment. They are deployed in production by global brands such as Twitter, Starbucks, Uber, and Pinterest.</p>
<p>They solve the complexity of maintaining multiple native apps while offering near-native performance and usability.</p>
<hr />
<h2 id="heading-2-definition-and-browser-support"><strong>2. Definition and Browser Support</strong></h2>
<p>A <strong>Progressive Web App (PWA)</strong> is a web application that:</p>
<ul>
<li><p>Uses a <strong>Web App Manifest</strong> for metadata.</p>
</li>
<li><p>Registers a <strong>Service Worker</strong> for caching, offline, and background tasks.</p>
</li>
<li><p>Served over <strong>HTTPS</strong>.</p>
</li>
<li><p>Is <strong>installable</strong> and behaves like a native app.</p>
</li>
</ul>
<p><strong>Browser support (2025):</strong></p>
<p>Browser Support Level Notes <strong>Chromium (Chrome, Edge, Brave)</strong> Full Install prompt, push, background sync <strong>Firefox</strong> Partial No native installation prompt <strong>Safari (iOS / macOS)</strong> Partial → improving Offline &amp; push supported since iOS 16.4</p>
<hr />
<h2 id="heading-3-real-world-adoption"><strong>3. Real-World Adoption</strong></h2>
<p>According to <a target="_blank" href="https://www.pwastats.com/">PWA Stats</a>:</p>
<p>Company Impact Key Metric <strong>Twitter Lite</strong> Lightweight version +65% pages/session <strong>Starbucks</strong> Works offline 2× faster checkout <strong>Uber Web</strong> &lt;50 KB core bundle Loads in &lt;3 s on 2G <strong>Pinterest</strong> Rebuilt as a PWA +60% engagement <strong>Trivago</strong> Offline-ready hotel search +150% engagement time</p>
<p><strong>Insight:</strong> PWAs deliver app-store quality performance without app-store friction.</p>
<hr />
<h2 id="heading-4-core-building-blocks"><strong>4. Core Building Blocks</strong></h2>
<h3 id="heading-41-web-app-manifest"><strong>4.1 Web App Manifest</strong></h3>
<p>The <strong>Web App Manifest</strong> is a JSON file that defines your app’s metadata and tells the browser how to install and display it.</p>
<p>It connects your web experience to native-like behavior — title, icons, splash screens, colors, and launch parameters.</p>
<p>Example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"PWA App"</span>,
  <span class="hljs-attr">"short_name"</span>: <span class="hljs-string">"PWA"</span>,
  <span class="hljs-attr">"id"</span>: <span class="hljs-string">"com.app.pwa"</span>,
  <span class="hljs-attr">"start_url"</span>: <span class="hljs-string">"/"</span>,
  <span class="hljs-attr">"display"</span>: <span class="hljs-string">"standalone"</span>,
  <span class="hljs-attr">"background_color"</span>: <span class="hljs-string">"#ffffff"</span>,
  <span class="hljs-attr">"theme_color"</span>: <span class="hljs-string">"#1a1a1a"</span>,
  <span class="hljs-attr">"icons"</span>: [
    { <span class="hljs-attr">"src"</span>: <span class="hljs-string">"/images/favicon/icon-192.png"</span>, <span class="hljs-attr">"sizes"</span>: <span class="hljs-string">"192x192"</span>, <span class="hljs-attr">"type"</span>: <span class="hljs-string">"image/png"</span> },
    { <span class="hljs-attr">"src"</span>: <span class="hljs-string">"/images/favicon/icon-512.png"</span>, <span class="hljs-attr">"sizes"</span>: <span class="hljs-string">"512x512"</span>, <span class="hljs-attr">"type"</span>: <span class="hljs-string">"image/png"</span> }
  ],
  <span class="hljs-attr">"screenshots"</span>: [
    { <span class="hljs-attr">"src"</span>: <span class="hljs-string">"/images/screenshots/mobile.png"</span>, <span class="hljs-attr">"sizes"</span>: <span class="hljs-string">"1080x1920"</span> },
    { <span class="hljs-attr">"src"</span>: <span class="hljs-string">"/images/screenshots/desktop.png"</span>, <span class="hljs-attr">"sizes"</span>: <span class="hljs-string">"1920x1080"</span> }
  ],
  <span class="hljs-attr">"shortcuts"</span>: [
    { <span class="hljs-attr">"name"</span>: <span class="hljs-string">"About"</span>, <span class="hljs-attr">"url"</span>: <span class="hljs-string">"/about"</span> },
    { <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Contact"</span>, <span class="hljs-attr">"url"</span>: <span class="hljs-string">"/contact"</span> }
  ]
}
</code></pre>
<h3 id="heading-manifest-field-overview"><strong>Manifest Field Overview</strong></h3>
<ul>
<li><p><strong>name</strong> — Full app name shown in the install interface. Example: <code>"App PWA"</code></p>
</li>
<li><p><strong>short_name</strong> — Label displayed on the home screen or dock. Example: <code>"App"</code></p>
</li>
<li><p><strong>id</strong> — Unique identifier used for store packaging. Example: <code>"com.quran.pwa"</code></p>
</li>
<li><p><strong>start_url</strong> — URL the app loads when opened. Example: <code>"/"</code></p>
</li>
<li><p><strong>scope</strong> — Navigation boundary defining what the PWA controls. Example: <code>"/"</code></p>
</li>
<li><p><strong>display</strong> — Display mode such as <code>fullscreen</code>, <code>standalone</code>, <code>minimal-ui</code>, or <code>browser</code>. Example: <code>"standalone"</code></p>
</li>
<li><p><strong>orientation</strong> — Locks the screen orientation on launch. Example: <code>"portrait"</code></p>
</li>
<li><p><strong>background_color</strong> — Background color shown on the splash screen. Example: <code>"#ffffff"</code></p>
</li>
<li><p><strong>theme_color</strong> — Browser UI theme color. Example: <code>"#1a1a1a"</code></p>
</li>
<li><p><strong>description</strong> — Short text shown in install dialogs. Example: <code>"Offline-ready Quran reader"</code></p>
</li>
<li><p><strong>lang</strong> — Application language code. Example: <code>"ar"</code></p>
</li>
<li><p><strong>dir</strong> — Text direction (<code>ltr</code> or <code>rtl</code>). Example: <code>"rtl"</code></p>
</li>
<li><p><strong>icons</strong> — App icons in multiple sizes (required: 192×192 and 512×512). Example: <code>"/icons/512x512.png"</code></p>
</li>
<li><p><strong>screenshots</strong> — Images shown in install banners (Android). Example: <code>"/images/screenshot-1.png"</code></p>
</li>
<li><p><strong>shortcuts</strong> — Quick access actions from long press or context menu. Example: <code>"About"</code></p>
</li>
<li><p><strong>categories</strong> — Used for app store classification. Example: <code>"education"</code></p>
</li>
</ul>
<p><strong>Minimum Installable Manifest</strong></p>
<p>To qualify as installable across modern browsers, your manifest must include:</p>
<ul>
<li><p><code>name</code></p>
</li>
<li><p><code>short_name</code></p>
</li>
<li><p><code>icons</code> (must include <strong>192×192</strong> and <strong>512×512</strong> PNGs)</p>
</li>
<li><p><code>start_url</code></p>
</li>
<li><p><code>display</code></p>
</li>
<li><p><code>theme_color</code></p>
</li>
<li><p>Served over <strong>HTTPS</strong></p>
</li>
</ul>
<p>Additional fields like <code>id</code>, <code>shortcuts</code>, and <code>screenshots</code> enhance install banners and store packaging (e.g., with <a target="_blank" href="https://www.pwabuilder.com/">PWA Builder</a>)</p>
<hr />
<h3 id="heading-42-service-worker"><strong>4.2 Service Worker</strong></h3>
<p>A <strong>Service Worker</strong> is a background script that sits between your web app and the network.</p>
<p>It intercepts requests, caches assets, and enables offline behavior, background sync, and push notifications — all without user interaction.</p>
<hr />
<h3 id="heading-core-concepts"><strong>Core Concepts</strong></h3>
<p>Feature Description Typical Use <strong>Lifecycle</strong> Independent thread with <em>install → activate → fetch</em> phases Controls app updates <strong>Caching</strong> Stores static and dynamic assets using the Cache API Offline access, faster reloads <strong>Intercepting Fetch</strong> Handles every network request Choose when to serve cache or fetch fresh <strong>Offline Fallback</strong> Returns a predefined HTML when the user is offline <code>fallback.html</code> or custom error UI <strong>Background Sync / Push</strong> Queues updates or sends notifications when reconnected Messaging, analytics, reminders</p>
<hr />
<h3 id="heading-lifecycle-events"><strong>Lifecycle Events</strong></h3>
<ol>
<li><p><strong>Install Event</strong> – Pre-caches essential assets (app shell).</p>
</li>
<li><p><strong>Activate Event</strong> – Cleans old caches and takes control of open tabs.</p>
</li>
<li><p><strong>Fetch Event</strong> – Intercepts requests and applies a caching strategy.</p>
</li>
<li><p><strong>Message / Sync Events</strong> – Used for background tasks or two-way communication.</p>
</li>
</ol>
<hr />
<h3 id="heading-registration-in-your-app"><strong>Registration in Your App</strong></h3>
<p>Register the Service Worker once from your main script:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">if</span> (<span class="hljs-string">'serviceWorker'</span> <span class="hljs-keyword">in</span> navigator) {
  <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'load'</span>, <span class="hljs-function">() =&gt;</span> {
    navigator.serviceWorker
      .register(<span class="hljs-string">'/serviceWorker.js'</span>)
      .then(<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Service Worker registered'</span>))
      .catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'SW registration failed:'</span>, error));
  });
}
</code></pre>
<h3 id="heading-43-cache-types-management-and-purging"><strong>4.3 Cache Types, Management, and Purging</strong></h3>
<p>The <strong>Cache Storage API</strong> gives PWAs full control over how files, data, and network responses are stored locally.</p>
<p>Proper cache design determines performance, offline reliability, and update frequency.</p>
<hr />
<h3 id="heading-types-of-cache-in-pwas"><strong>Types of Cache in PWAs</strong></h3>
<p>Cache Type Description Typical Contents Lifetime <strong>Static Cache</strong> Preloaded during the <em>install</em> event (the “App Shell”) HTML, CSS, JS, icons Until next version release <strong>Dynamic Cache</strong> Created at runtime as users navigate API responses, images, user content Controlled manually or by quota <strong>Runtime Cache</strong> Short-lived cache for frequently updated assets Feeds, news, live data Regularly revalidated <strong>Third-Party Cache</strong> Assets fetched from CDNs or APIs Fonts, analytics scripts Managed by browser</p>
<hr />
<h3 id="heading-cache-versioning-and-naming"><strong>Cache Versioning and Naming</strong></h3>
<p>Use versioned cache names to differentiate builds and safely purge old assets:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> STATIC_CACHE = <span class="hljs-string">'quran-cache-v1'</span>;
<span class="hljs-keyword">const</span> DYNAMIC_CACHE = <span class="hljs-string">'quran-dynamic-v1'</span>;
</code></pre>
<p>When deploying a new version:</p>
<ul>
<li><p>Increment the cache name.</p>
</li>
<li><p>In the <strong>activate event</strong>, delete outdated caches.</p>
</li>
</ul>
<pre><code class="lang-plaintext">self.addEventListener('activate', (event) =&gt; {
  const allowList = [STATIC_CACHE, DYNAMIC_CACHE];
  event.waitUntil(
    caches.keys().then((keys) =&gt;
      Promise.all(keys.filter((key) =&gt; !allowList.includes(key)).map((key) =&gt; caches.delete(key)))
    )
  );
});
</code></pre>
<h3 id="heading-cache-purging-and-size-limiting"><strong>Cache Purging and Size Limiting</strong></h3>
<p>To prevent uncontrolled growth:</p>
<ul>
<li><p>Set a <strong>maximum cache size</strong>.</p>
</li>
<li><p>Remove the oldest entries when exceeding the limit.</p>
</li>
</ul>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> limitCacheSize = <span class="hljs-keyword">async</span> (name, size) =&gt; {
  <span class="hljs-keyword">const</span> cache = <span class="hljs-keyword">await</span> caches.open(name);
  <span class="hljs-keyword">const</span> keys = <span class="hljs-keyword">await</span> cache.keys();
  <span class="hljs-keyword">if</span> (keys.length &gt; size) {
    <span class="hljs-keyword">await</span> cache.delete(keys[<span class="hljs-number">0</span>]);
    <span class="hljs-keyword">await</span> limitCacheSize(name, size);
  }
};
</code></pre>
<p>Combine this with runtime caching (e.g., API responses) to ensure performance without bloating storage.</p>
<h3 id="heading-best-practices"><strong>Best Practices</strong></h3>
<ul>
<li><p><strong>Precache only essentials</strong>: HTML, JS, CSS, icons, manifest.</p>
</li>
<li><p><strong>Separate dynamic data</strong> into its own cache.</p>
</li>
<li><p><strong>Purge old caches</strong> during activation to avoid conflicts.</p>
</li>
<li><p><strong>Use Workbox</strong> for declarative caching and automatic cleanup.</p>
</li>
<li><p><strong>Test offline</strong> via Chrome DevTools → <em>Application → Service Workers → Offline</em>.</p>
</li>
</ul>
<p>Effective cache management balances <strong>speed, storage efficiency, and update reliability</strong>, ensuring users always receive fresh yet instantly loadable experiences.</p>
<h2 id="heading-5-progressive-web-application-full-example-step-by-step"><strong>5.</strong> Progressive Web Application — Full Example (step-by-step)</h2>
<p><strong>production-ready vanilla JavaScript PWA,</strong> providing offline access, caching, installability, and a graceful fallback page.</p>
<p>You can generate icons and screenshots for your manifest via <a target="_blank" href="https://www.pwabuilder.com/imageGenerator">pwabuilder.com/imageGenerator</a></p>
<h3 id="heading-project-structure-recommended">Project structure (recommended)</h3>
<pre><code class="lang-jsx">/index.html
/fallback.html
/manifest.json
/serviceWorker.js
/js/app.js
/css/style.css
/images/icons/<span class="hljs-number">192</span>x192.png
/images/icons/<span class="hljs-number">512</span>x512.png
</code></pre>
<p>Everything lives at the root so the Service Worker can control all routes.</p>
<h3 id="heading-step-1-indexhtml"><strong>Step 1 – index.html</strong></h3>
<p>The main page registers the Service Worker and links the manifest.</p>
<pre><code class="lang-jsx">&lt;!DOCTYPE html&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width,initial-scale=1"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>PWA<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"manifest"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/manifest.json"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"theme-color"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"#1a1a1a"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/images/icons/192x192.png"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/css/style.css"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>PWA<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Offline-ready Application.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"installBtn"</span> <span class="hljs-attr">hidden</span>&gt;</span>Install App<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>

  <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"/js/app.js"</span> <span class="hljs-attr">defer</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
</code></pre>
<p><em>Explanation</em></p>
<ul>
<li><p><code>&lt;link rel="manifest"&gt;</code> → enables installability.</p>
</li>
<li><p><code>&lt;meta name="theme-color"&gt;</code> → sets system UI color.</p>
</li>
<li><p>The install button is hidden until <code>beforeinstallprompt</code> fires.</p>
</li>
</ul>
<h3 id="heading-step-3-manifestjson"><strong>Step 3 – manifest.json</strong></h3>
<p>(Already detailed earlier; skip duplication)</p>
<p>Contains app metadata, icons 192×192 + 512×512, shortcuts, screenshots, etc.</p>
<h3 id="heading-step-4-fallbackhtml"><strong>Step 4 – fallback.html</strong></h3>
<p>Displayed when offline navigation fails, Keep it <strong>lightweight</strong> and <strong>self-contained</strong> so it loads instantly even when offline.</p>
<pre><code class="lang-jsx">&lt;!DOCTYPE html&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width,initial-scale=1"</span> /&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Offline Mode<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
    <span class="hljs-selector-tag">body</span> {
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#eceff1</span>;
      <span class="hljs-attribute">font-family</span>: Roboto, Arial, sans-serif;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;
    }
    <span class="hljs-selector-id">#msg</span> {
      <span class="hljs-attribute">max-width</span>: <span class="hljs-number">360px</span>;
      <span class="hljs-attribute">margin</span>: <span class="hljs-number">100px</span> auto;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">32px</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#fff</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">6px</span>;
      <span class="hljs-attribute">text-align</span>: center;
    }
    <span class="hljs-selector-tag">a</span> {
      <span class="hljs-attribute">display</span>: block;
      <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">20px</span>;
      <span class="hljs-attribute">background</span>: <span class="hljs-number">#039be5</span>;
      <span class="hljs-attribute">color</span>: <span class="hljs-number">#fff</span>;
      <span class="hljs-attribute">text-decoration</span>: none;
      <span class="hljs-attribute">padding</span>: <span class="hljs-number">12px</span>;
      <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;
    }
  </span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"msg"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>You’re Offline<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This page can’t be displayed without an internet connection.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/index.html"</span>&gt;</span>Return to Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
</code></pre>
<p>Keep it <strong>lightweight</strong> and <strong>self-contained</strong> so it loads instantly even when offline.</p>
<h3 id="heading-step-5-serviceworkerjs"><strong>Step 5 – serviceWorker.js</strong></h3>
<p>Handles caching and offline logic.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">/* serviceWorker.js */</span>
<span class="hljs-keyword">const</span> STATIC_CACHE = <span class="hljs-string">'quran-static-v1'</span>;
<span class="hljs-keyword">const</span> DYNAMIC_CACHE = <span class="hljs-string">'quran-dynamic-v1'</span>;
<span class="hljs-keyword">const</span> MAX_DYNAMIC_ITEMS = <span class="hljs-number">30</span>;

<span class="hljs-keyword">const</span> ASSETS = [
  <span class="hljs-string">'/'</span>,
  <span class="hljs-string">'/index.html'</span>,
  <span class="hljs-string">'/fallback.html'</span>,
  <span class="hljs-string">'/css/style.css'</span>,
  <span class="hljs-string">'/images/icons/192x192.png'</span>,
  <span class="hljs-string">'/images/icons/512x512.png'</span>,
  <span class="hljs-string">'/js/app.js'</span>
];

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">limitCacheSize</span>(<span class="hljs-params">name, max</span>) </span>{
  <span class="hljs-keyword">const</span> cache = <span class="hljs-keyword">await</span> caches.open(name);
  <span class="hljs-keyword">const</span> keys = <span class="hljs-keyword">await</span> cache.keys();
  <span class="hljs-keyword">if</span> (keys.length &gt; max) {
    <span class="hljs-keyword">await</span> cache.delete(keys[<span class="hljs-number">0</span>]);
    <span class="hljs-keyword">await</span> limitCacheSize(name, max);
  }
}

self.addEventListener(<span class="hljs-string">'install'</span>, <span class="hljs-function">(<span class="hljs-params">evt</span>) =&gt;</span> {
  evt.waitUntil(caches.open(STATIC_CACHE).then(<span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> c.addAll(ASSETS)));
  self.skipWaiting();
});

self.addEventListener(<span class="hljs-string">'activate'</span>, <span class="hljs-function">(<span class="hljs-params">evt</span>) =&gt;</span> {
  evt.waitUntil(
    caches.keys().then(<span class="hljs-function">(<span class="hljs-params">keys</span>) =&gt;</span>
      <span class="hljs-built_in">Promise</span>.all(keys.filter(<span class="hljs-function">(<span class="hljs-params">k</span>) =&gt;</span>
        k !== STATIC_CACHE &amp;&amp; k !== DYNAMIC_CACHE).map(<span class="hljs-function">(<span class="hljs-params">k</span>) =&gt;</span> caches.delete(k))
      )
    ).then(<span class="hljs-function">() =&gt;</span> self.clients.claim())
  );
});

self.addEventListener(<span class="hljs-string">'fetch'</span>, <span class="hljs-function">(<span class="hljs-params">evt</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { request } = evt;
  <span class="hljs-keyword">if</span> (request.method !== <span class="hljs-string">'GET'</span>) <span class="hljs-keyword">return</span>;

  <span class="hljs-keyword">if</span> (request.mode === <span class="hljs-string">'navigate'</span>) {
    evt.respondWith(
      (<span class="hljs-keyword">async</span> () =&gt; {
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(request);
          <span class="hljs-keyword">const</span> cache = <span class="hljs-keyword">await</span> caches.open(DYNAMIC_CACHE);
          cache.put(request, res.clone());
          <span class="hljs-keyword">return</span> res;
        } <span class="hljs-keyword">catch</span> {
          <span class="hljs-keyword">return</span> caches.match(<span class="hljs-string">'/fallback.html'</span>);
        }
      })()
    );
    <span class="hljs-keyword">return</span>;
  }

  evt.respondWith(
    caches.match(request).then(<span class="hljs-function">(<span class="hljs-params">cached</span>) =&gt;</span> {
      <span class="hljs-keyword">return</span> (
        cached ||
        fetch(request)
          .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
            <span class="hljs-keyword">return</span> caches.open(DYNAMIC_CACHE).then(<span class="hljs-function">(<span class="hljs-params">cache</span>) =&gt;</span> {
              <span class="hljs-keyword">if</span> (request.url.startsWith(self.location.origin)) {
                cache.put(request, res.clone());
                limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_ITEMS);
              }
              <span class="hljs-keyword">return</span> res;
            });
          })
          .catch(<span class="hljs-function">() =&gt;</span> cached)
      );
    })
  );
});
</code></pre>
<p><em>Explanation</em></p>
<ol>
<li><p><strong>Install → Precache</strong> core assets.</p>
</li>
<li><p><strong>Activate → Clean</strong> old versions.</p>
</li>
<li><p><strong>Fetch → Cache-first</strong> for static, <strong>Network-first</strong> for navigation.</p>
</li>
<li><p><strong>limitCacheSize</strong> prevents unlimited growth.</p>
</li>
</ol>
<h3 id="heading-step-6-jsappjs"><strong>Step 6 – js/app.js</strong></h3>
<p>Registers the Service Worker and handles installation UI.</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// js/app.js</span>
<span class="hljs-keyword">if</span> (<span class="hljs-string">'serviceWorker'</span> <span class="hljs-keyword">in</span> navigator) {
  <span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'load'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> reg = <span class="hljs-keyword">await</span> navigator.serviceWorker.register(<span class="hljs-string">'/serviceWorker.js'</span>);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Service Worker registered'</span>, reg);
    } <span class="hljs-keyword">catch</span> (err) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'SW registration failed:'</span>, err);
    }
  });
}

<span class="hljs-comment">// Custom install prompt</span>
<span class="hljs-built_in">window</span>.addEventListener(<span class="hljs-string">'beforeinstallprompt'</span>, <span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> {
  e.preventDefault();
  <span class="hljs-built_in">window</span>.deferredPrompt = e;
  <span class="hljs-keyword">const</span> btn = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'installBtn'</span>);
  btn.hidden = <span class="hljs-literal">false</span>;
  btn.addEventListener(<span class="hljs-string">'click'</span>, <span class="hljs-keyword">async</span> () =&gt; {
    btn.hidden = <span class="hljs-literal">true</span>;
    <span class="hljs-keyword">const</span> prompt = <span class="hljs-built_in">window</span>.deferredPrompt;
    prompt.prompt();
    <span class="hljs-keyword">await</span> prompt.userChoice;
    <span class="hljs-built_in">window</span>.deferredPrompt = <span class="hljs-literal">null</span>;
  });
});
</code></pre>
<p><em>Explanation</em></p>
<ul>
<li><p>Registers <code>/serviceWorker.js</code> once on load.</p>
</li>
<li><p>Listens for <code>beforeinstallprompt</code> and shows the custom <em>Install App</em> button.</p>
</li>
</ul>
<h3 id="heading-step-7-cssstylecss"><strong>Step 7 – css/style.css</strong></h3>
<p>Simple theme styling.</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">body</span>{<span class="hljs-attribute">font-family</span>:system-ui,Arial,sans-serif;<span class="hljs-attribute">margin</span>:<span class="hljs-number">0</span>;<span class="hljs-attribute">padding</span>:<span class="hljs-number">2rem</span>;<span class="hljs-attribute">background</span>:<span class="hljs-number">#fff</span>;<span class="hljs-attribute">color</span>:<span class="hljs-number">#222</span>}
<span class="hljs-selector-tag">h1</span>{<span class="hljs-attribute">color</span>:<span class="hljs-number">#1a1a1a</span>}
<span class="hljs-selector-tag">button</span>{<span class="hljs-attribute">background</span>:<span class="hljs-number">#039be5</span>;<span class="hljs-attribute">color</span>:<span class="hljs-number">#fff</span>;<span class="hljs-attribute">border</span>:none;<span class="hljs-attribute">padding</span>:.<span class="hljs-number">8rem</span> <span class="hljs-number">1.2rem</span>;<span class="hljs-attribute">border-radius</span>:<span class="hljs-number">4px</span>;<span class="hljs-attribute">cursor</span>:pointer}
<span class="hljs-selector-tag">button</span><span class="hljs-selector-pseudo">:hover</span>{<span class="hljs-attribute">background</span>:<span class="hljs-number">#0277bd</span>}
</code></pre>
<p><strong>Step 8 – Testing Checklist</strong></p>
<p>Test Tool Expected Lighthouse → PWA Audit Chrome DevTools Score &gt; 90 Offline mode DevTools → Network → Offline Fallback shown Install prompt Chromium Android/Desktop “Install App” visible iOS Add to Home Screen Safari Share Menu App icon added Cache inspection DevTools → Application → Cache Storage Static + dynamic entries</p>
<h3 id="heading-step-9-result"><strong>Step 9 – Result</strong></h3>
<p>You now have a <strong>fully functional Progressive Web App</strong> that:</p>
<ul>
<li><p>Works offline.</p>
</li>
<li><p>Installs on desktop + mobile.</p>
</li>
<li><p>Caches intelligently.</p>
</li>
</ul>
<p>This <code>ipfs.quran.us.kg</code> build demonstrates how far a <strong>vanilla PWA</strong> can go without any framework or dependency.</p>
<p>Modern frameworks automate manifest injection, SW registration, and caching strategies.</p>
<hr />
<h2 id="heading-6-modern-frameworks-in-practice"><strong>6. Modern Frameworks in Practice</strong></h2>
<p>Modern frameworks make building PWAs easier by automating manifest injection, service worker setup, and offline caching.</p>
<p>Framework PWA Support Package Notes <strong>Next.js</strong> <code>next-pwa</code> Generates manifest, caches routes <strong>Expo (React Native)</strong> Built-in PWA export One codebase → iOS, Android, Web <strong>Angular</strong> <code>@angular/pwa</code> schematic Adds manifest, SW, and icons automatically <strong>Vite / React</strong> <code>vite-plugin-pwa</code> Runtime caching and auto-update <strong>SvelteKit / Nuxt 3</strong> Native manifest + SW support File-based configuration</p>
<p>These frameworks hide most low-level details while maintaining flexibility for custom caching and install behavior.</p>
<hr />
<h3 id="heading-7-native-packaging-and-distribution">7. <strong>Native Packaging and Distribution</strong></h3>
<p>PWAs can be wrapped as native apps for app stores using modern tooling.</p>
<p>Tool Platform Output Description <strong>Bubblewrap (Google)</strong> Android APK / TWA Packages PWAs as Trusted Web Activities <strong>PWA Builder (Microsoft)</strong> Windows, Android, iOS MSIX / APK / IPA GUI + CLI packaging <strong>Capacitor (Ionic)</strong> Android, iOS Native wrapper Adds native APIs + store deployment <strong>Google Unified Web (UW)</strong> Experimental Universal package Future standard bridging PWA + Play Store</p>
<p><strong>Recommended Flow</strong></p>
<ol>
<li><p><strong>Validate</strong> your PWA’s manifest, service worker, and installability using <strong>Lighthouse</strong> or <strong>Chrome DevTools</strong>.</p>
</li>
<li><p><strong>Generate assets and installers</strong> with <a target="_blank" href="https://www.pwabuilder.com/">PWA Builder</a> — including icons, screenshots, and store metadata.</p>
</li>
<li><p><strong>Package for app stores</strong> using <strong>Bubblewrap</strong>, <strong>Capacitor</strong>, or <strong>PWA Builder</strong> to create ready-to-publish builds for <strong>Google Play</strong>, <strong>Apple App Store</strong>, and <strong>Microsoft Store</strong>.</p>
</li>
<li><p><strong>Submit your packaged PWA</strong> through the appropriate store submission process for distribution across platforms.</p>
</li>
</ol>
<h2 id="heading-8-pros-and-cons">8. Pros and Cons</h2>
<p>Pros Cons Cross-platform single codebase Limited access to native APIs Offline-ready &amp; installable iOS storage and background limits Auto-update via Service Worker Limited monetization channels Lower cost than native apps Discoverability mainly via web, not app stores</p>
<h2 id="heading-9-conclusion"><strong>9. Conclusion</strong></h2>
<p><strong>PWAs are mature, production-ready, and here to stay.</strong></p>
<p>They empower developers to build fast, installable, secure, and cross-platform apps — using only the web stack.</p>
<p>With tools like <strong>PWA Builder</strong>, <strong>Bubblewrap</strong>, and <strong>Capacitor</strong>, PWAs can reach <strong>Google Play, Microsoft Store, and even iOS users</strong>.</p>
<p>Modern frameworks like <strong>Next.js</strong> and <strong>Expo</strong> make this integration seamless.</p>
<blockquote>
<p><strong>Start small:</strong></p>
<p>Add a manifest.json, register a serviceWorker.js, and test offline.</p>
<p>Your next web app can be installable in under an hour.</p>
</blockquote>
<hr />
<p>Attribution: cover photo from <a target="_blank" href="https://pixabay.com/users/athree23-6195572/"><strong>athree23</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[This New React Feature Will Make Your App 20% Faster With Zero Code Changes.]]></title><description><![CDATA[What Is the React Compiler?
The React Compiler (formerly React Forget) is an optimizing compiler that analyzes your React components and automatically memoizes computations, props, and state transitions, the same things you’ve been manually optimizin...]]></description><link>https://letscode.adelpro.us.kg/this-new-react-feature-will-make-your-app-20-faster-with-zero-code-changes</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/this-new-react-feature-will-make-your-app-20-faster-with-zero-code-changes</guid><category><![CDATA[React]]></category><category><![CDATA[Expo]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Tue, 23 Sep 2025 10:38:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758623740817/13428afc-de6e-46f6-a2f0-eb64171d260b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-is-the-react-compiler">What Is the React Compiler?</h2>
<p>The React Compiler (formerly React Forget) is an optimizing compiler that analyzes your React components and automatically memoizes computations, props, and state transitions, the same things you’ve been manually optimizing with useMemo, useCallback, and React.memo.</p>
<p>But here’s the kicker: you don’t have to write any of that boilerplate anymore. The compiler does it for you at build time.</p>
<p>The compiler uses static analysis to understand your component’s data flow, then rewrites your code to eliminate unnecessary re-renders and redundant calculations. It’s like having a senior React performance engineer silently refactoring your entire codebase while you sleep.</p>
<hr />
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>Let’s be honest: React performance optimization has always been a manual, error-prone chore.</p>
<p>You’ve probably written code like this countless times:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> expensiveValue = useMemo(<span class="hljs-function">() =&gt;</span> computeExpensiveThing(data), [data]);
<span class="hljs-keyword">const</span> handleClick = useCallback(<span class="hljs-function">() =&gt;</span> doSomething(id), [id]);
</code></pre>
<p>Yet inevitably, dependency arrays get missed. Or unnecessary ones get added. Sometimes you wrap components in React.memo when they don’t need it, or fail to wrap the ones that genuinely would benefit. These mistakes are practically unavoidable. This mental overhead is significant and decreases your productivity.</p>
<hr />
<h2 id="heading-how-it-works-simplified">How It Works (Simplified)</h2>
<p>The compiler doesn’t use magic. It uses math.</p>
<p>It builds a control-flow graph of your component, tracks how values derive from props and state, and identifies which values remain “stable” across renders. If a value hasn’t changed, the compiler ensures it’s reused, no re-computation, no re-render.</p>
<p>It even handles tricky cases:</p>
<ul>
<li><p>Functions passed as props? Automatically stabilized.</p>
</li>
<li><p>Objects and arrays? Memoized if their contents haven’t changed.</p>
</li>
<li><p>Complex hooks and context? Tracked and optimized.</p>
</li>
</ul>
<p>And if your code is too dynamic for static analysis? The compiler gracefully falls back. Your app still works, just without the optimizations.</p>
<p>No breaking changes. No migration headaches.</p>
<hr />
<h2 id="heading-the-developer-experience">The Developer Experience</h2>
<p>One of the most exciting aspects is the DX improvement.</p>
<p>Imagine:</p>
<p>✓ No more dependency arrays</p>
<p>✓ No more “why is this re-rendering?” debugging sessions</p>
<p>✓ No more premature optimization debates in PRs</p>
<p>✓ No more useMemo/useCallback fatigue</p>
<p>You write code naturally, declaratively, functionally, without worrying about performance traps. The compiler handles the rest.</p>
<blockquote>
<p>You should be able to write React like it’s 2015 — and still get 2024 performance.</p>
</blockquote>
<hr />
<h2 id="heading-first-check-your-apps-health-dont-skip-this">First: Check Your App’s Health (Don’t Skip This!)</h2>
<p>Before you install the compiler — run a compatibility check. The React Compiler only works if your components follow React’s “Rules of React”, immutability, no side effects in render, etc.</p>
<p>If your code violates these rules, the compiler will silently skip optimization — and you’ll have no idea why your app still re-renders like crazy.</p>
<p>How to Check Compatibility</p>
<h3 id="heading-step-1-use-health-check-package-from-react-team">Step 1: Use Health Check package from react team</h3>
<p>First Check how compatible your project is with the React Compiler.</p>
<pre><code class="lang-jsx">npx react-compiler-health-check
</code></pre>
<p>This will generally verify if your app is following the <a target="_blank" href="https://react.dev/reference/rules">rules of React</a> and will highlight any issues</p>
<h3 id="heading-step-2-use-the-official-react-compiler-eslint-plugin-recommended">Step 2: Use the Official React Compiler ESLint Plugin (Recommended)</h3>
<p>Install the plugin:</p>
<pre><code class="lang-jsx">npm install eslint-plugin-react-compiler --save-dev
</code></pre>
<p>Add it to your <code>.eslintrc</code>:</p>
<pre><code class="lang-jsx">{
  <span class="hljs-string">"plugins"</span>: [<span class="hljs-string">"react-compiler"</span>],
  <span class="hljs-string">"rules"</span>: {
    <span class="hljs-string">"react-compiler/react-compiler"</span>: <span class="hljs-string">"error"</span>
  }
}
</code></pre>
<p>Then run:</p>
<pre><code class="lang-jsx">npx eslint .
</code></pre>
<p>It will flag components that are unsafe for compilation — e.g.:</p>
<ul>
<li><p>Mutating refs during render</p>
</li>
<li><p>Using <code>eval</code> or dynamic keys</p>
</li>
<li><p>Breaking hook rules</p>
</li>
<li><p>Passing unstable props via spread (<code>{...props}</code>)</p>
</li>
</ul>
<p>Fix these first. The compiler can’t optimize around them. You don’t need to rewrite your app, but you do need to understand what breaks the compiler’s assumptions.</p>
<blockquote>
<p>Compatibility ≠ Optimization. Even “100% compatible” code may not be optimized unless you adjust patterns like useMutation, dynamic keys, or inline JSX children.</p>
</blockquote>
<hr />
<h2 id="heading-how-to-try-it-today-step-by-step">How to Try It Today (Step-by-Step)</h2>
<h3 id="heading-step-1-install-the-experimental-build">Step 1: Install the Experimental Build</h3>
<p>You need React 19 experimental builds. In your project:</p>
<pre><code class="lang-jsx">npm install react@experimental react-dom@experimental
</code></pre>
<p>The compiler is opt-in and only works with the experimental channel for now.</p>
<hr />
<h3 id="heading-step-2-enable-the-compiler">Step 2: Enable the Compiler</h3>
<p>If you’re using Vite + TypeScript</p>
<p>Install the Babel plugin:</p>
<pre><code class="lang-jsx">npm install babel-plugin-react-compiler --save-dev
</code></pre>
<p>Then, in your <code>babel.config.js</code>:</p>
<pre><code class="lang-jsx"><span class="hljs-built_in">module</span>.exports = {
<span class="hljs-attr">plugins</span>: [
[<span class="hljs-string">"babel-plugin-react-compiler"</span>, {
<span class="hljs-attr">compilationMode</span>: <span class="hljs-string">"infer"</span>
}]
]
};
</code></pre>
<p><code>compilationMode: "infer"</code> tells the compiler to auto-detect which components are safe to optimize.</p>
<p>If You’re Using Next.js (Recommended Setup):</p>
<hr />
<p>Next.js includes <strong>built-in, optimized support</strong> for the React Compiler — using SWC under the hood for faster builds.</p>
<p>Install the babel plugin:</p>
<pre><code class="lang-jsx">npm install babel-plugin-react-compiler
</code></pre>
<p>Then, in your <code>next.config.js</code>:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> type { NextConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">const</span> nextConfig: NextConfig = {
<span class="hljs-attr">experimental</span>: {
<span class="hljs-attr">reactCompiler</span>: <span class="hljs-literal">true</span>,
},
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> nextConfig
</code></pre>
<blockquote>
<p>Next.js only compiles files that actually use JSX or React Hooks — so builds stay fast.</p>
</blockquote>
<p>For Expo, you need to install the babel plugin (not needed for SDK54 and above)</p>
<pre><code class="lang-jsx">npx expo install babel-plugin-react-compiler@beta
</code></pre>
<p>Then update App.json</p>
<pre><code class="lang-jsx">{
  <span class="hljs-string">"expo"</span>: {
    <span class="hljs-string">"experiments"</span>: {
      <span class="hljs-string">"reactCompiler"</span>: <span class="hljs-literal">true</span>
    }
  }
}
</code></pre>
<p>Then update your eslint, first install the react compiler plugin</p>
<pre><code class="lang-jsx">npx expo install eslint-plugin-react-compiler -D
</code></pre>
<p>Then update the eslint configuration</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// &lt;https://docs.expo.dev/guides/using-eslint/&gt;</span>
<span class="hljs-keyword">const</span> { defineConfig } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'eslint/config'</span>);
<span class="hljs-keyword">const</span> expoConfig = <span class="hljs-built_in">require</span>(<span class="hljs-string">'eslint-config-expo/flat'</span>);
<span class="hljs-keyword">const</span> reactCompiler = <span class="hljs-built_in">require</span>(<span class="hljs-string">'eslint-plugin-react-compiler'</span>);

<span class="hljs-built_in">module</span>.exports = defineConfig([
  expoConfig,
  reactCompiler.configs.recommended,
  {
    <span class="hljs-attr">ignores</span>: [<span class="hljs-string">'dist/*'</span>],
  },
]);
</code></pre>
<h3 id="heading-optional-opt-in-mode">Optional: Opt-In Mode</h3>
<pre><code class="lang-jsx">reactCompiler: {
<span class="hljs-attr">compilationMode</span>: <span class="hljs-string">'annotation'</span>,
},
}
</code></pre>
<p>Then, in any component:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyComponent</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-string">"use memo"</span>
<span class="hljs-comment">// ... your code</span>
}
</code></pre>
<p>Or to opt-out:</p>
<pre><code class="lang-jsx"><span class="hljs-string">"use no memo"</span>
</code></pre>
<h3 id="heading-step-3-write-naive-code-let-the-compiler-do-the-work">Step 3: Write “Naive” Code — Let the Compiler Do the Work</h3>
<p>Take a look at this component written <em>without</em> any useMemo or useCallback:</p>
<pre><code class="lang-jsx"><span class="hljs-comment">// Before: “Naive” version — no manual memoization</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Counter</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> [counter, setCounter] = useState(<span class="hljs-number">0</span>);
<span class="hljs-keyword">const</span> [name, setName] = useState(<span class="hljs-string">"Mohamed"</span>);
<span class="hljs-keyword">const</span> handleButtonClick = <span class="hljs-function">() =&gt;</span> {
setCounter(<span class="hljs-function"><span class="hljs-params">value</span> =&gt;</span> value + <span class="hljs-number">1</span>);
};
<span class="hljs-keyword">const</span> welcomeMessage = Welcome, Mr. ${name}!; <span class="hljs-comment">// This gets auto-memoized!</span>
<span class="hljs-keyword">return</span> (
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>{welcomeMessage}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Counter value: {counter}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{handleButtonClick}</span>&gt;</span>Increment<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">value</span>=<span class="hljs-string">{name}</span>
<span class="hljs-attr">onChange</span>=<span class="hljs-string">{event</span> =&gt;</span> setName(event.target.value)}
/&gt;
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
);
}
</code></pre>
<p>When you edit the input (<code>name</code>), the welcome string and <code>handleButtonClick</code> function are <em>automatically memoized</em> by the compiler — even though you didn’t write useMemo or useCallback.</p>
<p>You can verify this in React DevTools → “Highlight updates when components render.” You’ll see only the necessary parts re-render.</p>
<hr />
<h3 id="heading-step-4-check-compiler-output-optional-for-the-curious">Step 4: Check Compiler Output (Optional — for the Curious)</h3>
<p>The compiler rewrites your code behind the scenes. You can see the output by:</p>
<ol>
<li><p>Running your dev server</p>
</li>
<li><p>Opening DevTools → Sources</p>
</li>
<li><p>Looking for <code>.hot-update.js</code> or compiled component files</p>
</li>
</ol>
<p>You’ll see generated code like:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> $ = useMemo(<span class="hljs-function">() =&gt;</span> Welcome, Mr. ${name}!, [name]);
<span class="hljs-keyword">const</span> handleButtonClick = useCallback(<span class="hljs-function">() =&gt;</span> { ... }, []);
</code></pre>
<p>inserted automatically. You didn’t write it. The compiler did.</p>
<hr />
<h2 id="heading-gotchas-amp-limitations">Gotchas &amp; Limitations</h2>
<p>These limitations are based on real-world testing (see: <a target="_blank" href="https://www.developerway.com/posts/i-tried-react-compiler">I Tried React Compiler</a> ) and React Summit 2024 demos. Even if your app is “compatible,” the compiler may not optimize everything unless you adjust patterns like <code>useMutation</code>, dynamic keys, or inline JSX children.</p>
<p>✗ The compiler can’t optimize components that use <code>eval</code>, <code>new Function</code>, or highly dynamic patterns.</p>
<p>✗ Avoid <code>useRef</code> mutations in render — the compiler can’t track those.</p>
<p>✗ Spread props (<code>{...props}</code>) may limit optimization — prefer explicit props when possible.</p>
<p>✗ Libraries returning non-memoized objects from hooks (e.g., react-query’s <code>useMutation</code>) can break memoization. Extract <code>.mutate</code> or <code>.data</code> before passing to callbacks.</p>
<p>✓ Safe patterns: useState, useEffect, simple derived values, inline functions — all optimized automatically.</p>
<blockquote>
<p>If the compiler can’t optimize it, it leaves your code as-is. No breakage. Just missed opportunities.</p>
</blockquote>
<hr />
<h2 id="heading-im-already-using-it-in-production">I'm Already Using It in Production</h2>
<p>I don't wait for "stable" to test real tools.</p>
<p>I've implemented the React Compiler in two production apps:</p>
<h3 id="heading-1-open-tarteelhttpsgithubcomadelproopen-tarteel-nextjs-web-app">1. <a target="_blank" href="https://github.com/adelpro/open-tarteel">Open Tarteel</a> — Next.js Web App</h3>
<p>A Quran recitation and correction platform built with Next.js App Router.</p>
<ul>
<li><p>Compiler enabled via <code>next.config.js</code></p>
</li>
<li><p>ESLint plugin running in CI</p>
</li>
</ul>
<h3 id="heading-2-open-mushaf-nativehttpsgithubcomadelproopen-mushaf-native-expo-react-native-app">2. <a target="_blank" href="https://github.com/adelpro/open-mushaf-native">Open Mushaf Native</a> — Expo (React Native) App</h3>
<p>A mobile Quran reader.</p>
<ul>
<li><p>Compiler enabled via Babel plugin in <code>babel.config.js</code></p>
</li>
<li><p>Running on Android in internal builds and in production</p>
</li>
<li><p>Zero crashes. Zero state bugs.</p>
</li>
</ul>
<p>Yes, the React Compiler works in React Native. It doesn't care if you're rendering to DOM or native views. If it's JSX and hooks, it optimizes it.</p>
<blockquote>
<p>Tip: In Expo, make sure you're on SDK 49+ and React 18.3+ experimental. Clear your Metro cache after enabling: npx expo start -c</p>
</blockquote>
<p>This isn't a lab experiment. This is live, user-facing code.</p>
<p>The compiler isn't magic, but it's real. When paired with a health check and minor pattern tweaks, it delivers results.</p>
<p>You don't need permission to try it.</p>
<p>Just don't skip the audit.</p>
<hr />
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>The React Compiler isn’t just an incremental improvement. It’s a paradigm shift.</p>
<p>It takes one of React’s biggest pain points, performance optimization — and automates it. Completely.</p>
<p>20% faster apps. Zero extra code. Zero extra effort.</p>
<p>If that doesn’t get you excited, you’re not paying attention.</p>
<p>But, and this is critical, it’s not magic. It’s math. And math needs clean inputs.</p>
<p>Run the health check. Fix the gotchas. Then let the compiler fly.</p>
<hr />
<p>Attribution: cover photo from <a target="_blank" href="https://pixabay.com/users/clickerhappy-324082/">ClickerHappy</a></p>
]]></content:encoded></item><item><title><![CDATA[Infinite Scroll in React: The Smooth, Built-in Way with Intersection Observer (No Libraries Needed)]]></title><description><![CDATA[Introduction: Why Infinite Scroll Matters: Keeping Users Engaged
You know that feeling? You’re scrolling through a feed, lost in the content, new content keeps appearing, No clunky “Load More” button. No jarring page refreshes. Just… more. That’s inf...]]></description><link>https://letscode.adelpro.us.kg/infinite-scroll-in-react-the-smooth-built-in-way-with-intersection-observer-no-libraries-needed</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/infinite-scroll-in-react-the-smooth-built-in-way-with-intersection-observer-no-libraries-needed</guid><category><![CDATA[React]]></category><category><![CDATA[UI]]></category><category><![CDATA[UX]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Thu, 11 Sep 2025 19:05:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757617464140/f950e703-a7cc-469a-b8bd-beecd8a60571.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-why-infinite-scroll-matters-keeping-users-engaged">Introduction: Why Infinite Scroll Matters: Keeping Users Engaged</h2>
<p>You know that feeling? You’re scrolling through a feed, lost in the content, new content keeps appearing, No clunky “Load More” button. No jarring page refreshes. Just… more. That’s infinite scroll. And today, we’re wiring it up in React using the browser’s built-in API: the <strong>Intersection Observer API</strong>.</p>
<p>We’ll be pulling in real product data from <code>dummyjson.com/products</code>, this endpoint is perfect for testing and prototyping, and it uses <code>limit</code> and <code>skip</code> parameters for pagination. We’ll use exactly that to fetch our data in neat, manageable chunks.</p>
<hr />
<p>You’re on Part 3 of our series on the Intersection Observer API.</p>
<p>Here we’ll build infinite scroll, but first, check out Parts 1 and 2 to get familiar with the API.</p>
<ol>
<li><p><a target="_blank" href="https://peerlist.io/adelpro/articles/javascript-intersection-observer-api-master-animations--opti"><strong>Part 1: JavaScript Intersection Observer API — Master Scroll-Triggered Animations</strong></a></p>
</li>
<li><p><a target="_blank" href="https://peerlist.io/adelpro/articles/how-to-use-the-intersection-observer-api-in-react-lazy-load-"><strong>Part 2: How to Use the Intersection Observer API in React — Lazy Load Images &amp; Components</strong></a></p>
<hr />
<h2 id="heading-lets-start-by-creating-a-reusable-hook-useinfinitescroll">Let’s start by creating a reusable Hook: useInfiniteScroll.</h2>
<p> First, we create a custom hook. Why? Because you’ll want to reuse this magic everywhere. This hook watches an element. When that element scrolls into view, it triggers a function to fetch more data.</p>
<p> Here’s the goods:</p>
</li>
</ol>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">type</span> Props = { fetchData: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>, hasMore: <span class="hljs-built_in">boolean</span> };
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useInfiniteScroll = <span class="hljs-function">(<span class="hljs-params">{ fetchData, hasMore }: Props</span>) =&gt;</span> {
 <span class="hljs-keyword">const</span> loadMoreRef = (React.useRef &lt; HTMLDivElement) | (<span class="hljs-literal">null</span> &gt; <span class="hljs-literal">null</span>);
 <span class="hljs-keyword">const</span> handleIntersection = React.useCallback(
 <span class="hljs-function">(<span class="hljs-params">entries: IntersectionObserverEntry[]</span>) =&gt;</span> {
 <span class="hljs-keyword">const</span> isIntersecting = entries[<span class="hljs-number">0</span>]?.isIntersecting;
 <span class="hljs-keyword">if</span> (isIntersecting &amp;&amp; hasMore) {
 fetchData();
 }
 },
 [fetchData, hasMore]
 );
 React.useEffect(<span class="hljs-function">() =&gt;</span> {
 <span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> IntersectionObserver(handleIntersection);
 <span class="hljs-keyword">if</span> (loadMoreRef.current) {
 observer.observe(loadMoreRef.current);
 }
 <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> observer.disconnect();
 }, [handleIntersection]);
 <span class="hljs-keyword">return</span> { loadMoreRef };
};
</code></pre>
<h3 id="heading-whats-happening-here"><strong>What's happening here?</strong></h3>
<ul>
<li><p><code>fetchData</code>: The function that fetches your next batch of data (like our products from <code>dummyjson.com</code>).</p>
</li>
<li><p><code>hasMore</code>: A simple boolean flag. Are there more items to load from the server? If not, we stop fetching.</p>
</li>
<li><p><code>loadMoreRef</code>: This is our sentinel element. We attach it to the bottom of our list. When this element becomes visible in the viewport, our <strong>Intersection Observer</strong> triggers the "Fetch more!" action.</p>
</li>
<li><p>The <code>useEffect</code> creates the <code>IntersectionObserver</code> when the component mounts and cleans it up when it unmounts, keeping everything tidy.</p>
<hr />
<h2 id="heading-implementing-the-product-card-and-the-products-list"><strong>Implementing the Product card and the Products List</strong></h2>
<p>  Let’s start by creating the Product type:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> ProductType = {
   id: <span class="hljs-built_in">number</span>,
   thumbnail: <span class="hljs-built_in">string</span>,
   title: <span class="hljs-built_in">string</span>,
   price: <span class="hljs-built_in">number</span>,
  };
</code></pre>
<p>  Then create a reusable component Product Card, to display each item (product) we pull from the API:</p>
</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { ProductType } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types"</span>;

<span class="hljs-keyword">type</span> ProductCardProps = Pick&lt;ProductType, <span class="hljs-string">"thumbnail"</span> | <span class="hljs-string">"title"</span> | <span class="hljs-string">"price"</span>&gt;;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ProductCard</span>(<span class="hljs-params">{
 thumbnail,
 title,
 price,
}: ProductCardProps</span>) </span>{
 <span class="hljs-keyword">return</span> (
 &lt;div className=<span class="hljs-string">"product-card"</span>&gt;
 &lt;img
 src={thumbnail}
 alt={title}
 loading=<span class="hljs-string">"lazy"</span>
 className=<span class="hljs-string">"product-card__image"</span>
 /&gt;
 &lt;h3 className=<span class="hljs-string">"product-card__title"</span> itemProp=<span class="hljs-string">"name"</span>&gt;
 {title}
 &lt;/h3&gt;
 &lt;p
 className=<span class="hljs-string">"product-card__price"</span>
 itemProp=<span class="hljs-string">"offers"</span>
 itemScope
 itemType=<span class="hljs-string">"https://schema.org/Offer"</span>
 &gt;
 &lt;strong itemProp=<span class="hljs-string">"priceCurrency"</span> content=<span class="hljs-string">"USD"</span>&gt;
 $
 &lt;/strong&gt;
 &lt;strong itemProp=<span class="hljs-string">"price"</span>&gt;{price.toFixed(<span class="hljs-number">2</span>)}&lt;/strong&gt;
 &lt;/p&gt;
 &lt;/div&gt;
 );
}
</code></pre>
<p>For the CSS, add these CSS to styles.css file in the route of our project</p>
<pre><code class="lang-css">
<span class="hljs-selector-class">.product-card</span> {  <span class="hljs-attribute">height</span>: <span class="hljs-number">350px</span>;  <span class="hljs-attribute">padding</span>: <span class="hljs-number">20px</span>;  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#f8f9fa</span>;  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">8px</span>;  <span class="hljs-attribute">border</span>: <span class="hljs-number">2px</span> solid <span class="hljs-number">#a29a9c</span>;  <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">2px</span> <span class="hljs-number">4px</span> <span class="hljs-built_in">rgba</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0.1</span>);  <span class="hljs-attribute">display</span>: flex;  <span class="hljs-attribute">flex-direction</span>: column;  <span class="hljs-attribute">align-items</span>: center;  <span class="hljs-attribute">justify-content</span>: space-between;}
<span class="hljs-selector-class">.product-card__image</span> {  <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;  <span class="hljs-attribute">height</span>: <span class="hljs-number">200px</span>;  <span class="hljs-attribute">object-fit</span>: cover;  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">4px</span>;  <span class="hljs-attribute">margin-bottom</span>: <span class="hljs-number">10px</span>;}
<span class="hljs-selector-class">.product-card__title</span> {  <span class="hljs-attribute">margin</span>: <span class="hljs-number">10px</span> <span class="hljs-number">0</span>;  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1rem</span>;  <span class="hljs-attribute">text-align</span>: center;}
<span class="hljs-selector-class">.product-card__price</span> {  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span>;  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">1.2rem</span>;  <span class="hljs-attribute">color</span>: <span class="hljs-number">#e91e63</span>;}
</code></pre>
<p>Now, let's put our hook to work. We'll build a Products component that fetches items from the <strong>dummyjson.com/products</strong></p>
<p>API and renders them in a list. Each product is rendered in a card with a title, thumbnail, price, and reviews, perfect for a rich, engaging feed.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { ProductType } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types"</span>;
<span class="hljs-keyword">import</span> { useInfiniteScroll } <span class="hljs-keyword">from</span> <span class="hljs-string">"../hooks/use-infinite-scroll"</span>;
<span class="hljs-keyword">import</span> ProductCard <span class="hljs-keyword">from</span> <span class="hljs-string">"./product-card"</span>;
<span class="hljs-keyword">import</span> LoadMoreElement <span class="hljs-keyword">from</span> <span class="hljs-string">"./load-more-element"</span>;
<span class="hljs-keyword">import</span> { delay } <span class="hljs-keyword">from</span> <span class="hljs-string">"../types/utils/delay"</span>;

<span class="hljs-keyword">const</span> ITEMS_PER_PAGE = <span class="hljs-number">6</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Products</span>(<span class="hljs-params"></span>) </span>{
 <span class="hljs-keyword">const</span> [products, setProducts] = React.useState&lt;ProductType[]&gt;([]);
 <span class="hljs-keyword">const</span> [hasMore, setHasMore] = React.useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">true</span>);
 <span class="hljs-keyword">const</span> [page, setPage] = React.useState&lt;<span class="hljs-built_in">number</span>&gt;(<span class="hljs-number">0</span>);

 <span class="hljs-keyword">const</span> fetchProducts = React.useCallback(<span class="hljs-keyword">async</span> () =&gt; {
 <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(
 <span class="hljs-string">`https://dummyjson.com/products?limit=<span class="hljs-subst">${ITEMS_PER_PAGE}</span>&amp;skip=<span class="hljs-subst">${
 page * ITEMS_PER_PAGE
 }</span>`</span>
 );
 <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> response.json();

 <span class="hljs-keyword">await</span> delay(<span class="hljs-number">2000</span>); <span class="hljs-comment">//delay for 2sec</span>

 <span class="hljs-comment">// Slowdown timer</span>

 <span class="hljs-keyword">if</span> (data.products.length === <span class="hljs-number">0</span>) {
 setHasMore(<span class="hljs-literal">false</span>);
 } <span class="hljs-keyword">else</span> {
 setProducts(<span class="hljs-function">(<span class="hljs-params">prevProducts</span>) =&gt;</span> [...prevProducts, ...data.products]);
 setPage(<span class="hljs-function">(<span class="hljs-params">prevPage</span>) =&gt;</span> prevPage + <span class="hljs-number">1</span>);
 }
 }, [page]);

 <span class="hljs-comment">// Plug in our custom hook.</span>
 <span class="hljs-keyword">const</span> { loadMoreRef } = useInfiniteScroll(fetchProducts, hasMore);

 <span class="hljs-keyword">return</span> (
 &lt;&gt;
 &lt;div className=<span class="hljs-string">"products"</span>&gt;
 {products.map(<span class="hljs-function">(<span class="hljs-params">product: ProductType</span>) =&gt;</span> (
 &lt;ProductCard
 key={product.id}
 thumbnail={product.thumbnail}
 title={product.title}
 price={product.price}
 /&gt;
 ))}
 &lt;/div&gt;
 {<span class="hljs-comment">/* This is our sentinel. The hook watches this div. */</span>}
 {hasMore &amp;&amp; &lt;LoadMoreElement loadMoreRef={loadMoreRef} /&gt;}
 &lt;/&gt;
 );
}
</code></pre>
<p>Update the styles.css, add these CSS</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.products</span> {  <span class="hljs-attribute">margin</span>: <span class="hljs-number">20px</span>;  <span class="hljs-attribute">display</span>: grid;  <span class="hljs-attribute">gap</span>: <span class="hljs-number">20px</span>;  <span class="hljs-attribute">grid-template-columns</span>: <span class="hljs-built_in">repeat</span>(auto-fill, minmax(<span class="hljs-number">250px</span>, <span class="hljs-number">1</span>fr));}
</code></pre>
<h3 id="heading-breaking-it-down">Breaking it down:</h3>
<ul>
<li><p>We manage three essential state variables: the <code>products</code> array to store our items, the <code>hasMore</code> flag to track if additional data exists, and the current <code>page</code> for pagination.</p>
</li>
<li><p>The <code>fetchProducts</code> function handles our data retrieval. It calculates the <code>skip</code> parameter (<code>page * ITEMS_PER_PAGE</code>) for the API request, pulling the next set of products from <code>dummyjson.com</code>. When it receives an empty array, it sets <code>hasMore</code> to <code>false</code> to stop further requests.</p>
</li>
<li><p>We feed <code>fetchProducts</code> and <code>hasMore</code> into our custom <code>useInfiniteScroll</code> hook.</p>
</li>
<li><p>The hook returns <code>loadMoreRef</code>, which we attach to a small <code>&amp;lt;div&amp;gt;</code> at the bottom of our product list. When this sentinel element becomes visible as the user scrolls down, it automatically triggers <code>fetchProducts</code>—creating that seamless infinite scroll experience.</p>
</li>
<li><p>We have added a slowdown function: delay() just to slow down the loading process, so we can see the load more element in action, you can disable it.</p>
</li>
</ul>
<hr />
<h3 id="heading-implementing-the-load-more-component"><strong>Implementing the Load-more component</strong></h3>
<p>Now let’s create a dedicated, reusable component to handle our loading state. This isn’t just a blank <code>&lt;div&gt;</code> anymore, it’s a visual signal to the user that magic is happening.</p>
<p>Create a new file: <code>components/Load-more-element.tsx</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

type Props = {
 <span class="hljs-attr">loadMoreRef</span>: React.MutableRefObject&lt;HTMLDivElement | <span class="hljs-literal">null</span>&gt;;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LoadMoreElement</span>(<span class="hljs-params">{ loadMoreRef }: Props</span>) </span>{
 <span class="hljs-keyword">return</span> (
 <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"load-more__container"</span>&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">ref</span>=<span class="hljs-string">{loadMoreRef}</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"load-more"</span>&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>Loading more<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"load-more__dots"</span>&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">span</span> /&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">span</span> /&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">span</span> /&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
 );
}
</code></pre>
<p>Add some CSS to our styles.css file</p>
<pre><code class="lang-css"><span class="hljs-comment">/* Load more styles */</span><span class="hljs-selector-class">.load-more</span> {  <span class="hljs-attribute">display</span>: flex;  <span class="hljs-attribute">flex-direction</span>: row;  <span class="hljs-attribute">gap</span>: <span class="hljs-number">10px</span>;  <span class="hljs-attribute">align-items</span>: center;  <span class="hljs-attribute">justify-content</span>: center;  <span class="hljs-attribute">padding</span>: <span class="hljs-number">16px</span>;  <span class="hljs-attribute">margin</span>: <span class="hljs-number">10px</span> auto;  <span class="hljs-attribute">width</span>: <span class="hljs-number">180px</span>;  <span class="hljs-attribute">background</span>: <span class="hljs-number">#e0e0e0</span>;  <span class="hljs-attribute">color</span>: <span class="hljs-number">#555</span>;  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">6px</span>;  <span class="hljs-attribute">cursor</span>: default;  <span class="hljs-attribute">font-weight</span>: normal;  <span class="hljs-attribute">transition</span>: none;}
<span class="hljs-selector-class">.load-more</span><span class="hljs-selector-pseudo">:hover</span> {  <span class="hljs-attribute">background</span>: <span class="hljs-number">#e0e0e0</span>;  <span class="hljs-attribute">transform</span>: none;}
<span class="hljs-selector-class">.load-more__dots</span> {  <span class="hljs-attribute">display</span>: flex;  <span class="hljs-attribute">justify-content</span>: center;  <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">8px</span>;  <span class="hljs-attribute">gap</span>: <span class="hljs-number">5px</span>;}
<span class="hljs-selector-class">.load-more__dots</span> <span class="hljs-selector-tag">span</span> {  <span class="hljs-attribute">width</span>: <span class="hljs-number">5px</span>;  <span class="hljs-attribute">height</span>: <span class="hljs-number">5px</span>;  <span class="hljs-attribute">background</span>: <span class="hljs-number">#999</span>;  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">50%</span>;  <span class="hljs-attribute">display</span>: inline-block;  <span class="hljs-attribute">animation</span>: bounce <span class="hljs-number">1s</span> infinite;}
<span class="hljs-selector-class">.load-more__dots</span> <span class="hljs-selector-tag">span</span><span class="hljs-selector-pseudo">:nth-child(2)</span> {  <span class="hljs-attribute">animation-delay</span>: <span class="hljs-number">0.2s</span>;}
<span class="hljs-selector-class">.load-more__dots</span> <span class="hljs-selector-tag">span</span><span class="hljs-selector-pseudo">:nth-child(3)</span> {  <span class="hljs-attribute">animation-delay</span>: <span class="hljs-number">0.4s</span>;}
<span class="hljs-keyword">@keyframes</span> bounce {  0%,  80%,  100% {    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateY</span>(<span class="hljs-number">0</span>);  }  40% {    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateY</span>(-<span class="hljs-number">6px</span>);  }}
</code></pre>
<hr />
<h2 id="heading-live-demo-amp-source-code"><strong>Live Demo &amp; Source Code</strong></h2>
<p>Live demo: <a target="_blank" href="https://dn6nv4-5173.csb.app/">https://dn6nv4-5173.csb.app/</a></p>
<p>Source code: <a target="_blank" href="https://github.com/adelpro/products-infinite-scroll">https://github.com/adelpro/products-infinite-scroll</a></p>
<hr />
<h2 id="heading-conclusion-infinite-scroll-the-native-way"><strong>Conclusion: Infinite Scroll, the Native Way</strong></h2>
<p>And that’s it—you’ve just built a fully working infinite scroll in React using nothing but the browser’s built-in <strong>Intersection Observer API</strong>. No heavy libraries, no hacks, just smooth, native performance.</p>
<p>This technique isn’t just for product feeds. You can apply it to:</p>
<ul>
<li><p><strong>Blog posts</strong>: load article previews endlessly, keeping readers engaged.</p>
</li>
<li><p><strong>Social feeds</strong>: keep the timeline alive without page reloads.</p>
</li>
<li><p><strong>Image galleries</strong>: stream in photos as users scroll.</p>
</li>
<li><p><strong>Dashboards</strong>: fetch logs, analytics, or messages chunk by chunk.</p>
</li>
<li><p><strong>E-commerce</strong>: show products, reviews, or search results without friction.</p>
</li>
</ul>
<p>Next time you’re tempted to reach for another dependency, remember: the browser already has your back. Keep scrolling, keep it smooth.</p>
<hr />
<p>Cover image credit: <a target="_blank" href="https://unsplash.com/@martenbjork">Marten Bjork</a></p>
]]></content:encoded></item><item><title><![CDATA[Turn Your Web UI into a Mobile App with Expo DOM Components]]></title><description><![CDATA[Did you know that...
Expo’s DOM Components, introduced around SDK 52, let you embed web React components directly in your mobile app by simply adding the use dom directive at the top of your file. It’s like running a mini-web app inside your native U...]]></description><link>https://letscode.adelpro.us.kg/turn-your-web-ui-into-a-mobile-app-with-expo-dom-components</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/turn-your-web-ui-into-a-mobile-app-with-expo-dom-components</guid><category><![CDATA[Expo]]></category><category><![CDATA[React Native]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Thu, 11 Sep 2025 19:03:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757617224425/4431d13d-3c77-4078-aca0-ac767772452f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-did-you-know-that">Did you know that...</h2>
<p>Expo’s <strong>DOM Components</strong>, introduced around <strong>SDK 52</strong>, let you <strong>embed web React components directly in your mobile app</strong> by simply adding the <code>use dom</code> directive at the top of your file. It’s like running a mini-web app inside your native UI—no rewrite required.</p>
<h2 id="heading-why-this-matters">Why this matters</h2>
<ul>
<li><p><strong>One codebase for web + mobile</strong>: Share components across platforms, cut maintenance, enforce consistency.</p>
</li>
<li><p><strong>Fast prototyping &amp; incremental migration</strong>: Wrap your web app in Expo, ship quickly, then gradually swap in native bits where it matters.</p>
</li>
</ul>
<h2 id="heading-example-embedding-a-rich-text-editor-using-expo-dom-components">Example: Embedding a Rich Text Editor using Expo DOM Components</h2>
<pre><code class="lang-javascript"><span class="hljs-string">'use dom'</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Welcome</span>(<span class="hljs-params"></span>) </span>{
 <span class="hljs-keyword">return</span> (
 <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">padding:</span> <span class="hljs-attr">20</span>, <span class="hljs-attr">textAlign:</span> '<span class="hljs-attr">center</span>' }}&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Welcome to the App!<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>We’re glad to have you here.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
 );
}
</code></pre>
<p>Drop this into your Expo app, and it runs just like on the web—inside your mobile shell.</p>
<p><strong>Expo DOM Components (since SDK 52)</strong> let you plug in your web UI into mobile apps with minimal friction. Think “web within native”—ship faster, stay unified, and improve incrementally.</p>
]]></content:encoded></item><item><title><![CDATA[Speed Up Your Code: Update ESLint to v9.34.0 for Multithread Linting]]></title><description><![CDATA[ESLint v9.34.0 introduces multithread linting, a decade-long effort to speed up linting by processing multiple files simultaneously.
Why it matters:

Multiple CPU cores? Fast SSD? You’ll see big gains.

Early tests show 1.3x–3x faster linting, especi...]]></description><link>https://letscode.adelpro.us.kg/speed-up-your-code-update-eslint-to-v9340-for-multithread-linting</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/speed-up-your-code-update-eslint-to-v9340-for-multithread-linting</guid><category><![CDATA[Frontend Development]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[devtools]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Thu, 11 Sep 2025 18:59:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757617114905/7c84d676-b65d-4b95-a274-e11dd0e22437.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>ESLint v9.34.0 introduces <strong>multithread linting</strong>, a decade-long effort to speed up linting by processing multiple files simultaneously.</p>
<h2 id="heading-why-it-matters"><strong>Why it matters:</strong></h2>
<ul>
<li><p>Multiple CPU cores? Fast SSD? You’ll see big gains.</p>
</li>
<li><p>Early tests show <strong>1.3x–3x faster linting</strong>, especially on large projects with hundreds or thousands of files.</p>
</li>
</ul>
<h2 id="heading-how-to">How to</h2>
<p>Update your eslint package in your package.json to <strong>v9.34.0</strong> to take advantage of <strong>multithread linting</strong>.</p>
<p>Then, update your scripts:</p>
<pre><code class="lang-json"><span class="hljs-string">"scripts"</span>: {
 <span class="hljs-attr">"lint"</span>: <span class="hljs-string">"eslint --fix --cache --concurrency=auto"</span>
}
</code></pre>
<p>If you’re using lint-staged for pre-commit linting, configure it like this:</p>
<pre><code class="lang-json"><span class="hljs-string">"lint-staged"</span>: {
 <span class="hljs-attr">"*.{js,jsx,ts,tsx}"</span>: [

 <span class="hljs-string">"prettier --write"</span>,

 <span class="hljs-string">"eslint --fix --cache --concurrency=auto"</span>

 ],

 <span class="hljs-attr">"*.{json,md,css}"</span>: <span class="hljs-string">"prettier --write"</span>

}
</code></pre>
<p>This will dramatically speed up linting on large projects by running multiple files in parallel. Perfect for faster pre-commit hooks and CI pipelines!</p>
]]></content:encoded></item><item><title><![CDATA[How to Boost React App Performance with the Intersection Observer API (No Libraries)]]></title><description><![CDATA[Introduction: Solving Performance Issues in React with Native APIs
Loading a gallery of large, high-resolution images can cripple your React application's performance, leading to slow initial load times, a poor Largest Contentful Paint (LCP) score, a...]]></description><link>https://letscode.adelpro.us.kg/how-to-boost-react-app-performance-with-the-intersection-observer-api-no-libraries</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/how-to-boost-react-app-performance-with-the-intersection-observer-api-no-libraries</guid><category><![CDATA[Frontend Development]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[UI]]></category><category><![CDATA[UX]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Thu, 11 Sep 2025 18:57:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757616893685/af17649c-8f04-497e-a980-b4fa689377ed.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-solving-performance-issues-in-react-with-native-apis"><strong>Introduction: Solving Performance Issues in React with Native APIs</strong></h2>
<p>Loading a gallery of large, high-resolution images can cripple your React application's performance, leading to slow initial load times, a poor <strong>Largest Contentful Paint (LCP)</strong> score, and a frustrating user experience. The solution isn't to reduce image quality, but to be smarter about <em>when</em> you load them.</p>
<p>In our <a target="_blank" href="https://peerlist.io/adelpro/articles/javascript-intersection-observer-api-master-animations--opti">previous article</a> , we explored the fundamentals of the <strong>Intersection Observer API</strong> in vanilla JavaScript, learning how it provides a performant, asynchronous way to detect when elements enter the viewport—without the jank of traditional scroll listeners.</p>
<p>Now, we're going to apply that powerful native browser API directly within a <strong>React</strong> and <strong>Next.js</strong> application. We'll build a real-world example: a <strong>heavy image gallery</strong> that uses the Intersection Observer to implement <strong>lazy loading</strong> and <strong>infinite scroll</strong> from the ground up, using only React's core hooks (<code>useRef</code>, <code>useEffect</code>, <code>useState</code>) and <strong>no third-party libraries</strong>.</p>
<p>By the end of this guide, you'll know how to integrate the Intersection Observer API into your React components to dramatically improve performance, reduce bandwidth usage, and create a seamless, professional user experience. This is the essential technique for building fast, scalable React applications that handle large amounts of content.</p>
<hr />
<p>👉 This article is part 2 of Intersection Observer series. If you haven’t yet, start with [<a target="_blank" href="https://peerlist.io/adelpro/articles/javascript-intersection-observer-api-master-animations--opti">JavaScript Intersection Observer API: Master Scroll-Triggered Animations...]</a> before continuing</p>
<hr />
<h2 id="heading-how-the-intersection-observer-api-works-in-react-core-concepts"><strong>How the Intersection Observer API Works in React: Core Concepts</strong></h2>
<p>To effectively use the Intersection Observer API in React, it's crucial to understand its core mechanics, as detailed in our <a target="_blank" href="https://peerlist.io/adelpro/articles/javascript-intersection-observer-api-master-animations--opti">previous guide</a> . The API provides an asynchronous way to monitor when a target element intersects with the browser's viewport (or a specified root element), firing a callback function when the visibility changes.</p>
<p>The API is created with the <code>new IntersectionObserver(callback, options)</code> constructor. The <code>callback</code> function is executed whenever the intersection state of a target element changes. It receives an array of <code>IntersectionObserverEntry</code> objects, each containing vital properties:</p>
<ul>
<li><p><code>isIntersecting</code>: A boolean that is <code>true</code> when the element is visible in the viewport.</p>
</li>
<li><p><code>intersectionRatio</code>: A number between <code>0</code> and <code>1</code> indicating the percentage of the element that is visible.</p>
</li>
</ul>
<p>The <code>options</code> object allows you to fine-tune the observation:</p>
<ul>
<li><p><code>threshold</code>: Defines at what percentage of visibility the callback should fire. A value of <code>0</code> triggers on the first visible pixel, <code>1</code> requires the entire element to be visible, and an array like <code>[0, 0.25, 0.5, 0.75, 1]</code> triggers at multiple points.</p>
</li>
<li><p><code>rootMargin</code>: Applies a margin (like CSS) around the viewport. A positive value like <code>'50px'</code> triggers the callback <em>before</em> the element enters the viewport, which is perfect for pre-loading content. A negative value like <code>'-50px'</code> requires the element to be deeper inside the viewport to trigger.</p>
</li>
</ul>
<p>The key to using this in React is the <strong>second parameter of the callback</strong>: a reference to the <code>IntersectionObserver</code> instance itself. This is essential for cleanup and is the foundation of our custom hook. As the <a target="_blank" href="https://medium.com/coding-beauty/intersection-observer-in-javascript-everything-you-need-to-know-cded4e80a377">Medium article</a> explains, this allows us to call <code>unobserve()</code> on a specific element or <code>disconnect()</code> on the entire observer, which is critical for preventing memory leaks when components unmount.</p>
<hr />
<h2 id="heading-real-world-example-building-a-performant-image-gallery-in-react"><strong>Real-World Example: Building a Performant Image Gallery in React</strong></h2>
<p>Imagine a portfolio site or a stock photo app. A page with 50 large images (each 1-2MB) could easily exceed 100MB of data. This leads to:</p>
<ul>
<li><p><strong>Poor LCP (Largest Contentful Paint):</strong> The main content takes forever to appear.</p>
</li>
<li><p><strong>High Bandwidth Usage:</strong> Wastes data for mobile users.</p>
</li>
<li><p><strong>Janky Scrolling:</strong> The browser struggles to render so many heavy elements.</p>
</li>
</ul>
<p>Our solution is a <strong>lazy-loading image with hashed light placeholder</strong>. We'll use the Intersection Observer to ensure images are only fetched and rendered when they're about to enter the viewport.</p>
<h3 id="heading-creating-a-reusable-useinviewclass-hook-in-react-and-typescript"><strong>Creating a Reusable</strong> <code>useInViewClass</code> <strong>Hook in React and TypeScript</strong></h3>
<p>To integrate the Intersection Observer API seamlessly into our React components, we can encapsulate its logic into a custom hook. This promotes reusability, adheres to the DRY (Don't Repeat Yourself) principle, and keeps our component code clean and declarative.</p>
<p>The <code>useInViewClass</code> hook provided in your CodeSandbox is a perfect example of this. Let's break it down:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { useEffect, useRef } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useInViewClass</span>(<span class="hljs-params">className = "show", threshold = 0.5</span>) </span>{
 <span class="hljs-keyword">const</span> ref = useRef&lt;HTMLDivElement | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

 useEffect(<span class="hljs-function">() =&gt;</span> {
 <span class="hljs-comment">// Guard clause: Exit if the ref is not attached to a DOM element</span>
 <span class="hljs-keyword">if</span> (!ref.current) <span class="hljs-keyword">return</span>;

 <span class="hljs-comment">// Create a new Intersection Observer</span>
 <span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> IntersectionObserver(
 <span class="hljs-function">(<span class="hljs-params">[entry]</span>) =&gt;</span> {
 <span class="hljs-comment">// Use destructuring to get the first (and only) entry</span>
 <span class="hljs-comment">// Toggle the specified class based on visibility</span>
 entry.target.classList.toggle(className, entry.isIntersecting);
 },
 { threshold } <span class="hljs-comment">// Configuration object</span>
 );

 <span class="hljs-comment">// Start observing the DOM element referenced by `ref`</span>
 observer.observe(ref.current);

 <span class="hljs-comment">// Cleanup function: Disconnect the observer when the component unmounts</span>
 <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> observer.disconnect();
 }, [className, threshold]); <span class="hljs-comment">// Re-run the effect if these dependencies change</span>

 <span class="hljs-comment">// Return the ref so it can be attached to a JSX element</span>
 <span class="hljs-keyword">return</span> ref;
}
</code></pre>
<p><strong>How It Works:</strong></p>
<ol>
<li><p><code>useRef</code>: Creates a mutable ref object (<code>ref</code>) that will hold a reference to the actual DOM element we want to observe.</p>
</li>
<li><p><code>useEffect</code>: This hook runs after the component renders. It sets up the imperative logic for creating and managing the <code>IntersectionObserver</code>.</p>
</li>
<li><p><strong>Observer Creation</strong>: Inside the effect, a new <code>IntersectionObserver</code> is instantiated. Its callback uses array destructuring (<code>[entry]</code>) to get the first <code>IntersectionObserverEntry</code> from the <code>entries</code> array.</p>
</li>
<li><p><strong>Class Toggling</strong>: The callback uses <code>classList.toggle(className, entry.isIntersecting)</code> to add the class (e.g., <code>show</code>) when the element is visible and remove it when it's not. This directly links the element's visibility to its visual state.</p>
</li>
<li><p><strong>Configuration</strong>: The <code>threshold</code> option is passed in, allowing the hook to be configured for different use cases (e.g., trigger at 50% visibility).</p>
</li>
<li><p><strong>Cleanup</strong>: The <code>return () =&gt; observer.disconnect();</code> line is critical. It ensures the observer stops all observation when the component is unmounted, preventing memory leaks—a best practice emphasized in the <a target="_blank" href="https://medium.com/coding-beauty/intersection-observer-in-javascript-everything-you-need-to-know-cded4e80a377">Medium article</a> .</p>
</li>
</ol>
<p>This hook perfectly demonstrates how to bridge the imperative nature of the DOM API with React's declarative paradigm, providing a simple, reusable tool for scroll-triggered effects. In the next section, we'll use this hook to build our heavy image gallery.</p>
<p>Let's apply the <code>useInViewClass</code> hook to a real-world performance challenge: <strong>a gallery of large, high-resolution images</strong>. Loading all these images at once can cause a massive initial payload, slow down your app, and waste bandwidth for users who may never scroll to see them all.</p>
<h3 id="heading-the-recipe-card-component"><strong>The Recipe Card Component</strong></h3>
<p>Here is a <code>LazyRecipe</code> component that uses our <code>useInViewClass</code> hook to create a smooth, professional loading experience:</p>
<pre><code class="lang-csharp">import { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
import { BlurhashCanvas } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-blurhash"</span>;
import { useInViewClass } <span class="hljs-keyword">from</span> <span class="hljs-string">"./useViewClass"</span>;

<span class="hljs-function">export <span class="hljs-keyword">default</span> function <span class="hljs-title">LazyRecipe</span>(<span class="hljs-params">{ recipe }</span>)</span> {
 <span class="hljs-keyword">const</span> [imageLoading, setImageLoading] = useState(<span class="hljs-literal">true</span>);
 <span class="hljs-keyword">const</span> <span class="hljs-keyword">ref</span> = useInViewClass(); <span class="hljs-comment">// Observe this card's visibility</span>

 <span class="hljs-keyword">const</span> { 
 Image_4_3_BlurHash, 
 imageUrl, 
 Short_Title, 
 imageWidth, 
 imageHeight 
 } = recipe;

 <span class="hljs-keyword">return</span> (
 &lt;div className=<span class="hljs-string">"relative h-48 w-full overflow-hidden"</span> <span class="hljs-keyword">ref</span>={<span class="hljs-keyword">ref</span>}&gt;
 {<span class="hljs-comment">/* 1. BlurHash Placeholder */</span>}
 {imageLoading &amp;&amp; (
 &lt;BlurhashCanvas
 hash={Image_4_3_BlurHash}
 width={imageWidth}
 height={imageHeight}
 punch={<span class="hljs-number">1</span>} <span class="hljs-comment">// Increases contrast</span>
 className=<span class="hljs-string">"absolute inset-0 h-full w-full object-cover"</span>
 style={{
 transition: <span class="hljs-string">"opacity 1.2s ease-out"</span>,
 willChange: <span class="hljs-string">"transform, opacity"</span>,
 }}
 /&gt;
 )}

 {<span class="hljs-comment">/* 2. The Actual Image */</span>}
 {imageUrl &amp;&amp; (
 &lt;img
 src={imageUrl}
 alt={Short_Title}
 onLoad={() =&gt; setImageLoading(<span class="hljs-literal">false</span>)} <span class="hljs-comment">// Hide placeholder when image loads</span>
 className={`absolute inset<span class="hljs-number">-0</span> h-full w-full <span class="hljs-keyword">object</span>-cover transition-all duration<span class="hljs-number">-700</span> ease-<span class="hljs-keyword">out</span> ${
 imageLoading
 ? <span class="hljs-string">"scale-105 opacity-60 blur-md"</span> <span class="hljs-comment">// Slightly zoomed and blurred while loading</span>
 : <span class="hljs-string">"scale-100 opacity-100 blur-0"</span> <span class="hljs-comment">// Crisp and full size when loaded</span>
 }`}
 loading=<span class="hljs-string">"lazy"</span>
 decoding=<span class="hljs-string">"async"</span>
 fetchPriority=<span class="hljs-string">"low"</span>
 sizes=<span class="hljs-string">"(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"</span>
 style={{
 willChange: <span class="hljs-string">"transform, opacity, filter"</span>,
 }}
 /&gt;
 )}
 &lt;/div&gt;
 );
}
</code></pre>
<p><strong>How It Works:</strong></p>
<ol>
<li><p><code>useInViewClass</code> <strong>Hook:</strong> The <code>ref</code> from our custom hook is attached to the card's container. When the card enters the viewport, the <code>show</code> class is added, triggering any CSS animations (e.g., a fade-in).</p>
</li>
<li><p><strong>BlurHash Placeholder:</strong> While the high-resolution image is loading, a <code>BlurhashCanvas</code> displays a compact, low-bandwidth representation of the image. This provides immediate visual feedback and prevents layout shifts.</p>
</li>
<li><p><strong>Image Loading State:</strong> The <code>useState</code> hook manages the <code>imageLoading</code> state. The <code>onLoad</code> event of the <code>&lt;img&gt;</code> tag triggers <code>setImageLoading(false)</code>, which removes the BlurHash and reveals the final image.</p>
</li>
<li><p><strong>Smooth Transitions:</strong> CSS classes and <code>will-change</code> are used to create a beautiful transition from the blurred placeholder to the sharp final image, enhancing the perceived performance.</p>
</li>
</ol>
<p>This component is a prime example of combining the Intersection Observer API with modern web techniques to build a fast, user-friendly, and visually appealing application.</p>
<h3 id="heading-the-recipes-list-our-data-source"><strong>The Recipes List: Our Data Source</strong></h3>
<p><code>RECIPES</code> is powered by a static array of recipe data, containing objects, each representing a single recipe with the following properties:</p>
<ul>
<li><p><code>Short_Title</code>: The name of the dish.</p>
</li>
<li><p><code>imageWidth</code> &amp; <code>imageHeight</code>: The dimensions of the image.</p>
</li>
<li><p><code>Image_4_3_BlurHash</code>: A compact string representation of the image's placeholder (BlurHash).</p>
</li>
<li><p><code>imageUrl</code>: The URL to the high-resolution image.</p>
</li>
</ul>
<p>This structure allows the <code>LazyRecipe</code> component to know exactly what placeholder to show and what image to load. The consistent use of BlurHash strings ensures that every card has a fast-loading, visually representative preview, creating a cohesive and high-performance user experience across the entire recipes' gallery.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> RECIPES = [
 {
 Short_Title: <span class="hljs-string">"Pasta Delight"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1600891964599-f61ba0e24092?w=500&amp;auto=format&amp;fit=crop&amp;q=60"</span>,
 },

 {
 Short_Title: <span class="hljs-string">"Avocado Toast"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1551183053-bf91a1d81141?w=500&amp;auto=format&amp;fit=crop&amp;q=60"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Steak Perfection"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1543353071-873f17a7a088?w=500&amp;auto=format&amp;fit=crop&amp;q=60"</span>,
 },

 {
 Short_Title: <span class="hljs-string">"Pasta Delight"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1511690656952-34342bb7c2f2?q=80&amp;w=764&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Fresh Salad"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LKO2?U%2Tw=^]-;C,ogD9ZNH$j[}"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1555939594-58d7cb561ad1?q=80&amp;w=687&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Fruit Bowl"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LGF5]+Yk^6#M@-5c,1J5@[or[Q6."</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1565299624946-b28f40a0ae38?q=80&amp;w=781&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Grilled Chicken"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1484723091739-30a097e8f929?q=80&amp;w=749&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Burger Stack"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://plus.unsplash.com/premium_photo-1663858367001-89e5c92d1e0e?q=80&amp;w=715&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Berry Smoothie"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1504754524776-8f4f37790ca0?q=80&amp;w=1170&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Avocado Toast"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1432139555190-58524dae6a55?q=80&amp;w=1176&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
 {
 Short_Title: <span class="hljs-string">"Steak Perfection"</span>,
 imageWidth: <span class="hljs-number">500</span>,
 imageHeight: <span class="hljs-number">300</span>,
 Image_4_3_BlurHash: <span class="hljs-string">"LEHV6nWB2yk8pyo0adR*.7kCMdnj"</span>,
 imageUrl:
 <span class="hljs-string">"https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?q=80&amp;w=687&amp;auto=format&amp;fit=crop&amp;ixlib=rb-4.1.0&amp;ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"</span>,
 },
];

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> RECIPES;
</code></pre>
<h3 id="heading-the-entry-point-bringing-it-all-together"><strong>The Entry Point: Bringing It All Together</strong></h3>
<p>The final piece of our application is the <code>App</code> component, which serves as the entry point and orchestrates all the parts we've built. It imports the <code>RECIPES</code> data and the <code>LazyRecipe</code> component, then renders a responsive grid of recipe cards.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> LazyRecipe <span class="hljs-keyword">from</span> <span class="hljs-string">"./lazy-recipe"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./styles.css"</span>;
<span class="hljs-keyword">import</span> RECIPES <span class="hljs-keyword">from</span> <span class="hljs-string">"./RECIPES"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
 <span class="hljs-keyword">return</span> (
 <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"App p-4 space-y-6"</span>&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-2xl font-bold"</span>&gt;</span>Lazy Image<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
 <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-lg text-gray-600"</span>&gt;</span>Lazy image loading<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>

 <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"grid gap-4 sm:grid-cols-2 lg:grid-cols-3"</span>&gt;</span>
 {RECIPES.map((recipe, index) =&gt; (
 <span class="hljs-tag">&lt;<span class="hljs-name">LazyRecipe</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{index}</span> <span class="hljs-attr">recipe</span>=<span class="hljs-string">{recipe}</span> /&gt;</span>
 ))}
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
 );
}
</code></pre>
<hr />
<h2 id="heading-live-demo-amp-source-code"><strong>Live Demo &amp; Source Code</strong></h2>
<p>Live demo: <a target="_blank" href="https://s2lwx4.csb.app/">https://s2lwx4.csb.app/</a></p>
<p>Source code: <a target="_blank" href="https://github.com/adelpro/react-lazy-images-with-blurhash">https://github.com/adelpro/react-lazy-images-with-blurhash</a></p>
<hr />
<h2 id="heading-conclusion-mastering-performance-with-native-apis"><strong>Conclusion: Mastering Performance with Native APIs</strong></h2>
<p>The Intersection Observer API is a powerful tool for building performant web applications. As shown in the <a target="_blank" href="https://peerlist.io/adelpro/articles/javascript-intersection-observer-api-master-animations--opti">First guide</a> , it allows you to detect when elements enter the viewport—enabling lazy loading, infinite scroll, and scroll animations—without causing jank or layout thrashing.</p>
<p>By integrating this native API into React with a custom hook, we can create smooth, efficient experiences like our lazy-loading recipe gallery. The key is combining the Observer's efficiency with techniques like BlurHash to provide instant visual feedback.</p>
<p>Remember to <strong>prevent memory leaks</strong> by using <code>unobserve()</code> or <code>disconnect()</code> when elements are no longer needed. This simple practice ensures your app stays fast and responsive.</p>
<p>This pattern—using native APIs within React—is a powerful way to solve complex frontend challenges. In the next article, we'll explore more ways to optimize performance for heavy DOM loads and other resources.</p>
<hr />
<p><strong>What's Next: Implementing Infinite Scroll</strong></p>
<p>In this article, we focused on lazy loading individual images as they come into view. In the next part of this series, we'll build upon this foundation to implement <strong>infinite scroll</strong>. We'll learn how to use a "sentinel" element at the end of our list, observe it with the Intersection Observer, and automatically load the next batch of recipes when it enters the viewport, creating a seamless, endless browsing experience.</p>
<hr />
<p>If you’d like a ready-to-use version with these optimizations already applied, you can find it here → Next.js Starter <a target="_blank" href="https://gumroad.adelpro.us.kg/l/aehleb">Me Portfolio on Gumroad</a>.</p>
<hr />
<p>Cover image credit: <a target="_blank" href="https://pixabay.com/users/stocksnap-894430/?utm_source=link-attribution&amp;utm_medium=referral&amp;utm_campaign=image&amp;utm_content=2558279">StockSnap</a></p>
]]></content:encoded></item><item><title><![CDATA[JavaScript Intersection Observer API: Master Scroll-Triggered Animations (Step-by-Step Guide)]]></title><description><![CDATA[Introduction: The Problem with Scroll Events & The Rise of Intersection Observer
For years, developers relied on the window.onscroll event to detect when elements entered the viewport, enabling features like lazy loading and scroll animations. Howeve...]]></description><link>https://letscode.adelpro.us.kg/javascript-intersection-observer-api-master-scroll-triggered-animations-step-by-step-guide</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/javascript-intersection-observer-api-master-scroll-triggered-animations-step-by-step-guide</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[UI]]></category><category><![CDATA[UX]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Thu, 11 Sep 2025 18:53:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757616710236/2c2c8308-6900-4b21-b5ed-38c2d3be942c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-the-problem-with-scroll-events-amp-the-rise-of-intersection-observer"><strong>Introduction: The Problem with Scroll Events &amp; The Rise of Intersection Observer</strong></h2>
<p>For years, developers relied on the <code>window.onscroll</code> event to detect when elements entered the viewport, enabling features like lazy loading and scroll animations. However, this approach is a major performance killer. Since the <code>scroll</code> event fires constantly, any DOM queries inside its listener (like <code>getBoundingClientRect()</code>) trigger <strong>layout thrashing</strong>, blocking the main thread and causing janky, unresponsive scrolling.</p>
<p>The <strong>Intersection Observer API</strong> solves this problem with an elegant, asynchronous solution. Instead of actively checking positions, you create an observer that <em>passively</em> watches your elements. The browser efficiently handles all intersection calculations in the background and only calls your callback when an element's visibility changes, keeping the main thread free for smooth interactions.</p>
<p>This makes it the ideal tool for performance-critical tasks like:</p>
<ul>
<li><p><strong>Lazy Loading</strong> images and videos.</p>
</li>
<li><p><strong>Scroll-Triggered Animations</strong> (e.g., "reveal" effects).</p>
</li>
<li><p><strong>Infinite Scrolling</strong>.</p>
</li>
<li><p><strong>Ad Impression Tracking</strong>.</p>
</li>
</ul>
<p>With excellent browser support (over 95% globally), the Intersection Observer API is a modern, native JavaScript essential for building fast, engaging web experiences. Let's dive into how it works with a practical example. (source: <a target="_blank" href="https://caniuse.com/mdn-api_intersectionobserver">caniuse</a>)</p>
<hr />
<p>This is part (1) of my series. You can read the next part (2) here → [<a target="_blank" href="https://peerlist.io/adelpro/articles/how-to-use-the-intersection-observer-api-in-react-lazy-load-">link</a>].</p>
<hr />
<h2 id="heading-how-the-intersection-observer-api-works-core-concepts"><strong>How the Intersection Observer API Works: Core Concepts</strong></h2>
<p>Now that we understand <em>why</em> the Intersection Observer API exists—to replace janky scroll listeners with a performant, asynchronous alternative—let's break down <em>how</em> it actually works.</p>
<p>The API revolves around three key components: the <strong>Observer</strong>, the <strong>Target Element</strong>, and the <strong>Callback Function</strong>.</p>
<p>You create an observer by instantiating a new <code>IntersectionObserver</code> object. Its constructor takes two arguments:</p>
<ol>
<li><p>A <strong>callback function</strong> that fires whenever the visibility of a target element changes.</p>
</li>
<li><p>An optional <code>options</code> <strong>object</strong> to configure <em>when</em> the callback should fire.</p>
</li>
</ol>
<p>The callback function receives two parameters:</p>
<ul>
<li><p><code>entries</code>: An array of <code>IntersectionObserverEntry</code> objects, each representing a target element being observed.</p>
</li>
<li><p><code>observer</code>: A reference to the <code>IntersectionObserver</code> instance itself (useful for cleanup).</p>
</li>
</ul>
<p>The <code>IntersectionObserverEntry</code> object contains all the data you need about the intersection state. The most important properties are:</p>
<ul>
<li><p><code>isIntersecting</code>: A boolean that is <code>true</code> when the target element is intersecting with the root (viewport).</p>
</li>
<li><p><code>intersectionRatio</code>: A number between <code>0</code> and <code>1</code> representing the percentage of the target element that is visible (e.g., <code>0.5</code> means 50% visible).</p>
</li>
</ul>
<hr />
<h2 id="heading-building-scroll-triggered-animations-a-real-world-example"><strong>Building Scroll-Triggered Animations: A Real-World Example</strong></h2>
<p>Scroll-triggered animations, often called "scroll-reveal" effects, are one of the most visually impactful uses of the Intersection Observer API. They create a dynamic and engaging user experience by animating content as it enters the viewport. The key to doing this efficiently is to separate concerns: use <strong>JavaScript</strong> to detect visibility, and <strong>CSS</strong> to handle the animation itself. This ensures smooth, 60fps performance.</p>
<p>Let's break down a real-world example based directly on this <a target="_blank" href="https://codesandbox.io/p/sandbox/intersection-observer-api-example-y4riim">CodeSandbox</a> project. This code smoothly animates cards into view as the user scrolls down the page.</p>
<h3 id="heading-how-it-works-the-html-amp-css-foundation"><strong>How It Works: The HTML &amp; CSS Foundation</strong></h3>
<p>The magic starts with CSS. We define two classes:</p>
<ul>
<li><p><code>.hidden</code>: The initial, "out-of-view" state of the element.</p>
</li>
<li><p><code>.show</code>: The final, "revealed" state of the element.</p>
</li>
</ul>
<pre><code class="lang-css"><span class="hljs-comment">/* Initial state: Hidden and off-screen */</span>
<span class="hljs-selector-class">.hidden</span> {
 <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
 <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(-<span class="hljs-number">100%</span>);
 <span class="hljs-attribute">filter</span>: <span class="hljs-built_in">blur</span>(<span class="hljs-number">5px</span>);
 <span class="hljs-attribute">transition</span>: all <span class="hljs-number">1s</span> ease-out; <span class="hljs-comment">/* Smooth transition for all properties */</span>
}

<span class="hljs-comment">/* Optional: Respect user preferences for reduced motion */</span>
<span class="hljs-keyword">@media</span> (prefers-reduced-motion) {
 <span class="hljs-selector-class">.hidden</span> {
 <span class="hljs-attribute">transition</span>: none;
 }
}

<span class="hljs-comment">/* Final state: Fully visible and in place */</span>
<span class="hljs-selector-class">.show</span> {
 <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
 <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">0</span>);
 <span class="hljs-attribute">filter</span>: <span class="hljs-built_in">blur</span>(<span class="hljs-number">0</span>);
}
</code></pre>
<p>The <code>transition</code> property on <code>.hidden</code> is crucial. It tells the browser to animate any changes to <code>opacity</code>, <code>transform</code>, and <code>filter</code> over one second. This means when we add the <code>.show</code> class, the element will glide into place.</p>
<h3 id="heading-the-javascript-logic-detecting-visibility"><strong>The JavaScript Logic: Detecting Visibility</strong></h3>
<p>The JavaScript uses the Intersection Observer to listen for when an element becomes visible and then applies the <code>.show</code> class.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// 1. Select all elements you want to animate (e.g., cards with class '.hidden')</span>
<span class="hljs-keyword">const</span> hiddenElements = <span class="hljs-built_in">document</span>.querySelectorAll(<span class="hljs-string">".hidden"</span>);

<span class="hljs-comment">// 2. Create the Intersection Observer</span>
<span class="hljs-keyword">const</span> observer = <span class="hljs-keyword">new</span> IntersectionObserver(
 <span class="hljs-function">(<span class="hljs-params">entries</span>) =&gt;</span> {
 entries.forEach(<span class="hljs-function">(<span class="hljs-params">entry</span>) =&gt;</span> {
 <span class="hljs-comment">// 3. Check if the element is currently intersecting with the viewport</span>
 <span class="hljs-keyword">if</span> (entry.isIntersecting) {
 <span class="hljs-comment">// 4. Add the 'show' class to trigger the CSS animation</span>
 entry.target.classList.add(<span class="hljs-string">"show"</span>);

 <span class="hljs-comment">// 5. (Optional) Stop observing after the animation starts</span>
 <span class="hljs-comment">// This prevents the animation from re-triggering and saves resources</span>
 <span class="hljs-comment">// observer.unobserve(entry.target);</span>
 }
 <span class="hljs-comment">// Note: Removing the 'show' class on exit is often unnecessary for one-time reveals</span>
 });
 },
 {
 <span class="hljs-comment">// Trigger the callback when 50% of the element is visible</span>
 <span class="hljs-attr">threshold</span>: <span class="hljs-number">0.5</span>
 <span class="hljs-comment">// rootMargin: '0px' // You can also use this to trigger earlier or later</span>
 }
);

<span class="hljs-comment">// 6. Start observing every element in the 'hiddenElements' collection</span>
hiddenElements.forEach(<span class="hljs-function">(<span class="hljs-params">el</span>) =&gt;</span> {
 observer.observe(el);
});
</code></pre>
<p><strong>Key Points of This Real-World Implementation:</strong></p>
<ul>
<li><p><strong>Performance First:</strong> The animation runs on the GPU (thanks to <code>transform</code> and <code>opacity</code>), ensuring it doesn't block the main thread.</p>
</li>
<li><p><strong>User-Centric:</strong> The <code>prefers-reduced-motion</code> media query respects users who have requested less animation.</p>
</li>
<li><p><strong>Configurable:</strong> The <code>threshold: 0.5</code> means the animation starts when half of the card is visible, creating a natural feel. You can adjust this to <code>0</code> (on first pixel) or <code>1</code> (only when fully visible). You can also fine-tune <em>when</em> the animation starts by using the <code>rootMargin</code> option. Think of <code>rootMargin</code> as padding around the viewport. By setting a positive margin, like <code>{ rootMargin: '100px' }</code>, you tell the observer to trigger the callback when the target element is still 100 pixels <em>away</em> from entering the viewport. This is perfect for pre-loading content or starting an animation just before the user sees it, ensuring a seamless experience.</p>
</li>
<li><p><strong>Scalable:</strong> A single observer watches multiple elements, making it efficient for long pages with many animated sections.</p>
</li>
</ul>
<p>This pattern, as demonstrated in your CodeSandbox, is a professional standard for creating smooth, performant scroll animations on modern websites.</p>
<hr />
<h2 id="heading-live-demo-amp-source-code"><strong>Live Demo &amp; Source Code</strong></h2>
<p>Live demo: <a target="_blank" href="https://y4riim.csb.app/">https://y4riim.csb.app/</a></p>
<p>Source code: <a target="_blank" href="https://github.com/adelpro/intersection-observer-reveal-cards">https://github.com/adelpro/intersection-observer-reveal-cards</a></p>
<hr />
<h2 id="heading-whats-next-master-lazy-loading-amp-performance"><strong>What's Next: Master Lazy Loading &amp; Performance</strong></h2>
<p>In this article, we've focused on using the Intersection Observer API to create smooth, performant <strong>scroll-triggered animations</strong>.</p>
<p>In the <strong>next article of this series</strong>, we'll shift gears to <strong>performance optimization</strong>. We'll dive into <strong>lazy loading images and videos</strong>, implementing <strong>infinite scroll</strong>, and managing <strong>heavy DOM loads</strong>—all with step-by-step examples.</p>
<p>Finally, we'll wrap up the series by bringing it all into the modern world of React and Next.js.</p>
<p>👉 Continue with [<a target="_blank" href="https://peerlist.io/adelpro/articles/how-to-use-the-intersection-observer-api-in-react-lazy-load-">How to Boost React App Performance ...</a>] to see the full results.</p>
<hr />
<p>Credit: Cover image by <a target="_blank" href="https://pixabay.com/users/real-napster-323187/">real-napser</a></p>
]]></content:encoded></item><item><title><![CDATA[Next.js Performance Optimization: How I Boosted My Google PageSpeed Insights Score from Orange to Green Using inlineCss]]></title><description><![CDATA[If you’ve been struggling to push your Next.js app from the dreaded orange zone (80s) into the green zone (90+) on Google PageSpeed Insights, you’re not alone.
I tried every common performance tweak:

Optimizing next.config.js

Profiling heavy compon...]]></description><link>https://letscode.adelpro.us.kg/nextjs-performance-optimization-how-i-boosted-my-google-pagespeed-insights-score-from-orange-to-green-using-inlinecss</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/nextjs-performance-optimization-how-i-boosted-my-google-pagespeed-insights-score-from-orange-to-green-using-inlinecss</guid><category><![CDATA[Next.js]]></category><category><![CDATA[devtools]]></category><category><![CDATA[React]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Thu, 11 Sep 2025 18:49:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757616161256/cf027bb5-8e81-45cb-b766-4ba28a7afbbb.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you’ve been struggling to push your <strong>Next.js app</strong> from the dreaded <strong>orange zone (80s)</strong> into the <strong>green zone (90+)</strong> on <strong>Google PageSpeed Insights</strong>, you’re not alone.</p>
<p>I tried every common performance tweak:</p>
<ul>
<li><p>Optimizing <strong>next.config.js</strong></p>
</li>
<li><p>Profiling heavy components and splitting code</p>
</li>
<li><p>Using <code>dynamic()</code> with <code>suspense</code> for lazy loading</p>
</li>
<li><p>Applying <strong>Next.js Image Optimization</strong></p>
</li>
</ul>
<p>And still, my app hovered around <strong>82/88 performance score</strong>.</p>
<p>The real breakthrough came when I discovered the <strong>experimental</strong> <code>inlineCss</code> <strong>feature</strong> in Next.js.</p>
<h2 id="heading-what-is-inlinecss-in-nextjs">What is <code>inlineCss</code> in Next.js?</h2>
<p>According to the <a target="_blank" href="https://nextjs.org/docs/app/api-reference/config/next-config-js/inlineCss">Next.js documentation</a></p>
<ul>
<li><p><strong>Definition</strong>: <code>inlineCss</code> inlines CSS directly into the rendered HTML instead of linking to external <code>.css</code> files.</p>
</li>
<li><p><strong>Benefit</strong>: Eliminates extra network requests for CSS, which reduces <strong>render-blocking resources</strong>.</p>
</li>
<li><p><strong>Performance Gains</strong>: Improves <strong>First Contentful Paint (FCP)</strong> and <strong>Largest Contentful Paint (LCP)</strong> — two critical <strong>Core Web Vitals</strong>.</p>
</li>
<li><p><strong>Best Use Cases</strong>:</p>
<ul>
<li><p>Small-to-medium apps (e.g., portfolios, landing pages, dashboards)</p>
</li>
<li><p>Projects where <strong>fast initial render</strong> is more important than keeping HTML payload smaller</p>
</li>
</ul>
</li>
<li><p><strong>Caution</strong>: For apps with huge global stylesheets, HTML size may increase.</p>
</li>
</ul>
<h2 id="heading-how-to-enable-inlinecss-in-nextjs">How to Enable <code>inlineCss</code> in Next.js</h2>
<p>Add the following to your <code>next.config.js</code> (or <code>next.config.ts</code>):</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// next.config.js</span>
<span class="hljs-keyword">const</span> nextConfig = {
 <span class="hljs-attr">experimental</span>: {
 <span class="hljs-attr">inlineCss</span>: <span class="hljs-literal">true</span>,
 },
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> nextConfig
</code></pre>
<p>After enabling <code>inlineCss</code>, my Next.js app finally broke through into the <strong>green zone (90+ score)</strong> on Google PageSpeed Insights 🎉.</p>
<p><strong>Before:</strong></p>
<ul>
<li><p>Score: 88–89 (orange)</p>
</li>
<li><p>FCP/LCP: slowed by CSS request</p>
</li>
</ul>
<p><strong>After:</strong></p>
<ul>
<li><p>Score: 90+ (green)</p>
</li>
<li><p>Faster <strong>First Contentful Paint (FCP)</strong></p>
</li>
<li><p>Improved <strong>Largest Contentful Paint (LCP)</strong></p>
</li>
<li><p>More stable rendering, reduced layout shift<br />  After enabling this, my app jumped into the <strong>green zone (90+)</strong>.</p>
</li>
</ul>
<h2 id="heading-why-this-matters">Why This Matters</h2>
<p>For developers aiming to <strong>improve Next.js performance</strong> and <strong>optimize Core Web Vitals</strong>, CSS delivery is often an overlooked bottleneck.</p>
<p>My application: <a target="_blank" href="https://me-portfolio.adelpro.us.kg">Me Portfolio</a>, get if from <a target="_blank" href="https://gumroad.adelpro.us.kg/l/aehleb?layout=profile">Gumroad</a></p>
<p><img src="https://dqy38fnwh4fqs.cloudfront.net/UHNNB8PRR8QMGOAIN98L7BPMP667/blog/blog-content-image-0cb26534-f012-4875-b847-18c5fcf6a117.webp" alt /></p>
<h3 id="heading-key-takeaways">Key Takeaways</h3>
<ol>
<li><p>Don’t stop at image optimization and dynamic imports.</p>
</li>
<li><p>Test <strong>experimental features</strong>—sometimes they solve the exact bottleneck.</p>
</li>
<li><p><code>inlineCss</code> is especially powerful for smaller apps, where CSS size isn’t huge.</p>
</li>
<li><p>Always re-test with <strong>Google PageSpeed Insights</strong> after each change.</p>
</li>
</ol>
<p>Sometimes it’s not about piling more optimizations, but finding that one bottleneck and in my case, it was CSS delivery.</p>
]]></content:encoded></item><item><title><![CDATA[Need help debugging decentralized Quran audio streaming with WebTorrent]]></title><description><![CDATA[I'm working on a new decentralized audio streaming app using WebTorrent to deliver Quran audio without relying on centralized servers. For this, I’ve built two open-source projects:
Open Quran
https://github.com/adelpro/open-quran
 
An open-source Qu...]]></description><link>https://letscode.adelpro.us.kg/need-help-debugging-decentralized-quran-audio-streaming-with-webtorrent</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/need-help-debugging-decentralized-quran-audio-streaming-with-webtorrent</guid><category><![CDATA[Quran]]></category><category><![CDATA[Torrent]]></category><category><![CDATA[webtorrent]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Tue, 10 Jun 2025 10:57:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1749554176722/ef20ab90-73de-46af-a317-e4ce29650b9c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I'm working on a new decentralized audio streaming app using WebTorrent to deliver Quran audio without relying on centralized servers. For this, I’ve built two open-source projects:</p>
<h2 id="heading-open-quran"><strong>Open Quran</strong></h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/adelpro/open-quran">https://github.com/adelpro/open-quran</a></div>
<p> </p>
<p>An open-source Quran app built with <strong>Next.js</strong> and hosted on <strong>Vercel</strong>. It’s designed to provide an optimal Quran audio streaming experience across platforms. Audio content is distributed via <strong>WebTorrent in the browser</strong>, meaning it relies on <strong>WebRTC peers</strong> or <strong>web seeds</strong>—traditional TCP/UDP torrent seeds are not supported.</p>
<h2 id="heading-open-quran-tracker-amp-seeder"><strong>Open Quran Tracker &amp; Seeder</strong></h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/adelpro/open-quran-tracker">https://github.com/adelpro/open-quran-tracker</a></div>
<p> </p>
<p>This repo includes <strong>two Dockerized services</strong>:</p>
<ol>
<li><p><strong>Tracker</strong>: A self-hosted WebTorrent tracker acting as a private WebRTC bridge to connect browser peers.</p>
</li>
<li><p><strong>Seeder</strong>: A torrent seeder that continuously seeds all Quran audio files to speed up availability and streaming.</p>
</li>
</ol>
<h2 id="heading-the-issue"><strong>The issue</strong></h2>
<p>While streaming works perfectly with public torrents like the Ubuntu ISO (plenty of seeds), Quran audio streaming fails or stalls. I suspect it's due to <strong>insufficient WebRTC-compatible peers or missing web seeds</strong>. Since WebTorrent in the browser can't connect to traditional torrent clients, the network needs more compatible peers (like other browsers or my custom seeder).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749552951712/216dc556-64e0-4d52-ac09-f274efc6c574.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-how-you-can-help"><strong>How you can help</strong></h2>
<ul>
<li><p>Test the <a target="_blank" href="https://github.com/adelpro/open-quran">Open Quran app</a></p>
</li>
<li><p>Check if the torrents load for you</p>
</li>
<li><p>Suggest improvements for peer seeding or WebRTC connectivity</p>
</li>
<li><p>Help debug the seeder or tracker setup</p>
</li>
</ul>
<p>Any help from those familiar with WebTorrent, browser P2P, or decentralized distribution is appreciated. Let’s push forward a censorship-resistant and scalable way to share Islamic content.</p>
<p>Thanks in advance!</p>
]]></content:encoded></item><item><title><![CDATA[Blazing Fast: How a Go-Powered TypeScript Compiler Slashed Our Build Times by 88%]]></title><description><![CDATA[In modern web and mobile development, TypeScript has become essential for building strong, scalable applications. Its strong typing and tools offer great benefits, but as projects grow, a common issue arises: build times. For large repositories, the ...]]></description><link>https://letscode.adelpro.us.kg/blazing-fast-how-a-go-powered-typescript-compiler-slashed-our-build-times-by-88</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/blazing-fast-how-a-go-powered-typescript-compiler-slashed-our-build-times-by-88</guid><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Tue, 10 Jun 2025 10:51:58 GMT</pubDate><content:encoded><![CDATA[<p>In modern web and mobile development, TypeScript has become essential for building strong, scalable applications. Its strong typing and tools offer great benefits, but as projects grow, a common issue arises: build times. For large repositories, the time spent waiting for the compiler and bundler can add up, greatly affecting developer productivity and speed. This was exactly the challenge we faced with Open-Mushaf Native, our open-source Quran application built with Expo and React Native.</p>
<p>But what if there was a way to use the power of TypeScript without long waits? We recently tried a Go-based TypeScript compiler, and the results have been truly transformative.</p>
<h3 id="heading-the-challenge-taming-a-large-typescript-project">The Challenge: Taming a Large TypeScript Project</h3>
<p>Open-Mushaf Native ( <a target="_blank" href="https://github.com/adelpro/open-mushaf-native">https://github.com/adelpro/open-mushaf-native</a> ) is a feature-rich application. As it evolved, so did its codebase. With a significant number of modules (for instance, a key bundle like expo-router/node/render.js alone comprises 2735 modules), our development server startup times using yarn start were becoming a bottleneck.</p>
<p>Our typical startup sequence with the standard TypeScript tooling looked like this:</p>
<ul>
<li>Standard TypeScript yarn start duration: 351.19 seconds Over five minutes to get the development server ready is a considerable interruption to a developer's flow state. We knew we needed a faster alternative if we wanted to maintain agility.</li>
</ul>
<h3 id="heading-the-typescript-go-experiment-a-leap-of-faith">The 'TypeScript-Go' Experiment: A Leap of Faith</h3>
<p>Intrigued by the potential performance gains from compilers written in languages like Go, known for their speed and efficiency, we decided to integrate and test a Go-based TypeScript compiler within our existing Expo (managed workflow) setup. The goal was simple: to see if it could significantly reduce our yarn start times without requiring a massive overhaul of our existing TypeScript codebase, which adheres to strict mode and functional component patterns.</p>
<h3 id="heading-the-results-a-staggering-8836-speed-boost">The Results: A Staggering 88.36% Speed Boost!</h3>
<p>The impact was immediate and dramatic. After switching to the 'TypeScript-Go' powered workflow, our yarn start times plummeted:</p>
<ul>
<li><p>'TypeScript-Go' yarn start duration: 40.88 seconds Let's break that down:</p>
</li>
<li><p>Time Saved: 351.19s - 40.88s = 310.31 seconds</p>
</li>
<li><p>Percentage Improvement: Approximately 88.36% faster! From over five minutes to under 41 seconds – the difference is night and day. This isn't just a marginal improvement; it's a fundamental shift in our development experience. The initial bundling of core components, which previously took several minutes, now completes in a fraction of the time.</p>
</li>
</ul>
<h3 id="heading-why-this-matters-more-than-just-saved-seconds">Why This Matters: More Than Just Saved Seconds</h3>
<p>An 88% reduction in development server startup time translates to tangible benefits:</p>
<ol>
<li><p>Increased Developer Productivity: Less time waiting means more time coding, testing, and innovating.</p>
</li>
<li><p>Faster Iteration Cycles: Quick restarts encourage more frequent testing and experimentation, leading to higher quality code and faster feature delivery.</p>
</li>
<li><p>Improved Developer Experience: Reducing a common frustration point makes the development process more enjoyable and less prone to context switching.</p>
</li>
<li><p>Onboarding New Developers: Faster setup times can make it easier for new team members to get up and running. For a project like Open-Mushaf Native, which relies on community contributions and active development, these improvements are invaluable.</p>
</li>
</ol>
<h3 id="heading-conclusion-the-future-is-fast">Conclusion: The Future is Fast</h3>
<p>Our experience with integrating a Go-based TypeScript compiler into the Open-Mushaf Native project has been overwhelmingly positive. An 88.36% reduction in our yarn start times is a testament to the potential of leveraging high-performance languages for critical developer tooling.</p>
<p>While "TypeScript-Go" is our internal moniker for the specific setup we're testing, this experiment highlights a broader trend: the quest for faster, more efficient development tools is relentless. As TypeScript projects continue to grow in scale and complexity, innovations in compiler technology will be crucial for maintaining developer velocity.</p>
<p>If you're grappling with slow build times in your TypeScript projects, exploring alternative compilers or tooling could unlock significant performance gains. For Open-Mushaf Native, this shift has been a game-changer, allowing us to focus more on building a great app and less on waiting for builds.</p>
<p>You can check out the Open-Mushaf Native project on GitHub: <a target="_blank" href="https://github.com/adelpro/open-mushaf-native">https://github.com/adelpro/open-mushaf-native</a></p>
<p>#TypeScript #TSGo #DevTools #Performance #Expo #ReactNative #OpenSource #BuildTimes #DeveloperExperience</p>
]]></content:encoded></item><item><title><![CDATA[Trae: Introducing MCP Server Integration With Practical Examples]]></title><description><![CDATA[ByteDance’s AI IDE Trae version 1.3.0 (April 2025) adds full support for MCP servers, embracing the new Model Context Protocol (MCP) standard for connecting LLMs to external data and tools​.
MCP is an open, client-server protocol designed to break do...]]></description><link>https://letscode.adelpro.us.kg/trae-introducing-mcp-server-integration-with-practical-examples</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/trae-introducing-mcp-server-integration-with-practical-examples</guid><category><![CDATA[trae]]></category><category><![CDATA[mcp]]></category><category><![CDATA[mcp server]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Fri, 25 Apr 2025 20:25:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1745612639353/c13f6524-24a9-4bf5-8d68-4f73929654ba.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://www.trae.ai/">ByteDance’s AI IDE Trae</a> version 1.3.0 (April 2025) adds full support for MCP servers, embracing the new Model Context Protocol (MCP) standard for connecting LLMs to external data and tools​.</p>
<p><a target="_blank" href="https://www.anthropic.com/news/model-context-protocol#:~:text=MCP%20addresses%20this%20challenge,to%20the%20data%20they%20need">MCP</a> is an open, client-server protocol designed to break down data silos: it provides a universal interface so that AI models can query databases, code repos, APIs, and other systems via lightweight “servers”​, <a target="_blank" href="https://modelcontextprotocol.io/introduction#:~:text=MCP%20is%20an%20open%20protocol,different%20data%20sources%20and%20tools"></a>In effect, MCP is like a USB-C port for AI – a standardized way for applications to plug in context and capabilities. This post explains what MCP servers do, what’s new in Trae’s MCP implementation, the technical underpinnings (performance, architecture, transports), and a hands-on example of configuring an MCP server in Trae.</p>
<h2 id="heading-what-are-mcp-servers-model-context-protocol">What Are MCP Servers (Model Context Protocol)?</h2>
<p>The Model Context Protocol (MCP) is a recently released open standard (open-sourced by Anthropic in late 2024) that <strong>standardizes how AI models access data and tools</strong>.​</p>
<p>An MCP server is a lightweight program that exposes a specific capability – for example, fetching documents from a database, running code queries, or interfacing with GitHub – over a well-defined protocol. An MCP server implements a set of "commands" or tools that an LLM can invoke. From the LLM’s perspective, all MCP servers look the same: the protocol abstracts away the details of each data source.</p>
<p>MCP solves the problem of fragmented, one-off integrations. Traditionally, each AI assistant or agent needs a custom connector (for Slack, for Google Drive, for company APIs, etc.). MCP replaces that with a single <strong>client-server architecture</strong>: a host application (like Trae) connects to one or more MCP servers, and the servers connect to local or remote data sources​.</p>
<p>Key benefits of MCP include:</p>
<ul>
<li><p><strong>Unified integrations</strong>: Use pre-built MCP servers (GitHub, Supabase, Postgres, etc.) or write your own, all conforming to one spec​.</p>
</li>
<li><p><strong>Flexible deployment</strong>: MCP servers can run locally or remotely over HTTP.</p>
</li>
<li><p><strong>Model-agnostic</strong>: The same MCP server can be used with different LLMs or AI assistants, so you’re not locked into one platform​.</p>
</li>
<li><p><strong>Secure and consistent</strong>: The protocol encourages security best practices (auth, origin checks) and consistent command schemas across tools​.</p>
</li>
</ul>
<p>In summary, MCP servers let an AI IDE or agent <strong>call external tools as if they were built-in functions</strong>. Trae’s new support for MCP means developers can seamlessly extend the IDE’s AI with whatever services they need.</p>
<h2 id="heading-trae-v130-new-mcp-integration">Trae v1.3.0: New MCP Integration</h2>
<p>In Trae 1.3.0, ByteDance has integrated MCP in several ways to streamline external tool access and automation. The highlights include:</p>
<ul>
<li><p><strong>Standardized MCP plugin system</strong>: Trae can now connect to any external tool via the MCP protocol. Developers can wire up services like Supabase, GitHub, custom REST APIs, or any local tool by adding it as an MCP server​, This expands Trae’s “contextual capabilities”.</p>
</li>
<li><p><strong>Project and global configuration</strong>: You define MCP servers in JSON config files. Trae supports both <strong>global</strong> and <strong>project-level</strong> settings. Global MCP servers live in a user-wide file (e.g. <code>~/.trae/mcp.json</code>), while a project can include its own <code>.trae/mcp.json</code> or <code>mcp_settings.json</code> in the root. Either way, you list servers under an <code>"mcpServers"</code> key and provide the command to run them​. This makes integration repeatable and shareable.</p>
</li>
<li><p><strong>Built-in MCP Marketplace</strong>: Trae includes a searchable <strong>MCP marketplace</strong> in the IDE, where developers can browse and add pre-made MCP server definitions (for services like GitHub, Postgres, etc.) without manual config​, This one-click deployment lowers the barrier: if you need GitHub integration, simply select the GitHub MCP server and authenticate, and Trae handles the rest.</p>
</li>
<li><p><strong>Agent and builder integration</strong>: The new Builder Agent mode in Trae can <strong>dynamically call MCP servers</strong> as part of AI-driven tasks​, For example, an automated agent can use an MCP server to fetch repo data, call external APIs, or even manipulate the filesystem. This means more complex workflows (multi-step builds, testing, deployment, etc.) can be handled by the AI.</p>
</li>
<li><p><strong>Cross-platform support</strong>: Trae’s MCP features work on <strong>macOS, Windows, and Linux</strong>​, It supports advanced LLMs like Claude 3.5 Sonnet and GPT-4o, either locally or via cloud APIs. According to the release notes, Trae’s AI engine is accelerated with Intel’s OpenVINO for on-device inference, so many LLM tasks and MCP calls run efficiently even without a GPU​.</p>
</li>
</ul>
<p>Taken together, Trae 1.3.0 treats MCP as a first-class citizen: it not only adds a configuration mechanism, but also a marketplace and UI for MCP servers, and the underlying ability for its AI agents to use any connected service.</p>
<h2 id="heading-example-1-setting-up-puppeteerhttpsgithubcommodelcontextprotocolserverstreemainsrcpuppeteer-a-build-in-mcp-server-in-trae">Example 1: Setting Up <a target="_blank" href="https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer"><strong>puppeteer</strong></a><strong>,</strong> a build in MCP Server in Trae</h2>
<p>In this example we will setup a built in MCP server: <a target="_blank" href="https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer"><strong>puppeteer</strong></a> that let`s us search the internet.</p>
<p>First go to the MCP tab and click on the ADD button:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745604684636/3e814c78-ce88-478c-abab-76af4fe91086.png" alt class="image--center mx-auto" /></p>
<p>Then search for: puppeteer</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745605805782/ff4c061c-1dff-4b0c-8a6d-03c026660f59.png" alt class="image--center mx-auto" /></p>
<p>Click on the (+) button</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745605865282/b9242776-d698-414f-a52d-0c3fa15dd9e4.png" alt class="image--center mx-auto" /></p>
<p>Then click on confirm (This MCP server needs no configuration)</p>
<p>If all goes Ok, you will see this in your MCP servers list:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745605962992/83bf61c9-29ab-41f3-a99a-153a201af56f.png" alt class="image--center mx-auto" /></p>
<p>Now to use it, i tried this prompt, after selecting the option “@Builder with MCP” from you “@Agents” list:</p>
<p>“explore <a target="_blank" href="http://quran.us.kg">quran.us.kg</a> and extract the list of quran surahs“</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745606767839/18932dd9-29f4-4e34-8ec4-815b6e7dffb5.png" alt class="image--center mx-auto" /></p>
<p>Or you can explicitly run a predefined command from the puppeteer MCP list:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745606864089/dcd34a36-35e0-4020-ab43-31859de855f1.png" alt class="image--center mx-auto" /></p>
<p>Run the prompt “/puppeteer_navigate explore <a target="_blank" href="http://quran.us.kg">quran.us.kg</a> and extract the list of quran surahs“</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745607195145/c68d1c46-e4d3-4f28-ba48-1d2d1720e1e9.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-example-2-setting-up-lighthouse-mcp-server-a-custom-mcp-server-in-trae">Example 2: Setting Up lighthouse-mcp server, a custom MCP Server in Trae</h2>
<p>As a second example, we will add <a target="_blank" href="https://github.com/priyankark/lighthouse-mcp">lighthouse-mcp server</a> to our Trae editor, first go to the MCP tab and click on the ADD button:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745604684636/3e814c78-ce88-478c-abab-76af4fe91086.png" alt class="image--center mx-auto" /></p>
<p>Then select the “Configure Manually” link.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745604750185/2f38d6c1-2794-4dfd-8f9f-76f86e810216.png" alt class="image--center mx-auto" /></p>
<p>Then click on the “Row Config (JSON)” button</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745604829128/bbc6f1e1-9f94-40f8-b143-bb1908613ab0.png" alt class="image--center mx-auto" /></p>
<p>Then past this config at the bottom of the new opened file in your editor:</p>
<pre><code class="lang-plaintext">    "lighthouse": {
      "command": "npx",
      "args": ["lighthouse-mcp"],
      "disabled": false,
      "autoApprove": []
    },
</code></pre>
<p>If all goes Ok, you will see this in the MCP servers list:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745604998493/d5d0354a-28c7-4a7a-ab37-867debc96432.png" alt class="image--center mx-auto" /></p>
<p>Now let’s see how it work, first select the option “@Builder with MCP” from you “@Agents” list, Then enter this example prompt: “analyze my lighthouse scores for <a target="_blank" href="http://quran.us.kg">quran.us.kg</a>“ Trae will use Chrome on the background and make a lighthouse test over the given website ( <a target="_blank" href="https://quran.us.kg">https://quran.us.kg</a> in my case) then will return this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1745605280991/8e739e3e-de13-47be-bc97-962b2250f058.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, the integration of MCP servers in Trae version 1.3.0 marks a significant advancement in how AI models interact with external data and tools. By adopting the Model Context Protocol, Trae provides a unified, flexible, and secure way to extend its capabilities, allowing developers to seamlessly connect to a wide range of services. The introduction of features like the MCP marketplace, standardized plugin system, and cross-platform support enhances the IDE's functionality, making it easier for developers to automate complex workflows and leverage AI-driven tasks. With practical examples like setting up the puppeteer and lighthouse-mcp servers, Trae demonstrates the practical benefits of MCP integration, empowering developers to efficiently access and utilize external resources within their projects.</p>
]]></content:encoded></item><item><title><![CDATA[Supercharge Your Coding with Trae + OpenRouter: Free Access to Powerful AI Models]]></title><description><![CDATA[What is Trae?
Trae (/treɪ/) is your helpful coding partner. It offers features like AI Q&A, code auto-completion, and agent-based AI programming capabilities. When developing projects with Trae, you can collaborate with AI to enhance your development...]]></description><link>https://letscode.adelpro.us.kg/supercharge-your-coding-with-trae-openrouter-free-access-to-powerful-ai-models</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/supercharge-your-coding-with-trae-openrouter-free-access-to-powerful-ai-models</guid><category><![CDATA[AI]]></category><category><![CDATA[Programming Tips]]></category><category><![CDATA[trae]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Wed, 09 Apr 2025 13:05:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744200545739/6123dea8-6b28-41c1-b882-00a797547f8c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-what-is-traehttpswwwtraeai">What is <a target="_blank" href="https://www.trae.ai/">Trae</a>?</h2>
<p><a target="_blank" href="https://www.trae.ai/">Trae</a> <a target="_blank" href="https://www.trae.ai/">(/tr</a>eɪ/) is your helpful coding partner. It offers features like AI Q&amp;A, code auto-completion, and agent-based AI programming capabilities. When developing projects with Trae, you can collaborate with AI to enhance your development efficiency.​  </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744200269893/3d0fd556-0c0e-4527-983a-60272827dd46.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-what-is-openrouterhttpsopenrouterai">What is <a target="_blank" href="https://openrouter.ai/">OpenRouter</a>?</h2>
<p>OpenRouter provides a unified API that gives you access to hundreds of AI models through a single endpoint, while automatically handling fallbacks and selecting the most cost-effective options. Get started with just a few lines of code using your preferred SDK or framework.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744201107875/9788e830-f615-4265-a23a-49dd89e76df4.jpeg" alt class="image--center mx-auto" /></p>
<p>With OpenRouter, we can access the most powerful AI available today for free, with no extra costs, no need for self-hosting, and no requirement for a powerful PC.  </p>
<h2 id="heading-why-combine-trae-openrouter">Why Combine Trae + OpenRouter?</h2>
<p>By integrating OpenRouter into Trae, you unlock access to cutting-edge AI models directly in your code editor—for free. No overhead, just productivity.</p>
<h2 id="heading-ia"> </h2>
<p>Step-by-Step: Use OpenRouter in Trae</p>
<ul>
<li><p><strong>Open Trae</strong>, then click on the <strong>AI Model Selection</strong> button at the bottom-right corner.</p>
</li>
<li><p>Click <strong>+ Add Model</strong>.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744201734913/81b6734d-a79d-47bf-b748-92050159ffb5.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>In the modal:</p>
<ul>
<li><p>Choose <strong>OpenRouter</strong> from the provider dropdown.</p>
</li>
<li><p>Select <strong>Other models</strong> in the second dropdown.</p>
</li>
</ul>
</li>
<li><p>Now fill in:</p>
<ul>
<li><p><strong>Model ID</strong> (choose one below).</p>
</li>
<li><p><strong>API Key</strong> (generate one <a target="_blank" href="https://openrouter.ai/settings/keys">here</a>).  </p>
</li>
</ul>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744201890544/6d8c9609-8c0f-47d0-9730-2e694b5423c8.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1744201981517/b1c9ced7-34cb-4c3f-88dc-be102a9ea98f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-recommended-free-models">Recommended Free Models</h2>
<p>You can use any model labeled as <code>free</code> on <a target="_blank" href="https://openrouter.ai/models">OpenRouter Models</a> <a target="_blank" href="https://openrouter.ai/models">or Rankings</a><a target="_blank" href="https://openrouter.ai/rankings">. Here a</a>re a <a target="_blank" href="https://openrouter.ai/models">few top pick</a><a target="_blank" href="https://openrouter.ai/rankings">s:</a></p>
<h3 id="heading-gemini-20-flash-thinking-experimentalhttpsopenrouteraigooglegemini-20-flash-exp">🧠 <a target="_blank" href="https://openrouter.ai/google/gemini-2.0-flash-exp">Gemini 2.0 Flash Thinking Experimental</a></h3>
<p><strong>ID:</strong> <code>google/gemini-2.0-flash-thinking-exp</code><a target="_blank" href="https://openrouter.ai/models"><code>:free</code>  
</a>An experimental version of Gemini 2.0 Flash with enhanced reasoning and "thinking process" generation.</p>
<h3 id="heading-gemini-25-pro">🚀 Gemini 2.5 Pro</h3>
<p><strong>ID:</strong> <code>google/gemini-2.5-pro-exp-03-25:free</code><br />Google’s most advanced AI—excels at coding, reasoning, scientific queries, and ranked #1 on the LMArena leaderboard.</p>
<h3 id="heading-llama-31-nemotron-ultra-253b-v1httpsopenrouterainvidiallama-31-nemotron-ultra-253b-v1free">🧩 <a target="_blank" href="https://openrouter.ai/nvidia/llama-3.1-nemotron-ultra-253b-v1:free">Llama-3.1-Nemotron-Ultra-253B-v1</a></h3>
<p><strong>ID:</strong> <code>nvidia/llama-3.1-nemotron-ultra-253b-v1:free</code><br />Optimized for reasoning, RAG, and tool use. Designed for large-context, low-latency inference.</p>
<p><strong>Note:</strong> Add detailed reasoning instructions in the <em>system prompt</em> to unlock full capabilities.</p>
<h3 id="heading-qwen25-vlhttpsopenrouteraiqwenqwen25-vl-72b-instructfreeapi">📊 <a target="_blank" href="https://openrouter.ai/qwen/qwen2.5-vl-72b-instruct:free/api">Qwen2.5-VL</a></h3>
<p><strong>ID:</strong> <code>qwen/qwen2.5-vl-72b-instruct:free</code><br />Great for visual tasks—recognizes objects, charts, graphics, and layouts within images.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>By connecting <strong>Trae</strong> with <strong>OpenRouter</strong>, you bring the world’s best free AI models into your editor. Whether you’re coding, debugging, or experimenting, this integration levels up your development flow—without extra cost or complexity.</p>
]]></content:encoded></item><item><title><![CDATA[Page Transitions In Next.js 13 With App Router And The Built-In View Transitions API (No Third-Party Libraries)]]></title><description><![CDATA[In this tutorial, we will go through a step-by-step process of animating transitions between different routes (or views) of our application using Next.js 13 with the new App Router structure. All that using only CSS and no extra third-party libraries...]]></description><link>https://letscode.adelpro.us.kg/page-transitions-in-nextjs-13-with-app-router-and-the-built-in-view-transitions-api-no-third-party-libraries</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/page-transitions-in-nextjs-13-with-app-router-and-the-built-in-view-transitions-api-no-third-party-libraries</guid><category><![CDATA[Next.js]]></category><category><![CDATA[CSS]]></category><category><![CDATA[animation]]></category><category><![CDATA[transition]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JavaScript]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Tue, 17 Dec 2024 18:11:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/nk4OAb64-Rk/upload/61da6e327136a9dc4bd80ee181186c95.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this tutorial, we will go through a step-by-step process of animating transitions between different routes (or views) of our application using Next.js 13 with the new App Router structure. All that using only CSS and no extra third-party libraries.</p>
<h2 id="heading-what-is-the-view-transitions-api">What is the View Transitions API</h2>
<p>View Transitions API provides a way to create an animated transition between two documents (Views), without creating an overlap in the transition.</p>
<p>View Transitions make this process easy and strait-full, by allowing you to make your DOM change without any overlap between states, by creating a transition animation between the states using snap-shotted views.</p>
<p>The current implementation of this API targets single page applications (SPAs), In this tutorial, we will explain how to do that using NextJS 13 with the new App folder.</p>
<p>Read more about View Transitions API <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API">here</a>.</p>
<h2 id="heading-is-view-transitions-api-wildly-adopted-by-modern-browsers">Is View Transitions API wildly adopted by modern browsers?</h2>
<p>At the time when this article is written, View Transitions API is adopted by Chrome (v 111+), Edge browser (v 111+) and Opera browser (v 97+) all in the production versions, and on Android system : on android browser and Chrome for android.</p>
<h2 id="heading-before-going-with-this-tutorial">Before going with this tutorial</h2>
<p>We have talked about View Transitions API before in these articles:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://adelpro.hashnode.dev/create-dynamic-web-page-transitions-with-the-built-in-view-transitions-api-no-third-party-libraries">https://adelpro.hashnode.dev/create-dynamic-web-page-transitions-with-the-built-in-view-transitions-api-no-third-party-libraries</a></div>
<p> </p>
<p>We had explained what this API, how to enable it in all supported browsers and how to use it, All that in a step-by-step guide, with a working application (MPA: multipage application), with full source code, all using only Vanilla JavaScript.</p>
<p>You must carefully read the CSS implementation before going on with this tutorial, because we will not explain the View Transitions API again.</p>
<p>Then in a second article, in the same subject, we have talked about using the same <a target="_blank" href="https://www.w3.org/TR/css-view-transitions-1/">View Transitions API</a>, but we had used ReactJS.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://adelpro.hashnode.dev/page-transitions-in-reactjs-with-react-router-v6-and-the-built-in-view-transitions-api-no-third-party-libraries">https://adelpro.hashnode.dev/page-transitions-in-reactjs-with-react-router-v6-and-the-built-in-view-transitions-api-no-third-party-libraries</a></div>
<p> </p>
<p>Now we will continue in the same approach, but now using the new NextJS 13 with app router and without using any extra third parties libraries, all of that using CSS.</p>
<h2 id="heading-what-we-are-building">What we are building</h2>
<p>In this article, we will create a multipage application, using NextJS 13 and the App Router structure, we will use the new View Transitions API to create animations when we navigate between different views (pages) all of that using only CSS.</p>
<p>In our application, we have three different animations:</p>
<ul>
<li><p>View animation (grow up)</p>
</li>
<li><p>Header Animation (slide from the right)</p>
</li>
<li><p>Content Animation (slide from the left)</p>
</li>
</ul>
<p>This is an animated GIF, showing our final application in action:</p>
<h2 id="heading-lets-code">Let’s code</h2>
<p>First, we will create a new NextJS 13 app with the App router enabled:</p>
<pre><code class="lang-bash">npx create-next-app@latest
</code></pre>
<p>Be sure to choose “Yes” for the App router:</p>
<p>Now we should clean up the code:</p>
<p>For src/app/global.css:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<p>And for src/app/page.tsx</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;&gt;</span><span class="hljs-tag">&lt;/&gt;</span></span>;
}
</code></pre>
<p>We will start par extending the default document type in NextJS, to add the new experimental View Transinions API:</p>
<p>In the <strong>src</strong> folder, Create a new folder: <strong>types</strong>, and in it create a file with the name: extendedDocument.ts</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">export</span> interface ExtendedDocument <span class="hljs-keyword">extends</span> Document {
  startViewTransition?: any;
}
</code></pre>
<p>We will use this new extendedDocument type to replace the default document in our NextJS code.</p>
<p>With the same logic, we will extend the <code>useRouter</code> hook from NextJS to make it support the new View Transitions API:</p>
<h3 id="heading-useanimatedrouter-hook">useAnimatedRouter hook:</h3>
<p>In the <strong>src</strong> folder create a new folder: <strong>hook</strong>, and in it create a file named: useAnimatedRouter.ts</p>
<pre><code class="lang-jsx">    <span class="hljs-string">"use client"</span>;
    <span class="hljs-keyword">import</span> { ExtendedDocument } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/types/extendedDocument"</span>;
    <span class="hljs-keyword">import</span> { useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/navigation"</span>;

    <span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useAnimatedRouter</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> router = useRouter();
      <span class="hljs-keyword">const</span> viewTransitionsStatus = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> extendedDocument = <span class="hljs-built_in">document</span> <span class="hljs-keyword">as</span> ExtendedDocument;
        <span class="hljs-keyword">let</span> status = <span class="hljs-string">"Opss, Your browser doesn't support View Transitions API"</span>;
        <span class="hljs-keyword">if</span> (extendedDocument?.startViewTransition) {
          status = <span class="hljs-string">"Yess, Your browser support View Transitions API"</span>;
        }
        <span class="hljs-keyword">return</span> status;
      };
      <span class="hljs-comment">// Navigate to the new route</span>
      <span class="hljs-keyword">const</span> animatedRoute = <span class="hljs-function">(<span class="hljs-params">url: string</span>) =&gt;</span> {
        <span class="hljs-keyword">const</span> extendedDocument = <span class="hljs-built_in">document</span> <span class="hljs-keyword">as</span> ExtendedDocument;
        <span class="hljs-keyword">if</span> (!extendedDocument.startViewTransition) {
          <span class="hljs-keyword">return</span> router.push(url);
        } <span class="hljs-keyword">else</span> {
          extendedDocument.startViewTransition(<span class="hljs-function">() =&gt;</span> {
            router.push(url);
          });
        }
      };
      <span class="hljs-keyword">return</span> { animatedRoute, viewTransitionsStatus };
    }
</code></pre>
<p>This hook exports two functions:</p>
<ul>
<li><p><strong>viewTansitionsStatus</strong> : Check if the browser support the new View Transitions API.</p>
</li>
<li><p><strong>animatedRoute</strong>: Encapsulate the default <code>router.push()</code> method and changes its behavior, if the browser support our API, it encapsulates it with the startViewTransition() function (please see the previous articles for more detail about this function), if not it will return with default behavior of router.push()</p>
</li>
</ul>
<h3 id="heading-animatedlink-component">animatedLink component</h3>
<p>Now we will overwrite the default bahavior of NextJS Link component, to make it support the new View Transitions API:</p>
<p>In the <strong>src</strong> folder create a new folder: <strong>components,</strong> and in it create a file: animatedLink.tsx</p>
<pre><code class="lang-jsx"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> useAnimatedRouter <span class="hljs-keyword">from</span> <span class="hljs-string">"@/hooks/useAnimatedRouter"</span>;
<span class="hljs-keyword">import</span> Link <span class="hljs-keyword">from</span> <span class="hljs-string">"next/link"</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

type Props = {
  <span class="hljs-attr">href</span>: string,
  <span class="hljs-attr">children</span>: React.ReactNode,
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AnimatedLink</span>(<span class="hljs-params">{ href, children }: Props</span>) </span>{
  <span class="hljs-keyword">const</span> { animatedRoute } = useAnimatedRouter();
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Link</span>
      <span class="hljs-attr">href</span>=<span class="hljs-string">{href}</span>
      <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> {
        animatedRoute(href);
      }}
      passHref
    &gt;
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">Link</span>&gt;</span></span>
  );
}
</code></pre>
<p>As you can see, we are using the animatedRoute() function from our newly created hook to overwrite the default onClick behavior of the Link component.</p>
<p>We are using “use client” here, to make this component client only.</p>
<p>And we are passing the <strong>passHref</strong> props here, this will allow the Link component to pass the href prop to the children.</p>
<h3 id="heading-header-component">Header component:</h3>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> Link <span class="hljs-keyword">from</span> <span class="hljs-string">"next/link"</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> AnimatedLink <span class="hljs-keyword">from</span> <span class="hljs-string">"./animatedLink"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Header</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-slate-700 text-slate-50 py-4 "</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"container mx-auto flex gap-2"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">AnimatedLink</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">AnimatedLink</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">AnimatedLink</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/about"</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">AnimatedLink</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">AnimatedLink</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/contact"</span>&gt;</span>Contact<span class="hljs-tag">&lt;/<span class="hljs-name">AnimatedLink</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>We are using the AnimatedLink component here like the default Link component. To point the three pages: Home, About and Contact.</p>
<h3 id="heading-footer-component">Footer component:</h3>
<pre><code class="lang-jsx"><span class="hljs-string">"use client"</span>;
<span class="hljs-keyword">import</span> useAnimatedRouter <span class="hljs-keyword">from</span> <span class="hljs-string">"@/hooks/useAnimatedRouter"</span>;
<span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Footer</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { viewTransitionsStatus } = useAnimatedRouter();
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">footer</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-gray-800 opacity-75 text-white p-1 text-center fixed bottom-0 left-0 right-0"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>{viewTransitionsStatus()}<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">footer</span>&gt;</span></span>
  );
}
</code></pre>
<p>We are using the viewTransitionsStatus() function from our new created hook: useAnimatedRouter to show a message in the bottom of our application.</p>
<h3 id="heading-layout-file">Layout File</h3>
<p>Now in the Layout.tsx file in the <strong>src/app</strong> folder, modify the code to include the &lt;Header/&gt; and the &lt;Footer/&gt; components</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> Header <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/header"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./globals.css"</span>;
<span class="hljs-keyword">import</span> type { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">"next"</span>;
<span class="hljs-keyword">import</span> { Inter } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/font/google"</span>;
<span class="hljs-keyword">import</span> Footer <span class="hljs-keyword">from</span> <span class="hljs-string">"@/components/footer"</span>;

<span class="hljs-keyword">const</span> inter = Inter({ <span class="hljs-attr">subsets</span>: [<span class="hljs-string">"latin"</span>] });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
  <span class="hljs-attr">title</span>: <span class="hljs-string">"Create Next App"</span>,
  <span class="hljs-attr">description</span>: <span class="hljs-string">"Generated by create next app"</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{
  children,
}: {
  children: React.ReactNode,
}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">body</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{inter.className}</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Header</span> /&gt;</span>
        {children}
        <span class="hljs-tag">&lt;<span class="hljs-name">Footer</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></span>
  );
}
</code></pre>
<h3 id="heading-our-three-pages">Our three pages:</h3>
<p>Modify the page.tsx in the <strong>src/app</strong> folder like this:</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col h-screen items-center justify-center bg-amber-100 gap-10"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-4xl pageHeader"</span>&gt;</span>Home Page<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mx-10 pageContent text-center line-clamp-3"</span>&gt;</span>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac urna
        auctor, viverra sapien. Donec euismod turpis eget massa lobortis, eget
        scelerisque justo.
      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>This is our Home page.</p>
<p>The same for the About page in the folder: <strong>src/app/about</strong>, create a new file named: page.tsx</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">About</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col h-screen items-center justify-center bg-amber-200 gap-10"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-4xl pageHeader"</span>&gt;</span>About Page<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mx-10 pageContent text-center line-clamp-3"</span>&gt;</span>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac urna
        auctor, viverra sapien. Donec euismod turpis eget massa lobortis, eget
        scelerisque justo.
      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>In the folder: <strong>src/app/contact</strong>, create a new file named: page.tsx</p>
<pre><code class="lang-jsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Contact</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex flex-col h-screen items-center justify-center bg-amber-300 gap-10"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-4xl pageHeader"</span>&gt;</span>Contact Page<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mx-10 pageContent text-center line-clamp-3"</span>&gt;</span>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ac urna
        auctor, viverra sapien. Donec euismod turpis eget massa lobortis, eget
        scelerisque justo.
      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>Take a close look at the <code>&lt;h1&gt;</code> and <code>&lt;p&gt;</code> tags in the three pages:</p>
<ul>
<li><p><code>&lt;h1&gt;</code> contain the class: pageHeader</p>
</li>
<li><p><code>&lt;p&gt;</code> contain the class: pageContent</p>
</li>
</ul>
<p>We will use these CSS classes to animate these section later</p>
<h3 id="heading-globalscss">globals.css</h3>
<p>Now we will use the magic of View Transitions API to animate our application using only CSS.</p>
<p>All the magic happen in this file: globals.css:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;

<span class="hljs-comment">/* Animate the page header separatly */</span>
<span class="hljs-selector-class">.pageHeader</span> {
  <span class="hljs-attribute">view-transition-name</span>: page-header;
}

<span class="hljs-comment">/* Animate the page content separatly */</span>
<span class="hljs-selector-class">.pageContent</span> {
  <span class="hljs-attribute">view-transition-name</span>: page-content;
}

<span class="hljs-selector-pseudo">::view-transition-old(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-out <span class="hljs-number">0.5s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-new(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-in <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-old(page-header)</span> {
  <span class="hljs-attribute">animation</span>: hide <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-new(page-header)</span> {
  <span class="hljs-attribute">animation</span>: slide-right <span class="hljs-number">2s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-old(page-content)</span> {
  <span class="hljs-attribute">animation</span>: hide <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-new(page-content)</span> {
  <span class="hljs-attribute">animation</span>: slide-left <span class="hljs-number">2.5s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-comment">/* First Animation */</span>

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-in {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-out {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
}

<span class="hljs-comment">/* Second Animation */</span>

<span class="hljs-keyword">@keyframes</span> hide {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
  }
}

<span class="hljs-keyword">@keyframes</span> slide-left {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(-<span class="hljs-number">100%</span>);
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">0</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> slide-right {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">100%</span>);
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">0</span>);
  }
}
</code></pre>
<p>We will explain every part of this file:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<p>This is a part of TailwindCSS default configuration, it's present by default in our globals.css file.</p>
<pre><code class="lang-css"><span class="hljs-comment">/* ... */</span>

<span class="hljs-selector-pseudo">::view-transition-old(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-out <span class="hljs-number">0.5s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-new(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-in <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-comment">/* ... */</span>

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-in {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-out {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
}
</code></pre>
<p>This part of the code will animate the root element of the views (our pages), and the state of the animation changes between the old and the new state, for that we have two CSS keyframe animations:</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">-</span>
  <span class="hljs-selector-tag">fade-and-scale-in</span>
  <span class="hljs-selector-tag">-</span>
  <span class="hljs-selector-tag">fade-and-scale-out</span>.../\*
  <span class="hljs-selector-tag">Animate</span>
  <span class="hljs-selector-tag">the</span>
  <span class="hljs-selector-tag">page</span>
  <span class="hljs-selector-tag">header</span>
  <span class="hljs-selector-tag">separatly</span>
  \*/
  <span class="hljs-selector-class">.pageHeader</span> {
  <span class="hljs-attribute">view-transition-name</span>: page-header;
}
...<span class="hljs-selector-pseudo">::view-transition-old(page-header)</span> {
  <span class="hljs-attribute">animation</span>: hide <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}
<span class="hljs-selector-pseudo">::view-transition-new(page-header)</span> {
  <span class="hljs-attribute">animation</span>: slide-right <span class="hljs-number">2s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}
.../\* <span class="hljs-selector-tag">Second</span> <span class="hljs-selector-tag">Animation</span> \*/<span class="hljs-keyword">@keyframes</span> hide {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
  }
}
<span class="hljs-keyword">@keyframes</span> slide-right {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">100%</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">0</span>);
  }
}
</code></pre>
<p>In this part of the code, we had created a new view-transition container, with the using: view-transition-name: page-header, with is assigned to the CSS class: pageHeader</p>
<p>We will use this name: “page-header” to assign a new animation (hide and slide-right).</p>
<pre><code class="lang-css"><span class="hljs-comment">/* ... */</span>

<span class="hljs-comment">/* Animate the page header separatly */</span>
<span class="hljs-selector-class">.pageContent</span> {
  <span class="hljs-attribute">view-transition-name</span>: page-content;
}

<span class="hljs-comment">/* ... */</span>

<span class="hljs-selector-pseudo">::view-transition-old(page-content)</span> {
  <span class="hljs-attribute">animation</span>: hide <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-new(page-header)</span> {
  <span class="hljs-attribute">animation</span>: slide-left <span class="hljs-number">2.5s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-comment">/* ... */</span>

<span class="hljs-comment">/* Second Animation */</span>

<span class="hljs-keyword">@keyframes</span> hide {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
  }
}

<span class="hljs-keyword">@keyframes</span> slide-left {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(-<span class="hljs-number">100%</span>);
  }

  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">0</span>);
  }
}
</code></pre>
<p>With the same logic, we had created a new view-transition container, with the using: view-transition-name: page-content, with is assigned to the CSS class: pageContent.</p>
<p>We will use this name: “page-content” to assign a new animation (hide and slide-left).</p>
<h2 id="heading-full-complete-source-code-on-github">Full complete source code (on GitHub)</h2>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/adelpro/nextjs-view-transitions">https://github.com/adelpro/nextjs-view-transitions</a></div>
<p> </p>
<h2 id="heading-live-demo-with-source-code-on-codesandbox">Live Demo with source code (on Codesandbox)</h2>
<p><a target="_blank" href="https://codesandbox.io/p/github/adelpro/nextjs-view-transitions/main">Page Transitions In Next.js</a></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Using view-transition containers in CSS allows for smooth animations of page elements, like headers and content. By defining custom animations such as "hide" and "slide-left," you can enhance user experience in applications like Next.js. The full implementation can be explored through the provided GitHub and CodeSandbox links.</p>
]]></content:encoded></item><item><title><![CDATA[Page Transitions In ReactJS With React Router V6 and the Built-In View Transitions API (No Third-Party Libraries)]]></title><description><![CDATA[We will go step by step in a new tutorial without using any third-party libraries, we will only use a build-in JavaScript API: View Transitions API.
What is the View Transitions API
View Transitions API provides a way to create an animated transition...]]></description><link>https://letscode.adelpro.us.kg/page-transitions-in-reactjs-with-react-router-v6-and-the-built-in-view-transitions-api-no-third-party-libraries</link><guid isPermaLink="true">https://letscode.adelpro.us.kg/page-transitions-in-reactjs-with-react-router-v6-and-the-built-in-view-transitions-api-no-third-party-libraries</guid><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[animation]]></category><category><![CDATA[transition]]></category><category><![CDATA[CSS]]></category><dc:creator><![CDATA[adelpro]]></dc:creator><pubDate>Tue, 17 Dec 2024 17:52:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Y-VYK0SDLxs/upload/47729ba0ff55ba6be0934b2f3be0a6cb.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We will go step by step in a new tutorial without using any third-party libraries, we will only use a build-in JavaScript API: <a target="_blank" href="https://www.w3.org/TR/css-view-transitions-1/">View Transitions API</a>.</p>
<h2 id="heading-what-is-the-view-transitions-api">What is the View Transitions API</h2>
<p>View Transitions API provides a way to create an animated transition between two documents (Views), without creating an overlap in the transition.</p>
<p>View Transitions make this process easy and strait-full , by allowing you to make your DOM change without any overlap between states, by creating a transition animation between the states using snap-shotted views.</p>
<p>The current implementation of this API targets single page applications (SPAs), In this tutorial we will explain how to do that using ReactJS.</p>
<h2 id="heading-view-transitions-api-browser-support">View Transitions API browser support?</h2>
<ul>
<li><p>The View transition API should be on by default for Chrome 111 beta users.</p>
</li>
<li><p>You can enable it by accessing the experiment flag: chrome://flags/#view-transition in Chrome 109 and above.</p>
</li>
<li><p>The same process for Chromium based browsers (Microsoft Edge, Brave, Opera …).</p>
</li>
<li><p>For Mozilla Firefox, The View transitions API is not implemented yet.</p>
</li>
</ul>
<h2 id="heading-before-going-with-this-tutorial"><strong>Before going with this tutorial</strong></h2>
<p>We have talked about View Transitions API before (<a target="_blank" href="https://adelpro.hashnode.dev/create-dynamic-web-page-transitions-with-the-built-in-view-transitions-api-no-third-party-libraries">In this tutorial</a>).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://adelpro.hashnode.dev/create-dynamic-web-page-transitions-with-the-built-in-view-transitions-api-no-third-party-libraries">https://adelpro.hashnode.dev/create-dynamic-web-page-transitions-with-the-built-in-view-transitions-api-no-third-party-libraries</a></div>
<p> </p>
<p>We explained what this API is, how to enable it in supported browsers and how to use it, a step by step guide, with a final working example application (MPA: multi page application), with full source code, all using only Vanilla JavaScript.</p>
<p>You must carefully read the CSS implementation, because we will go on with the same logic in this tutorial.</p>
<p>But To apply that to a ReactJS application we have to do that with a different approach.</p>
<h2 id="heading-what-we-are-building">What we are building</h2>
<p>This is an animated GIF, showing our final application in action</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734457375920/f54562db-a512-428e-8c28-2c6bb75b7b2f.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-lets-code">Let’s code</h2>
<p>Let’s see what we are building</p>
<p>In our Application, we will use create-react-app to create a starting project, that let’s as navigate between three pages or views: home, download and about.</p>
<p>First</p>
<pre><code class="lang-bash">npx create-react-app reactjs-react-router-view-transitions-api 
&amp;&amp; <span class="hljs-built_in">cd</span> reactjs-react-router-view-transitions-api
</code></pre>
<p>Second</p>
<pre><code class="lang-bash">npm install react-router-dom
</code></pre>
<p>To install React Router V6 package.</p>
<p>Our application will contain these files:</p>
<pre><code class="lang-plaintext">ReactJs - React Router - View Transitions API

└─ src
   └─ img
      ├─ home.png
      ├─ about.png
      └─ download.png
   ├─ About.js
   ├─ App.js
   ├─ Download.js
   ├─ Header.js
   ├─ Home.js
   ├─ PageNotFound.js
   ├─ Index.js
   ├─ styles.css
   └─ ...
</code></pre>
<h3 id="heading-the-img-folder">- The “/img” folder</h3>
<p>contain the images used in our project.</p>
<h3 id="heading-indexjs">- Index.js</h3>
<p>It’s the entry point of our project</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { StrictMode } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-dom/client"</span>;
<span class="hljs-keyword">import</span> { BrowserRouter, Route, Routes } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">"./App"</span>;

<span class="hljs-keyword">const</span> rootElement = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"root"</span>);
<span class="hljs-keyword">const</span> root = createRoot(rootElement);

root.render(
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">StrictMode</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">BrowserRouter</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Routes</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"/*"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">App</span> /&gt;</span>} /&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">Routes</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">BrowserRouter</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">StrictMode</span>&gt;</span></span>
);
</code></pre>
<p>As you can see, we are routing all pages to the &lt;App/&gt; component, using the “/*” in the path prop.</p>
<p>In React Router v6, All routes : &lt;Route&gt; must be contained in the &lt;BrowserRouter&gt; and &lt;Routes&gt;</p>
<pre><code class="lang-jsx">&lt;BrowserRouter&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Routes</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"/*"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">App</span> /&gt;</span>} /&gt;
  <span class="hljs-tag">&lt;/<span class="hljs-name">Routes</span>&gt;</span></span>
&lt;/BrowserRouter&gt;
</code></pre>
<h3 id="heading-appjs">- App.js</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { Routes, Route } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./styles.css"</span>;

<span class="hljs-keyword">import</span> Home <span class="hljs-keyword">from</span> <span class="hljs-string">"./Home"</span>;
<span class="hljs-keyword">import</span> Header <span class="hljs-keyword">from</span> <span class="hljs-string">"./Header"</span>;
<span class="hljs-keyword">import</span> PageNotFound <span class="hljs-keyword">from</span> <span class="hljs-string">"./PageNotFound"</span>;
<span class="hljs-keyword">import</span> About <span class="hljs-keyword">from</span> <span class="hljs-string">"./About"</span>;
<span class="hljs-keyword">import</span> Download <span class="hljs-keyword">from</span> <span class="hljs-string">"./Download"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">let</span> isViewTransition = <span class="hljs-string">"Opss, Your browser doesn't support View Transitions API"</span>;
  <span class="hljs-keyword">if</span> (<span class="hljs-built_in">document</span>.startViewTransition) {
    isViewTransition = <span class="hljs-string">"Yess, Your browser support View Transitions API"</span>;
  }

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Header</span> /&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Routes</span>&gt;</span>
        {/* Routes */}
        <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">index</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Home</span> /&gt;</span>} /&gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"About"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">About</span> /&gt;</span>} /&gt;
        <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"download"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Download</span> /&gt;</span>} /&gt;

        {/* 404 page */}
        <span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"*"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">PageNotFound</span> /&gt;</span>} /&gt;
      <span class="hljs-tag">&lt;/<span class="hljs-name">Routes</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">footer</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span>&gt;</span>Complete tutorial on Medium<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{isViewTransition}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">footer</span>&gt;</span>
    <span class="hljs-tag">&lt;/&gt;</span></span>
  );
}
</code></pre>
<p>After routing all page to our &lt;App&gt; component, we will point each page to to corresponding component, All contained in &lt;Routes&gt;…&lt;/Routes&gt;</p>
<pre><code class="lang-jsx">{<span class="hljs-comment">/* Routes */</span>}
&lt;Route index element={<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Home</span> /&gt;</span></span>} /&gt;
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"about"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">About</span> /&gt;</span>} /&gt;</span>
<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Route</span> <span class="hljs-attr">path</span>=<span class="hljs-string">"download"</span> <span class="hljs-attr">element</span>=<span class="hljs-string">{</span>&lt;<span class="hljs-attr">Download</span> /&gt;</span>} /&gt;</span>

{<span class="hljs-comment">/* 404 page */</span>}
&lt;Route path=<span class="hljs-string">"*"</span> element={<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">PageNotFound</span> /&gt;</span></span>} /&gt;
</code></pre>
<ul>
<li><p>Route “index” point to “/”, will show the &lt;Home&gt; component.</p>
</li>
<li><p>path about and download point to &lt;About&gt; and &lt;Download&gt; components respectively.</p>
</li>
<li><p>All the other paths “*” will point to &lt;PageNotFound&gt;c component.</p>
</li>
</ul>
<p>Now, we did a check to show if our browser supports or not Our View Transitions API, and show the result at the bottom of every page, in our footer, By checking the “document.startViewTransition”.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">let</span> isViewTransition = <span class="hljs-string">"Oops, Your browser doesn't support View Transitions API"</span>;

<span class="hljs-keyword">if</span> (<span class="hljs-built_in">document</span>.startViewTransition) {
  isViewTransition = <span class="hljs-string">"Yes, Your browser supports View Transitions API"</span>;
}

...

&lt;footer&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{isViewTransition}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>
&lt;/footer&gt;

...

return (
  <span class="xml"><span class="hljs-tag">&lt;&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Header</span> /&gt;</span>
    ...</span>
</code></pre>
<p>It’s here were we will implement our View Transitions API</p>
<h3 id="heading-headerjs">- Header.js</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./styles.css"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Header</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> navigate = useNavigate();

  <span class="hljs-keyword">const</span> viewNavigate = <span class="hljs-function">(<span class="hljs-params">newRoute</span>) =&gt;</span> {
    <span class="hljs-comment">// Navigate to the new route</span>
    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">document</span>.startViewTransition) {
      <span class="hljs-keyword">return</span> navigate(newRoute);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.startViewTransition(<span class="hljs-function">() =&gt;</span> {
        navigate(newRoute);
      });
    }
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"header__container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">span</span>&gt;</span>ReactJs - React Router - View Transitions API<span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"link__container"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> {
            viewNavigate("/");
          }}
        &gt;
          Home
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> {
            viewNavigate("/download");
          }}
        &gt;
          Download
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
          <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> {
            viewNavigate("/about");
          }}
        &gt;
          About
        <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
  );
}
</code></pre>
<p>The main function in this tutorial is <strong>viewNavigate()</strong></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> navigate = useNavigate();

<span class="hljs-keyword">const</span> viewNavigate = <span class="hljs-function">(<span class="hljs-params">newRoute</span>) =&gt;</span> {
  <span class="hljs-comment">// Navigate to the new route</span>
  <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">document</span>.startViewTransition) {
    <span class="hljs-keyword">return</span> navigate(newRoute);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.startViewTransition(<span class="hljs-function">() =&gt;</span> {
      navigate(newRoute);
    });
  }
};
</code></pre>
<p>This function will encapsulate the ReactJS build-in navigate() function imported from <a target="_blank" href="https://reactrouter.com/en/main/hooks/use-navigate">useNavigate</a> hook</p>
<p>If the browser do not support our View Transitions API, it will return a simple navigate() function with the parameter: newRoute</p>
<p>But when our browser supports our View transitions api, we will encapsulate our navigate(newRoute) with: document.startViewTransition() with one parameter; an arrow function.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">return</span> <span class="hljs-built_in">document</span>.startViewTransition(<span class="hljs-function">() =&gt;</span> {
  navigate(newRoute);
});
</code></pre>
<p>When document.startViewTransitionis called:</p>
<p>→ The API captures the current state of the page including a screenshot. This includes taking a screenshot, which is async as it happens in the render steps of the event loop.</p>
<p>→ the API constructs a pseudo-element tree:</p>
<pre><code class="lang-plaintext">view-transition

└─ ::view-transition-group(root)

 └─ ::view-transition-image-pair(root)

   ├─ ::view-transition-old(root)

   └─ ::view-transition-new(root)
</code></pre>
<p>→ Then the call-back function is called: ”() =&gt; {navigate(newRoute)}”.</p>
<p>→ Capture the new state of our page.</p>
<p>→ Reconstruct the DOM to include the new elements.</p>
<p>→ All that happens asynchronously, the Rendering is paused, so the user doesn’t see a flash of the new content.</p>
<p>View Transitions flow</p>
<p>→ view-transition: sits on top of the animation as an overlay.</p>
<p>→ view-transition-group: Responsible for animating the size and position between the old and the new stats.</p>
<p>→ view-transition-image-pair: hold the current state of the animation.</p>
<p>→ The animation transits from the starting view (view-transition-image-old) to the new view (view-transition-image-new).</p>
<p>→ By default The animation is a cross-fade CSS transition.</p>
<p>→ The animation is done with CSS animations, both stats render as CSS ( the new CSS style will replace the old one), From opacity:1 to opacity:0 for the old view, then from opacity:0 to opacity:1 for the new view.</p>
<p>→ The root key means that the animation i’ll be applied to the entire page.</p>
<p>We can easily customise these transitions with CSS, we will talk about that in the next part of the tutorial, and will explain the style.css.</p>
<h3 id="heading-stylecss">- Style.css</h3>
<p>→ We will start by changing the default animation timing, the outgoing View go out fast and the incoming View come in slow</p>
<pre><code class="lang-css"><span class="hljs-selector-pseudo">::view-transition-old(root)</span> {
  <span class="hljs-attribute">animation-duration</span>: <span class="hljs-number">0.5s</span>;
}

<span class="hljs-selector-pseudo">::view-transition-new(root)</span> {
  <span class="hljs-attribute">animation-duration</span>: <span class="hljs-number">10s</span>;
}
</code></pre>
<p>→ Then change the default animation, by creating two CSS keyframes; one for the old stat exit animation and the second for the new stat entrance.</p>
<pre><code class="lang-css"><span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-in {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-out {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
}
</code></pre>
<p>→ Now grouping all these properties using CSS Shorthand to have less code</p>
<pre><code class="lang-css"><span class="hljs-comment">/* Views Animation */</span>

<span class="hljs-selector-pseudo">::view-transition-old(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-out <span class="hljs-number">0.5s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-new(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-in <span class="hljs-number">10s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-in {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-out {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
}

<span class="hljs-comment">/* Explanation of this line of CSS code */</span>
{
  <span class="hljs-attribute">animation</span>: fade-and-scale-out <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}
</code></pre>
<ul>
<li><p><strong>.fade-and-scale-out</strong>: This is the name of the animation. It's also used as the class name for the element that will be animated.</p>
</li>
<li><p><strong>1s</strong>: This is the duration of the animation, which is set to 1 second.</p>
</li>
<li><p><strong>ease-in-out</strong>: This is the timing function of the animation. In this case, it starts slowly, accelerates through the middle, and slows down at the end.</p>
</li>
<li><p><strong>1</strong>: This is the number of times the animation will repeat. In this case, it will only run once.</p>
</li>
<li><p><strong>forwards</strong>: This is the value of the <strong>animation-fill-mode</strong> property, which determines what styles are applied to the element before and after the animation runs. In this case, <strong>forwards</strong> means that the element will retain the styles from the last keyframe of the animation after the animation ends.</p>
</li>
</ul>
<p>And this line too</p>
<pre><code class="lang-css">{
  <span class="hljs-attribute">animation</span>: fade-and-scale-in <span class="hljs-number">1s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}
</code></pre>
<ul>
<li><p><strong>.fade-and-scale-in</strong>: This is the name of the animation. It's also used as the class name for the element that will be animated.</p>
</li>
<li><p><strong>1s</strong>: This is the duration of the animation, which is set to 1 second.</p>
</li>
<li><p><strong>ease-in-out</strong>: This is the timing function of the animation. In this case, it starts slowly, accelerates through the middle, and slows down at the end.</p>
</li>
<li><p><strong>1</strong>: This is the number of times the animation will repeat. In this case, it will only run once.</p>
</li>
<li><p><strong>forwards</strong>: This is the value of the <strong>animation-fill-mode</strong> property, which determines what styles are applied to the element before and after the animation runs. In this case, <strong>forwards</strong> means that the element will retain the styles from the last keyframe of the animation after the animation ends.</p>
</li>
</ul>
<h3 id="heading-our-views-or-pages">- Our Views or pages</h3>
<p>We have three pages: Home, About and Download, all linked to their respective components.</p>
<p>The code is identical for these pages, will will take Home as example</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> home <span class="hljs-keyword">from</span> <span class="hljs-string">"./img/home.png"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
        tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
        veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
        commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
        velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
        occaecat cupidatat non proident, sunt in culpa qui officia deserunt
        mollit anim id est laborum
      <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"img"</span> <span class="hljs-attr">src</span>=<span class="hljs-string">{home}</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">"Home"</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<p>We have a &lt;main&gt; section with a &lt;h1&gt; title and paragraph &lt;p&gt; with a “lorem ipsum…” random text, and an image &lt;img&gt; imported as variable: home at the beginning of the file.</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.header__container</span> {
  <span class="hljs-attribute">display</span>: flex;
  <span class="hljs-attribute">justify-content</span>: space-between;
}

<span class="hljs-selector-class">.link__container</span> <span class="hljs-selector-tag">a</span> {
  <span class="hljs-attribute">margin-right</span>: <span class="hljs-number">10px</span>;
}

<span class="hljs-selector-class">.main__container</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">90%</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">3px</span>;
  <span class="hljs-attribute">max-width</span>: <span class="hljs-number">600px</span>;
  <span class="hljs-attribute">margin</span>: auto;
  <span class="hljs-attribute">display</span>: flex;
  <span class="hljs-attribute">justify-content</span>: center;
  <span class="hljs-attribute">align-items</span>: center;
  <span class="hljs-attribute">flex-direction</span>: column;
}

<span class="hljs-selector-class">.img</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">300px</span>;
  <span class="hljs-attribute">height</span>: <span class="hljs-number">400px</span>;
  <span class="hljs-attribute">margin</span>: auto;
  <span class="hljs-attribute">width</span>: <span class="hljs-number">100%</span>;
}

<span class="hljs-comment">/* Views Animation */</span>
<span class="hljs-selector-pseudo">::view-transition-old(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-out <span class="hljs-number">0.5s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-selector-pseudo">::view-transition-new(root)</span> {
  <span class="hljs-attribute">animation</span>: fade-and-scale-in <span class="hljs-number">10s</span> ease-in-out <span class="hljs-number">1</span> forwards;
}

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-in {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> fade-<span class="hljs-keyword">and</span>-scale-out {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">1</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">scale</span>(<span class="hljs-number">0</span>);
  }
}

<span class="hljs-comment">/* Second animation */</span>
<span class="hljs-keyword">@keyframes</span> slide-in {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">100%</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">0</span>);
  }
}

<span class="hljs-keyword">@keyframes</span> slide-out {
  <span class="hljs-selector-tag">from</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">1</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(<span class="hljs-number">0</span>);
  }
  <span class="hljs-selector-tag">to</span> {
    <span class="hljs-attribute">opacity</span>: <span class="hljs-number">0</span>;
    <span class="hljs-attribute">transform</span>: <span class="hljs-built_in">translateX</span>(-<span class="hljs-number">100%</span>);
  }
}
</code></pre>
<p>We have added a second CSS animation that you can try as a practice, go on try it and tell us what animation is best.</p>
<h2 id="heading-full-complete-source-code">Full complete source code</h2>
<p><a target="_blank" href="https://codesandbox.io/p/sandbox/efbc5s">https://codesandbox.io/p/sandbox/efbc5s</a></p>
]]></content:encoded></item></channel></rss>