Extensive React Boilerplate to Kickstart a New Frontend Project
In this article, we'll tell you what an Extensive React Boilerplate is and share some of the best practices we implemented in this boilerplate.
Join the DZone community and get the full member experience.
Join For FreeHow much time do we typically spend on project setup? We're talking about configuring installed libraries and writing boilerplate code to structure and implement best practices for achieving optimal website performance.
At Brocoders, we often start new projects from scratch. That's why, over three years ago, we created a NestJS boilerplate for the backend so that we wouldn't have to spend time developing core functionality that the end user doesn't see but is crucial for developers. Over this time, the boilerplate has received 1.9k stars on GitHub and has gained significant popularity beyond our company. Now, we've decided to take it a step further and created the Extensive React Boilerplate for the front end. In purpose to keep our best practices in project development together, avoiding familiar pitfalls and reducing development time, we've created bc boilerplates.
Modules and Libraries Included in the Boilerplate
To have server-side rendering out of the box, automatic page caching, preloading, and other speed optimizations, we use the Next.js framework. It extends React by adding static site generation and is a powerful tool for creating productive and SEO-friendly web applications. To ensure code reliability, performance, and readability in the boilerplate, TypeScript is utilized.
To prepare the website to support local languages and settings, we use an experienced language expert for web applications, the internationalization framework i18next. It helps organize all added language versions and adapt the content of the site, menu, and messages for different languages and regional settings. We expanded it with packages to detect the user's browser language and transform resources into the server side of i18next.
Material-UI is used for quickly creating interfaces without spending time writing components from scratch, such as buttons, input fields, tables, modal windows, and more. With dark mode support, the application based on the boilerplate is automatically configured to use the user's system theme.
React Hook Form library is integrated for form management, providing a simple and intuitive API optimized for high performance as it works with data without unnecessary re-renders of the entire form.
"React Query" is used for state management and data caching. It automatically optimizes queries, reducing their duplication, and supports data caching on both the client and server sides, allowing easy cache management across environments.
The Cypress library provides an interface for tracking and debugging tests, supporting various types of tests, including unit tests, integration tests, user interface tests, and more.
ESLint helps to ensure that the style of the code in the project is consistent with the rules already established in the .eslintrc.json file to avoid potential problems and warn about possible errors.
The Architecture of the React Boilerplate Project and Folder Structure
The project structure allows for easy navigation and editing of various parts of the application. Automated tests are located in the /cypress folder, divided into different specifications for testing various aspects of the application. All source code of the project, following the logical structure of the application, is concentrated in the /src folder. Nested within it, the /app folder displays various application pages, such as the administrative panel with pages for creating and editing users, email confirmation pages, password reset, password change, user profile, login, and registration. The /components folder contains common components that can be used on different pages of the application. The services section is responsible for interacting with the API server; its files contain modules that are important for proper functionality and interaction with the backend and external services.
As far as this boilerplate uses the Next.js framework for building React applications, the folders are used as routes. This means the more folders you add to your app folder, the more routes you will get. Additionally, if you create a new folder inside of another folder, you will get nested routes. To better understand these concepts, we suggest looking at the image below.
We use dynamic segments in routing when flexible routes are needed. Within the file structure in the /app folder, such routes wrap the folder name in square brackets. Thus, it is easy to guess that the variable segments in the route src/app/[language]/admin-panel/users/edit/[id]/ will be language and id.
Mechanisms of Authentication and User Interaction
Since the web application supports internationalization, additional middleware is added to each page to determine the language, so the language of the authentication form will be displayed depending on the basic system settings of the user's device.
Sign Up Page
The Sign Up page contains a registration form with fields for user registration, as well as the option to register via Google and Facebook. The necessary API for requests to the server to create a new account is specified, and saving user data is implemented using a context.
export function useAuthGoogleLoginService() {
const fetchBase = useFetchBase();
return useCallback(
(data: AuthGoogleLoginRequest) => {
return fetchBase(`${API_URL}/v1/auth/google/login`, {
method: "POST",
body: JSON.stringify(data),
}).then(wrapperFetchJsonResponse<AuthGoogleLoginResponse>);
},
[fetchBase]
);
}
export function useAuthFacebookLoginService() {
const fetchBase = useFetchBase();
return useCallback(
(data: AuthFacebookLoginRequest, requestConfig?: RequestConfigType) => {
return fetchBase(`${API_URL}/v1/auth/facebook/login`, {
method: "POST",
body: JSON.stringify(data),
...requestConfig,
}).then(wrapperFetchJsonResponse<AuthFacebookLoginResponse>);
},
[fetchBase]
);
}
Access and refresh tokens are acquired and stored for future requests if the backend status is ok. Otherwise, error-handling procedures are executed.
Sign In Page
The Sign In page contains an authentication form with fields for logging in an already registered user, and again, the option to log in via Google or Facebook. After successful authentication, the user receives an access token and a refresh token, which are stored for future requests.
if (status === HTTP_CODES_ENUM.OK) {
setTokensInfo({
token: data.token,
refreshToken: data.refreshToken,
tokenExpires: data.tokenExpires,
});
setUser(data.user);
}
const setTokensInfo = useCallback(
(tokensInfo: TokensInfo) => {
setTokensInfoRef(tokensInfo);
if (tokensInfo) {
Cookies.set(AUTH_TOKEN_KEY, JSON.stringify(tokensInfo));
} else {
Cookies.remove(AUTH_TOKEN_KEY);
setUser(null);
}
},
[setTokensInfoRef]
);
Restore and Update Password
A user may forget their password, so functionality for resetting the old password by sending a link to the email is created. Of course, for such cases, there should be a corresponding API on the server, like in our nestjs-boilerplate, which is perfect for two-way interaction.
Also, there is an ability to update the password. The logic of sending an API request to the server to update the user's password and further processing its results is specified.
After registering a new account on the server, a link for email confirmation must be generated. Therefore, the boilerplate has logic for the confirm-email route as well.
Public and Private Routes
Both public and private routes are implemented - the user's authorization is checked before displaying certain pages, and if the user is not authorized or the authorization data has not yet been loaded, the user is redirected to the sign-in page. Below is the HOC function that implements this logic:
function withPageRequiredAuth(
Component: FunctionComponent<PropsType>,
options?: OptionsType
) {
// …
return function WithPageRequiredAuth(props: PropsType) {
// …
useEffect(() => {
const check = () => {
if (
(user && user?.role?.id && optionRoles.includes(user?.role.id)) ||
!isLoaded
)
return;
const currentLocation = window.location.toString();
const returnToPath =
currentLocation.replace(new URL(currentLocation).origin, "") ||
`/${language}`;
const params = new URLSearchParams({
returnTo: returnToPath,
});
let redirectTo = `/${language}/sign-in?${params.toString()}`;
if (user) {
redirectTo = `/${language}`;
}
router.replace(redirectTo);
};
check();
}, [user, isLoaded, router, language]);
return user && user?.role?.id && optionRoles.includes(user?.role.id) ? (
<Component {...props} ></Component>
) : null;
};
}
Cypress tests have been added for sign-in, sign-up, and forgot-password to detect errors and check that all the functionalities of the authentication forms work on different browsers and devices.
User’s Profile Management
The boilerplate includes user data pages and pages for editing their data. Functionality has been added to implement an avatar component that allows users to upload or change their profile photo.
The /profile/edit page has been created to implement the ability to edit the profile, which includes a form with personal data that the user entered during registration, such as name, surname, and password, as well as adding/changing an avatar. Additionally, to ensure code quality, detect potential security issues, and verify that the profile editing functionality works properly, this part of the code is also covered by Cypress tests.
describe("Validation and error messages", () => {
beforeEach(() => {
cy.visit("/sign-in");
});
it("Error messages should be displayed if required fields are empty", () => {
cy.getBySel("sign-in-submit").click();
cy.getBySel("email-error").should("be.visible");
cy.getBySel("password-error").should("be.visible");
cy.getBySel("email").type("useremail@gmail.com");
cy.getBySel("email-error").should("not.exist");
cy.getBySel("sign-in-submit").click();
cy.getBySel("password-error").should("be.visible");
cy.getBySel("password").type("password1");
cy.getBySel("password-error").should("not.exist");
cy.getBySel("email").clear();
cy.getBySel("email-error").should("be.visible");
});
it("Error message should be displayed if email isn't registered in the system", () => {
cy.intercept("POST", "/api/v1/auth/email/login").as("login");
cy.getBySel("email").type("notexistedemail@gmail.com");
cy.getBySel("password").type("password1");
cy.getBySel("sign-in-submit").click();
cy.wait("@login");
cy.getBySel("email-error").should("be.visible");
});
});
To automate the process of detecting and updating dependencies, we use the Renovate bot. It helps avoid issues related to using outdated dependencies and allows controlling the dependency update process according to the project's needs.
Conclusion
We refer to the Extensive React Boilerplate as a structured starting point for front-end development. It pairs beautifully with our NestJS boilerplate for the backend, and with them, the development team can get started, minimizing setup time and focusing on developing unique aspects of the project, knowing that fundamentals are already correctly implemented. We also keep track of regular library updates and maintain the project in an up-to-date state.
Published at DZone with permission of Rodion Salnik. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments