Hot on the heels of today’s post about what a pain in the ass React can be, I managed to sort out most of my confusion and get back to the project after a self imposed hiatus last week. Most of the project thus far has been about setting up the user authentication/authorization portion of the site, along with connecting it to the Firestore database for extended data storage, but with that kinda squared away I decided to move back to actual front-end updates, starting with the main layout wrapper and the horizontal scroller.

Main Layout

The main layout is a wrapper that provides the framework for header and footer elements that should appear on about 90% of the website. As you can see in the image above, that constitutes the logo, navigation, search, and what I’m calling the “auth presence” — the logged in/sign in control at the end of the header row. The banner with the Star Citizen advert from this site is just there to take up space but which might actually become advertising (ideally internal, but I’m not ruling out external). As of this writing only the navigation and the auth presence items are separate components. Eventually the logo (which leads to the homepage) and the search (I have no idea yet how that’ll work) will become their own components.

Right now you’re looking at the “home” component around which the “layout” wrapper is placed. Each page which isn’t the main workspace will have this wrapper applied so as to keep the theme constant. It follows the same higher-order component methodology that I’ve been using for withAuthentication, withAuthorization, and withFirebase.

Horizontal Scroller

This component actually does something, as limited as might be. In the finished site, this horizontal section will feature publicly released modules. The default surfacing will be “newest modules” which simply orders the top 12 in the database by date of public release (when a user opts to make their module public). Users can opt to change the view to display “most popular”. Popularity will be determined by the number of (sorry) “likes” that a module receives. 12 panels fit right now with no scrolling, but I might need to make the panels wider, and I do want to allow actual horizontal scrolling to offer maybe 20-25 options in this space .

This was kind of fun to build because it forced me to think more about how a “module” is represented in data. As the highest level of data structure, a module is basically a “header” for a project. It contains details about the module like the title and banner image, a blurb for interested parties, and important information like the system it’s designed for, the level target, and min and max number of players. A module also has a few flags to represent the state of the project, as well as dates for tracking [1].

I also had to add a new section to my FirebaseDB repo class to start pulling data for things other than profiles. The trick here was to create a pull that would allow me to alternate between “recent” and “popular” as the user clicked the links. To do that, I made three methods: getModulesByRelease, getModulesByLikes, and getModules (which might be renamed since it’s a little too generic). getModules is the method that’s called, and it returns a Promise, which is an async wrapper that returns to the caller immediately, but then notifies the caller when the work it was assigned is complete. I had to do this because this method calls one of the other two depending which link the user clicked. As a result, I had to “do stuff” in the database method to choose the right query to run, and the standard call to Firestore wouldn’t have allowed me to query and delay the return, which I can do with a Promise.

getModules = (selectBy = SELECTBY.RECENT, perPage = 12, bookmark = null) =>{
        return new Promise((resolve, reject)=>{
            switch(selectBy){
                case SELECTBY.RECENT:
                    this.getModulesByRelease(perPage, bookmark)
                    .then((records)=>{
                        resolve(records);
                    })
                    .catch(error=>{
                        console.log(error);
                        reject(error)
                    });
                    break;
                case SELECTBY.POPULAR:
                    this.getModulesByLikes(perPage, bookmark)
                    .then((records)=>{
                        resolve(records);
                    })
                    .catch(error=>{
                        console.log(error);
                        reject(error)
                    });
                    break;
                default:
                    break;
            }
        })
    }

The component itself has a few state variables, a useEffect for listening to some of those state variables for changes, an event handler for clicking the links, and then the rendering of the output. Here’s some highlights for the insomniac crowd (do not read while driving).

State

const [ selectBy, setSelectBy ] = useState(SELECTBY.RECENT);
const [ viewLabel, setViewLabel ] = useState("Newest Modules");

const [ isLoading, setIsLoading ] = useState(true);
const [ lastRecord, setLastRecord ] = useState(null);
const [ results, setResults ] = useState(null);

selectBy is how I’m tracking which version of the data we’re looking at. I’ve set up an enum to handle the representation of the two current states. I also have a state variable for the text displayed at the front of the display selection, which is based on the selectBy value.

The three other states deal with the issues of rendering versus loading and tracking the “last” record in the case of paging. The page may render before we have data to populate the page with, so the isLoading tracks whether or not we even try to display anything. If we try and we don’t have data, we get errors. Data we do receive is stored in results, and the last record in the batch is stored in lastRecord. This record-plus-one becomes the first record in the next batch should we need to add paging to our display.

useEffect

useEffect(()=>{
    firestore.getModules(selectBy, 12, lastRecord)
    .then(records=>{
        if (records.docs.length > 0){
            setLastRecord(records.docs[records.docs.length - 1]);
            setResults(records.docs);

            setIsLoading(false);
        }
    })
    .catch(error => console.log(error));
        

},[selectBy, firestore, lastRecord]);

