Skip to main content

How it works

A reasonably deep dive into how found does what it does.

An example

Consider the following wireframe of a web app with side navigation and detail area. The user can navigate to sections (such as "products") and see a list of products. Clicking further on a product navigates to a nested detail view with tab navigation for that specific product.

Product wireframe Product wireframe

With Found we can represent this UI with the following route tree and config:

<Route path="/" Component={AppPage}>
<Route path="customers">
<Route Component={CustomersIndexPage} />
<Route path=":customerId" Component={CustomerPage} />
</Route>

<Route path="products">
<Route Component={ProductsIndexPage} getData={fetchProducts} />
<Route path="create" Component={ProductCreatePage} />

<Route
path=":productId"
Component={ProductPage}
getData={fetchProduct}
>
<Route
path="edit"
Component={ProductEditPage}
getData={fetchProductById}
/>
<Route
path="history"
Component={ProductHistoryPage}
getData={fetchProductHistory}
/>
</Route>
</Route>

<Route path="settings" Component={SettingsPage} />
</Route>

Which can also be illustrated via the following tree graph:

route-tree route-tree

Let's consider a single URL and break down how found determines what UI to show and when. If we want to navigate to a Product's history page we would use the following URL: /products/1/history. Clicking on a <Link>, calling router.push('/products/1/history'), or updating the browser URL bar triggers a new "match" resolution.

Matching

The first step in routing is to produce a match. A match is created by a class called the Matcher, which is generally an implementation detail of Found. Simply put, the Matcher takes a URL and produces a set of route objects that correspond to that URL.

This is accomplished by decomposing the URL into path segments and "matching" them with nodes in our route tree. Illustrated here is a match to our Product history page.

route-match route-match

If the matcher is able to map the entire URL to a set of Routes, the matching succeeds and a match object is produced (simplified a bit here):

Match {
location: {
pathname: '/products/1/history',
query: {},
state: null,
},
params: { projectId: '1' },
routes: [
{ path: '/', Component: AppPage, },
{ path: 'products' },
{ path: ':projectId', Component: ProductPage, getData: fetchProductById },
{ path: 'history', Component: ProductHistoryPage, getData: fetchProductHistory },
]
}

info

matching can only complete at "leaf" nodes in the route tree. This means that routes that could be leaf or branch nodes (such as /products) need to include an "index" route in order to match sucessfully. An index route is a route with a Component and no path, as seen above in the route config.

Resolution

The next part in the process is "resolving". The router Resolver is responsible for mapping the matched routes to a set of React elements in order to render the UI for the URL. To accomplish this it may need to fetch components or server data asynchronously, as specified by the route.

resolver resolver

Data fetching and code splitting

Each route in the matched set may specify a getData property to fetch data necessary to render its Component. The Resolver calls each and collects the returned Promises which are all allowed to resolve in parallel--unless a route is deferred, in which case the promises are split into batches based on the route hierarchy.

const routeData = await Promise.all(
matchedRoutes.map((route: Route) => {
return route.getData ? route.getData(match) : undefined;
});
)

Similarly Route components can be asynchronously loaded by specifing getComponent instead of Component. Components are also loaded in parallel and awaited before resolving to a set of elements.

const routeComponents = await Promise.all(
matchedRoutes.map((route: Route) => {
return route.getComponent ? route.getComponent(match) : route.Component;
});
)

Element construction

After data and components are resolved, each route is constructed into an element that will be rendered by the Router. Consider how our matched routes relate to the proposed UI:

route-ui-map route-ui-map

Each route is ordered by its depth in the route tree and is responsible for rendering itself as well as any nested routes it may contain. Before we can compose the UI together we need to construct its individual pieces. This is done by combining our fetched components and data into elements:

const routeComponents = [
AppPage,
null,
ProductPage,
ProductHistoryPage,
];
const routeData = [null, null, {}, {}];

routes.map((route, index) => {
const Component = routeComponents[index];
const data = routeData[index];

return Component ? <Component data={data} /> : null;
});

Finally this array is folded together by the Router into:

<AppPage>
<ProductPage>
<ProductHistoryPage />
</ProductPage>
</AppPage>

Loading states

One point that was glossed over is how routes can control their own loading UI while data and components are being fetched. To handle these states the Resolver actually produces multiple arrays of elements. While waiting for components and data to be fetched it calls each route.render, with the intermediary state. Once the async values are resolved each route.render is called again with the final values.

This gives the route total control over how it handles loading fallbacks for itself and child routes. A Route can explicitly render nothing (null), a loading spinner, a skeleton UI, or whatever else it wants while it waits for data.

routes = [
// ...
{
path: ":productId",
getComponent: () =>
import("./components/ProductPage").then((m) => m.default),
getData: fetchProductById,
render({ props, Component }) {
// if Component is not present, it is still being fetched
if (!Component) {
return <Spinner />;
}

// props is the return value of `getData`
// If it's not present, data are still loading
if (!props) {
<Component showSkeleton />;
}

// Otherwise render the component with its props
return <Component {...props} />;
},
},
];
tip

null and undefined mean different things as a return value of router.render! If you return null the router will render nothing for that element, whereas undefined it treated as a special value that means "I can't render yet", which tells the router to continue to show the existing UI until the route is ready.

This flexibility allows for easy implementation of many different loading UI patterns!

Customizing

Nearly all of the behavior covered here is customizable and extensible, making found ideal for all sorts of of React applications. Checkout our examples and extensions for more ideas.