Before you go, check out these stories!

0
Hackernoon logoA Quick Breakdown of A Modern React App by@nikhilwins

A Quick Breakdown of A Modern React App

Author profile picture

@nikhilwinsNikhil Bhaskar

I invest, write code, travel the world, and cook.

Including common-sense tips on how I commit code with confidence

I’m going to give you a quick but thorough breakdown of the front-end architecture of Risk And Relax.

If you are unfamiliar with the app, the best is to just play around with it first.

You can search for any American ticker and see a score based on the six most predictive fundamental factors that influence its price.

These six factors are based on research conducted on over 10 years of data spanning multiple recessions and across hundreds of tickers.

And I am adding new features every day. Soon, you will be able to get a list of high-scoring tickers sent directly to your phone without logging into the app.

But back to the topic of this writeup — front-end architecture.

When I say “architecture”, I really just mean things like …

  1. Folder structure
  2. File structure
  3. State management
  4. (Reusable) services
  5. Static variables
  6. CSS/Styling
  7. Unit & integration testing
  8. Abstraction of logic
  9. Single-purpose functions

And perhaps a few more things, sprinkled in.

I’ll cover all of these topics by simply walking through the folder structure first, making small but necessary diversions when something slightly unrelated comes up (i.e styling).

Writing maintainable, well-structured, and testable code is as much of an art as it is a science, and everybody has their opinion on everything, from which library to use, to even how to name a variable.

In fact, the same code you write and are proud of today may be embarrassing to look at tomorrow.

That’s how fast things change.

As such, this writeup is a snapshot of the present. Right now.

To me, “good” code means I have to answer YES to what I call the Big 3 questions.

The Big 3 Questions

  1. Can I add a feature with the confidence that I won’t break existing functionality?
  2. Can I refactor code with the confidence that I won’t break existing functionality?
  3. Can I easily teach a new developer to do the above two steps in my codebase?

If the answer is YES to all three questions, then we’re headed in the right direction.

Okay — so let’s cut right to the chase.

Let’s get a few basic things out of the way.

The front-end of the application is built with React and uses hooks and Redux for state management. I’ll step through some of those shortly.

Also, you’ll notice that the code is in TypeScript.

Why?

With the colossal, almost daily progress made by the open-source community toward writing scalable JavaScript code, I believe there is no excuse to be writing loosely-typed, bug-prone, plain JavaScript code.

Typescript is rarely overkill, and it’s not a “nice to have”.

It is easy to set up, and creating types for all of your objects comes naturally after you do it just three to five times.

It saves hours of debugging (think runtime type-related errors).

Typescript or no script is how I see it.

If you are not too familiar with Typescript, this writeup will show you bits of it in a practical sense.

But, this writeup is not meant to be a comprehensive tutorial on Typescript, Redux, testing, or any of that stuff.

Once again, this writeup provides an overview of the front-architecture of Risk And Relax where I can confidently answer YES to the Big 3 Questions.

With that said, let’s get started.

Let’s take a peek at the folder structure.

Folder Structure

Here, you can see the folder structure.

.
├── components
├── models
├── pages
├── services
├── state
├── static
└── tests

Right now, I’ll give a brief overview of each folder, but later in this writeup, I’ll jump into specifics of each folder.

State

  • The engine of the application.
  • Houses all the Redux actions, reducers, and sagas.
  • Hypothetically, I could take this entire folder and drop it into another application, and with minimal re-wiring, can make the new app work the exact same way as Risk And Relax.

Components

  • Houses all of the components of this application.

Models

  • Houses the types and interface (based off of Typescript) of objects that are used throughout the application.

Pages

  • House all the pages of this application.
  • Pages are just components that are linked to a route, using React Router.

Services

  • Houses the wrappers around key services like HTTP, Local Storage, and cookies, used throughout the application.

Tests

  • Houses all the unit and integration tests.
  • All components and pure functions have tests written for them.
  • This gives the ultimate confidence to add a new feature or refactor code without worrying about breaking old functionality.

Static

  • Houses all the constant variables and strings used throughout the application.
  • Strings, even static ones, should never be hard-coded.

Now, let’s get a little bit more granular, starting with a deeper dive into components.

