React is a popular and powerful Javascript library to build user interfaces. Also, with its component based architecture, React makes it easy to create reusable and scalable components. However, writing scalable React components requires best practices to ensure that your code is maintainable and efficient.
Hence, in this post, we will see some of the best practices for writing the scalable React components as mentioned below.
- Use Functional Components
- Keep the Components small and Single Responsibility
- Use PropTypes
- Use Fragment or React.Fragment
- Utilise React Memo and PureComponent
- Keep the State in Highest Level Component
- Implement Code splitting and Lazy Loading
- Use Context and Custom Hooks for Global state management
- Maintain clear Data flow and Sate management Pattern
- Periodic Code reviews and Refactoring
1. Use Functional Components
Since, functional components are a great way to write lightweight and reusable components in React. Also, with the introduction of React hooks functional components can now manage state and side effects, making them a powerful alternative for the class based components.
//Sample functional component
import React from 'react';
const MyComponent = () => {
return(
<div>
<h1>This is a sample functional component</h1>
</div>
);
}
export default MyComponent;
2. Keep the Components small and Single Responsibility
Also, it’s important to keep your components small and focused on a single responsibility. Since, this helps to improve reusability and maintainability of your code. If a component becomes too large and has many responsibilities, then consider breaking it down into smaller components, so that your code can be more focused towards it’s responsibility.
/*
sample small and single resposibility component,
it is responsible for rendering the user image
*/
import React from 'react';
const UserAvatar = ({ userInfo }) => {
return <img src={userInfo.avatar} alt={`Avatar of ${userInfo.name}`} />;
}
export default UserAvatar;
3.Use PropTypes
PropTypes are a great way to document the expected props of your components and catch the bugs earlier. Also, by defining the type and shape of the props, you can ensure that your components receive the correct data. Hence, below I have mentioned how you can use PropTypes in a component.
import React from 'react';
import PropTypes from 'prop-types';
const MyComponent = ({ text, count }) =>{
return(
<div>
<h1>{text}</h1>
<p>Count: {count}</p>
</div>
)
}
//MyComponent expecting PropTypes are defined below
MyComponent.propTypes = {
text: PropTypes.string.isRequired,
count: PropTypes.number.isRequired
}
export default MyComponent;
4. Use Fragment or React.Fragment
When a component needs to return multiple elements without a parent div, It is best to use Fragment or React.Fragment. Also, this helps to keep the DOM tree clean and to avoid unnecessary nesting. Hence, below I have mentioned how you can use the Fragment.
import React, { Fragment } from 'react';
//Sample usage of react Fragment in a component
const MyComponent1 = () => {
return(
<Fragment>
<h1>This is a sample component</h1>
<p>which shows how to use fragment</p>
</Fragment>
);
}
//Alternate sample usage of react Fragment in a component
const MyComponent2 = () => {
return(
<>
<h1>This is a sample component</h1>
<p>which shows how to use fragment</p>
</>
);
}
export { MyComponent1, MyComonent2 };
5. Utilise React Memo and PureComponent
When dealing with performance optimisation for your React components, it’s important to consider the usage of React.memo and PureComponent. Since, React.memo is a higher order component that can be memoize the functional component to prevent unnecessary re-renders based on the prop changes.
// MyMemoizedComponent.js
import React, { memo } from 'react';
// React's memo is used below
const MyMemoizedComponent = memo(({ text }) => {
return(
<div>
<h1>{text}</h1>
</div>
);
});
export default MyMemoizedComponent;
Meanwhile, PureComponent is a base class for class components that performs a shallow comparison of props and state to prevent re-renders incase of unchanged data.
// MyPureComponent.js
import React, { PureComponent } from 'react';
class MyPureComponent extends PureComponent {
render(){
return(
<div>
<h1>{this.props.text}</h1>;
</div>
);
}
}
export default MyPureComponent;
By utilising React.memo or PureComponent when applicable, we can optimise the performance of your React components, especially when dealing with large or frequently update data.
6. Keep State in the Highest Level Component
Also, to avoid unnecessary prop drilling and to manage the state efficiently, it’s often a good practice to keep that state in the highest level component. By passing down state and related functions as props to the child components, then the application’s state remains centralised, making it easier to manage and update as required.
import React,{ useState } from 'react';
// Sample highest level component
const ParentComponent = () => {
const [count,setCount] = useState(0);
const handleIncrement = ()=> {
setCount( count + 1 );
}
return <ChildComponent count={count} onIncreament={handleIncreament} />;
}
const ChldComponent = ({ count, onIncreament }) =>{
return(
<div>
<p>{`Count: ${count}`}</p>
<button onClick={onIncreament}>Increament</button>
</div>
)
}
export default ParentComponent;
7. Implement Code Splitting and Lazy Loading
Hence, we need follow code splitting and lazy loading of the component to increase the performance of your react application. This approach ensures that only the necessary components are loaded whenever it is required, by reducing the initial load time of your application. Also, React provides a built in dynamic import function for asynchronous loading of components.
Code splitting is a technique to split our code into smaller bundles, since, this helps in reducing the initial load time by only loading what’s require for the particular page or component.
Lazy Loading is a practice of loading components or modules only when they are required.
import React, { Suspense, lazy } from 'react';
// Lazy loading the component
const LazyLoadedComponent = lazy(() => import('./LazyLoadingComponent'));
const MyComponent = () =>{
return(
<div>
{/* Suspense with a fallback loader */}
<Suspense fallback={<div>Loading...!</div>}>
<LazyLoadedComponent/>
</Suspense>
</div>
)
}
export default MyComponent;
8. Use Context and Custom Hooks for Global State Management
Also, React’s Context API and custom hooks are powerful tools for managing application’s global state. Also, by using context, you can share state and functionality across the component tree without the need of prop drilling. Since, custom hooks allow you to extract the component logic in to a reusable functions, which enables a more modular and maintainable codebase. Also, below I have mentioned how you can create the context and custom hook.
// MyContext.js
import React, { createContext, useContext, useState } from 'react';
const MyContext = createContext();
const MyProvider = ({ children }) =>{
const [data,setData] = useState([]);
return(
<MyContext.Provider value={{ data,setData }}>
{ children }
</MyContext.Provider>
)
}
// Custom Hook
const useMyContext = () =>{
const context = useContext(MyContext);
if(!context){
throw new Error('useMyContext must be used within a MyProvider')
}
return context;
}
export { MyContext, useMyContext, MyProvider };
Also, in below code I have mentioned, how you can make use of the React context and custom hook.
import React from 'react';
import { MyContext, useContext } from './MyContext.js';
import MyMemoizedComponent from './MyMemoizedComponent.js';
import MyPureComponent from './MyPureComponent.js';
const MyComponent = () => {
// Above mentioned custom hook is used below
const { data, setData } = useContext();
const addItem = () => {
setData([...data, `Item ${data.length + 1}`]);
};
return(
<div>
<h2>Context and custom Hook example</h2>
<ul>
{
data.map(( item, index ) => (
<li key={`item-${index}`}>
{/*
Above mentioned MemoizedComponent and MyPureComponent is used here.
Even number indexed items will gets rendered with MemoizedComponent and
Odd number indexed items will gets rendered with PureComponent
*/}
{
idx % 2 == 0 ?
<MyMemoizedComponent text={item} /> :
<MyPureComponent text={item} />
}
</li>
))
}
</ul>
<button onClick={addItem}>Add Item</button>
</div>
)
}
export default MyComponent;
// App component with MyProvider
import React from 'react';
import { MyProvider } from './MyContext.js'
import MyComponent from './MyComponent.js'
const App = () => {
return(
<MyProvider>
<div>
<h2>Context and custom Hook example</h2>
<MyComponent/>
</div>
</MyProvider>
)
}
export default App;
9. Maintain Clear Data flow and State Management Pattern
Adopt a consistent data flow and state management pattern across your components, such as using Redux for global state management between parent child communication in complex application. React’s context can also be used for prop drilling avoidance. Since, a clear and uniform state management approach can reduce complexity and make the codebase more predictable.
10. Periodic Code Reviews and Refactoring
Also, frequent code checks and improvements can help to keep the code clean and high quality, by making sure it follows good coding habits. Encourage team members to regularly make the parts of the code better and also to improve the overall design of the application, based on the needs and feedbacks from the end users.
Conclusion
By implementing these best practices, you can further improve the quality, scalability and maintainability of your React components and applications. Also, these strategies can help you in approaching the front end development more efficiently.
Hence, if you are interested in implementing Role Based Access to your react application using CASL. Also, for more information and resources, visit our blog
Senior Software Engineer, Ceegees Software Solutions Pvt Ltd.