5 Concepts That Will Make You a Better React Developer
Learn how to leverage advanced React concepts to become a better React developer.
1. Custom Hooks โ
In React, a custom hook is a function that allows you to reuse stateful logic across multiple components. It allows you to extract and reuse logic that was previously scattered across multiple components. Custom hooks are typically named with the use
prefix and can call other hooks if necessary.
Building your custom hooks is a great way of extracting component logic into functions that can be reused and tested independently.
Example ๐๐ป
// CUSTOM 'useFetch' HOOK FOR DATA FETCHING ๐
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
To use this custom hook in a component, you simply call it and destructure the state object it returns:
import React from 'react';
import useFetch from './useFetch';
function MyComponent() {
const { data, loading, error } = useFetch('https://example.com/api/data');
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error: {error.message}</div>;
}
return (
<div>
{data.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
2. Suspense
Suspense is a feature that lets your component declaratively wait for something to load before it can be rendered. Suspense can be used to wait for some code to load using React.Lazy in combination with React.Suspense, or since React 18.0.0 it can be used to wait for some asynchronous data to load as well. Iโll cover these two primary use cases briefly below;
Lazy Loading and Code Splitting
Code-splitting is a technique where a web application is โsplitโ into pieces to improve performance and load time. The idea is that initially you only load scripts and assets that are immediately required to display some page. The rest of the scripts and assets are loaded lazily whenever needed.
Example ๐๐ป
import React, { Suspense } from 'react';
const ArticlePage = React.lazy(() => import('./ArticlePage'));
// Fallback to a skeleton while the ArticlePage is loading
<Suspense fallback={<ArticleSkeleton />}>
<ArticlePage />
</Suspense>
In the above example, the scripts and assets for ArticlePage are not loaded until it needs to be displayed.
Data Fetching with Suspense
Data fetching with suspense is a new feature of React 18.0.0, albeit released as an experimental feature in earlier versions. The typical approach for data-fetching with React has been to start rendering components. Then using the useEffect hook, each of these components may trigger some data fetching logic eg. calling an API, and eventually updating state and rendering. This approach often leads to โwaterfallsโ where nested components initiate fetching only when parent components are ready as depicted by the code below.
const Article = ({ data }) => {
const [suggestions, setSuggestions] = useState(null);
useEffect(() => {
fetch(`/suggestions/${data.title}`)
.then(response => response.json())
.then(setSuggestions)
.catch(error => console.error(error));
}, [data.title]);
return suggestions ? <Suggestions suggestions={suggestions} /> : null;
};
const ArticlePage = ({ id }) => {
const [article, setArticle] = useState(null);
useEffect(() => {
fetch(`/article/${id}`)
.then(response => response.json())
.then(setArticle)
.catch(error => console.error(error));
}, [id]);
return article ? <Article data={article} /> : null;
};
Often a lot of these operations could be parallelized.
With suspense, we donโt wait for responses to come in, we just kick off the asynchronous requests and immediately start rendering. React will then try to render the component hierarchy. If something fails because of missing data it will just fall back to whatever fallback is defined in the Suspense wrapper.
// This is not a Promise. It's a special object from our Suspense integration.
const initialArticle = fetchArticle(0);
function Articles() {
const [article, setArticle] = useState(initialArticle);
return (
<>
<button onClick={() => { setArticle(fetchArticle(article.id + 1)) } }>
Next
</button>
<ArticlePage article={article} />
</>
);
}
function Article({ article }) {
return (
<Suspense fallback={<Spinner />}>
<ArticleContent article={article} />
<Suspense fallback={<h1>Loading similar...</h1>}>
<Similar similar={article} />
</Suspense>
</Suspense>
);
}
function ArticleContent({ article }) {
const article = article.content.read();
return (
<>
<h1>{article.title}</h1>
...
</>
);
}
In the example above the article will show only when loaded and otherwise a spinner, whilst similar articles will show only when they are loaded. There is some magic happening behind the curtains in the fetchArticle function which I will cover in a later post.
3. Higher Order Components
In React, a Higher-Order Component (HOC) is a function that takes a component and returns a new component with some additional props or functionality. HOCs are a common pattern in React for sharing functionality between components or abstracting away complex logic.
here's a real-world example of using a Higher-Order Component to add authentication to a component:
import React, { useEffect, useState } from 'react';
const withAuth = (Component) => (props) => {
const [user, setUser] = useState(null);
useEffect(() => {
// Check if the user is authenticated
const isAuthenticated = // Some authentication logic here...
if (isAuthenticated) {
// If the user is authenticated, set the user state
setUser({ name: 'John Doe', email: 'john.doe@example.com' });
} else {
// If the user is not authenticated, redirect to the login page
window.location.href = '/login';
}
}, []);
if (!user) {
// If the user is not authenticated yet, show a loading indicator
return <div>Loading...</div>;
}
// If the user is authenticated, render the wrapped component with the user prop
return <Component {...props} user={user} />;
};
function Dashboard({ user }) {
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Your email address is {user.email}.</p>
<p>This is your dashboard.</p>
</div>
);
}
const AuthenticatedDashboard = withAuth(Dashboard);
function App() {
return (
<div>
<AuthenticatedDashboard />
</div>
);
}
In this example, withAuth
is a Higher-Order Component that takes a Component
as an argument and returns a new component that adds authentication logic. Inside the HOC, we use the useState
hook to create a user
state that will hold the user's information, and the useEffect
hook to check if the user is authenticated.
If the user is not authenticated, the HOC redirects to the login page. If the user is authenticated, the HOC renders the wrapped component with the user
prop passed in.
The AuthenticatedDashboard
component is created by passing the Dashboard
component to the withAuth
function. Now, AuthenticatedDashboard
can be used just like any other React component, but it has authentication logic added to it.
4. Context
React Context is a powerful feature that allows you to share data between components more efficiently, without the need to pass props down through each level of the component hierarchy. It provides a way to share data that can be considered "global" throughout your application, such as user authentication credentials, theming, language settings, and much more.
With React Context, you can easily manage and access this shared data, making your code cleaner and more organized.
import { useState, useContext, createContext } from 'react';
const themeContext = createContext();
const useTheme = () => useContext(themeContext);
const ThemeProvider = ({ theme, ...rest }) => {
const [theme, setTheme] = useState(theme);
return <ThemeContext.Provider value={[theme, setTheme]} />;
}
const Toolbar = () => {
const [theme, setTheme] = useTheme();
return (
<>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light' )}
...
</>
);
}
const App = () => (
<ThemeProvider theme="light">
<Toolbar />
<Routes />
</ThemeProvider>
);
In the simple example above you can easily change the theme between โlightโ or โdarkโ using the useTheme hook, and the change will propagate to all components in the hierarchy since the value is provided by the context.
5. Portals
In React, portals are a way to render a child component into a different part of the DOM that is outside of the parent component's hierarchy. This allows you to render a component in a different place in the document tree, while still being controlled by the parent component's logic.
Portals are useful when you need to render a child component outside of the parent's DOM hierarchy, such as when creating modals, tooltips, or popovers. Instead of manually manipulating the DOM to render the child component in a different part of the document, portals let you do this in a more React-friendly way.
To use a portal in React, you need to first create a new DOM node to render the child component into. Then, you can use the ReactDOM.createPortal()
method to render the child component into this new node.
Here's an example of how to use a portal in React:
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
const Modal = ({ onClose }) => {
const [isOpen, setIsOpen] = useState(true);
const handleClose = () => {
setIsOpen(false);
onClose();
};
return ReactDOM.createPortal(
<div className={`modal ${isOpen ? 'open' : ''}`}>
<div className="modal-content">
<span className="close" onClick={handleClose}>×</span>
<p>This is a modal!</p>
</div>
</div>,
document.body
);
};
App.js
๐๐ป
import Modal from './Modal'
const App = () => {
const [showModal, setShowModal] = useState(false);
const handleOpenModal = () => {
setShowModal(true);
};
const handleCloseModal = () => {
setShowModal(false);
};
return (
<div>
<h1>Hello, world!</h1>
<button onClick={handleOpenModal}>Open modal</button>
{showModal && <Modal onClose={handleCloseModal} />}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
In this example, we have a Modal
component that creates a modal dialog and renders it using a portal. The App
component has a button that, when clicked, opens the modal by setting a state variable. When the modal is closed, the onClose
prop is called to update the state and close the modal.
The Modal
component renders the modal dialog outside of its parent component's DOM hierarchy using a portal. The modal content is defined in the JSX and includes a close button that updates the state to close the modal.
This is just one example of how portals can be used in a React application. They provide a way to render content outside of the normal component hierarchy, which can be useful in many different scenarios.
That concludes 5 Concepts That Will Make You a Better React Developer. Hope you enjoyed the reading and learned something new! ๐
Let's Connect
Hopefully, this has helped you for learning something new :) As always, you are welcome to leave comments with suggestions, questions, corrections, and any other feedback you find useful.
Thank you for reading!
Subscribe to my newsletter
Read articles from Mahendra Bishnoi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mahendra Bishnoi
Mahendra Bishnoi
Software Engineer ๐ฉ๐ปโ๐ป. Part Time Open-Source Developer ๐