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}>&times;</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!

Β