Some Problems I Face When Building React Single Page Application And How I Solve Them
Photo by Elisa Ventur on Unsplash
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
andelay
. 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 auseState
hook.The
useEffect
hook takes in asetTimeout
function that delays according to the value ofdelay
parameter before changing thevalue
.delay
andvalue
parameters are also theuseEffect
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.