Bye-bye useState & useEffect: Revolutionizing React Development!

Bye-bye useState & useEffect: Revolutionizing React Development!

ยท

7 min read

Many developers continue to use the useState and useEffect hooks to update states, but I have not been fond of this approach. The issue is that it causes the component to mount, remount, and unmount simultaneously, leading to unexpected behavior. As a result, when logging something into the console, you may see the result repeated three times.

Introducing the useLoaderData Hook:

The useLoaderData hook is a custom hook in React that helps you load data into your component. It simplifies the process of fetching data from an API or performing any asynchronous operation.

When you use the useLoaderData hook, you provide it with a function that returns a Promise. This Promise represents an asynchronous operation that will fetch the data you need. Once the Promise resolves, the data becomes available to your component.

The useLoaderData hook handles the loading state for you, so you don't need to manually track whether the data is still loading or if it has finished loading. It provides you with a convenient way to access the data and also handles any potential errors that might occur during the data loading process.

By using the useLoaderData hook, you can keep your component code clean and organized, separating the data-loading logic from the rest of your component's responsibilities. It allows you to easily fetch and manage data in a more beginner-friendly way.

Why the useLoaderHook?

The useLoaderHook from react-router helps achieve the same functionality with minimal effort. These are some examples of why you should use it.

  • Loading state management: Loaders handle the loading state for you, providing a clear indication of when data is being fetched. This helps you manage loading spinners, progress indicators, or any other UI elements related to data loading.

  • Error handling: Loaders often include error handling mechanisms, allowing you to handle and display errors that occur during the data loading process. They provide a standardized way to handle errors, making it easier to implement consistent error handling across your application.

  • Separation of concerns: Loaders allow you to separate the data loading logic from other aspects of your component. This promotes better code organization and maintainability, as you can focus on specific responsibilities without mixing them.

And lots more.

Let's see How This Works.

It's assumed that you have a good knowledge of how react-router 6 works. If you don't, Feel free to check out the docs here

Firstly, we have to set up the routing system in our application to work with the Loader API. Before now, we have been using the BrowserRouter setup to handle the various routes for our application.
Let's spend a little time talking about this.

import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"
import HomeComponent from "./home"
import AboutCompoent from "./about"
function App () {
    <BrowserRouter>
        <Routes>
            <Route path='/' element={<Outlet />}>
                <Route index element={<HomeComponent /> } />
                <Route path='about' element={<AboutComponent/> } />
            </Route>
        </Routes>
    </BrowserRouter>
};
export default App;

Here, we have set up a routing system traditionally using those imports from react-router.
Think for a second about what's happening.

Well. The BrowserRouter from react-router creates an array of object from the Routes children. The snippet below provides a clear illustration of how this is working.

BrowserRouter([
{
    path: '/',
    element: <HomeComponent />,
    children: []
},
{
    path: '/about',
    element: <AboutComponent/>,
    children: []
}
])

If they were to be a nested route, then it appends the children's route to the children's key in the parent route.
Yes, That's how it keeps being recursive.

However, this method can't be used to use the loaderData hook. We have to do a bit of refactoring. Don't panic, It's a bit similar to this. I highly recommend you check out the react-router docs for more information.

import { 
createBrowserRouter,
createRoutesFromElements,
RouterProvider,
Route, 
Outlet
 } from "react-router-dom"

import HomeComponent from "./home"
import AboutComponent from "./about"

function App() {
    const browserRoutes = createBrowserRouter(createRoutesFromElements(
       <Route path='/' element={<Outlet />}>
                <Route index element={<HomeComponent /> } />
                <Route path='about' element={<AboutComponent /> } />
        </Route>
    ))

     return (
        <RouterProvider router={browserRoutes} />
    );
}

I have imported createBrowserRouter, createRoutesFromElement, RouterProvider.
Then, initialize a variable named browserRoutes to serve as that object that should be rendered. Noticed that I called the createRoutesFromElements function inside of the createBrowserRouter function. This was because I want to parse or convert the Routes to an object and the createRoutesFromElements as the name implies can help me do that. Then lastly the RouterProvider was returned with the value of the new browserRouter. Let's take a look at what we would have done without using the createRoutesFromElements function.

createBrowserRouter([
{
    path: '/',
    element: <HomeComponent />,
    children: []
},
{
    path: '/about',
    element: <AboutComponent/>,
    children: []
}])

I am not a big fan of this as your route can even go nested and at some point, this becomes confusing. You should keep things very simple.

Exploring the Loader functions:

As we now have a bit of an understanding of how we can set up our application to use the Loader API, let's see how we can use the API.

