2. Defining urls

Lets look at one of the main parts of @42.nl/react-url; urlBuilder. It fixes the string based route problem via one simple abstraction: URL functions. URL functions are functions which return URLs. Optionally these URL functions take query and path parameters as arguments.

A function is a URL function if it matches the following criteria:

  1. The function has a return type of Url.
  2. If the Url has path parameter, they are parameters to the url function.
  3. If the Url has query parameters, they are a parameter to the url function.

The benefit of using functions to navigate to routes is type safety. TypeScript cannot differentiate between strings, but it can type check functions. So a change to the url function will give you compiler errors, which is exactly what we want!

Let’s take a look at some examples:

2.1 Basic: no parameters at all

The easiest routes to define are routes which do not have any parameters at all. The URL function is one that simply returns a string:

import { Url } from '@42.nl/react-url';

/* This const should be used to define the `Route` and in the url function */
export const USER_CREATE_URL = '/users/create';

/* The simplest way to define a Url function is to return a string. */
export function toUserCreate(): Url {
  return USER_CREATE_URL;
}

/* To use it in react-router use the URL const */
<Route path={USER_CREATE_URL} component={UserCreate}/>

/* To use it to navigate to a route you can use it in a react router Link: */
<Link to={toUserCreate()}>/users/create</Link>

/* Or programmatically -> /users/create */
history.push(toUserCreate());

It is very important to define a const for the URL and to use that const when creating the Route. This ensures type safety.

2.2 With path parameters

If the route has path parameters:

import { Url, urlBuilder } from '@42.nl/react-url';

/* We recommend defining this type in the UserEdit component's file. */
type UserEditPathParams = {
  id: string; /* All path params are strings. */
}

/* This const should be used to define the `Route` and in the url function */
export const USER_EDIT_URL = '/users/:id/edit';

/* This url function takes a required path parameter called :id */
export function toUserEdit(pathParams: UserEditPathParams): Url {
  return urlBuilder({
    url: USER_EDIT_URL,
    pathParams
  });
}

/* To use it in react-router use the URL const */
<Route path={USER_EDIT_URL} component={UserEdit} />

/* To use it to navigate to a route you can use it in a react-router Link: */
<Link to={toUserEdit({ id: '42' })}>/users/42/edit</Link>

/* Or programmatically -> /users/42/edit */
history.push(toUserEdit({ id: '42'}));

It is important that the argument pathParams to the function toUserEdit is required. Once again, this ensures type safety.

2.3 With optional path params

If the route has optional path params. Optional path params are not required for the route to activate. You can combine

import { Url, urlBuilder } from '@42.nl/react-url';

/* We recommend defining this type in the UserList component's file. */
type UserListPathParams = {
  tab?: 'all' | 'inactive' | 'active'; /* All path params are strings. */
}

/* This const should be used to define the `Route` */
export const USER_LIST_URL = '/users/:action?';

export function toUserList(
  pathParams?: UserListPathParams, // The path params can be optional now
): Url {
  return urlBuilder({
    url: USER_LIST_URL,
    pathParams
  });
}

/* To use it in react-router use the URL const */
<Route path={USER_LIST_URL} component={UserList} />

/* To use it to navigate to a route you can use it in a react-router Link: */
<Link to={toUserList()}>/users</Link>

/* The tab is optional but can be provided */
<Link to={toUserList({ tab: 'all'})}>/users/all</Link>

/* Or programmatically -> users/active */
history.push(toUserEdit({tab: 'active'}));

2.4 With required path params and optional path params

The route can also have both required path params and optional path params.

import { Url, urlBuilder } from '@42.nl/react-url';

/* We recommend defining this type in the UserList component's file. */
type UserDetailPathParams = {
  id: string; /* All path params are strings. */
  mode?: 'view' | 'edit'; /* All path params are strings. */
}

/* This const should be used to define the `Route` */
export const USER_DETAIL_URL = '/users/:id/:action?';

