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:
TypeErrorfor 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:
TypeErrorif not an object withnamefield
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:
fromStaterequired — 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:
fromStateoptional — supports initial navigation- Validation of
nodeNameandtoState - 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-pathdependency from package.json - Replace
import shouldUpdateNode from 'router5-transition-path'withrouter.shouldUpdateNode() - IMPORTANT: Check logic that relied on
falsefor deactivating nodes - Ensure
nodeNameis always a string - Ensure
toStateis 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-pathto router methodrouter.shouldUpdateNode().Required:
- Remove
router5-transition-pathdependency- 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)