Skip to content

Comments

React Migration: Dynamic Nest API, course sidebar, breadcrumbs#6100

Merged
ekowidianto merged 76 commits intomasterfrom
phillmont/new-sidebar-2
Jun 27, 2023
Merged

React Migration: Dynamic Nest API, course sidebar, breadcrumbs#6100
ekowidianto merged 76 commits intomasterfrom
phillmont/new-sidebar-2

Conversation

@pchunky
Copy link
Contributor

@pchunky pchunky commented May 29, 2023

Adds the new course sidebar and breadcrumbs. There are some pages that were refactored and/or restyled to be consistent.

Closes #2542.

Remaining tasks

  • Integrate dynamic breadcrumbs for Workbin, Forums, Surveys, and Videos
  • Fix margin/padding issues in some subpages by migrating to Page
  • Handle sign out and stop masquerading
  • 403 and 401 pages

Dynamic Nest API

React Router 6.11.x provides NavLink and useMatches that can help us:

  1. determine the current active route (useful for highlighting active sidebar item), and
  2. generate breadcrumbs based on matches on the current URL.

The gold standard for this routing is Course Settings. However, other pages have rather unconventional routing systems. We are still transitioning from Rails' router to React Router, and not all pages are nested according to their routes (see Manage Users, Submissions, or Skills). This means using NavLink's isActive alone isn't enough, because:

  • in assessments/:id/*, the correct category on the sidebar won't be highlighted because the sidebar item's URLs are in the form of assessments?category=*,
  • while Manage Students will highlight Manage Users on the sidebar, Manage Staff and other tabs won't because the sidebar item's URL is students,
  • and other weirdly nested routes.

Note For brevity, when I write URLs or routes here, I omit the courses/:id/* part because we are largely talking about in-course routing.

By generating breadcrumbs in the front-end, we also lose the ability to generate dynamic breadcrumbs. Consider these routes:

Route Crumb after course name
assessments The default categories' default tab
assessments?category=1 Category 1's default tab
assessments?category=1&tab=2 Category 1's Tab 2
assessments/3/* The tab in which Assessment 3 is kept
assessments/submissions Submissions
assessments/skills Skills

The first 4 cases are the hardest. Unlike Rails, when we go to these routes, we don't have the luxury of traversing from ApplicationController to the assessment's controller and gradually generate the crumbs. Also, in the client, we don't have access to the database, so there's no knowing the category of assessment 3 to generate the crumb (unless via GETs). Enter the Dynamic Nest API.

Read the API documentation

Description

The Dynamic Nest API allows us to generate crumbs based, but not dependent on the route.

Note "crumb" here no longer means portions of a breadcrumb, rather portions of a route (more aptly named "match").

The Dynamic Nest API is NOT for generating breadcrumbs, rather defining dynamic nesting behaviours that defy the static nature of routes and their matches. The information it generates allows us to generate an overridable active route nest (problem 1 above) and a dynamic breadcrumb (problem 2 above).

It allows for an extensible way of defining custom logic for generating crumbs (matches) by leveraging React Router's handles as React Router traverses from / to the target route. This is the equivalent to Rails' inside-out controller traversal (not to be confused with Rails' outside-in routing). This is the strategy that allows us to generate the correct crumbs whether the user is coming (a) from somewhere else in our app, or (b) directly via some URL.

These handles can be as simple as a string or translation object, to as complex as async network calls with custom flags (for the use case of assessments above).

We thus define a handle as a nest-specific abstraction of information that the Dynamic Nest API will use to generate its matches, which are used to generate a breadcrumb and specify an active path.

Implementation

A handle in the Dynamic Nest API can be a:

  • null or undefined (crumb will not be generated)
  • string,
  • Descriptor (object passed to useTranslation)
  • DataHandle (this is the big brain moment)

Note null, undefined, string, and Descriptor will appear multiple times. These are unioned as the CrumbTitle type. It was by design that there is always a way to return CrumbTitle (simplest crumb information) at every stage of complexity of a handle: itself, function return, or promised value (via HandleRequest below).

DataHandle for rendering a dynamic breadcrumb

A DataHandle is a handle that produces a data for the API to generate crumbs from.

export type DataHandle = (match: Match, location: Location) => HandleData;

match and location are provided by React Router and exposed here for convenience. With these, you can access params and loader's data (from match) or search params (from location). It returns HandleData, that could be a CrumbTitle or HandleRequest (this is the big brain moment)

HandleRequest for defining a dynamic nest

HandleRequest is really the power of the Dynamic Nest API. Think of HandleRequest as an object filled with configurations to "request" the API to generate crumbs according to our liking.

HandleRequest is currently an object with 2 attributes: shouldRevalidate and getData.

getData is a function that returns, directly or Promised, either a CrumbTitle or CrumbPath. A CrumbPath is a partial of CrumbData (internal), the object passed directly to the Crumb component that appears in Breadcrumbs. This way, in addition to just providing the custom crumb title (as you could with CrumbTitle before), you can provide:

  • url?: string: a custom URL for the crumb,
  • activePath?: boolean: the prefix that signifies the current active nest,
  • pathname?: string: the identifier that the API uses to cache and invalidate rendered crumbs (advanced use only).

useDynamicNest's rendering algorithm

To understand the use of shouldRevalidate, one must first understand how useDynamicNest handles crumb generation (see buildCrumbsData, later referred to as the builder).

If a route has N nests, it will have N matches, and thus N handles will be collected by the builder. At worst, if these N handles make network calls each, then there will be N network calls, too. The API is designed to only make these calls when the builder handles them, and that they are done asynchronously. The API also handles race conditions.

Note Some browsers may have limits on concurrent network requests per origin. Chrome, for example, is known to allow 6 concurrent requests per origin.

Whenever the route changes, useMatches will generate new set of matches that useDynamicNest will use to render by sieving (my own term). It will go through the last crumbs and the new matches, and only build the new crumbs. The diff is done by pathnames. All this is done in one pass of N iterations.

The case for shouldRevalidate

Now think the assessments case above (see the table above). What if we switch between tabs in an assessment category? What if we jump from a mission in category A tab A to category B tab B? The crumb for category-tab will not change, because the pathname is the same: assessments/.

Note As of React Router 6.11.x, it doesn't recognise search params as part of a "route" to be routed.

useMatches won't even know we have jumped into a new nest (new category-tab), because all it knows is just the assessments/ match. Enter shouldRevalidate.

shouldRevalidate: true notifies the builder that for this particular match M, we want to keep it, but still additionally collect its handle, in case it may give us a new result. Hence, "revalidate". Note that without shouldRevalidate, kept matches/crumbs will not have their handles collected.

This way, the user will not see the old category/tab's crumb disappear all the time, and if we indeed has jumped into a new nest, the new crumb data is built, and it just replaces that crumb. Neat, eh?

Finally, the useDynamicNest hook

This hook internally is just a state management function. The heavy-lifting is done by the builder. It returns an object of 3 attributes, each which is self-explanatory at this point.

interface UseDynamicNestHook {
  crumbs: CrumbData[];
  loading: boolean;
  activePath?: string;
}

loading will be true if the number of expected final crumbs > number of crumbs remaining in state. loading false does NOT necessarily mean there are no network requests or pending handle collection. It is designed this way because when loading is true, we render a loading animation at the end of the breadcrumb.

@pchunky pchunky added the UI label May 29, 2023
@pchunky pchunky self-assigned this May 29, 2023
@pchunky pchunky changed the title React Migration: Course sidebar, breadcrumbs, Dynamic Nest API React Migration: Dynamic Nest API, course sidebar, breadcrumbs May 29, 2023
@pchunky pchunky force-pushed the phillmont/new-sidebar-2 branch 5 times, most recently from 85d2fcc to b934e58 Compare May 29, 2023 08:02
Copy link
Member

@ekowidianto ekowidianto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments below. I know it's still WIP and some things are not cleaned yet but I left some comments just as a reminder.

  • Dont forget to remove existing rails sidebar and course slim files.
  • Port the rest of PageHeader to Page as well?
  • To replace relevant <a /> and <Link /> (from components/core) to react-router's Link.

@pchunky pchunky force-pushed the phillmont/new-sidebar-2 branch 14 times, most recently from 2fac312 to 8662579 Compare June 23, 2023 16:04
@pchunky pchunky force-pushed the phillmont/new-sidebar-2 branch from 8662579 to 3988ead Compare June 24, 2023 12:18
@pchunky pchunky force-pushed the phillmont/new-sidebar-2 branch from f5c0fc3 to 22f0fc9 Compare June 26, 2023 05:21
Copy link
Member

@ekowidianto ekowidianto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work and thanks! 1 step closer to finishing the porting..

@ekowidianto ekowidianto merged commit ed6ed2a into master Jun 27, 2023
@ekowidianto ekowidianto deleted the phillmont/new-sidebar-2 branch June 27, 2023 07:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement JavaScript Pull requests that update JavaScript code Ruby Pull requests that update Ruby code Technical Story UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Sidebar using React

2 participants