Picture this: you're building a workflow editor where users can create nodes and connect them. Each node can reference variables from connected upstream nodes. Your first requirement seems simple enough:
Requirement 1: Show users which variables they can reference in the current node, updating in real-time as connections change.
Easy! You create a hook that calculates available variables based on the current graph structure:
Life is good. Your hook works beautifully for UI components that need real-time updates.
Then Comes a New Requirement
A few days later, your product manager comes with a new request:
Requirement 2: When users save the workflow, validate that all variable references are still valid and show errors for broken ones.
No problem! You already have useNodeVariables, so you use it in your save handler:
functionSaveButton(){const[errors,setErrors]=useState([]);consthandleSave=async()=>{constallNodes=getAllNodes();constvalidationErrors=[];for(constnodeofallNodes){constvariables=useNodeVariables(node.id);// 🚨 This won't work!constbrokenRefs=findBrokenReferences(node,variables);validationErrors.push(...brokenRefs);}setErrors(validationErrors);};return<buttononClick={handleSave}>Save</button>;}
Wait... you can't call hooks inside loops or event handlers! React yells at you with the dreaded Rules of Hooks error.
The First Solution: Move Hook to Component Level
Okay, let's move the hook to the component level:
functionSaveButton(){const[currentNodeId,setCurrentNodeId]=useState(null);constvariables=useNodeVariables(currentNodeId);// 😵 But which node?consthandleSave=async()=>{// How do I get variables for ALL nodes?// I can only get variables for one node at a time...};return<buttononClick={handleSave}>Save</button>;}
This doesn't work either. You need variables for ALL nodes during validation, but your reactive hook only works for one node at a time, and only within the component render cycle.
You realize you have two fundamentally different use cases:
UI Display: Show variables for a specific node, update automatically when graph changes
Event Handling: Get variables for any node on-demand during save validation
Your reactive hook works great for #1 but fails completely for #2. You need a different approach for event handlers—something that can be called imperatively, outside of React's render cycle.
The Solution: Two Different Hooks
You decide to create a second hook specifically for imperative use:
// For UI components - reactive, auto-updatingfunctionuseNodeVariables(nodeId: string){constgraph=useWorkflowGraph();returnuseMemo(()=>{constupstreamNodes=findUpstreamNodes(graph,nodeId);returnextractVariables(upstreamNodes);},[graph,nodeId]);}// For event handlers - imperative, call on-demandfunctionuseGetNodeVariables(){constgetGraph=useGetWorkflowGraph();// Also imperativereturnuseCallback((nodeId: string)=>{constgraph=getGraph();constupstreamNodes=findUpstreamNodes(graph,nodeId);returnextractVariables(upstreamNodes);},[getGraph],);}
Now your save validation works perfectly:
functionSaveButton(){constgetNodeVariables=useGetNodeVariables();consthandleSave=async()=>{constallNodes=getAllNodes();constvalidationErrors=[];for(constnodeofallNodes){constvariables=getNodeVariables(node.id);// ✅ Works in event handlers!constbrokenRefs=findBrokenReferences(node,variables);validationErrors.push(...brokenRefs);}if(validationErrors.length===0){awaitsaveWorkflow();}else{showErrors(validationErrors);}};return<buttononClick={handleSave}>Save</button>;}
Great! You now have:
useNodeVariables for UI components that need automatic updates
useGetNodeVariables for event handlers that need on-demand access
But Wait, There's a Problem: Code Duplication
Looking at your two hooks, you notice they share the same core logic:
This duplication is dangerous. What happens when the business logic changes? You'll need to update it in two places, and it's easy to forget one. Plus, if the logic has bugs, you'll have to fix them twice.
The key insight is to extract the core business logic into pure functions:
// Pure function - no React, no hooks, just logicfunctioncalculateNodeVariables(graph: WorkflowGraph,nodeId: string){constupstreamNodes=findUpstreamNodes(graph,nodeId);returnextractVariables(upstreamNodes);}// Reactive hook - uses pure functionfunctionuseNodeVariables(nodeId: string){constgraph=useWorkflowGraph();returnuseMemo(()=>calculateNodeVariables(graph,nodeId),[graph,nodeId]);}// Imperative hook - uses same pure functionfunctionuseGetNodeVariables(){constgetGraph=useGetWorkflowGraph();returnuseCallback((nodeId: string)=>{constgraph=getGraph();returncalculateNodeVariables(graph,nodeId);},[getGraph],);}
Now your business logic lives in one place! But the benefits go beyond just avoiding duplication:
Benefits of Pure Functions
Easy to test: No React context needed, just pass inputs and check outputs
Cacheable: Can be memoized for performance since output only depends on input
Reusable: Can be used in other contexts (server-side, workers, etc.)
Debuggable: Easy to step through without React complexity
// Easy unit testingdescribe('calculateNodeVariables',()=>{it('should return variables from upstream nodes',()=>{constgraph=createMockGraph();constvariables=calculateNodeVariables(graph,'node-1');expect(variables).toEqual([...]);});});// Can be cached for performanceconstmemoizedCalculateNodeVariables=memoize(calculateNodeVariables);
At this point, you've discovered something important. Inspired by the classic article "What Color is Your Function?", you realize that React hooks also have "colors"—not about sync/async, but about reactivity and imperative access.
🔵 Reactive Hooks (Blue Hooks)
Subscribe to state changes and auto-update
Trigger re-renders when dependencies change
Perfect for UI that needs real-time updates
🔴 Imperative Hooks (Red Hooks)
Return functions that fetch data on-demand
Don't trigger re-renders or subscribe to changes
Perfect for event handlers and business logic
Let's revisit our example: useNodeVariables uses useWorkflowGraph (blue), while useGetNodeVariables uses useGetWorkflowGraph (red). What if we swapped them—using red hooks inside blue hooks, or vice versa?
Experiment 1: Using Red Hook in Blue Hook
// 🔵 Blue hook trying to use red hook ❌functionuseNodeVariables(nodeId: string){constgetGraph=useGetWorkflowGraph();// 🔴 Red hook inside blue hookreturnuseMemo(()=>{constgraph=getGraph();constupstreamNodes=findUpstreamNodes(graph,nodeId);returnextractVariables(upstreamNodes);},[getGraph,nodeId]);}
What goes wrong? The component shows stale data. getGraph() returns a snapshot from when the hook was created, so the UI doesn't update as the graph changes.
Experiment 2: Using Blue Hook in Red Hook
// 🔴 Red hook trying to use blue hook ❌functionuseGetNodeVariables(){constgraph=useWorkflowGraph();// 🔵 Blue hook inside red hookreturnuseCallback((nodeId: string)=>{constupstreamNodes=findUpstreamNodes(graph,nodeId);returnextractVariables(upstreamNodes);},[graph],);}
What goes wrong? Performance suffers. Even if you try to use useMemo to maintain a stable callback reference, since getNodeVariables depends on graph, the callback gets recreated every time the graph updates, causing unnecessary re-renders and resource waste, even though you only need fresh data at the moment the user clicks the save button.
The Color Contamination Problem
Hook "colors" are contagious. Mixing them causes issues:
Red hooks using blue hooks become reactive, losing their on-demand nature
Blue hooks using red hooks become static, losing automatic updates
Key insight: Build red hooks from red hooks, and blue hooks from blue hooks.
LoadingBuild blue hooks from blue hooks, and red hooks from red hooks. Pure functions are colorless and can be safely used by both.
Not Every Hook Needs Both Colors
Another point I want to emphasize is: not every hook needs both colors.
Going back to our useNodeVariables example, you might have noticed a performance issue. Suppose your graph contains node position information, so every time users drag nodes around the canvas, the graph updates, which triggers findUpstreamNodes to recalculate—even though node positions don't affect variable availability.
functionuseNodeVariables(nodeId: string){constgraph=useWorkflowGraph();// Updates on every node drag!returnuseMemo(()=>{constupstreamNodes=findUpstreamNodes(graph,nodeId);// Expensive recalculationreturnextractVariables(upstreamNodes);},[graph,nodeId]);}
To solve this problem, you create an optimized hook useAbstractWorkflowGraph specifically for useWorkflowGraph, filtering out position changes:
functionuseAbstractWorkflowGraph(){constfullGraph=useWorkflowGraph();// This hook returns a graph that ignores position changes// Only updates when node IDs, connections, or node data changereturnuseCustomCompareMemo(()=>fullGraph,[fullGraph],(prevGraph,nextGraph)=>{// Custom comparison: only care about structural changes, not positionsreturnisGraphStructurallyEqual(prevGraph,nextGraph);},);}// Now our variable hook doesn't recalculate on position changesfunctionuseNodeVariables(nodeId: string){constgraph=useAbstractWorkflowGraph();// Only updates on structural changesreturnuseMemo(()=>{constupstreamNodes=findUpstreamNodes(graph,nodeId);returnextractVariables(upstreamNodes);},[graph,nodeId]);}
After careful consideration, you'll find: useAbstractWorkflowGraph doesn't need an imperative version. Why?
It's a reactive optimization: This hook exists specifically to reduce unnecessary reactive updates in UI components
Imperative scenarios don't have this problem: When you call getWorkflowGraph() imperatively in an event handler, you're not subscribing to updates, so frequent changes don't matter—you only get data when you explicitly request it
Imperative scenarios can use the base hook: useGetWorkflowGraph() is sufficient for on-demand access since event handlers don't need to care about filtering out position changes
This reveals an important principle: not every hook needs both colors. Some hooks are naturally suited to only one color based on their purpose.
useAbstractWorkflowGraph is naturally blue—it exists specifically to optimize reactive updates in UI components. An imperative version would be meaningless because red scenarios don't have the "frequent update" problem this hook solves. They can just use useGetWorkflowGraph() directly when they need data.
Naming Conventions: Making Intent Clear
To make hook colors obvious at a glance, we should follow consistent naming conventions:
One-time operations that don't need continuous updates
// ✅ Event handlingfunctionSaveButton(){constvalidateAndSave=useValidateAndSave();return<buttononClick={()=>validateAndSave()}>Save</button>;}// ✅ Side effectsfunctionuseAutoSave(){constgetWorkflowData=useGetWorkflowData();useEffect(()=>{consttimer=setInterval(()=>{constdata=getWorkflowData();if(data.isDirty)saveToServer(data);},30000);return()=>clearInterval(timer);},[getWorkflowData]);}
What started as a simple feature request—showing available variables to users—led us to discover a fundamental pattern in React architecture. Just like the original "What Color is Your Function?" article revealed the hidden complexity of async/sync functions, React hooks also have "colors".
The key insight: hooks inherit their color. Blue hooks compose with other blue hooks, and red hooks compose with other red hooks. Mix them incorrectly, and you get performance issues, stale data, and subtle bugs. Every hook you write is making an implicit choice about reactivity that will constrain every hook that depends on it.
So next time you're designing hooks, maybe ask yourself: