Developers often wonder how to to get their side-project apps finished, and now that I’ve finished building Scorecard.dev I wanted to document my process before everything fades from memory. You can read the story of where the idea for the app came from if you’d like some context.
Starting with a blueprint
The most common mistake I see developers making when trying to get their product to market is not creating a diagram representing the final product before they begin coding. Borrowing the term from the construction industry, I call these comprehensive diagrams “blueprints.” When you want to build your user interface around reusable components, working from a plan will make it easier for you to identify opportunities for reuse. These reuse opportunities tend to be far harder to spot when you don’t have an over-arching vision for how the final app will look.
If you take a look at the blueprint I created, you’ll notice that while it’s not the same as the final app, the diagram represents all of the major concepts. Many developers think a blueprint has to be pixel perfect, but achieving that level of perfection is usually not necessary, as you can adjust with CSS in a second what might take someone an hour to change in a drawing program (I used Visio). The key is to understand the app holistically so that any surprises you run into will be minor.
I worked on the blueprint as I was recording and editing podcasts, and on average, I probably spent about an hour working on it every day for around a month. This period is also where I made significant technical design decisions so that when the time would come to code, I would only have to focus on the implementation, not the overarching system architecture.
Throughout the coding process, I was constantly referring back to the blueprint to remind myself of where I was and what I was trying to achieve at each stage of development. It is common to get lost in the weeds after several thousand lines of code, and occasionally you’ll need to come up for air and reorient yourself by studying the blueprint.
I picked the technologies of React, Redux, and CoffeeScript over contemporary alternatives. Here was my rationale:
- React vs. Angular – I have no experience with Angular, and five years of experience building sophisticated apps in React. Why I chose to adopt React over Angular years ago is a complex topic, but whatever my reasons originally were, the fact remains is that I am a novice at Angular and an expert with React, which was the primary driver of my decision.
- React vs. Vue – While the frameworks are similar, and while I don’t think there would have been a noticeable learning curve, Vue didn’t offer me any advantages over React. For people who lack significant experience with React, and depending on their background, I can think of many situations where Vue might have been the better choice.
- React vs. React Native – I wanted a web app, not a native app, as embedding part of the app on specific pages on my blog was a core requirement. Should I ever chose to pivot into native apps, I would most likely choose React Native over a Kotlin/Swift Andriod/iOS build as the component hierarchy would be directly transferable, and would represent a massive time-savings.
- Redux vs. React’s State – I’ve used Redux for around three years now, and the React State didn’t offer me any advantages for this project over Redux. Designing an application with Redux, however, has a steep learning curve for people unfamiliar with Flux-style architectures, and React state is a far easier starting point.
I made sure the stack I chose was a close fit for what I needed, but if I were building a different style of app, I may have chosen another type of stack. For example, if I was making an app for a large enterprise that was to be maintained by hundreds of developers, I might have chosen ExtJS and Java Spring. The job should always dictate the tools, but if you don’t know how to use something and have a short timeframe to get something finished, stick with what you know. It’s not the most exciting advice in the world, but it’s the most pragmatic if your priority is completion.
Designing the Component hierarchy
While I do start with a blueprint, I don’t typically begin coding with a fixed plan for implementing specific components. Instead, two concepts guide me towards the final component hierarchy:
- Evolutionary design – This is my standard approach, whereby I iterate into the final design through rapid, proactive refactoring. When finished, I can draw a line between where I started and where I ended, even though the two may have dramatically different implementations.
- Emergent design – This is when through the process of evolving the design, I make a realization that indicates that I need a completely different design. It is these paradigm shifts that tend to yield the most valuable simplifications.
I reverse engineered a diagram of what the component hierarchy looked like for the sake of this article but bear in mind that I had no plan when I started. You should be able to tell where exactly I use each component by referring back to the blueprint.
Implementing a Flux-style architecture with Redux
Flux style architectures tend to be difficult for developers to wrap their heads around. The trouble is not the basic idea (a single state for the entire app), it’s when you get into specific scenarios that people’s head starts to spin. Ultimately, Flux-style architectures are very different than other user interface data models, and at times are incredibly counter-intuitive, and often seem to have no apparent benefit.
Flux-style architectures, as well as how they can or should interact with different styles of backend APIs, is a sprawling topic, so in the name of brevity I’ve made a diagram showing state management for the most sophisticated component in Scorecard.dev, which is the custom audio player. The audio player is a sextuple state-machine, which each state serving a specific purpose:
- Server – The server determines what options in the player are enabled based on: if the user has taken the quiz; if they have created an account; and if they have unlocked all results.
- Redux – Data from the server is combined with static data on the client to form the final set of properties used to initialize the react component.
- React Component – For performance reasons, ScorecardPlayer needed to manage part of its state outside of Redux to avoid over-rendering in high-event rate scenarios like sliding the player position.
- Audio Element – The HTML5 audio media element has an internal state which has to be kept synchronized with the state of the component.
- LocalStorage – For network performance reasons, localStorage is used to remember the state of the UI, as attempting to save player position on the server would create a lot of network traffic, and would still potentially not capture the state precisely if the user unloaded the page between server updates.
- Slider – Much like the Audio Element, the 3rd party slider has an internal state that has to be kept synchronized with the state of the component.
For your sanity, your application should have a limited number of state representations. If you use a JSON store such as Mongo, a REST API, Redux, and stateless React components, you should be able to have only one state if the data in Mongo is structured precisely as the React component hierarchy expects. Suffice to say, having six states to keep synchronized is far more complicated than only having one.
A custom routing system
Typically React apps use React Router for navigation, but I elected to create a custom system, as I needed more precise control over what happens when the user navigates. I also wanted tighter encapsulation over how the system behaves when the state changes the URL, or vice versa, which I achieved using a Redux Enhancer. Finally, I wanted the concern of routing to be as decoupled from the components as possible, which is not a primary concern of the latest version of React Router. Having said that, if you have a relatively straightforward relationship between the URL and the components and state, React Router will be dramatically more manageable than writing a custom solution.
For part 2 of the series, we will be covering styling the frontend with SASS and CSS3 transitions and responsive Flexbox.