Components

As stated before, the components folder houses all of the components of the application.

.
├── components
│   ├── AutoCompleteInput
│   ├── Footer
│   ├── FundamentalCriteria
│   ├── LoadingSpinner
│   ├── MenuBar
│   ├── Navigation
│   ├── Overlay
│   ├── Overview
│   ├── Profile
│   ├── Score
│   └── StockFundamentals

As you can see, this folder quite literally houses all of the reusable components in my application.

Someone people like to make this complicated by creating folders within folders of components, so as to house “similar” components together, like this …

├── components
│   ├── Stock Fundamentals
│   ├────── Profile
│   └────── Overview

In my view, not only does this add complexity to your folder structure, but it defeats the purpose (reusability) of components, to begin with.

If you put a component inside another component, your application structure assumes that both components go together, always.

And it’s hard to use words like never or always when it comes to software development, as things can change rapidly.

So, I am much more comfortable simply “dumping” all of my reusable components into a single folder.

This also makes refactoring less of a chore.

Imagine putting a component in another component and then later finding out that another component is “similar” to it, too.

Yeah, let’s avoid all that trouble by keeping it simple.

What does a typical component folder look like? Let’s take a peek into the Navigation component.

.
├── Navigation.tsx
├── index.ts
└── types.ts

Each component typically consists of three files:

  • The component (.tsx) file
  • The types files
  • The index file

types.ts

This file houses all types exclusive to that particular component.

For example, here is what types.ts looks like for Navigation.

The file has all properties that the Navigation component will be using.

Here’s what I mean — inside my component, I make use of the props like this …

Because my code is strongly typed, I am always aware of the structure of all objects, including props to components.

With getting lost in detail, let’s move to another part of the folder structure.

As a reminder, here’s what the overarching folder structure looks like.

.
├── components <--- We just covered this
├── models
├── pages <--- Now we'll cover this
├── services
├── state
├── static
└── tests

Let’s talk briefly about pages before we move on to more exciting stuff.

Pages

├── pages
│   ├── AccountRecovery
│   ├── Contact
│   ├── Favorites
│   ├── Login
│   ├── NewPassword
│   ├── ScoreView
│   └── Signup

There’s not a whole lot to say about pages, because it is the same as components except with one key difference.

All components inside pages have an entire route dedicated to them, via the React Router.

Okay, now let’s get into a pretty important folder — services.

.
├── components
├── models
├── pages <--- We just covered this
├── services <--- Now we'll cover this
├── state
├── static
└── tests

Services

Here is what the services folder looks like …

├── services
│   ├── api
│   ├── http
│   ├── scoring
│   ├── cookies.service.ts
│   ├── re-entry-routes.service.ts
│   └── session-storage.service.ts

This folder holds all of the reusable services used through the application.

Some of these services include wrappers around …

  • Setting, updating and removing cookies
  • Setting, updating and removing local/session storage
  • Making HTTP requests

Here is an example of a wrapper around an HTTP request …

Here, I use the axios library to perform GET and POST requests.

However, I don’t use it directly. Instead, I wrap it inside my own get and post functions.

Doing this is good because it accomplishes two important things:

  1. I’ve defined my own API (and/or domain-specific language) that I can use throughout the application instead of depending on a third-party.
  2. If I ever want to switch from axios to another HTTP library, I only have to make the change in one place. The rest of my application need not know about this change.

Here’s another example inside my service for updating the session storage.

I define my own API that my application uses, and my application does not (and need not) know about the implementation details.

Abstracting logic and hiding implementation details via simple wrapper functions makes your codebase more adaptive and scalable, and allows you to play from a position of strength.

Also, this principle applies when you are dealing with third-party APIs.

In my case, one of the financial APIs I use has a different opinion of what should go into a stock’s “fundamentals” than me.

But that’s okay because I have a transformer function that takes the third-party API’s object and transforms it into mine …

This way, I can write code with the structure and names I desire.

Side Note: I normally don't like using any for types, as that defeats the purpose of Typescript, but here, I use it for flexibility, as the API I use can change at any time. There’s really no use in spending your time and energy creating the perfect types for a third-party API response when you’re just at the whim of the API changes. Play from a position of strength, always.