Say you intend to fetch data from an endpoint andto be displayed on the homeComponent. What most developers would do is: initialize a state and update the state in the useEffect hook. The snippet below provides a clear illustration of what I am talking about.

import { useState } from 'react'

const HomeComponent = () => {
    const [data, setData] = useState([]);

    useEffect(async () => {
        const request = await fetch('http://localhost:3004/file');
         if(!request.ok) throw new Error('Failed to fetch data')
        const item= await request.json()
        setData(item)  
    }, [])

    return (
        <section>
            { data.length > 0 ? data.map((foundData) => (
                    <div key={foundData.id}>
                        <strong>{foundData.name}</strong>
                     </div>
                 )) : <p>Data currently unavailable</p>}
        </section>
    )
}
export default HomeComponent

This is a tonne of lines as we might want to simplify this a bit and maybe reuse the same function.

To use Loaders, you have to define a loader function. Loader functions are like Custom Hooks.
Besides, the naming convention of the function doesn't matter as you can call it anything. In the code snippet below, I will create a basic loader function that fetches data from an API like I showed in the snipppet above

export async function LoaderFunction () {
    const request = await fetch('http://localhost:3004/file');
    if (!request.ok) throw new Error ('Failed to fetch item')
    const item = await  response.json();
    return item;
};

Now, we have to import the loader function to component where our routes are being handled. After setting up your route system using the createBrowserRouter and createRouteFromElements you should have access to a prop called loader. There you should pass in the LoaderFunction you created as the value.
In the code snippet below provides a clear illustration of this.

import { 
createBrowserRouter,
createRoutesFromElements,
RouterProvider,
Route, 
Outlet
 } from "react-router-dom"
import HomeComponent from "./home"
import AboutComponent from "./about"
import { LoaderFunction as HomeLoader} from "./loader"

function App() {
    const browserRoutes = createBrowserRouter(createRoutesFromElements(
       <Route path='/' element={<Outlet />}>
                <Route index element={<HomeComponent /> }
                     loader={HomeLoader}/>
                <Route path='about' element={<AboutComponent /> } />
        </Route>
    ))

     return (
        <RouterProvider router={browserRoutes} />
    );
}

After that, We can access the data returned by the loader function using the useLoaderData Hook from react-router in the HomeComponent.
The code snippet below best explains what just read.

import { useLoaderData } from "react-router-dom"

const HomeComponent = () => {
    const data = useLoaderData();

    return (
        <section>
            {data.map((foundData) => (
                    <div key={foundData.id}>
                         <strong>{foundData.name}</strong> 
                    </div> 
            ))}
        </section>
    )
}
export default HomeComponent

Wow! ๐Ÿ˜ฒ..
Now see how we have just cleaned up the HomeComponent :)
Noticed we got rid of the guard clause that checks if the data is null.
This is because react-router makes it load the data as soon as the url/path is active. So, it Makes the necessary requests even before the Component is Mounted. Yes!

We are only making provisions for the happy path. What if we pass a non-existing endpoint? If that's the case, don't panic as react-router also allow us to pass components to another prop called errorElement .
This is specifically for Errors just as we use ErrorBoundaries. Let's see how this works in the snippet below

import { 
createBrowserRouter,
createRoutesFromElements,
RouterProvider,
Route, 
Outlet
 } from "react-router-dom"
import HomeComponent from "./home"
import AboutComponent from "./about"
import { LoaderFunction as HomeLoader} from "./loader"

function App() {
    const browserRoutes = createBrowserRouter(createRoutesFromElements(
       <Route path='/' element={<Outlet />}>
                <Route index element={<HomeComponent /> }
                    loader={HomeLoader} errorElement={<h1>An Error occured</h1>}/>
                <Route path='about' element={<AboutComponent /> } />
        </Route>
    ))

     return (
        <RouterProvider router={browserRoutes} />
    );
}

I have just used a header tag to show the error. It is advisable you use a Component so that you can also get access to the useRouteError Hook. I'd show how to use the useRouteError Hook in one of my upcoming blog posts. If you're keen to learn about it, Kindly use this link.
Since it pre-fetches the data before mounting the component, the loading state becomes irrelevant as it might either get the data or return the error message the you pass as a value to the errorElement prop.

That's all of the basics you need to know about making requests using the Data Layer API

If you found this helpful, please consider following me on Twitter, reacting to this post, leaving a comment, or support me by buying me a coffee through this link.

Did you find this article valuable?

Support Emmanuel odii by becoming a sponsor. Any amount is appreciated!

ย