shouldUpdateNode - greydragon888/real-router GitHub Wiki

router.shouldUpdateNode

1. Overview

  • What it does: Creates a predicate function to check whether a route node should update during a transition between states. Used internally by the transition system and integrations (React, Vue).
  • When to use:
    • For optimizing rerenders in routing components
    • For determining which parts of UI need to update on navigation
    • For implementing custom framework integrations

2. Signature

router.shouldUpdateNode(nodeName: string): (toState: State, fromState?: State) => boolean

Usage Examples

// Create predicate for node
const shouldUpdate = router.shouldUpdateNode("users");

// Check during transition
const willUpdate = shouldUpdate(toState, fromState);

// In React component
const RouteComponent = ({ routeName }) => {
  const shouldUpdate = router.shouldUpdateNode(routeName);

  useEffect(() => {
    return router.subscribe((state, prevState) => {
      if (shouldUpdate(state, prevState)) {
        // Update component
      }
    });
  }, []);
};

// Initial navigation (without fromState)
const predicate = router.shouldUpdateNode("home");
predicate(initialState); // true for activating nodes

3. Parameters

nodeName (required)

  • Type: string
  • Purpose: Route node name to check
  • Allowed values: Any string (including empty "" for root node)
  • On error: TypeError for non-string values
Value Behavior
"home" ✅ Checks home node
"users.list" ✅ Checks nested node
"" ✅ Checks root node
null, 123, {} TypeError

Returned Function

(toState: State, fromState?: State) => boolean;

toState (required)

  • Type: State
  • Purpose: Target state of transition
  • On error: TypeError if not an object with name field

fromState (optional)

  • Type: State | undefined
  • Purpose: Source state of transition
  • Default value: undefined (initial navigation)

4. Return Value

The shouldUpdateNode function returns a predicate (toState, fromState?) => boolean.

The predicate returns true if the node should update:

Situation Result Description
state.transition.reload is true true All nodes update on reload
Root node + initial navigation true Root always updates on first navigation
Node = intersection true Node at change boundary
Node in toActivate true Node is activating
Node in toDeactivate true Node is deactivating
Otherwise false Node not affected by transition

5. Side Effects

Method has no side effects — it's a pure function that creates a pure predicate.

6. Possible Errors

Condition Error Message
nodeName not a string TypeError [router.shouldUpdateNode] nodeName must be a string, got {type}
toState not an object TypeError [router.shouldUpdateNode] toState must be valid State object
toState without name TypeError [router.shouldUpdateNode] toState must be valid State object
// Invalid nodeName
router.shouldUpdateNode(123); // TypeError: nodeName must be a string
router.shouldUpdateNode(null); // TypeError: nodeName must be a string
router.shouldUpdateNode({}); // TypeError: nodeName must be a string

// Invalid toState
const predicate = router.shouldUpdateNode("home");
predicate(null); // TypeError: toState must be valid State object
predicate({}); // TypeError: toState must be valid State object
predicate({ path: "/home" }); // TypeError: toState must be valid State object (no name)

7. Related Methods

Method When to use
getTransitionPath() Low-level determination of intersection, toActivate, toDeactivate
subscribe() Subscribe to state changes
getState() Get current state

8. Behavior

Main Scenarios

  • Factory pattern: Each call creates a new predicate function
  • Closure: nodeName is captured in closure
  • Intersection: Node at change boundary always updates
  • Activation/Deactivation: Nodes in toActivate and toDeactivate update

Test Examples

// Factory pattern — each call creates new function
const predicate1 = router.shouldUpdateNode("home");
const predicate2 = router.shouldUpdateNode("home");
expect(predicate1).not.toBe(predicate2);

// Closure — nodeName is captured
const predicateHome = router.shouldUpdateNode("home");
const predicateSignIn = router.shouldUpdateNode("sign-in");

const homeState = { name: "home", params: {}, path: "/home" };
expect(predicateHome(homeState, homeState)).toBe(true); // intersection
expect(predicateSignIn(homeState, homeState)).toBe(false); // not affected

Intersection Node

// Node at change boundary (intersection) always updates
const fromState = makeState("a.b.c.d", { p1: 0, p2: 2, p3: 3 });
const toState = makeState("a.b.c.d", { p1: 1, p2: 2, p3: 3 });

// "a" — intersection (p1 changed)
expect(router.shouldUpdateNode("a")(toState, fromState)).toBe(true);

// "" — above intersection, doesn't update
expect(router.shouldUpdateNode("")(toState, fromState)).toBe(false);

Activation / Deactivation

