(code is hosted at: https://github.com/djamaile/react-boilerplate-2021)
(last years post: https://dev.to/djamaile/how-i-structure-my-react-ts-applications-160g)
Last year, I created a post on how I structure my ReactJS projects. Now that it is summer vacation again, I shook the structure up a bit. But honestly there are few changes, because in the end React changed little (which is a good thing). So, in this post I will highlight what I changed/added.
Lets first start with a picture of the whole folder structure!
Now lets discuss the changes :)
π Api
In the API folder, I only now have a generic request function and what I added was react-query. If you are not familiar with react-query, it is a library for fetching server state. React-query comes with a lot of power like caching, data synchronisation, etc.
In this project, I have kept react-query pretty simple by only adding a defaultQueryFn
, what looks like this:
import axios, { Method, AxiosResponse } from "axios";
const api = axios.create({
// baseURL: process.env.REACT_APP_HOST_BACKEND,
baseURL: "https://rickandmortyapi.com/api",
});
const request = <T>(
method: Method,
url: string,
params: any
): Promise<AxiosResponse<T>> => {
return api.request<T>({
method,
url,
params,
});
};
// Define a default query function that will receive the query key
export const defaultQueryFn = async ({ queryKey }: any): Promise<unknown> => {
const data = await request(queryKey[0], queryKey[1], queryKey[2]);
return data;
};
As you can see, the defaultQueryFn
is calling the request
function. Now we can add this to our QueryClient
and in our Home view we can call the useQuery
functionality like this:
import React from "react";
import "../../styles/home.css";
import { useQuery } from "react-query";
import { Header } from "../../components";
const Home: React.FC = () => {
const { data, error, isFetching } = useQuery(["GET", "/character", {}]);
if (isFetching) return <p>Is loading...</p>;
if (error) return <p>${error}</p>;
console.log(data);
return (
<div className="App">
<Header />
</div>
);
};
export default Home
I am still experimenting with react-query and see how I can use it better. But this is how I have been using it for now.
π§ͺ Tests/Cypress
Yes, the infamous test folder. I actually ended up deleting it! I still have tests but I put them directly into the views/[view] folder.
I have to admit that I am not using Jest as much anymore. I have switched over to using Cypress. Cypress is a tool for end-to-end tests and I have been liking it so far. So, in cypress/integration/404_page.ts
you can see I have a spec test that tests if the user can go back to the home page if the user has reached to 404 page.
describe('404 page', function() {
it('should give the option to return to home', function() {
cy.visit("/does-not-exists");
cy.contains('Return to Home');
cy.get('a')
.click()
cy.contains('Learn React', {timeout: 10000})
});
});
π³ Docker
I have added also Dockerfiles to my default repo. I have two separate two Dockerfiles, one for development and one for production.
FROM node:15-alpine AS builder
WORKDIR /app
COPY . .
RUN yarn install
RUN yarn build
FROM nginx:stable-alpine
WORKDIR /usr/share/nginx/html
RUN rm -rf *
COPY --from=builder /app/build .
ENTRYPOINT ["nginx", "-g", "daemon off;"
To build a image use:
$ docker build -t djam97/react-boilerplate-2021:prod -f docker/Dockerfile.prod .
βΈοΈ Kubernetes
I use Kubernetes daily so that's why I added also some k8s manifests. They are pretty bare bone, but they get the job done and are easily extensible.
apiVersion: apps/v1
kind: Deployment
metadata:
name: react-boiler-plate
labels:
app: react-boiler-plate
spec:
replicas: 1
selector:
matchLabels:
app: react-boiler-plate
template:
metadata:
labels:
app: react-boiler-plate
spec:
containers:
- name: react-boiler-plate
image: djam97/react-boilerplate-2021:prod
imagePullPolicy: Never
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: react-boiler-plate-service
spec:
selector:
app: react-boiler-plate
ports:
- protocol: TCP
port: 3000
targetPort: 3000
To apply the manifests use:
$ kubectl apply -f k8s/
πΊ Github workflow
I have also added a Github action that deploys your React app to Github pages. This is great for initial testing or for when your site is not going to have live on it's own server.
name: Deploy site
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
continue-on-error: true
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Setup Node
uses: actions/setup-node@v1
with:
node-version: "14.x"
- name: Get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache dependencies
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn installation
run: yarn install && CI='' yarn build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.ACCESS_TOKEN }}
publish_dir: ./buil
If you also want to use it, there are some steps you need to take. First, you need to create a personal access token in GitHub and add it as a secret to your repo as ACCESS_TOKEN
. Last, change the homepage in package.json
-
"homepage": "https://djamaile.github.io/react-boilerplate-2021",
-
"homepage": "https://<your username>.github.io/react-boilerplate-2021",
ποΈ Extra files
Besides the usual prettier, eslint, husky setup. I have also added @commitlint/config-conventional
to make sure every commit complies with being a conventional commit. If you don't know what that is, you can read up on it here: https://www.conventionalcommits.org/en/v1.0.0/#summary
Top comments (14)
I think this "nature based" code organisation is not so good. Each feature gets distributed over several folders and each folder becomes a huge dumping ground. It's awkward to deal with and not scaleable. I think it's much better to organise code according to feature, subfeature etc. where code for a feature is organised together. It's kinda like how Vue and React switched from lifecycle methods to hooks... it became much easier to deal with these frameworks when code is organised isomorphically rather than being distributed according to implementation details of the framework. Wouldn't it be weird if say, you put all your Python classes in a folder called classes, your functions in a folder called functions, your type annotations in a folder called types etc.? I don't understand why, when it comes to react projects, it's become semi-prevalent. Angular code standards advise against this (not that Angular is very good).
Totally agree.
The keyword here as I know is vertical folder structure vs horizontal folder structure or package by feature vs package by layer.
As the project grows, I think package by feature would be better, you know where the code should supposed be found when you are searching for it.
Sometimes, we do create a
shared
orcommon
folder (the same level with other features) to serve common codes.My personal term is "clothing code". All the shirts go in this drawer, the socks in this drawer, etc. The strategy works for clothing because the goal is to mix-and-match, but for code usually a set of files always belong together.
I have created quite sizable applications like this and didnβt find it hard to navigate. Also, most logical features I create is in the backend or in a utils folder if it is really needed in the frontend.
But I am still unsure on what you mean with your comment maybe you can show one of your own repos to display what you mean?
codeopinion.com/organizing-code-by...
There's hundreds of articles advocating this same method of organisation and almost none advocating for the system typically used by so many people starting out with web frameworks.
I think it's objectively better, in my contracting career almost every large or experienced team I've worked with have organised by feature. Those that didn't soon moved to feature based organisation when the size of the project got to the point that the flaws of nature-based organisation became particularly self-evident.
Do you also migrate towards a monorepo then? Or do you keep it feature based like in the example you showed. It is definitely interesting but I havenβt seen a frontend repo like this. So, it is hard to wrap my head around it.
I will try it for my next project, any tips?
I've used monorepos sometimes, but I think this method of code organisation is not really related to whether you decide to use a monorepo or not. It's about how you organise any package, whether that package is it's own repo or within a monorepo.
A good tip is to think about your features in advance, you should have some idea of what many of the top level feature folders are gonna be before you start your project, but of course you can add new features as you discover their need. Also, don't fear nesting one or more subfeatures inside of a feature when it feels right.
Really depends what "quite sizable applications" means.
The biggest frontend side projects that I worked on were 232k and 110k lines of code and if you're not organise code according to features then this will be mess and hard to maintain and understand what's going on.
There is not perfect project structure that will fill each project.
Helix design principles are worth looking at too helix.sitecore.com/introduction/wh.... I use some of those ideas in my FE projects.
Great article! A typo I noticed is Kubernetes and Github Workflow have the same description.
Thanks!
Hey, you put k8s text block under GitHub workflows title)))
Thanks!
I prefer structuring by feature as explained here: dev.to/dchowitz/7-reasons-why-i-fa...