React exception while prop drilling

3 weeks ago 10
ARTICLE AD BOX

The activeItem and setActiveItem props are not passed down to all descendent components. Specifically FolderItem not passing them down to subsequent FileItem components.

When you lifted the activeItem state up to the parent App component, you need to ensure that both FolderItem and FileItem continue to pass the activeItem and setActiveItem props down to their respective recursively rendered children.

I recommend giving your data unique GUIDs so they are easily identifiable regardless where they are located/defined in the data tree structure.

import { nanoid } from "nanoid"; const files = { name: "root", type: "directory", id: nanoid(), children: [ { name: "folder 1", type: "directory", id: nanoid(), children: [ { name: "file 1", type: "file", id: nanoid() }, { name: "file 2", type: "file", id: nanoid() }, { name: "file 3", type: "file", id: nanoid() }, ], }, { name: "folder 2", type: "directory", id: nanoid(), children: [ { name: "file 1", type: "file", id: nanoid() }, { name: "file 2", type: "file", id: nanoid() }, { name: "file 3", type: "file", id: nanoid() }, ], }, { name: "folder 3", type: "directory", id: nanoid(), children: [ { name: "file 1", type: "file", id: nanoid() }, { name: "file 2", type: "file", id: nanoid() }, { name: "file 3", type: "file", id: nanoid() }, ], }, ], }; function App() { const [activeItem, setActiveItem] = useState(null); return ( <div className="App"> <FileExplorer files={[files]} activeItem={activeItem} // <-- setActiveItem={setActiveItem} // <-- /> </div> ); } function FileExplorer({ files, activeItem, setActiveItem }) { return ( <div style={{ marginLeft: "10px" }}> {files.map((file, index) => file.type === "directory" ? ( <FolderItem key={file.name} folder={file} activeItem={activeItem} // <-- setActiveItem={setActiveItem} // <-- /> ) : ( <FileItem key={file.name} id={file.id} // <-- pass GUID here instead of name file={file} activeItem={activeItem} // <-- setActiveItem={setActiveItem} // <-- /> ) )} </div> ); } function FolderItem({ folder, activeItem, setActiveItem }) { const [isOpen, setIsOpen] = useState(false); const toggleOpen = () => setIsOpen(isOpen => !isOpen); return ( <div> <div onClick={toggleOpen} style={{ cursor: "pointer" }}> {isOpen ? "📂" : "📁"} {folder.name} </div> {isOpen && ( <FileExplorer files={folder.children} activeItem={activeItem} // <-- setActiveItem={setActiveItem} // <-- /> )} </div> ); } function FileItem({ id, file, activeItem, setActiveItem }) { function handleClick() { try { setActiveItem(id); } catch (error) { console.log(error); } } return ( <div onClick={handleClick} className={`highlight-div ${id === activeItem ? "active" : ""}`} > 📄 {file.name} </div> ); }

Improved Suggestion using React Context

To help resolve issues with "props drilling" React provides a solution via providing a React context.

Example:

Create a context and provide component and consumer hook:

const fileExplorerContext = createContext(null); const useFileExplorer = () => { const context = useContext(fileExplorerContext); if (!context) { throw new Error("Must be used within a FileExplorerProvider!"); } return context; }; const FileExplorerProvider = ({ children }) => { const [activeItem, setActiveItem] = useState(null); return ( <fileExplorerContext.Provider value={{ activeItem, setActiveItem }}> {children} </fileExplorerContext.Provider> ); };

No you no longer need to pass activeItem and setActiveItem through all the component layers. Render FileExplorer within the FileExplorerProvider and access the context value via useFileExplorer in the leaf node components, i.e. in FileItem.

function App() { // const [activeItem, setActiveItem] = useState(null); return ( <div className="App"> <FileExplorerProvider> // <-- provides context value <FileExplorer files={[files]} /> </FileExplorerProvider> </div> ); } function FileExplorer({ files }) { return ( <div style={{ marginLeft: "10px" }}> {files.map((file, index) => file.type === "directory" ? ( <FolderItem key={file.name} folder={file} /> ) : ( <FileItem key={file.name} id={file.id} file={file} /> ) )} </div> ); } function FolderItem({ folder }) { const [isOpen, setIsOpen] = useState(false); const toggleOpen = () => setIsOpen(isOpen => !isOpen); return ( <div> <div onClick={toggleOpen} style={{ cursor: "pointer" }}> {isOpen ? "📂" : "📁"} {folder.name} </div> {isOpen && ( <FileExplorer files={folder.children} /> )} </div> ); } function FileItem({ id, file }) { const { activeItem, setActiveItem } = useFileExplorer(); // <-- Access context value function handleClick() { try { setActiveItem(id); } catch (error) { console.log(error); } } return ( <div onClick={handleClick} className={`highlight-div ${id === activeItem ? "active" : ""}`} > 📄 {file.name} </div> ); }
Read Entire Article