{{announcement.body}}
{{announcement.title}}

Recursive JSX Rendering of A Deeply Nested Travel Gallery

DZone 's Guide to

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.

· Web Dev Zone ·
Free Resource

Suppose 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:

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:

JavaScript
 




xxxxxxxxxx
1
18


 
1
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
2
import 'react-tabs/style/react-tabs.css';
3
 
          
4
export default () => (
5
  <Tabs>
6
    <TabList>
7
      <Tab>Title 1</Tab>
8
      <Tab>Title 2</Tab>
9
    </TabList>
10
 
          
11
    <TabPanel>
12
      <h2>Any content 1</h2>
13
    </TabPanel>
14
    <TabPanel>
15
      <h2>Any content 2</h2>
16
    </TabPanel>
17
  </Tabs>
18
);


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.

JavaScript
 




xxxxxxxxxx
1
21


1
import React from 'react';
2
import { CarouselProvider, Slider, Slide, ButtonBack, ButtonNext } from 'pure-react-carousel';
3
import 'pure-react-carousel/dist/react-carousel.es.css';
4
 
5
export default class extends React.Component {
6
  render() {
7
    return (
8
      <CarouselProvider
9
        naturalSlideWidth={100}
10
        naturalSlideHeight={125}
11
        totalSlides={3}
12
      >
13
        <Slider>
14
          <Slide index={0}>I am the first Slide.</Slide>
15
          <Slide index={1}>I am the second Slide.</Slide>
16
          <Slide index={2}>I am the third Slide.</Slide>
17
        </Slider>
18
      </CarouselProvider>
19
    );
20
  }
21
}



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:

JavaScript
 




x


1
gallery:[
2
      {
3
        Russia:[{'Vladimir Area':[
4
          {Vladimir:[
5
            {title:"Uspenskii Cathedral",
6
             screen:"uspenski.JPG",
7
             description:
8
       "The cathedral was built in 12th century by Duke Andrey Bogoliubov."
9
            },
10
            {title:"Saint Dmitry Cathedral",
11
             screen:"saints.JPG",
12
             description:
13
       "Saints of Dmitrov cathedral. The cathedral was built in 12th century by Duke Andrey Bogoliubov."
14
                  },
15
                ]
16
              },
17
          {Bogoliubovo:[...]}
18
          ]
19
        },
20
        {'Moscow Area':[{Moscow:[...]},{Kolomna:[...]}]}
21
      ]
22
    },
23
    {Spain:[...]},{Italy:[...]}
24
]


The pattern is clear. For the tree nodes:

JSON
 




xxxxxxxxxx
1


1
{'Area':[
2
  {'Sub Area 1':[...]},
3
  {'Sub Area 2':[...]}
4
  ...]
5
}


For the leaves:

JSON
 




x





1
[
2
  {
3
    title:'Title1',
4
    screen:'photo1.jpeg',
5
    description:'Description of photo 1'
6
  },
7
  ...
8
]


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:

JavaScript
 




xxxxxxxxxx
1
34


1
const isLeaf=(arr,property)=>{
2
        if(Array.isArray(arr) && arr[0].hasOwnProperty(property)) return true;
3
        else return false;
4
      }
5
 
          
6
const giveJSXForLeaf = (arr,pathStack)=>{
7
        return   <Place list={arr} dirName={pathStack.join('/')}></Place>
8
      }
9
 
          
10
const giveGalleryJSX=(arr,pathStack)=>{
11
       if(isLeaf(arr,'screen')) return giveJSXForLeaf(arr,pathStack);
12
 
          
13
        const jsxResult = (()=>{ return(
14
          <Tabs>
15
            <TabList>
16
             {arr.map((el,ind)=>{ 
17
              return (<Tab key={ind} ><h4>{Object.keys(el)[0]}</h4></Tab>) })
18
                        }
19
            </TabList>
20
            {arr.map((el,ind)=>{ 
21
               pathStack.push(Object.keys(el)[0])
22
               const res = (()=>{ 
23
                 return (
24
                   <TabPanel key={ind}>
25
                   { giveGalleryJSX(Object.values(el)[0],pathStack) }
26
                   </TabPanel>); }
27
                        )();
28
               pathStack.pop();
29
               return res;
30
           })}
31
          </Tabs>
32
         ); })();
33
        return jsxResult;
34
}


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,pathStack(lines 5-8) generates the JSX code for a leaf; this code is a <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,pathStack 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 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:

JavaScript
 




xxxxxxxxxx
1
13


 
1
 <Slider className="Place-slider" >
2
    {this.props.list.map((el,ind)=>{
3
        return(
4
           <Slide index={ind} key={ind}> 
5
             <h3>{el.title}</h3>
6
             <Image  className="Place-image"
7
              src={`/images/travel-images/${this.props.dirName}/${el.screen}`}                   
8
              />
9
           </Slide>
10
            );
11
       })              
12
   }
13
</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:

Travel gallery

Let's see why this code works.

Explanation

The gallery data structure can be parsed by the following grammar:

Plain Text
 




xxxxxxxxxx
1


 
1
Arr -> [List];
2
List -> Obj,List;
3
Obj -> {Key:Arr};
4
Obj -> {Key:Cnt}


 Here Arr, List, and Obj are non-terminals. Key (area name string) and Cnt (leaf array) are terminals:

JavaScript
 




xxxxxxxxxx
1


1
Cnt = [
2
  {
3
    title:'Title1',
4
    screen:'photo1.jpeg',
5
    description:'Description of photo 1'
6
  },
7
  ...
8
]


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

Topics:
nested gallery, photo gallery, react, recursive jsx

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}