This is a react app I created as a test assignment to show my front-end skills. The application demonstrates working with a tree menu, which is often used in documentation sites. It displays navigation, switches links and shows selected page (for now the app shows only page title).
How to start application locally:
nvm use
npm ci
npm run dev
The requirements for the task determined the tech stack: use react and css modules, fetch data asynchronously, do not use any libraries for building menus and trees.
I used vite and vitest for building and testing.
So, from a bird's eye view, the structure is as follows:
- main.tsx is an entrypoint. It finds the root DOM element and render the App.
- app/ contains all app-level code
- features/ contains feature-specific code, directory per feature:
- components/ contains all bricks that are not bound to the app domain, e.g. Layout or Transitions.
- hooks/ contains any general logic shared between components, like:
- useRequestWithPlaceholder to fetch data. It works like an extremely simplified idea of react-query, that hook returns state of a query with some placeholders and flags
- useFilter holds shared logic for filter inputs
- toc.json is a data structure that API sends to the client. The structure was defined by the task requirements
Components composition:
- So, the Root component fetches the data from “API” (a static JSON file, actually), detects current url and renders the
Menu
by passing props to it. - The Menu component wraps all its content to the MenuProvider that holds menu data, filtering state and methods.
- Then the Menu renders menu
Filter
and menuList
components. - The Filter component just extracts data and methods from the menu context through hooks and render the Input presenter.
- The List component render the menu tree from the top level using
Section
component. - The Section render items on a specific level with
Item
component, and, if an item has children, renders nested sections recursively. - The Item component has two modifications:
- plain
Item
simply renders a single page in the menu with the desired styles and attributes - the
ItemToggle
renders anItem
with a chevron and nested section. Independently manages the open/closed state so that only this subtree is rerendered when the state changes
- plain
Core logic:
- So how does the Section know which items need to be rendered? The top
Section
is rendered by the List component, which passes theparentId=''
property to theSection
. - Then,
Section
uses the useSectionItems hook, which extracts data from the MenuContext and passes it to thebuildMenuSection
method, gets items from it, and returns them back to theSection
. - The buildMenuSection is as plain as its name:
- it takes
parentId
and finds its direct children in the API data - is takes
filter
, it sifts out mismatched pages - it takes
breadcrumbs
(path to the active page in a tree) and adjust item highlight mode based on active page url and its ancestors
- it takes
Finding ancestors of a current page:
- To properly highlight menu sections we have to know current page, and its ancestors up to the root. The path to the current page from the root of the tree is called “breadcrumbs”.
- The getBreadCrumbs methods takes the current page URL and ToC data, and simply traverse from the current page up to the root, collecting all visited pages in a breadcrumbs array.
- MenuProvider takes current page URL and all ToC data from the API, calls
getBreadCrumbs
method and store the result in the context. If the current URL changes, we need to rebuild the breadcrumbs as well. And vice versa, if the path remains the same, we don't need to rebuild the breadcrumbs. So, we can store computed breadcrumbs in context to reuse them when highlighting menu sections.
Filtering tree:
- When a user types text, we should find all the pages suitable for the input. Even if they are somewhere deep in the tree. So we have to inspect all pages in the tree.
- We can’t draw items “hanging in the air”, so if we render a page, we have to render all its ancestors as well.
- The filterTreeNodes method takes the ToC data and the filter text, and checks each page if it matches. If so, adds the page to the result set, and walks the tree up to the root, adding all parents pages to the result set as well.
- The MenuProvider uses useFilter hook to manage search results. So, when user types a text, the
useFilter
hook calls thefilterTreeNodes
method and store its results. TheMenuProvider
holds the results in its context. - So, when the
Section
component is rendered, it passes data from the MenuContext to thebuildMenuSection
, including the set of filtered pages. - And the buildMenuSection simply checks the section pages if they are in the filtered set.
Here is a diagram of the components:
In the project directory, you can run:
Runs the app in the development mode.
Open http://localhost:5173 to view it in the browser.
The page will reload if you make edits.
Uses Vite.
Serves the app in the production mode.
Open http://localhost:4173 to view it in the browser.
Uses Vite.
Launches the test runner in the interactive watch mode.
Uses Vitest and React Testing Library.
Builds the app for production to the build
folder.
Uses Vite.
Runs code linters to check dumb errors and code style.
Fixes code issues and style.