export function toUserDetail(
  pathParams: UserDetailPathParams,
): Url {
  return urlBuilder({
    url: USER_DETAIL_URL,
    pathParams
  });
}

/* To use it in react-router use the URL const */
<Route path={USER_DETAIL_URL} component={UserList} />

/* To use it to navigate to a route you can use it in a react-router Link: */
<Link to={toUserDetail({ id: '42'})}>/users/42</Link>

/* The mode is optional but can be provided */
<Link to={toUserDetail({ id: '42', mode: 'view'})}>/users/42/view</Link>

/* Or programmatically -> users/42/edit */
history.push(toUserEdit({ id: '42', mode: 'edit'}));

2.5 With query params

If the route has query params:

import { Url, urlBuilder } from '@42.nl/react-url';

/* We recommend defining this type in the UserList component's file. */
type UserListQueryParams = {
  page: number;
  search: string;
}

/* We recommend defining the default query params in the UserList component's file. */
export function defaultUserListQueryParams(): UserListQueryParams {
  return {
    page: 1,
    search: ''
  }
}

/* This const should be used to define the `Route` and in the url function */
export const USERS_URL = '/users';

/*
  This url function has the query params page and search. They are both
  optional because of Partial. Partial takes an object type definition
  and makes all keys optional and does not allow unknown keys.
*/
export function toUsers(queryParams?: Partial<UserListQueryParams>): Url {
  return urlBuilder({
    url: USERS_URL,
    queryParams,
    defaultQueryParams: defaultUserListQueryParams()
  });
}

/* To use it in react-router use the URL const */
<Route path={USERS_URL} component={UserList} />

/* To use it to navigate to a route you can use it in a react-router Link: */
<Link to={toUsers({ page: 13 })}>/users?page=13</Link>

/* Or programmatically -> /users?page=13 */
history.push(toUsers({ page: 13 }));

It is important that the argument queryParams to the toUsers function is optional and marked as Partial. This ensures that when no query params are given the defaults are used. It is Partial because all keys are optional when navigating to a URL whereas missing properties will be filled in by the defaultQueryParams.

The query parameters are not appended to the URL when they precisely match the default query parameters. This ensures that the URLs are as compact as possible.

Here are some examples of this behavior:

toUsers({ page: 1, search: '' }); /* -> /users */
toUsers({ page: 42, search: '' }); /* -> /users?page=42 */
toUsers({ page: 1, search: 'answer' }); /* -> /users?answer=42 */

2.6 With path params and query params

If the route has both path parameters and query parameters:

import { Url, urlBuilder } from '@42.nl/react-url';

/* We recommend defining this type in the UserEdit component's file. */
type UserEditPathParams = {
  id: string; /* All path params are strings. */
}

/* We recommend defining this type in the UserEdit component's file. */
type UserEditQueryParams = {
  modal: boolean;
}

/* We recommend defining the default query params in the UserList component's file. */
export function defaultUserEditQueryParams(): UserEditQueryParams {
  return {
    modal: false
  }
}

/* This const should be used to define the `Route` */
export const USER_EDIT_URL = '/users';

export function toUserEdit(
  pathParams: UserEditPathParams,
  queryParams?: Partial<UserEditQueryParams>
): Url {
  return urlBuilder({
    url: USER_EDIT_URL,
    pathParams,
    queryParams,
    defaultQueryParams: defaultUserEditQueryParams()
  });
}

/* To use it in react-router use the URL const */
<Route path={USER_EDIT_URL} component={UserEdit} />

/* To use it to navigate to a route you can use it in a react-router Link: */
<Link to={toUserEdit({id: '42'}, { modal: true })}>/users/42/edit?modal=true</Link>

/* Or programmatically -> users/42/edit?modal=true */
history.push(toUserEdit({id: '42'}, { modal: true }));

Note: you can also define routes with required path params, optional path params, and query params.

Now that we have setup URLs lets take a look at how query params work.