const fromState = makeState("a.b.c.d", { p1: 0, p2: 2, p3: 3 });
const toState = makeState("a.b.c.e", { p1: 1, p2: 2, p4: 3 });

// Reactivate (were active, parameters changed)
expect(router.shouldUpdateNode("a.b")(toState, fromState)).toBe(true);
expect(router.shouldUpdateNode("a.b.c")(toState, fromState)).toBe(true);

// Activating
expect(router.shouldUpdateNode("a.b.c.e")(toState, fromState)).toBe(true);

// Deactivating
expect(router.shouldUpdateNode("a.b.c.d")(toState, fromState)).toBe(true);

Reload Option

// With reload: true all nodes update
const state = api.makeState("a.b.c", { p1: 1, p2: 2 });
const toState = {
  ...state,
  transition: {
    reload: true,
    phase: "activating",
    reason: "success",
    segments: { deactivated: [], activated: [], intersection: "" },
  },
};

expect(router.shouldUpdateNode("a.b.c")(toState)).toBe(true);
expect(router.shouldUpdateNode("a")(toState)).toBe(true);
expect(router.shouldUpdateNode("unrelated")(toState)).toBe(true); // even unrelated!

Initial Navigation (without fromState)

const toState = makeState("a.b.c", { p1: 1, p2: 2 });

// All activating nodes update
expect(router.shouldUpdateNode("a")(toState)).toBe(true);
expect(router.shouldUpdateNode("a.b")(toState)).toBe(true);
expect(router.shouldUpdateNode("a.b.c")(toState)).toBe(true);

// Root node always updates on initial navigation
expect(router.shouldUpdateNode("")(toState)).toBe(true);

// Unrelated nodes don't update
expect(router.shouldUpdateNode("unrelated")(toState)).toBe(false);

Root Node

// Root node ("") has special behavior

// On initial navigation — always updates
expect(router.shouldUpdateNode("")(makeState("a.b.c", {}), undefined)).toBe(
  true,
);

// On transition to root — updates (intersection)
expect(
  router.shouldUpdateNode("")(makeState("", {}), makeState("app", {})),
).toBe(true);

// On transition between nested — doesn't update
expect(
  router.shouldUpdateNode("")(makeState("a.b.d", {}), makeState("a.b.c", {})),
).toBe(false);

Complex Transitions

const fromState = makeState("app.users.list", {});
const toState = makeState("app.settings.profile", {});

// "app" — intersection
expect(router.shouldUpdateNode("app")(toState, fromState)).toBe(true);

// Deactivating
expect(router.shouldUpdateNode("app.users")(toState, fromState)).toBe(true);
expect(router.shouldUpdateNode("app.users.list")(toState, fromState)).toBe(
  true,
);

// Activating
expect(router.shouldUpdateNode("app.settings")(toState, fromState)).toBe(true);
expect(
  router.shouldUpdateNode("app.settings.profile")(toState, fromState),
).toBe(true);

// Unrelated
expect(router.shouldUpdateNode("admin")(toState, fromState)).toBe(false);

Edge Cases

  • Empty string "": Valid nodeName (root node)
  • Non-existent node: Correctly returns false (not in intersection, toActivate, toDeactivate)
  • Deep nesting: Works correctly with 6+ levels
  • Same state: Intersection = the route itself, updates

Guarantees

  • Factory pattern — each call creates new function
  • nodeName validation for string type
  • toState validation for object with name
  • fromState can be undefined (initial navigation)
  • Root node always updates on initial navigation
  • reload: true updates all nodes

Migration from router5

Version Comparison

Master Current
Location Separate package router5-transition-path Router method router.shouldUpdateNode()
fromState signature fromState: State (required) fromState?: State (optional)
nodeName validation ❌ No TypeError for non-strings
toState validation ❌ No TypeError for invalid objects
Initial navigation ❌ Not supported (fromState required) fromState can be undefined
Root node on start ❌ No special handling ✅ Always updates on initial navigation
Deactivating nodes ❌ Returns false ✅ Returns true
TypeScript Basic typing ✅ Full typing

Breaking Changes

Severity What Changed Was Now Impact
🔴 CRITICAL Location import shouldUpdateNode from 'router5-transition-path' router.shouldUpdateNode() Change imports
🟠 HIGH Deactivating nodes false true Components will receive deactivation notifications
🟡 MEDIUM nodeName validation Silently accepted any TypeError Code with invalid nodeName will break
🟡 MEDIUM toState validation Silently worked TypeError Code with invalid toState will break
🟢 LOW fromState optional Required Optional Backward compatible

