Staff Picks: Refactoring a Page into an App

With hundreds of staff recommendations of books, Staff Picks now features 2016’s selection of Children’s 100 Books for Reading and Sharing and Best Books for Teens. Staff Picks started as a monthly selection of books—the first set of recommendations was released in August of 2015—chosen by NYPL staff members, from any time, which you can filter by selecting tags associated with the books.

Staff Picks drew inspiration from the previous lists of Children’s Books for Reading and Sharing. The 2013 list was created by NYPL Labs using NPR’s open source Book Concierge app and the 2014 list was then created by the Digital Experience team. The 2013 and 2014 lists were standalone apps and but starting from the 2015 list of books, they will be integrated into the dynamic system of Staff Picks.

On the technical side, the Digital Experience group had to determine how to update our first React application to organize the new data. We learned new React programming techniques from Book Lists and the site-wide header that we wanted to incorporate into Staff Picks. This blog post is targeted to those who are looking into making small and large internal changes to their React applications, and will cover how we updated the app architecture, updated configurations, and updated the app’s routes. If you want to find out more about how we initially used React for this app, read about it in this blog post.

Codebase and configuration

Staff Picks app structure

The application architecture dictates workflow and component structure. At first, we went with an architecture from a boilerplate. Although it worked well because we got off the ground quickly, we found out that it wasn’t flexible enough to extend configurations we wanted to make. At the same time, we found that the boilerplate added many configurations, such as Webpack Hot Reload and building a minified bundled Javascript file, but we wanted to fully understand what was going on. The next step was taking a deeper dive into how the boilerplate worked and build an application architecture from the bottom up that would fit our needs while also being lean.

