Recursive JSX Rendering of A Deeply Nested Travel Gallery
In this post, I demonstrate a concise React code for a deeply nested and easily extensible travel gallery.
Join the DZone community and get the full member experience.
Join For FreeSuppose you like to travel and have collected a large photo gallery. The photos are stored in a tree folder structure, where locations are structured according to the geography and administrative division:
The actual photos of particular places are stored in the corresponding leafs of the tree. Different branches of the tree may have different height. You want to show these photos in your portfolio website that is made on React. Each photo should have a title and description. Also, the gallery should be easily extendable with new locations and photos.
Problem
To show this gallery, we need two React libraries: react-tabs and pure-react-carousel or their equivalents. The tabs library provides a tree structure for the gallery locations, while the carousel shows the photos of a particular place.
Let's take a look at a simple tabs example:
xxxxxxxxxx
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';
export default () => (
<Tabs>
<TabList>
<Tab>Title 1</Tab>
<Tab>Title 2</Tab>
</TabList>
<TabPanel>
<h2>Any content 1</h2>
</TabPanel>
<TabPanel>
<h2>Any content 2</h2>
</TabPanel>
</Tabs>
);
For every tab, the tab title is a <Tab>
element of the <TabList>
, and the tab content is placed into a <TabPanel>
.
To add an extra level of tabs, one needs to insert a whole <Tabs>
block into the appropriate <TabPanel>
. Clearly, such construct becomes very cumbersome and hard to extend if the gallery becomes sufficiently deep. So we need to automatically inject JSX code into the <TabPanel>
elements and automatically pass the appropriate titles to the <Tab>
elements.
React carousel works as follows.
xxxxxxxxxx
import React from 'react';
import { CarouselProvider, Slider, Slide, ButtonBack, ButtonNext } from 'pure-react-carousel';
import 'pure-react-carousel/dist/react-carousel.es.css';
export default class extends React.Component {
render() {
return (
<CarouselProvider
naturalSlideWidth={100}
naturalSlideHeight={125}
totalSlides={3}
>
<Slider>
<Slide index={0}>I am the first Slide.</Slide>
<Slide index={1}>I am the second Slide.</Slide>
<Slide index={2}>I am the third Slide.</Slide>
</Slider>
</CarouselProvider>
);
}
}
The <CarouselProvider>
is supplied with props of the ratio of slide width, height, and the number of slides. <Slider>
wraps a list of individual <Slide>
elements.
To show our photos, every photo of a particular location is placed as a <src>
link to an individual <Slide>
. Apparently, we need to automatically construct and pass to the carousel the path to every photo, the photo's title, and description.
Let's see how to solve these problems.
Solution
First, let's pack our data to the following data structure:
x
gallery:[
{
Russia:[{'Vladimir Area':[
{Vladimir:[
{title:"Uspenskii Cathedral",
screen:"uspenski.JPG",
description:
"The cathedral was built in 12th century by Duke Andrey Bogoliubov."
},
{title:"Saint Dmitry Cathedral",
screen:"saints.JPG",
description:
"Saints of Dmitrov cathedral. The cathedral was built in 12th century by Duke Andrey Bogoliubov."
},
]
},
{Bogoliubovo:[ ]}
]
},
{'Moscow Area':[{Moscow:[ ]},{Kolomna:[ ]}]}
]
},
{Spain:[ ]},{Italy:[ ]}
]
The pattern is clear. For the tree nodes:
xxxxxxxxxx
{'Area':[
{'Sub Area 1':[ ]},
{'Sub Area 2':[ ]}
]
}
For the leaves:
x
[
{
title:'Title1',
screen:'photo1.jpeg',
description:'Description of photo 1'
},
]
It is easy to parse and generate JSX code for this data structure in a top-down recursive manner. Later in the Explanation section, we will see why. The parser and code generator is:
xxxxxxxxxx
const isLeaf=(arr,property)=>{
if(Array.isArray(arr) && arr[0].hasOwnProperty(property)) return true;
else return false;
}
const giveJSXForLeaf = (arr,pathStack)=>{
return <Place list={arr} dirName={pathStack.join('/')}></Place>
}
const giveGalleryJSX=(arr,pathStack)=>{
if(isLeaf(arr,'screen')) return giveJSXForLeaf(arr,pathStack);
const jsxResult = (()=>{ return(
<Tabs>
<TabList>
{arr.map((el,ind)=>{
return (<Tab key={ind} ><h4>{Object.keys(el)[0]}</h4></Tab>) })
}
</TabList>
{arr.map((el,ind)=>{
pathStack.push(Object.keys(el)[0])
const res = (()=>{
return (
<TabPanel key={ind}>
{ giveGalleryJSX(Object.values(el)[0],pathStack) }
</TabPanel>); }
)();
pathStack.pop();
return res;
})}
</Tabs>
); })();
return jsxResult;
}
Here, isLeaf(arr,property)
method (lines 1-4) checks if the node is a leaf. The arr
argument is an array of objects. If the objects have a key named property
, then the parser reached a leaf. In this case, we check for the 'screen' key.
The method giveJSXForLeaf(arr,
(lines 5-8) generates the JSX code for a leaf; this code is a pathStack
) <Place>
component with a carousel. The <Place>
component receives two props. The first one is arr
- an array of objects with titles, file names, and descriptions. The second one is an array of strings pathStack
: every string is a folder of the path to the photo files. The folders are joined (line 7) to form a path to the files.
Next, the method giveGalleryJSX(arr,pathStack)
(lines 14-34) generates the JSX code for a node; the method recursively calls itself. The method emits a <Tabs>
block (lines 15-32). The first argument is arr
- an array of objects, either of a node or a leaf. The second argument pathStack
is an array of strings, where every string is a folder of the path to the photo files - the same as in the giveJSXForLeaf
method.
The giveGalleryJSX(arr,
calls itself recursively at line 25. Before the call, a new folder name is pushed to the array pathStack
)
and popped from the array ones the recursive call returns. So, a pathStack
pathStack
is formed up and, after being joined, provided to the <Place>
component as a prop.
In the <Place> component the two props are used to render a carousel:
xxxxxxxxxx
<Slider className="Place-slider" >
{this.props.list.map((el,ind)=>{
return(
<Slide index={ind} key={ind}>
<h3>{el.title}</h3>
<Image className="Place-image"
src={`/images/travel-images/${this.props.dirName}/${el.screen}`}
/>
</Slide>
);
})
}
</Slider>
Here the this.props.list
is an array of leaf objects, each with a title, file name, and description. this.props.dirName
is a joined pathStack
array.
As a result, we get this:
Let's see why this code works.
Explanation
The gallery data structure can be parsed by the following grammar:
xxxxxxxxxx
Arr -> [List];
List -> Obj,List;
Obj -> {Key:Arr};
Obj -> {Key:Cnt}
Here Arr
, List
, and Obj
are non-terminals. Key
(area name string) and Cnt
(leaf array) are terminals:
xxxxxxxxxx
Cnt = [
{
title:'Title1',
screen:'photo1.jpeg',
description:'Description of photo 1'
},
]
It is easy to see that this is a LL(1) grammar. Indeed, we can distinguish between the Obj -> {Key:Arr}
and Obj -> {Key:Cnt}
productions if we use just the single non-recursive method isLeaf(arr,property)
. This check is made at the start of the giveGalleryJSX
, so we don't go deeper into the recursion. This is basically the same as checking for a first right-side terminal in a classical LL(1) grammar.
According to the compiler theory, LL(1) grammar can be parsed with a top-down recursive parser. Also, the compiler theory says, that we need a production function for every production rule. The rules 1 and 2 can be covered by a single JS array method Arr.map(...), rule 3 is covered by the giveGalleryJSX
, while rule 4 is covered by giveJSXForLeaf
.
This analysis demonstrates that our code should work and we don't miss anything.
Conclusion
In this post, we saw a simple top-down recursive algorithm to emit JSX code to build a deeply nested photo gallery with react-tabs and pure-react-carousel libraries
Opinions expressed by DZone contributors are their own.
Trending
-
Effortlessly Streamlining Test-Driven Development and CI Testing for Kafka Developers
-
Never Use Credentials in a CI/CD Pipeline Again
-
Seven Steps To Deploy Kedro Pipelines on Amazon EMR
-
Introduction To Git
Comments