Skip to content

Angular vs React Comparing Route Prefetching

Angular vs React Comparing Route Prefetching

At times during the development lifecycle, we need to perform specific strategies that accomplish a desired UX. A user may need to perform a task in the near future where the data is needed so we establish a browser cache for the necessary resources. Whether at the beginning of the app's initialization or during navigation, we can provide everything upfront or lazily as needed.

A number of frameworks and libraries allow for pre-fetching strategies as part of its packaging and they perform those tasks fluidly or require a few handshakes to make it possible. We'll compare how this can be done in Angular and React as well as some others like Remix and Vue.

Before we go too much further, let's understand the advantages of pre-fetching and when we should perform them.

What is Prefetching?

As mentioned briefly, data prefetching accomplishes two objectives:

  • data storage by caching in the browser
  • seemingly on-time retrieval by the user

When a user performs navigation whether initializing a web app or navigating within the app, the app either loads all or a portion of data. However, this isn't without cost or impact.

This data still requires network bandwidth for the transfer, processing time, and the cache. If a user decides to not use a portion of a web app but this data has been made available to them, performance is diminished.

When is Prefetching Useful?

While we know about the browser's limited caching resources and overall performance considerations for prefetching, the decision still needs to be made about whether or not to perform pre-fetching.

A question to keep in mind is: "does enabling prefetch increase or decrease the utility of the app?"

Determining the strategy to use will make an app sink under load times or swim with the right balance of load time and page rendering.

Angular Prefetch with Route Resolver

Angular has a relatively simple setup with its loading strategy. The resolver pattern binds data loading to a route where any navigation to the guarded route or its branches first loads the resolver and uses a snapshot of the route's data for use during the lifecycle of any components within that route. However this has limitations because the data isn't globally available to other routes.

Take, for instance, a User service and route that had a resolve guard. In order to access a user's data, some pre-requisites need to be available first. First a basic example resolver takes this shape.

user-resolver.service.ts

@Injectable({
  providedIn: "root"
})
export class UserResolverService implements Resolve<any> {
  constructor(private userService: UserService) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    return this.userService.getUser();
  }
}

And it called on the route guarded route

user-route.ts

const routes: Routes = [
  {
    path: "user",
    component: UserComponent,
    resolve: {
      user: UserResolverService // <----------
    }
  },
  {
    path: "",
    redirectTo: "user",
    pathMatch: "full"
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

Accessing the data in user now looks like this in the component where we look at the currently activated route for a hydrated data['user'] property:

@Component({...})
export class UserComponent implements OnInit {
  user: User;

  constructor(private _route: ActivatedRoute) {
    this.user = [];
  }

  ngOnInit() {
    this.user = this._route.snapshot.data["user"];
  }
}

This approach to prefetching doesn't take into account Server-Side Rendering and is purely client side. Projects like Angular Universal make it possible to perform some controlled data flows and setup than the client-side fetching would allow.

Considerations for Global State

We can overcome ths issue of limited app-wide or global state with a couple techniques:

  1. A data service layer that utilizes caching
  2. Lifting state to parent routes within the route tree
  3. Use state management libraries

While each of these strategies has advantages over the other, most developers tend to consider libraries like NgRx and Akita for Angular before thinking about the performance and overall impact these mechanisms add to the application.

Prefetching in React

If you did a Google search for prefetching in React, you'd probably get a number of developer posts on either React's experimental feature or you'll find resources on running flavoured extensions of React like NextJS and Remix. This is to say, due to React's "choose your adventure" ecosystem, it doesn't have its own implementation readily available thus needing to create your own. As mentioned, you could choose a flavour that best suites your needs.

We'll briefly discuss the similarities and differences between Angular and NextJS versions of prefetching in making an architectural decision.

Client-side Prefetching

A combination of react-router and useContext is a way to emulate what Angular does with its protected routes.

An example comparable to the earlier resolver pattern looks like setting up a protected route:

protected-route.tsx

export default function ProtectedRoute({ user, children }) {
  const location = useLocation();

  if (!user) {
    return <Navigate to="/404" state={{ from: location }} replace />;
  }

  return children;
}

Applying the new "route guard":

app.tsx

export default function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <Routes>
        <Route
            path="/user"
            element={
                <ProtectedRoute use={currentUser}>
                    <User />
                </ProtectedRoute>
            }
        />
    </Routes>    
  )
}

Add CurrentUserContext to act as the activated route's data handler:

app.tsx

const CurrentUserContext = useContext();

export default function MyApp() {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <CurrentUserContext.Provider value={{
        currentUser,
        setCurrentUser
      }}>
      <Routes>
        <Route
            path="/user"
            element={
                <ProtectedRoute use={currentUser}>
                    <User />
                </ProtectedRoute>
            }
        />
      </Routes>    
    </CurrentUserContext.Provider>
  )
}

Then use the context in a child component:

login-button.tsx

function LoginButton() {
  const {
    currentUser,
    setCurrentUser
  } = useContext(CurrentUserContext);

  if (!currentUser) {
    return <p>You logged in as {currentUser.name}.</p>;
  }

  return (
    <Button onClick={() => {
      setCurrentUser({ name: 'John' })
    }}>Log in</Button>
  );
}

When the user performs a click to "Log in", they be set with a user that's provided throughout the app. In this instance, we set it up globally because it recommended to have all providers as high up the component tree as possible.

Considering Server-side Rendering (SSR)

SSR is a great way to increase the performance of applications by making sure dynamic data is always up-to-date and hydrated. It makes the page interactive by rendering content, and with a bit a JavaScript, to make it dynamic. This impact is two-fold as it improves SEO and performance in page load times. Regardless of the applications, utilizing this mechanism, we know the outcome is the same.

SSR is a great way to extend the capabilities of the client-fetch strategies addressed earlier as they leverage the same content, but just make rendering requests at a controlled server level.

Ideally, one could load authorization, headers, localizations for languages, perform caching, and load data from a CMS.

Angular Universal

Angular Universal is an extension to a base Angular project by including an extra level of control. This involves adding the ability to perform server rendering capabilities to an Angular app via an express server extension. It shares similar context to NextJS's implementation of SSR although the pattern is a bit more verbose in a server.ts file generated with the following commands:

ng add @nguniversal/express-engine
npm run dev:ssr

Routes specified within the server.ts file can perform a number of server-related tasks standard to writing an express app. This also includes limitations like using browser API because server code doesn't run in the browser context. Therefore, window, document, navigator, and location is not available directly. Angular provides these as injectable.

Lastly, Angular Universal can handle Statically-Generated routes as well. However, an argument can be made for the effectiveness of this in Angular given how heavy a framework it is compared to other, smaller applications that handle this more efficiently.

NextJS

NextJS is a great tool compared to bare React when determining the cost-effectiveness of setup and ease. While one could use this framework to justify a client-side fetching strategy, it's unnecessary if all the performant features aren't used like its version "prefetching".

Before embarking on prefetching, consider that NextJS provides pre-rendering in two contexts: Static Generation and Server-side Rendering similarly to Angular Universal.

While each one has a specific use, the most effective one for this concept is Server-side Rendering because most scalable, enterprise applications rely on fresh, dynamic data.