And more importantly, I decide what goes in security fundamentals.

The API I use can have whatever opinion they want on what makes up a stock’s fundamentals.

But that should not matter to us. Let me explain.

Below is a typical HTTP request.

I make the call to my own backed server that acts as a proxy so as to hide confidential data like API keys.

In the backend, here is what is actually happening.

As you can see, we make a few API calls to get the balance sheet, growth statistics, ratios, and the stock quote.

You see, the API I use has its own opinions on how to organize its data.

However, we don’t need to follow them if you don’t want to.

I’ve defined by own model that houses the stock fundamentals, and my model is built from amassing data from four of the third-party API’s models (quote, growth, ratios, and balance sheet).

Defining your own models establishes a consistent data dialect across your application. You don’t need to succumb to your third-party API’s models if you don’t want to.

Speaking of models, let’s get into another folder — models.

.
├── components
├── models <--- Now we'll cover this
├── pages
├── services <--- We just covered this
├── state
├── static
└── tests

Models

Here is what this folder looks like …

├── models
│   ├── favorite
│   ├── favorite-to-user
│   ├── fundamental-criteria
│   ├── searchable-security
│   ├── security-fundamentals
│   ├── security-profile
│   ├── user
│   └── index.ts

As mentioned briefly before, this folder just houses all of the types (interfaces) used throughout the application.

For example, let’s peek inside the user folder.

├── user
│   ├── index.ts
│   └── types.ts

Inside the types.ts file, we see …

Here, I’ve defined an interface to describe the logged-in user.

This interface is used throughout the application, not just in one component, so that’s why it’s in a general folder called models, and not inside the folder of a specific component.

Make sense?

Great, let’s take a quick look at the index.ts file of the models folder.

Almost every folder has a barrel file like this.

A barrel is a way to gather (or pull in) exports from several modules into one single module.

Inside the barrel file, we just re-export specific exports of other modules.

Then, if I want to import the IUser interface, I can just do the following …

And I can avoid doing this below …

It’s just a mini convenience hack.

Now, let’s talk about the engine of the application — the state folder.

.
├── components
├── models <----- We just covered this
├── pages
├── services 
├── state <----- Now we'll cover this
├── static
└── tests

State

Here’s what this folder looks like:

├── state
│   ├── reducers
│   ├── sagas
│   ├── action-types.ts
│   ├── actions.ts
│   ├── root-reducer.js
│   ├── root-saga.ts
│   ├── store.js
│   └── types.ts

As you can see, we’ve got

  • reducers
  • sagas
  • actions
  • the store

As stated before, almost all of my state management is done with Redux, with my smaller, component-scoped state management done with hooks.

When building a customizable car, the engine should be modular enough that you can pop it out and drop it into another car and have it run the same way.

Similarly, the engine of Risk And Relax is the state folder.

Below is an example of a component dispatching an action, inside the App.tsx file.

As you can see, when the component mounts, I call props.searchableSecuritiesRequested().

This dispatches the action to get all of the securities that the user can search for in the autocomplete field.

And because there are side effects for this particular action, I handle the action inside of a saga.

Let’s take a look at the saga.

Notice that inside the saga, we do the following …

  1. Dispatch the startLoading() action to show the loading spinner.
  2. Call fetchSearchableSecurities() to make a request to our backend (which acts as a proxy, hiding the API keys), which in turn calls the API to get a list of all of the securities that the user can search for.
  3. Dispatch the endLoading() action to hide the loading spinner.

Notice also, that in the case of an error, we call a function showSearchableSecuritiesErrorMessage() to show an error message in the form of a toast.

I find that there is no need to fire an actual error action, as there is no need to store the error in the state if all you need to do is show an error message.

In other words, these are the two actions associated with searchable securities.

And like that, we follow the exact same logic for all of the other reducers, actions, and sagas.

Keep it simple.

Now, let’s talk about the static folder.

.
├── components
├── models
├── pages
├── services 
├── state <----- We just covered this
├── static <----- Now we'll cover this
└── tests

Static

Here is what this folder looks like …