Our first step involved restructuring the codebase. Initially, we had two folders at the root level: a `server` and a `client` folder to separate the Express server and the React codebase. We reduced the clutter at the root level by only having one folder called `src. The `src` folder contains our client side assets, server side code, and React components.

The main Express file is server.js, but the actual server side entry file is index.js. We use the index.js file to load Babel so we can write our Express server in ES6. We worked with NYPL’s IT department to set up this entry file as a convention to easily set up other Node.js apps.

Refactoring Components into NPM Modules

Even though we learned a lot from Staff Picks, we are always eager to learn more and to refactor based on both better React programming practices and ways of thinking about composing components.

We began to refactor Staff Picks by first converting the React Header app into an NPM-structured component. The Header app is what we currently see on nypl.org, Locations, Research Divisions, Catalog, and the Classic Catalog. The header on those sites are loaded through an embeddable script that loads the Header app and fetches data on the client-side. This approach worked well for non-React apps but not for React apps because having two versions of React on the same page broke the app. We, therefore, needed to isolate and create a header component that we could include in our React apps through the NPM system.

After successfully converting the header component into an NPM module, it was easier to refactor and break out other components into working NPM modules. For example, we refactored our Alt codebase into a separate module called dgx-alt-center. Creating a wrapper around Alt and converting it into a module allowed us to have one instance of Alt that the app and included components can share. In Staff Picks both the app and the header component use the dgx-alt-center component to load their respective stores into Alt.

Staff Picks Alt Component
Shared Alt module

Now we can populate the Stores on the server side before using Iso to render the React app and pass the data to the client side. This is a huge advantage for faster loading times of data and components.

Updating existing components

Staff Picks showcases staff’s selection of books every month and the app was built to handle traversing between months in the MonthPicker component. The specifications changed and now that we are featuring annual selections of YA and Children’s picks, the MonthPicker component needed to be updated.

Staff Picks Month Selector
Rendered month selected component

 

The first problem when refactoring this component was that it was part of a larger component that also displayed the grid of books. The interactive selection of months and the display of data needed to be decoupled immediately. Once that was completed, the `MonthPicker` component was refactored to be a pure component; a component that doesn’t deal with state and simply renders the properties (props) that were passed down.

For any staff pick data set, the previous and next selection of picks are part of the data set’s relationship, e.g. the October Staff Pick’s data set includes pointers to the previous picks in September and the next picks in November. Those relationships are passed into the time selector component, including the API endpoints needed to fetch data for those sets. Although we only have data sets for Children’s 100 and YA 50 from 2015 in the API, when we add the data sets from previous years, the component will render the appropriate year.

From this we learned to create smaller components that are decoupled from specific views or interactions, and that rely on rendering its props that are passed down.

Routing

Routing adds complexity to any app, and on top of that we dealt with reverse proxy-ing of the app onto a specific URL on nypl.org. For routing we used two different technologies, one for the server side data fetching using Express.Router(), and one for React app using React Router on the client side.

Before the Children’s 100 and YA 50 data sets were added to the app, Staff Picks contained three simple routes:


/recommendations/staff-picks/
/recommendations/staff-picks/:month/
/recommendations/staff-picks/:month/:id

The main path /recommendations/staff-picks/ displayed the latest data set, originally set to be the current month’s picks; the added /recommendations/staff-picks/:month/ URL displays the picks for a specific month; and /recommendations/staff-picks/:month/:id displays a specific book pick for a specific month through a modal popup. The purpose of the last route was also to deal with the possibility of a book pick being selected in multiple months, e.g. Star Wars, Aftermath appears in November 2015’s picks but could also appear in a later selection of picks.

To add the Children’s 100 and YA 50 data sets, these next routes had to be integrated:


/recommendations/staff-picks/annual/childrens
/recommendations/staff-picks/annual/childrens/:year
/recommendations/staff-picks/annual/childrens/:year/:id

/recommendations/staff-picks/annual/ya
/recommendations/staff-picks/annual/ya/:year
/recommendations/staff-picks/annual/ya/:year/:id

On the server side, the Express router dealt with reading the path and selecting the correct API endpoint. If 'annual' was the third parameter, then a check was done for ‘childrens’ or 'ya'. Finally, instead of selecting by month, the data set was selected by year, but monthly and yearly selections are just pointers to the previous or next data set if its available.

For the React Router side, the main `home` route is /browse/recommendations/staff-picks/? (the /? at the end signifies an optional trailing slash). Similar to the Express router configuration, the routes then split up into either the `annual` route or the `home` route. The annual selection then has an extra parameter `type` which can be either `ya` or `childrens`.

Up until this point, either /browse/recommendations/staff-picks/, /browse/recommendations/staff-picks/annual/ya, or /browse/recommendations/staff-picks/childrens can be selected. This will display the current monthly and the current yearly selection of Staff Picks and Children's or YA, respectively. Specifying the month, year, and book is the last part that is handled by two similar routes. In the gist below, the similar routes are denoted by the `year` and `month` Routes and the similar `annualModal` and `modal` Routes. Even though these routes are not being reused, the handler for both is the same BookModal component.

Parameter based components

React Router does a good job of passing down any prop at the root level to its children when the Router is initialized:


Router.run(routes.server, req.path, function (Root, state) {
    let app = React.renderToString();
});

In this case we pass down the URL `req.path` that the server reads to the app, and at the application level this value is passed down as a prop to the RouterHandler (or any other) component:

A difficulty we encountered was correctly rendering a component based on the route when the component is not part of the RouterHandler. For examples, the Agetabs component should only appear at the Staff Picks level /browse/recommendations/staff-picks/, but not the annual level for Children’s 100 or YA. The problem with including the AgeTabs component in the Route’s RouterHandler component is the placement of the component in the application structure. The AgeTabs component didn’t render nicely with the rest of the page, and so we took an approach to render the component based on the active URL.

Although this worked, this is not the best way to implement this. We are currently working on updating the nypl.org homepage and we’ve found that low-level components should not have too much logic. In this case, the AgeTabs components decides whether it should render itself or not. What should happen is have logic at the application level to decide whether the AgeTabs component should be rendered or not. This will also update AgeTabs to a pure component that just renders the props that were passed to it if it’s rendered.

Future Lists

Similar to refactoring any code base, refactoring the Staff Picks app was very involved. We started from the architecture and server side code, to the isomorphic React app. We are currently planning another stage of code refactoring and hope to make the reverse proxy routing more stable. The end result will be one central app where all the existing lists of Children's Books for Reading and Sharing, Best Books for Teens, and Staff Picks recommendations will coexist.