Examples

// ❌ Code that will break (import)
import shouldUpdateNode from "router5-transition-path";
const predicate = shouldUpdateNode("home");

// ✅ Code after migration
const predicate = router.shouldUpdateNode("home");

// ❌ Behavior change (deactivating nodes)
// Master: deactivating nodes DID NOT receive notification
const fromState = makeState("a.b.c.d", {});
const toState = makeState("a.b.c.e", {});
shouldUpdateNode("a.b.c.d")(toState, fromState); // false in master!

// ✅ Current version: deactivating nodes DO receive notification
router.shouldUpdateNode("a.b.c.d")(toState, fromState); // true

// ❌ Code that will break (invalid nodeName)
shouldUpdateNode(123); // Worked, returned function

// ✅ Current version throws error
router.shouldUpdateNode(123); // TypeError

Implementation Changes

Architectural Changes

Aspect Master Current Description
Package router5-transition-path @real-router/core (router method) Consolidation into main package
API Standalone function Router method Integration into router API
transitionPath External import getTransitionPath() from internals Internal dependency

Fixed Bugs

  • Deactivating nodes: In master version, nodes that were deactivating did not receive update notification. This was a bug — components should know about deactivation for proper cleanup.

Implementation issues in master branch:

  • fromState required — doesn't work for initial navigation
  • No input validation
  • Deactivating nodes don't receive notification (bug)
  • Complex level comparison logic (hard to understand and maintain)
  • No special root node handling

Now (current)

Improvements:

  • fromState optional — supports initial navigation
  • Validation of nodeName and toState
  • Deactivating nodes receive notification (bug fixed)
  • Simple and clear logic: intersection || toActivate || toDeactivate
  • Special root node handling on initial navigation
  • Integration into router API

Migration Guide

Checklist

  • Remove router5-transition-path dependency from package.json
  • Replace import shouldUpdateNode from 'router5-transition-path' with router.shouldUpdateNode()
  • IMPORTANT: Check logic that relied on false for deactivating nodes
  • Ensure nodeName is always a string
  • Ensure toState is always a valid State object

Migration Examples

// ❌ Was (master) — standalone function
import shouldUpdateNode from "router5-transition-path";

const MyComponent = ({ route, router }) => {
  const shouldUpdate = shouldUpdateNode(route);

  useEffect(() => {
    return router.subscribe((state, prevState) => {
      // prevState required in master!
      if (prevState && shouldUpdate(state, prevState)) {
        // update
      }
    });
  }, []);
};

// ✅ Now — router method
const MyComponent = ({ route, router }) => {
  const shouldUpdate = router.shouldUpdateNode(route);

  useEffect(() => {
    return router.subscribe((state, prevState) => {
      // prevState can be undefined (initial navigation)
      if (shouldUpdate(state, prevState)) {
        // update
      }
    });
  }, []);
};

// ❌ Was (master) — deactivating nodes ignored
// Component didn't know it was deactivating
shouldUpdateNode("users.list")(newState, oldState); // false on deactivation

// ✅ Now — deactivating nodes receive notification
// Component can perform cleanup
router.shouldUpdateNode("users.list")(newState, oldState); // true on deactivation

Summary

Category Status
Breaking Changes 🔴 CRITICAL (moved to router API)
Fixed bugs ✅ Deactivating nodes now receive notification
New validation ✅ nodeName, toState
Initial navigation ✅ fromState now optional
Root node ✅ Special handling on start
TypeScript ✅ Full typing

Maximum severity: 🔴 CRITICAL

Main change: Function moved from package router5-transition-path to router method router.shouldUpdateNode().

Required:

  1. Remove router5-transition-path dependency
  2. Replace imports with router method

Important bug fixed: Deactivating nodes now receive update notification. This allows components to properly perform cleanup on deactivation.

WARNING: If your code relied on deactivating nodes NOT receiving notification, you will need to change the logic.


5. Performance

The transition path computation is optimized for common cases:

  • Initial navigation (no previous state) — fast path, no comparison needed
  • Force reload (reload: true) — skips parameter comparison
  • Same route with no params — detected without full analysis
  • Typical 1-4 segment routes — optimized path splitting

Impact on shouldUpdateNode

Optimizations in getTransitionPath and nameToIDs directly improve shouldUpdateNode performance:

  • Initial navigation: Up to 5M ops/sec thanks to fast path
  • Typical transitions: 65-81% faster thanks to optimized nameToIDs
  • Validation: Errors detected earlier with clear messages
  • Fixed bugs: Correct handling of edge cases (empty params, root route)