For various reasons, our GitHub repo got 7,200 stars within 3 days. It hit number one on HackerNews, GitHub Trending, as well as 20,000 upvotes on Reddit.
This is a post I’ve been meaning to write for a while and with our repo blowing up I figured this would be as good a time as any to write it.
I work as part of a team of freelancers and the typical projects we do use React/React Native, NodeJS, GraphQL. This post is aimed at those interested to learn how we build apps, and as an on boarding tool for those that join us in the future.
These are our core principles.
Easier said than done. Most developers understand simplicity is an important principle, but it’s not always easy to do. Simple code makes maintenance easier and makes it easier for all team members to contribute. It will also help you manage your own code half a year down the line.
Some mistakes I see here:
- Being too clever. Copy paste code is sometimes okay. You don’t need to abstract every 2 pieces of code that look somewhat similar. I have made this mistake myself. We all do it. DRY is a good principal to follow, but choosing the wrong abstraction can be worse and complicates your codebase. If you’d like to read more on this, I recommend reading this blog post: AHA Programming.
- Not using the tools available. One example is using
filter. Of course you can write your
reduceinstead. But it will likely be more lines of code, and harder for others to understand. Granted, simplicity is subjective.
There are times you will need to use
reduce, and if you ever chain
reducewill likely be more performant as you can pass over the collection once instead of twice. This is a question of performance versus simplicity. In general I’d favour simplicity and avoid premature optimisation. If somehow the 2 pass
filterbecomes a bottleneck you can switch the code to use
Many of the following principles also aim to keep the codebase as simple as possible.
This principle applies to many parts of the app. Both client and server folder structure, keeping things in the same repo, and what code goes in each file.
Keep your client and server folder in the same repo. It’s easy. Don’t complicate things. Everyone will be in sync this way. Having worked on projects that used multiple repos, it’s not the end of the world, but life is easier with a monorepo.
We write full stack apps. So both client and server. A common client folder structure is to have separate folders for: components, containers, actions, reducers, and routes. (Actions and reducers for those that use redux. I try to avoid it.) I’m sure there are some very nice codebases out there that use this structure. Some of my own codebases have a separate components and containers folders. The components folder will hold something like
Profile and the containers folder will have a file called
ProfileContainer. The container will grab the data from the server and pass it to the dumb child component whose job it is to render the data to screen.
This structure works. At the least it’s consistent which is important and a new person that joins the codebase will understand what’s going on and what goes where. The downside of this approach, and why I personally avoid it nowadays, is that you have to jump around the codebase a lot.
BlogPostContainer have nothing to do with each other, but the files are located right next to each other and far away from where they’re actually going to be used.
I far prefer locating files that will be used together next to each other — a feature based approach. Stick the smart parent component and the dumb child component in the same folder. It will make your life a lot easier.
We typically use a
screens folder and a
components folder. Components will hold things like
Input that could be used on any page of the app. Each folder within the routes folder represents a different page of the app and all components and business logic specific to that route are placed within that folder. Components that are used on multiple screens go in the
Within each route you can create more folders inside it that group certain parts of the page. If route contains a lot this makes sense. One thing I’d warn against is nesting too deeply. It will make it harder to jump around the project. This is another sign of overcomplicating things that don’t need to be. (On a side note, using command-p and search are a great way to jump around a project and find what you need, but file structure also has an impact.)
A somewhat similar approach is grouping by feature rather than by route. This worked very well for me on a project that was a single page, but had lots of features on that page. Grouping by routes is easiest and doesn’t take a lot of brain power to figure out what should be grouped together and where to find items.
Taking this a step further, you may even like to stick your containers and components in the same file. Or even further, just the two components into one. I know what you’re thinking. “WTF is this guy on about? That’s blasphemy.” In reality, it’s not as bad as it sounds, it’s actually quite good, and if you’re using React Hooks and/or generated code I’d recommend this approach.
The real question is why you would even want to split your components into smart and dumb components? There are a few answers to this:
- Easier to test
- Easier to use with a tool like Storybook
- Can use the same dumb component with multiple different smart components (or vice-versa).
- Can share smart components across platforms (e.g. React and React Native).
These are all valid reasons, but often not relevant. In our codebases we often use Apollo Client with hooks. To test you can either mock Apollo responses or mock the hook. Same goes for Storybook. As for mixing and matching smart and dumb components, I’ve never actually seen this happen in practice. As for cross platform usage, there was one project I was going to do this, but it didn’t end up happening. It would have been a Lerna monorepo. Today you may well choose React Native Web instead of this approach in any case.
So there are legitimate reasons to separate between smart and dumb components. It is an important concept to be aware of, but often you don’t need to be as worried about it as you think, especially with the recent addition of hooks to React.
The upside of combining smart and dumb components in the same component is that it speeds up development time and it’s more simple.
Furthermore, you can always split your component into two separate components in the future if the need arises.
We use emotion/styled components for styling. There’s a temptation to split the styles into a separate file. I’ve seen people do it, but having tried both approaches, I don’t see any reason to put your styles in a different file. As with everything else listed here, your life will be easier if you colocate your styles in the same file as the components they relate to.
The same goes for the server. A typical structure that I personally avoid would look something like this:
│ app.js # App entry point
└───api # Express route controllers for all the endpoints of the app
└───config # Environment variables and configuration related stuff
└───jobs # Jobs definitions for agenda.js
└───loaders # Split the startup process into modules
└───models # Database models
└───services # All the business logic is here
└───subscribers # Event handlers for async task
└───types # Type declaration files (d.ts) for Typescript
We typically use GraphQL for our projects. There are models, services and resolvers files. Instead of splitting these 3 files across the app, stick them all in the same folder. The vast majority of the time they’ll be used together and it will be much easier for you to find them if they’re colocated.
We use a lot of type systems in our projects: TypeScript, GraphQL, database schema, and sometimes Mobx State Tree types.
You could end up writing the same type 3 or 4 times over. Avoid this. Use tools that auto generate the types for you.
On the server, you can use a combination of TypeORM/Typegoose and TypeGraphQL to cover all your types. TypeORM/Typegoose will define your database schema as well as the TypeScript typings for them. TypeGraphQL will generate the GraphQL types and TypeScript typings.
An example of defining both TypeORM (MongoDB) and TypeGraphQL types in a single file:
GraphQL Code Generator is able to generate lots of different types. We use it to generate TypeScript types on the client as well as the React hooks to make calls to the server.
If you use Mobx State Tree, you can automatically get TS types from it by adding 2 lines of code, and if you use it with GraphQL, there’s a new package called MST-GQL that will generate the state tree from the GQL schema.
Using these packages together will save you rewriting a lot of code and help you avoid potential bugs.
Other solutions such as Prisma, Hasura and AWS AppSync can also help avoid type duplication. There are pros and cons to using these. For the projects we do these aren’t always an option either as we need to deploy the code to on premise servers.
Beyond using the code generation tools above, you will find yourself writing the same code again and again. The number one tip I can give you here is to add snippets for everything you use often. If you write
console.log a lot, make sure you have a
cl snippet that will expand
console.log() for you. If you don’t and ask me to help debug your code I’ll be pissed off.
There are lots of snippet packages, but you can also generate your own very easily here:
Some of my favourite snippets:
- React component/hooks snippets
imes— import emotion/styled
sc— emotion/styled component
fn— print the filename of the file you’re currently in.
And here’s the code if you’d like to manually add them to VS Code:
Beyond snippets, something that can save you a tonne of time is writing code generators. I like using plop.
Angular has its own generators built in and through the command line you can create a new component with 4 files present that every Angular component is expected to have. It’s a pity that React doesn’t have a feature like this out the box, but you can create it yourself using
plop. If each new component you create should be a folder containing a component, test, and Storybook file, the generator can create this for you in one line. This makes our life a breeze in many instances. For example, adding a new feature on the server is one line in the command line that creates an entity, services and resolvers file with all the core pieces filled out automatically.
Another nice thing about generators is that it pushes your team to work in a consistent manner. If everyone is using the same plop generator the code will have an extremely consistent feel to it.
This is an easy one, but not always done unfortunately. Don’t waste time indenting code and adding or removing semi colons. Use Prettier to autoformat the code on every commit: