When developing any project, there are a few questions we are always asking.
Are we making good decisions for our clients?
Who’s going to be maintaining this?
Are we making something that will be easily maintainable in the future, and is open to extensibility?
Are we building the right thing?
Recently at Media Suite we’ve taken on a large project where we are working with a combination of technologies, using React as the core of our front end component.
React is a great component library, and lends itself to creating components that are easily reusable and extensible.
That being said, React is a component library, and does not mandate a specific file structure or method of designing components. In fact, most of the decisions around how to manage different aspects of the client – such as state management and routing – are left to the developer.
Engineering Consistency
While the simplicity of React is great for rapid development, consistency is key when developing in any technology. Having consistent patterns and practices makes it quicker and easier for other developers to pick up and start working on a project – thus facilitating agile and rapid development.
To make it easier for new developers to join a project, there are a few things we try to emphasise and make as clean and clear as possible.
- File structure
- Reusable common components
- Automated testing
These things are also important when taking a long term view of a project. When it comes to maintaining a project down the line, it’s often not the original developers maintaining said project. In this case, a project that makes predictable decisions and is consistent throughout is a major asset.
File Structure
As we work with a library as flexible as React, we’ve drawn on our experience working in other systems and technologies to inform our process. A large part of this is our experience working with Ember. Ember is rather prescriptive, and in (almost) all things there is an ‘Ember Way’ to do things. One of the great things about this is that everything is always in the place you expect it to be.
React on the other hand has some sage advice when it comes to file structure:
- Avoid too much nesting
- Don’t overthink it
And a popular method as written by Dan Abramov (Co-author of Redux, and Create React App): move files around until it feels right.
When determining the file structure for our projects with React, our goal is to make things as predictable as possible. The guidelines for this being:
- Direction should be able to be taken almost entirely from the URL. Anything else should be easy to infer.
- All globally used pieces of code – services, common components, config files, utilities – should live in the root of the app, clearly visible and accessible.
- Anything that is shared should live at the level closest to the places that require it. This means that if two sibling routes use the same component, but no other routes in the system do, that component should live adjacent to those routes.
We borrowed a lot from Ember here, in particular the module unification rfc, particularly around the structure of component hierarchy and the abstraction of common factors.
Example (with indicative files):
/src
/app
/components (common components)
/Button
/Modal
/Notifications
/components
/ButtonDismiss
/images
/locales
/index.js
/config
/constants.js
/redux
/routes
/Admin
/components
/routes
/Users
/Blog
/components
/BlogSpecificComponent.js
/routes (sub-routes of Blog)
/Edit
/New
/services (specific to the Blog route)
/index.js
/services
/api
/authentication
/session
/etc.
/utils
/index.js <-- entry point for the app. Loads routes, redux and
base app etc.
Reusing Common Components
No-one can be across all parts of the codebase at all times, and this is particularly true for developers just coming on to a project. We’ve found that creating a set of reusable components with documentation has been a great aid in increasing our developer velocity.
Our general approach in this area has been to create a generic component at the global level, and then create instances of that, specific to a particular route or purpose. If the component does not need to be reused, we simply use that component with specific props. This allows us to keep the styles consistent across the system, and provides extremely clear links to where particular logic is happening.
Most importantly, the vast majority of our top level components are functional. They don’t tend to deal with state, and they don’t deal with requests or API calls. This was a deliberate decision to allow us to create a truly reusable set of tools.
We’re currently using Storybook to curate our component library. Storybook is an environment for viewing components in isolation. There are some good add-ons for this too, including the Info add-on, which provides the source code for a particular story. This makes it easy for developers to, at a glance, figure out what component is required for a particular task and how to use this component.
Automated Tests
Automated tests are a valuable resource, and the benefits have been endlessly documented.
Tests should ensure that common paths and edge cases in business logic are responding in the intended manner, and be open to change. This doesn’t mean test less, and in fact often means test more. Having a really robust set of automated tests around reusable components provides us with a rock-solid foundation on which to build, particularly since the rest of the system is (for the most part) composing those components into forms and screens.
One of the biggest benefits we get from automated tests is the insurance of stability and predictability. If a new developer starts working on the project, well documented tests give them vision of the parts of the system their changes are affecting, and help to ensure that their changes aren’t having any unexpected effects.
Outcomes
Having a stable and consistent approach to building web applications that is transferable across projects has been extremely valuable for our team, and has allowed us to more easily draw upon resource external to the project team when appropriate.
Our approach has been formed over the course of a number of projects, keeping the long game in mind. We’re always looking to ensure that our projects will be maintainable in future, and open to change. Ensuring predictability and consistency goes a long way towards making that happen.
As developers, we are always asking, are we building the right thing?