type
Post
status
Published
date
Mar 4, 2023
slug
fe-menu-subtree
summary
We will look at how to implement iOS/Android submenu for the web, focusing on maintainability, easy-to-use, and scalability.
And do a little research about more complex implementations.
password
tags
front-end
coding
category
dev
icon
Property
Mar 30, 2023 08:31 AM
Menu Subtree structure
Such a tree structure depends on how we will implement keyboard navigation.
Keyboard navigation
Let’s assume it is just a standard navigation
- Tab first-level ▶️ item navigation
- We’ll loop over the same menu if we are inside a tree.
- Space/Enter ▶️ go 1 level deeper
- Esc ▶️ go 1 level upper or close the menu?
No matter what we will do here, it will be a tree-based data structure.
What can’t we escape?
Tree Level Data Structure - We must keep this data structure in mind since it’s “what it looks like.”
Multi-level tree-based data structures must “answer”(read implement) 3 questions(getters/selectors):
- Select a next (active) element
- Select a previous element
- Select parent Element
State
➕ State for Active Element
So knowing all of those, we can decide how “scalable” solution we need here.
What could be our options
Let’s review a couple of options
- Tree based Config
- Flat Config
- Scalable Option ▶️ Tree Data Structure
Tree based Config
The very first idea is to find a solution that uses nested data structures and loops over them. However, such solutions often increase complexity with little gain.
In my experience, it requires a deep focus for everyone who ever needs to touch this code since it’s a MUST to keep children's interactions and to figure out how everything is connected. Since it's loop over loop over the loop with a little magic possible like Recursion.
I’m sorry to say this, but most of the time, it looks overcomplicated or it was really better to use a well-known tree data structure.
Most of the time it looks overcomplicated, or using a well-known tree data structure was better.
And it's mainly about Declarative vs. Imperative way of expressing it.
Since we are in React world, Declarative solutions look pretty natural
Summary
➕ Config in 1 place
➖ High complexity from the start
➖ Harder to maintain
➖ Usually, our data structure is bounded with Components/Framework things
Good for: Write once, hopefully never touch in the future
Flat Config, aka Declarative Approach
In this implementation, we will build our tree in the way of “ATM”-like level logic, similar to a Finite State Machine. So we expect the user will only go the way we lead him, and a user can’t go in another direction.
And we’ll implement something that “looks and feels” like a tree. However, it will not 😃.
Our state keeps “Active Element” and Queue of previous elements.
When we go 1 level deeper, we push 1 more element to previous elements, so all our steps are “recorded”.
FYI: This example is not complete on purpose so that you can enhance the logic in a way you need to
import { useState } from "react"; const NavMenu = () => { const [activeMenuState, setActiveMenuState] = useState(null); const [prevActiveMenuState, setPrevActiveMenuState] = useState([]); const setActiveMenu = (name) => (event) => { setPrevActiveMenuState((prevElements) => [ ...prevElements, activeMenuState ]); setActiveMenuState(name); }; const getPrevActiveMenuState = () => { const copyPrevActivMenuState = prevActiveMenuState.slice(); const prevItem = copyPrevActivMenuState.pop(); setPrevActiveMenuState(copyPrevActivMenuState); setActiveMenuState(prevItem); }; const isOldDocsActive = activeMenuState === "Old Docs"; const isDocsActive = activeMenuState === "Docs"; return ( <MenuTree> {!activeMenuState && ( <> <MenuName onClick={setActiveMenu("resources")}>Resources</MenuName> <MenuName onClick={setActiveMenu("Docs")}>Docs</MenuName> <MenuName onClick={setActiveMenu("Old Docs")}>Old Docs</MenuName> </> )} <SubMenu isActive={activeMenuState}> <button onClick={getPrevActiveMenuState}>GO BACK</button> </SubMenu> <SubMenu isActive={isOldDocsActive}> <MenuName>Engineering blog</MenuName> <MenuName>Culture blog</MenuName> </SubMenu> <SubMenu isActive={isDocsActive}> <MenuName>Docs</MenuName> </SubMenu> <SubMenu isActive={activeMenuState === "resources"}> <MenuName onClick={setActiveMenu("Blog")}>Blog</MenuName> </SubMenu> <SubMenu isActive={activeMenuState === "Blog"}> <MenuName>Engineering blog</MenuName> <MenuName>Culture blog</MenuName> <MenuName onClick={setActiveMenu("React Docs")}>React Docs</MenuName> </SubMenu> <SubMenu isActive={activeMenuState === "React Docs"}> <MenuName>Hooks</MenuName> </SubMenu> </MenuTree> ); }; const MenuTree = ({ children, ...props }) => { return <div {...props}>{children}</div>; }; const MenuName = ({ children, ...props }) => { return ( <li {...props}> <a href="#">{children}</a> </li> ); }; const SubMenu = ({ children, ...props }) => { if (!props.isActive) { return null; } return <div {...props}>{children}</div>; };
Summary
➕ Our tree is flat, easier mental model
➕ Easy to maintain, easy to scale
➖ Not a Real tree data structure
Potential Enhancements
- Create Context for State and Selectors
- Consume this state in nested Components
- Add Focus Trap for entire tree for Mobile version
- Set autofocus for back button on menu open
Scalable Option ▶️ Tree Data Structure
Do we want to implement an Actual Tree Data Structure that knows how to do all those steps?
It will require knowledge of Trees 🌲 Data Structures
Why overkill?
For this particular case, it looks like overkill.
We don't need complex navigations here (based on how we limit keyboard requirements)
Summary
Choose what you need based on your case