useEffect is the catch-all React hook for listening for changes in state. The “what do we listen to” is defined at the end of the hook: selectBy, firestore, and lastRecord. I only intend to update selectBy as a result of the user clicking a link to update the display, but React demands that anything that can update the render be included in this list.

Out of the gate, I’m calling getModules with the value of selectBy, the number of records to pull, and the lastRecord if we have one. Remember that getModules returns a Promise, which is where the .then comes in: after making this call execution continues through until the work done by getModules is complete and then (get it?) whatever is in the .then block is run. getModules returns the data from the query, so if there are records in that object, I’ll pull the last record and store it in state, store the results in state, and update the state so the system knows that the app is done loading the data. As React re-renders due to changes in state, this will refresh the component display and hopefully display a list of modules in the scroller.

Handling Links

function handleLink(sb){
    if (sb !== selectBy){
        setLastRecord(null);
        switch(sb){
            case SELECTBY.RECENT:
                setViewLabel("Newest Modules");
                break;
            case SELECTBY.POPULAR:
                setViewLabel("Most Popular");
                break;
            default:
                break;
        }
    }

    setSelectBy(sb);
}

I had an issue which lead to this version of the link handler. Originally when the page loads I got the “newest” records with the Starfinder record being first and the D&D record being second. I purposefully set it up this way in the test data. I also set it up so that the “popular” query would switch the record order. However, clicking on the “popular” link made the D&D record vanish. What’s up with that?

Turned out that if I had just updated the state variable selectBy, the value of lastRecord was still in effect. As this affected paging, I was losing one record when I re-queried and re-sorted. So this version of the handler first compares the desired selectBy to the value stored in state and if they are different, I blank out the lastRecord value. If they are the same I want to keep the lastRecord in case the user needs to page to the next group of 12. I then change the label display and update selectBy using the value sent into the method.

Render

return(
        <div id="home-product-alley">
            <div id="product-alley-nav" className="flex-flex-start">
            <div id="product-alley-sort">{viewLabel}</div>&nbsp;&nbsp;|&nbsp;&nbsp;
            <strong>View:</strong>&nbsp;
            <div className="product-alley-links flex-flex-start">
                <div><Link onClick={() => handleLink(SELECTBY.RECENT)}>Newest Modules</Link></div>
                <div><Link onClick={() => handleLink(SELECTBY.POPULAR)}>Most Popular</Link></div>
            </div>
            </div>

            <div id="product-alley-container" className="flex-flex-start">
            {
                isLoading
                ? null
                : results.map((row)=>{
                    return <ScrollerItem 
                        key={row.id} 
                        productBanner={row.data().banner} 
                        productTitle={row.data().title} 
                        productSystem = {row.data().system} 
                        minPlayers = {row.data().minPlayers.toString()}
                        maxPlayers = {row.data().maxPlayers}
                        minLevel = {row.data().minLevel}/>
                })
            }            
            </div>
        </div>
    )

Most of this is pretty boilerplate. The key parts of note involve the links which, on click, call handleLink to set the new query, and the logic involved in displaying the data. The links gave me a split second issue because normally when calling a method via onClick, it looks something like this:

<Link onClick={handleLink(SELECTBY.RECENT)}>Newest Modules</Link>

The thing is, with React, the above version fires the handleLink when the page refreshes. As this method updates the state variable that useEffect listens to in order to re-render the page, I get the dreaded “too many re-renders” error. In order to stop this I had to turn the onClick handler assignment into a function, which will only fire when the onClick event is raised.

If isLoading is true, we display no content but if it’s false we use map on the data stored in the state variable results. map takes each entry in results and sends select values into a component called ScrollerItem. This component is responsible for the block display that you see in the image above: banner, title, system, and player count. The block still needs some formatting help to get more relevant info in there, but it’ll do for the time being.

That’s pretty much all for now. I’m pretty happy with the result thus far, but as this is “hardcoded data” it makes me wonder if the next step is to create an admin back-end where I’ll be able to set up and manage module data so I can create more test data. Although that will not be the main driver of the site, I suspect that there might be some need for a site admin to get into people’s projects in order to verify any copyright infringement claims or even to fix snarls that might occur in the data, because that’s how the development world rolls.

[1] NoSQL databases are a real headache for someone who is used to the world of relational databases. Because I can’t use a join between tables to pull and transform data I have to think about how to represent information that I need, when I need it. That means purposefully violating relational database best practices, which I am not happy about, but since Firestore is free-as-in-beer for a large number of daily and monthly transactions, I guess I can’t be choosy.

Scopique

Owner and author.

Leave a Reply

Your email address will not be published. Required fields are marked *