Setting up a global loading indicator in Remix
Global loading indicator is a common UX pattern notifying your users something is still going on. It might not be the best approach these days but serves its purpose at a much lower cost.
In this tutorial, we will go through how to make a top loading progress bar using tailwindcss. Let’s start by creating a dummy <Progress /> component:
import type { ReactElement } from 'react';
function Progress(): ReactElement {
return (
<div className="fixed top-0 left-0 right-0 h-1 flex">
<div
style={{ width: '30%' }}
className="bg-gradient-to-r from-green-400 via-blue-500 to-pink-500"
/>
</div>
);
}
export default Progress;
In this component, we have only two elements with one being the container which is fixed at the top and the inner div presenting the progress with a static 30% width. Next, we need to hook up some logic and make it move:
1-import type { ReactElement } from 'react';1+import type { ReactElement, MutableRefObject } from 'react';2+import { useRef } from 'react';23 4+export function useProgress(): MutableRefObject<HTMLElement> {5+ const el = useRef<HTMLElement>();6+7+ return el;8+}9+310 function Progress(): ReactElement {11+ const progress = useProgress();12+413 return (514 <div className="fixed top-0 left-0 right-0 h-1 flex">615 <div7- style={{ width: '30%' }}16+ ref={progress}817 className="bg-gradient-to-r from-green-400 via-blue-500 to-pink-500"918 />1019 </div>
It might be tempting to make the width a variable. But this is probably not an ideal solution as it adds unnecessary load to React which could block the other part of the UI. With this in mind, we will go with managing the element width by ourselves using ref.
11 import type { ReactElement, MutableRefObject } from 'react';2-import { useRef } from 'react';2+import { useEffect, useRef } from 'react';3+import { useTransition } from 'remix';34 45 export function useProgress(): MutableRefObject<HTMLElement> {56 const el = useRef<HTMLElement>();7+ const { location } = useTransition();68 9+ useEffect(() => {10+ if (!location || !el.current) {11+ return;12+ }13+14+ el.current.style.width = `0%`;15+16+ return () => {17+ el.current.style.width = `100%`;18+ };19+ }, [location]);20+721 return el;822 }
Usually, you might need a query client which keeps track of all outgoing requests for you. However, this gets much simpler with Remix’s route driven mechanism. It provides a built-in react hook named useTransition which returns the next location whenever a transition is happening. This allows us simply subscribe to the location value with useEffect and set the width accordingly.
55 export function useProgress(): MutableRefObject<HTMLElement> {66 const el = useRef<HTMLElement>();7+ const timeout = useRef<NodeJS.Timeout>();78 const { location } = useTransition();89 910 useEffect(() => {1011 if (!location || !el.current) {1112 return;1213 }14+15+ if (timeout.current) {16+ clearTimeout(timeout.current);17+ }1318 1419 el.current.style.width = `0%`;1520 1621 return () => {1722 el.current.style.width = `100%`;23+ timeout.current = setTimeout(() => {24+ if (el.current?.style.width !== '100%') {25+ return;26+ }27+28+ el.current.style.width = ``;29+ }, 200);1830 };1931 }, [location]);
For sure, the progress bar should be disappeared after a short time. Let’s add a timeout to clear the width after 200ms.
1919 el.current.style.width = `0%`;2020 21+ let updateWidth = (ms: number) => {22+ timeout.current = setTimeout(() => {23+ let width = parseFloat(el.current.style.width);24+ let percent = !isNaN(width) ? 10 + 0.9 * width : 0;25+26+ el.current.style.width = `${percent}%`;27+28+ updateWidth(100);29+ }, ms);30+ };31+32+ updateWidth(300);33+2134 return () => {35+ clearTimeout(timeout.current);36+37+ if (el.current.style.width === `0%`) {38+ return;39+ }40+2241 el.current.style.width = `100%`;2342 timeout.current = setTimeout(() => {2443 if (el.current?.style.width !== '100%') {
Even though we have no idea about the actual progress status, it is better to keep the progress moving as the page loads. To achieve this, we increase the progress slightly every 100ms with a smaller gap each time. There is also an initial delay of 300ms which avoid showing the progress in case the transition finish quickly.
5959 <div className="fixed top-0 left-0 right-0 h-1 flex">6060 <div6161 ref={progress}62- className="bg-gradient-to-r from-green-400 via-blue-500 to-pink-500"62+ className="transition-all ease-out bg-gradient-to-r from-green-400 via-blue-500 to-pink-500"6363 />6464 </div>6565 );
One final touch would be making use of the CSS transition property to make the animation smoother.
Reference: Gist