Some Problems I Face When Building React Single Page Application And How I Solve Them

ยท

6 min read

Going To Top Of Page On Route Change

By default, the React Router will not take you to the top of a new component after a route change, rather the page position will remain like before it navigated to the new page. To solve this, we will create ScrollToTop Javascript file in a helper folder helpers in the src folder and add the codes below.

import { useEffect } from "react"
import { useLocation } from "react-router-dom"

const ScrollToTop = () => {
  const { pathname } = useLocation()

  useEffect(() => {
    window.scrollTo(0, 0)
  }, [pathname])

  return null
}

export default ScrollToTop

We destructure the pathname of the current location from the useLocation() hook and added a useEffect() hook in the component. The useEffect() hook has the pathname as its dependency. On every page change, the page will be forced to scroll to the top.

This piece of instruction will be used in the App component like so:

import React from "react"
import { BrowserRouter } from "react-router-dom"
import ScrollToTop from "./helpers/ScrollToTop";

function App() {
  return (
    <BrowserRouter>
      <ScrollToTop />
      <App />
    </BrowserRouter>
  );
}

Using Denounce

Debouncing is a programming pattern in which the execution of a certain function that performs an expensive or time-consuming operation is delayed for a particular time, usually in seconds, to improve the application's performance.

It takes a delay parameter that determines how long the debounce function will wait before performing a task. Such tasks could include searching through a list or most importantly, performing an asynchronous request.

It is commonly used to limit the number of unnecessary calls to an API when the user is still typing.

Unlike in Vue, debounce is not so straightforward to use in React. There are a lot of solutions out there, some of which use hooks like useMemo and useCallback.

This is how I have been able to take on this problem:

Let's create a useDebounce file in the helpers folder.

import { useState, useEffect } from "react"

const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value)

  useEffect(
    () => {
      const handler = setTimeout(() => {
        setDebouncedValue(value)
     }, delay)
      return () => {
        clearTimeout(handler)
      }
    },
    [value, delay]
  )
  return debouncedValue
}

export default useDebounce
  • We created a useDebounce function that takes two parameters, value an delay. This is the value we want to debounce and how long we want to delay before changing this value in milliseconds.

  • The value is then stored in a useState hook.

  • The useEffect hook takes in a setTimeout function that delays according to the value of delay parameter before changing the value.

  • delay and value parameters are also the useEffect dependencies.

  • After this, the setTimeout is cleared or aborted, useEffect is also exited.

  • The useEffect hook will only run again when one or both of its dependencies change.

Below is a use case of the useDebounce function.

import React, { useEffect, useState  } from "react"
import useDebounce from "./utils/useDebounce"

const Universities = () => {
  const [data, setData] = useState([])
  const [term, setTerm] = useState("")
  const debouncedTerm = useDebounce(term, 500)

  const search = name => {
    fetch(`http://universities.hipolabs.com/search?name=${name}`)
      .then(res => res.json())
      .then(data => {
        setData(data)
        console.log(data)
      })
  }
  const clear = () => {
    setData([])
    setTerm("")
  }
  useEffect(() => {
    search(debouncedTerm)
  }, [debouncedTerm])

  return (
    <div>
      <div className="relative w-[98%] mx-auto">
        <input
          type="text"
          autoComplete="off"
          onChange={e => setTerm(e.target.value)}
          value={term}
          placeholder="Search movies"
          style={{
            backgroundColor: " rgb(229 231 235)",
            color: "rgb(17 24 39)",
            width: "100%",
            padding: "1rem",
          }}
        />
        <button onClick={() => clear()}>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            strokeWidth={1.5}
            stroke="currentColor"
            style={{ cursor: "pointer", width: "5rem", height: "5rem" }}
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              d="M6 18L18 6M6 6l12 12"
            />
          </svg>
        </button>
      </div>
      {data.map((item, index) => (
        <div key={index}>{item.name}</div>
      ))}
     Demonstrating debounce with asynchronous call for a university api.
    </div>
  )
}

We debounced the term state using the useDebounce function we created earlier and used the debouncedTerm in both the search function and th useEffect array dependency.

Protecting Routes

Unlike Vue, React does not provide an out-of-the-box solution for the protection of routes against users that do not have certain authentication and authorization, for example, guests.

To demonstrate this, I am going to use the context API to create an auth state and a toggleAuth function that mimics the login and logout of a user that can be used anywhere in the application.

//AuthContext.js
import React, { createContext, useState } from "react"

export const AuthContext = createContext()

const AuthContextProvider = ({ children }) => {
  const [auth, setAuth] = useState(false)

  const toggleAuth = () => setAuth(!auth)

  return (
    <AuthContext.Provider value={{ auth, toggleAuth }}>
      {children}
    </AuthContext.Provider>
  )
}

export default AuthContextProvider

We now have to create a ProtectedRoutes file. In this file, we extract the auth state and determine whether to display the routes or navigate the user to a login page based on the value of auth, that is, if it is true or false.

An <Outlet> should be used in parent route elements to render their child route elements. This allows nested UI to show up when child routes are rendered. If the parent route matched exactly, it will render a child index route or nothing if there is no index route.

<Outlet> is a special component that is used to render the child components of a parent route. It acts as a placeholder that allows the child components to be rendered within the parent component.

Don't forget to wrap your application with the AuthContextProvider.

import React from "react"
import { Navigate, Outlet } from "react-router-dom"
import { AuthContext } from "../context/AuthContext"

const ProtectedRoutes = () => {
  const { auth } = useContext(AuthContext)

  return auth ? <Outlet /> : <Navigate to="/login" />
}

export default ProtectedRoutes

We can now use the protected route like so in the route definitions.

<Route element={<ProtectedRoutes />}>
  <Route element={<Home />} path="/" exact />
  <Route element={<Products />} path="/products" />
</Route>

We provided the ProtectedRoutes file as the element component prop of the route component that will wrap the components that we want to protect.

Conclusion

In conclusion, building a React single-page application can come with its own set of challenges. Some of these challenges may include protecting routes, using debounce, and going to the top of the page on route change. However, there are several solutions to overcome these problems.

Protecting routes can be solved using authentication and authorization techniques. This ensures that only authenticated users can access certain parts of the application. The use of debouncing helps to prevent performance issues and overload by reducing the number of function calls. Going to the top of the page on route change can be achieved by using the scrollTo window method.

By applying these solutions, developers can improve the functionality and user experience of their React SPA.

ย