Sunday, December 14, 2025

React NavLink Active State Breaks in SPFx

 

Why React NavLink Active State Breaks in SPFx (and the Only Workaround That Works)

After days of regression, refactoring, and second-guessing React itself, the truth finally emerged: the problem was never React — it was SharePoint.


🧩 The Problem

When building a custom sidebar or header using an SPFx Application Customizer, you may expect:

  • NavLink to stay active

  • useLocation() to update on navigation

  • React Router to behave like an SPA

None of that reliably works in modern SharePoint.

Symptoms you will see:

  • Menu highlights only on the second click

  • Active state disappears on page navigation

  • useLocation() doesn’t update

  • React state resets randomly

  • URLs change without React knowing


Why This Happens (The Root Cause)

Modern SharePoint does NOT do full page reloads.

Instead, it uses:

  • Partial page navigation

  • History API manipulation

  • Internal routing outside React

This means:

  • React Router never receives navigation events

  • The page URL updates, but your extension is unaware

  • NavLink active logic silently fails

Note: SPFx Application Customizers are not SPAs


Why Common Solutions Fail

AttemptWhy it fails
NavLink isActiveNo router events
useLocation()URL changes outside React
<a> tagsFull reload breaks UI
basename tricksSharePoint rewrites paths
force re-renderPage DOM replaced

✅ The Only Reliable Approach (Workaround)

Instead of reacting to routing events that don’t exist,
watch the browser URL directly and update the DOM manually.

Yes — this breaks “pure React” rules.
But in SPFx Application Customizers, this is the correct approach.


🛠️ Working Solution: URL Polling + DOM Class Swap

/**
 * SharePoint performs partial page navigation.
 * React Router does not detect URL changes.
 *
 * This workaround monitors location.href
 * and manually updates active menu states.
 */

let _lastUrl: string = location.href;

setInterval(() => {
  const currentUrl = location.href;

  if (currentUrl !== _lastUrl) {
    _lastUrl = currentUrl;
    console.log("URL updated to:", currentUrl);

    const menuAnchors: HTMLAnchorElement[] = Array.from(
      document.querySelectorAll<HTMLAnchorElement>(
        'a[class^="menuAnchor"], a[class^="activeItem"]'
      )
    );

    menuAnchors.forEach(anchor => {
      if (
        currentUrl === anchor.href ||
        currentUrl.indexOf(anchor.href) > -1
      ) {
        // Convert inactive → active
        anchor.className = anchor.className.replace(
          'menuAnchor',
          'activeItem'
        );
      } else {
        // Convert active → inactive
        anchor.className = anchor.className.replace(
          'activeItem',
          'menuAnchor'
        );
      }
    });
  }
}, 500);

🎯 Why This Works

  • SharePoint always updates location.href

  • We bypass React Router entirely

  • DOM survives partial navigation

  • Works for:

    • EN / AR URLs

    • Deep SitePages

    • Document libraries

    • RTL layouts


⚠️ Is This Bad Practice?

In normal React apps → Yes ❌
In SPFx Application Customizers → No ✅

Microsoft’s own SharePoint UI uses:

  • Observers

  • URL polling

  • Direct DOM mutation

This is expected in extensions.


🧠 Lessons Learned

  • SPFx ≠ SPA

  • React Router ≠ SharePoint Router

  • Purity must sometimes yield to reality

  • If highlighting works on 2nd click → URL timing issue


🏁 Final Thoughts

If you are building:

  • Custom sidebars

  • Mega menus

  • Headers

  • Language switchers

Do not fight SharePoint’s navigation model.
Observe it.

This workaround saved days of regression — and will save yours too.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.