DEV Community

Cover image for Stop Prop Drilling: Rendering Deeply Nested Dynamic Roadmaps in React and Node.js
Teja
Teja

Posted on

Stop Prop Drilling: Rendering Deeply Nested Dynamic Roadmaps in React and Node.js

Rendering flat lists in React is trivial. Rendering a deeply nested, dynamically generated UI tree without crashing the browser is a completely different nightmare.

When I was building Skill Path—an application that generates dynamically structured learning roadmaps—I quickly ran into major state management bottlenecks. Passing data down seven levels of a visual roadmap creates an unmaintainable mess of prop drilling.

Here is the architecture I ended up using to bridge a Flask algorithm, a Node.js API, and a React frontend utilizing CSS Grid and glassmorphism.

The Two-Brain Backend Pipeline
Instead of forcing one framework to do everything, I split the backend into two distinct services.

The Engine (Flask): Python is significantly better for handling the complex algorithms needed to build the learning paths. The Flask service generates the raw node relationships.

The API Gateway (Node.js/Express): Node.js handles the client requests, authentication, and caching. It fetches the heavy JSON from Flask, flattens it, and sends it to the frontend.

The JSON Payload Structure
The absolute best trick to making React render fast is formatting your backend payload as a flat dictionary with parent-child ID references, rather than a deeply nested JSON object.

JSON
// How our Node.js server formats the payload for React
{
"roadmap_id": "fullstack_2026",
"nodes": {
"node_1": { "title": "HTML/CSS", "children": ["node_2", "node_3"] },
"node_2": { "title": "CSS Grid", "children": [] },
"node_3": { "title": "JavaScript Basics", "children": ["node_4"] }
}
}
The Frontend: Avoiding the Re-render Trap
If you try to map over a massive nested array in React, any state change (like clicking "Mark as Complete" on a skill) triggers a re-render of the entire tree.

To fix this, I used a centralized state store and rendered individual node components that only care about their specific ID.

The Component Setup
Instead of passing the entire tree down, the top-level component just renders the root node.

JavaScript
import React, { useState } from 'react';
import RoadmapNode from './RoadmapNode';

const SkillPathViewer = ({ roadmapData }) => {
// roadmapData is the flattened JSON from our Node.js backend
const [completedNodes, setCompletedNodes] = useState(new Set());

const toggleComplete = (nodeId) => {
setCompletedNodes(prev => {
const newSet = new Set(prev);
newSet.has(nodeId) ? newSet.delete(nodeId) : newSet.add(nodeId);
return newSet;
});
};

return (


{/* We only render the starting node. It handles its own children. */}


);
};
The Recursive Layout
Because the layout relies on a modern SaaS aesthetic, I used CSS Grid combined with a glassmorphism wrapper for the UI cards. The component is recursive—it calls itself if it detects children.

JavaScript
const RoadmapNode = ({ nodeId, data, toggleComplete }) => {
const node = data[nodeId];
if (!node) return null;

return (


toggleComplete(nodeId)}>

{node.title}


  {/* Recursively render children in a CSS Grid row */}
  {node.children.length > 0 && (
    <div className="grid-children-container">
      {node.children.map(childId => (
        <RoadmapNode data={data} key={childId} nodeId={childId} toggleComplete={toggleComplete} />
      ))}
    </div>
  )}
</div>

);
};
Why This Works
By flattening the state shape on the Node.js server before it ever hits the browser, React only has to execute shallow lookups. Combining this with CSS Grid allowed the visual tree to expand dynamically without requiring messy JavaScript height calculations.

Top comments (1)

Collapse
 
linb profile image
Jack Lee

Nice writeup, the flat dictionary with parent-child id refs is basically the normalized state pattern (same idea redux normalization use) and people underrate it a lot for tree UI.

one thing i would add: even with flat state, your recursive RoadmapNode will still re-render the whole subtree when completedNodes change, because the Set is a new reference every toggle and you pass toggleComplete down. two small fix that help a lot:

wrap RoadmapNode in React.memo, so a node only re-render when its own props change
useCallback on toggleComplete so the reference is stable, otherwise memo is useless
and instead of passing the whole completedNodes Set down, let each node read only its own isComplete boolean (from context or a selector). then clicking one node only re-render that one node, not siblings.
for really deep/huge tree you can also look at react-window for virtualization, but for roadmap size your approach is good enough.

the Flask + Node split is interesting, did you measure the latency of the extra hop? curious if caching in node is enough.