├── static
│   ├── cookies.ts
│   ├── index.ts
│   ├── lang.ts
│   ├── routes.js
│   └── session-storage.ts

The role of this folder is quite stress-free — store all static variables used throughout the application.

The key phrase here is “throughout the application”. If a variable is only used within a single component, then that variable can simply exist inside that component — no need to drop it into the static folder.

Let’s take a look at a snippet of lang.ts …

This is just a snippet but the point is — no string is hard-coded.

It’s a good practice to take all of your apps strings and put them into a lang file for two main reasons:

  1. In the event that you want to implement localization (i.e translation into other languages), you’d need all strings in a language file in order to seamlessly switch between language bundle depending on the user’s locale.
  2. If you want to test for the presence of a string in a component, having it hard-coded makes it a very treacherous process in the event you change the contents of the string (which you will do).

Now, let’s talk about the last but definitely not the least import folder of the application — the tests folder.

.
├── components
├── models
├── pages
├── services 
├── state 
├── static <----- We just covered this
└── tests <----- Now we'll cover this

Tests

Here’s what the folder looks like …

└── tests
   ├── components
   ├── pages
   ├── services
   │   └── scoring
   │       └── fundamentals
   └── state
      ├── reducers
      └── sagas

Notice how its folder stricture mimics the folder structure of the overall application.

This is by design.

Some developers like to throw in test files along with the components, but in my opinion (you may disagree), that makes for a messy codebase.

When I’m in the mood to read/write, I prefer to have my own space for it, almost “away” from the rest of the application.

As you can see, everything from components to services, to sagas are tested.

I believe that writing good tests is the number one most effective way to ensure that your codebase as little bugs as possible while giving the ultimate confidence to modify your codebase.

Here are the testing tools I use …

  • React testing library and enzyme to write integration tests for components
  • Jest as my testing library and to perform unit tests on pure functions
  • Sinon to create mocks and stubs of functions

This is not meant to be a comprehensive guide on tests, but because I know not all developers write integration tests for components, here is an example of how I’d write a test for a component …

This is just one test of many for the Login component. But I hope you get the gist.

That’s the end of all of the main folders in our application.

We’ve come a long way, but there are still a few other things to cover.

Single Purpose Functions

You may have heard it online, in a conference, from your tech lead at work, or even from your professor.

But I can’t stress enough the importance of writing single-purpose functions.

Not only does it make your testing life way easier, but it also drastically improves the maintainability, extensibility, and readability of your code.

Here is an example of a single-purpose function called emitSelection().

Notice how both handleKeypress and handleTickerSelect call emitSelection.

In other words, both of those functions need to emit a message via props back to the parent component.

I could have just put the props.onSymbolSelected in both handleKeypress and handleTickerSelect, but that would be bad for two reasons:

  1. Code is being repeated
  2. Functions are not single-purpose, making them harder to test.

Hope that makes sense.

Let’s move on to the notorious part of any modern, progressive web application — the CSS/styling.

CSS/Styling

All of the CSS in Risk And Relax is from scratch.

I do use a utility library called Tachyons, however, as this makes writing barebones CSS a lot easier and more modular.

All styles are in the form of inline-classes, and all classes are reusable.

Here is what the AutoCompleteInput component looks like, with its inline styles …

As an example, “di” simply means “display: inline;”.

The class names seem terse at first, but after using them just a few times, you get used to them.

You can find a list of all the class names of Tachyons here.

Again, writing CSS like this is not only lightweight but also modular, and of course, it allows for responsive design.

Conclusion

And there you have it — the front-end architecture for Risk And Relax

We covered a lot of ground here, and this is only the beginning.

I hope you enjoyed my writeup, and whether you agree/disagree, or want me to cover something more in-depth, just drop a comment below or send me a message!

I hope you found this post helpful. If you did, please repost, share, and comment.

Who Am I?

My name is Nikhil, and I’m from Canada.

I trade stocks & options, write code, and sell stuff online.

I’ve lived in 20+ countries while earning money remotely. If you want to follow my journey, the best is to simply follow me on Twitter.

I tweet about freedom, money, markets, and other truths I discover along the way.

See you on the inside!

Tags

Become a Hackolyte

Level up your reading game by joining Hacker Noon now!