Best Practices for Writing Clean React Code with Examples
Serif COLAKEL
Posted on October 29, 2023
Introduction
React is a JavaScript library for building user interfaces. It’s a very powerful library that can help you build amazing applications. However, it’s important to follow best practices to get the most out of React. This article will teach you some of the best practices for writing clean React code.
1. Component Structure and Organization
The structure of your components is very important. It can make or break your application. It’s important to follow best practices when writing React code. This article will teach you some of the best practices for writing clean React code.
1.1. Separation of Concerns
Divide your components into three categories:
Containers: They are responsible for fetching data and dispatching actions to the store. They are also called smart components.
Components: They are responsible for rendering the UI. They are also called dumb components.
Pages: They are responsible for connecting the containers and components to the router. They are also called views.
HOC: They are responsible for sharing common functionality between components.
Hooks: They are responsible for sharing common functionality between components.
Services: They are responsible for fetching data from the server.
Utils: They are responsible for sharing common functionality between components.
1.2. File Structure
src/
components/
Button/
index.js
Button.js
Button.test.js
Button.module.css
containers/
App/
index.js
App.js
App.test.js
App.module.css
pages/
Home/
index.js
Home.js
Home.test.js
Home.module.css
store/
index.js
rootReducer.js
rootSaga.js
configureStore.js
utils/
api.js
constants.js
helpers.js
index.js
validators.js
index.js
index.css
serviceWorker.js
setupTests.js
1.3. Naming Conventions
Components: Use PascalCase for naming the component file and the component itself. For example,
Button.js
andButton
respectively.Containers: Use PascalCase for naming the container file and the container itself. For example,
App.js
andApp
respectively.Pages: Use PascalCase for naming the page file and the page itself. For example,
Home.js
andHome
respectively.CSS Modules: Use PascalCase for naming the CSS Module file. For example,
Button.module.css
.Constants: Use SCREAMING_SNAKE_CASE for naming the constant file. For example,
constants.js
.Helpers: Use camelCase for naming the helper file. For example,
helpers.js
.Utils: Use camelCase for naming the util file. For example,
utils.js
.Tests: Use PascalCase for naming the test file. For example,
Button.test.js
.
2. Component Patterns
2.1. Container and Presentational Components
Define your components as either containers or presentational components. Containers are responsible for fetching data and dispatching actions to the store. Presentational components are responsible for rendering the UI.
2.2. Higher-Order Components (HOC)
React’s advanced methodology enables the reuse of component functionality within the render technique. A component can be changed to a high order by using an advanced level of the component.Consideration of higher-order components as one of the best practices for Reactjs developers. Higher-Order Components serve as an advanced technique in React, allowing reuse of the component logic inside the render method. When considering the advanced level, transform a component into a higher order of the component. For example, show some components when the user stays logged in and complement the similar code with every component.Read More:- Reasons Why Use React.js For Web Development
import React, { useState, useEffect } from 'react';
const withData = (WrappedComponent) => {
const WithData = (props) => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => setData(data));
}, []);
return <WrappedComponent data={data} {...props} />;
};
return WithData;
};
2.3. Render Props
Use render props to share common functionality between components. For example, you can use a render prop to share the logic for fetching data. You can also use a render prop to share the logic for handling form state.
import React, { useState, useEffect } from 'react';
const WithData = ({ render }) => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => setData(data));
}, []);
return render(data);
};
2.4. Hooks
Use hooks to share common functionality between components. For example, you can use a hook to share the logic for fetching data. You can also use a hook to share the logic for handling form state.
import React, { useState, useEffect } from 'react';
const useFetch = (input, { auto, ...init }) => {
const [result, setResult] = useState([null, null, true]);
const fetcher = useCallback(
(query, config) =>
fetch(query, config)
.then((res) => res.json())
.then((data) => setUsers([null, data, false]))
.catch((err) => setResult([err, null, false])),
[input, init]
);
useEffect(() => {
if (auto) {
fetcher(input, init);
}
}, []); // if you want to fetch data only once, do this.
return [...result, fetcher];
//fetcher(refetch) function or can be used for post api call
};
// USAGE
function App() {
const [err, users, loading] = useFetch(`/api/users`, { auto: true });
return (
<div>
{users.map((user) => (
<User key={user.id} user={user} />
))}
</div>
);
}
ℹ️ It’s similar to react-query/useSWR, both libraries have much more to offer. you can use these libraries, but if you have restrictions on your project you can go ahead with this approach to avoid some extra code.
3. Use an object instead of a switch inside the reducer
This is not a good idea if you have a lot of cases to handle. You can use an object literal as an alternative to switch statements. The object literal is more readable and easier to maintain.
// BAD
const initialState = {
status: 'idle',
error: null,
data: null,
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'FETCH_DATA_REQUEST':
return {
...state,
status: 'loading',
};
case 'FETCH_DATA_SUCCESS':
return {
...state,
status: 'success',
data: action.payload,
};
case 'FETCH_DATA_FAILURE':
return {
...state,
status: 'error',
error: action.payload,
};
default:
return state;
}
};
// GOOD
const initialState = {
status: 'idle',
error: null,
data: null,
};
const reducer = (state = initialState, action) => {
const { type, payload } = action;
const handlers = {
FETCH_DATA_REQUEST: {
...state,
status: 'loading',
},
FETCH_DATA_SUCCESS: {
...state,
status: 'success',
data: payload,
},
FETCH_DATA_FAILURE: {
...state,
status: 'error',
error: payload,
},
};
return handlers[type] || state;
};
The map variable must be declared outside the dispatch context otherwise it will always be re-evaluated.
A switch can be implemented using a tree which makes it O(log n)
. Searching on the map is O(1)
4. Code Splitting
use React.lazy, It is a very powerful tool that allows you to load components only when they are needed. The React.lazy function lets you render a dynamic import as a regular component.
A good place to start is with routes. When you go with the traditional approach, you have to load both components before rendering them, but this is not a good approach, because it will take extra time to load all components. Even though we are not showing the component.
We can use react.lazy to load the components asynchronously. So when you are at the first(Home) page, you can load the first component and when you are at the second(About) page, you can load the second component. This way we can avoid the unnecessary loading of components.
4.1. Code Splitting with React.lazy
import React, { Suspense } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = React.lazy(() => import('./routes/Home'));
const About = React.lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
);
ℹ️ This is a straightforward use case but what if we have hundreds of routes and components? You will see a massive difference in the performance.
4.2. CSS in JS
When it comes to large projects, one of the most fundamental of the React best practices is styling and theming. However, it turns out to be a challenging task like maintaining those big CSS files. So, this is where the CSS-in-JS solutions came into the picture.Similar to managing those enormous CSS files, designing and theming can be a complex effort in a larger project. Thus, the idea of CSS-in-JS solutions (i.e., embedding CSS in JavaScript) emerged. Diverse libraries are based on this idea.Among numerous libraries, you can use the needed one based on the requirement, like some for the complicated themes.
4.3. Lazy Loading Images
Lazy loading is a technique that defers loading of non-critical resources at page load time. Instead, these non-critical resources are loaded at the moment of need. This can help you reduce initial load time and save bandwidth.
import React, { useState, useEffect } from 'react';
const LazyImage = ({ src, alt, ...delegated }) => {
const [source, setSource] = useState(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setSource(src);
observer.unobserve(image);
}
},
{
rootMargin: '200px',
}
);
observer.observe(image);
return () => {
observer.unobserve(image);
};
}, [src]);
return <img {...delegated} ref={setImage} src={source} alt={alt} />;
};
5. Keeping State Business Logic Away from Components
Separate the state management logic from the UI logic that can help you in multiple ways. Components that do both are not impactful as they make them less reusable, more difficult to test, and difficult to refactor, especially when you’re looking for state management. Rather than writing the logic for placement of the state inside a component, the best move is to extract it into a hook of its own.and another child component that contains the user interface. Then, call the child inside while the adult hands over all the necessary props.
// BAD
const App = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [status, setStatus] = useState('idle');
useEffect(() => {
setStatus('loading');
fetch('https://api.example.com/data')
.then((response) => response.json())
.then((data) => {
setData(data);
setStatus('success');
})
.catch((error) => {
setError(error);
setStatus('error');
});
}, []);
if (status === 'idle' || status === 'loading') {
return <div>Loading...</div>;
}
if (status === 'error') {
return <div>{error}</div>;
}
if (status === 'success') {
return <div>{data}</div>;
}
};
// GOOD
const useFetch = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [status, setStatus] = useState('idle');
useEffect(() => {
setStatus('loading');
fetch(url)
.then((response) => response.json())
.then((data) => {
setData(data);
setStatus('success');
})
.catch((error) => {
setError(error);
setStatus('error');
});
}, []);
return { data, error, status };
};
const App = () => {
const { data, error, status } = useFetch('https://api.example.com/data');
if (status === 'idle' || status === 'loading') {
return <div>Loading...</div>;
}
if (status === 'error') {
return <div>{error}</div>;
}
if (status === 'success') {
return <div>{data}</div>;
}
};
6. Use TypeScript
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It offers classes, modules, and interfaces to help you build robust components. TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It offers classes, modules, and interfaces to help you build robust components.
import React from 'react';
interface Props {
name: string;
}
const Hello: React.FC<Props> = ({ name }) => <div>Hello {name}</div>;
7. Use PropTypes
PropTypes is a library that allows you to type-check your React components. It’s a good idea to use PropTypes to document the intended usage of your components.
import React from 'react';
interface Props {
name: string;
}
const Hello: React.FC<Props> = ({ name }) => <div>Hello {name}</div>;
Hello.propTypes = {
name: PropTypes.string.isRequired,
};
8. Use ESLint
ESLint is a tool for identifying and reporting on patterns found in ECMAScript/JavaScript code. It’s a good idea to use ESLint to enforce coding standards.
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"react/prop-types": "off",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off"
}
}
9. Use Prettier
Prettier is an opinionated code formatter. It’s a good idea to use Prettier to enforce a consistent code style.
{
"singleQuote": true,
"trailingComma": "all",
"arrowParens": "always",
"printWidth": 80,
"tabWidth": 2
}
10. Use Husky
Husky is a tool that makes it easy to use Git hooks as part of your development workflow. It’s a good idea to use Husky to enforce a consistent commit message format.
{
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
}
Conclusion
Writing clean and maintainable React code is essential for building robust and scalable applications. In this article, we've discussed several best practices and examples to help you improve your React development process:
- Component Structure and Organization:
Organize your components into Containers, Components, Pages, Higher-Order Components (HOC), Hooks, Services, and Utils for better separation of concerns and maintainability.
Use a consistent file structure to organize your code. 2. Component Patterns:
Distinguish between Container and Presentational Components to keep your code organized and maintainable.
Utilize Higher-Order Components (HOC), Render Props, and Hooks for sharing common functionality between components. 3. Use an Object Instead of a Switch Inside the Reducer:
Replace switch statements in reducers with an object literal for better readability and maintainability. 4. Code Splitting:
Use React.lazy for lazy-loading components, which can significantly improve performance in large applications. 5. Keeping State Business Logic Away from Components:
Separate state management logic from UI logic to improve reusability, testability, and maintainability. 6. Use TypeScript:
Adopt TypeScript to add static typing and enhance your code's reliability. 7. Use PropTypes:
Document your components and ensure proper prop types using PropTypes for enhanced code clarity. 8. Use ESLint:
Employ ESLint to enforce coding standards and catch potential issues early. 9. Use Prettier:
Use Prettier for code formatting to maintain a consistent code style. 10. Use Husky:
Implement Husky to enforce a consistent commit message format and maintain a clean version control history.
By following these best practices, you can write clean, organized, and maintainable React code, which will improve collaboration among developers, reduce the likelihood of bugs, and make it easier to scale and extend your React applications. Incorporating these practices into your development workflow will contribute to the success of your React projects.
Posted on October 29, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.