Releases: zilch/type-route
0.5.3
0.5.2
0.5.1
0.5.0 Type Route React Integration
- Adds explicit React integration with
useRoute
hook andRouteProvider
component. - Moves
listen
tosession.listen
- Introduces
type-route/core
version of lib that doesn't depend on React - See #39 for details
Type Route 0.4.0
Type Route 0.4.0 🚀
Type Route is just over one year old! Tons of great feedback has been given over the course of this last year. Some of that feedback has resulted in immediate changes to Type Route. Other comments though, needed to be more carefully considered since addressing them properly would influence the Type Route API in a breaking way. Type Route is used in production so, though not yet at version 1.0, being mindful of significant changes is still important.
After months of thinking about how to address these pieces of feedback in an elegant way, the time for action has finally come! This new version paves the way for Type Route v1.0
which should hopefully be released in the near future. Outlined below is a list of significant/breaking changes that have been made.
String parameter definitions replaced with a more flexible and powerful param.*
object
Previously you would do something like the following when creating a router:
import { createRouter, defineRoute } from "type-route";
const { routes } = createRouter({
home: defineRoute("/"),
user: defineRoute(
{
userId: "path.param.string",
},
(x) => `/users/${x.userId}`
),
userList: defineRoute(
{
page: "query.param.optional.number",
},
() => `/users`
),
});
With type-route
the above would instead look something like this:
import { createRouter, defineRoute, param } from "type-route";
const { routes } = createRouter({
home: defineRoute("/"),
user: defineRoute(
{
userId: param.path.string,
},
(x) => `/users/${x.userId}`
),
userList: defineRoute(
{
page: param.query.optional.default(1),
},
() => `/users`
),
});
Making the parameter definitions object based instead of string based expands the API but also makes it possible to give optional parameters default values and even provide a custom serializer for a parameter's value as in this example:
import {
createRouter,
defineRoute,
param,
ValueSerializer,
noMatch,
} from "type-route";
const userName: ValueSerializer<{ firstName: string; lastName: string }> = {
parse: (raw) => {
const [firstName, lastName, ...rest] = raw.split("-");
if (!firstName || !lastName || rest.length > 0) {
return noMatch;
}
return { firstName, lastName };
},
toString: (value) => `${value.firstName}-${value.lastName}`,
};
const { routes } = createRouter({
user: defineRoute(
{
userName: param.path.ofType(userName),
},
(x) => `/user-${x.userName}`
),
});
You can also use ofType<T>()
only passing in the type parameter without a ValueSerializer<T>
object. In that case Type Route will use the default JSON value serializer whose implementation looks like this:
import { ValueSerializer, noMatch } from "type-route";
const valueSerializer: ValueSerializer<TValue> = {
parse: (raw) => {
let value: TValue;
try {
value = JSON.parse(raw);
} catch {
return noMatch;
}
return value;
},
stringify: (value) => JSON.stringify(value),
};
Path parameter placement more flexible
Previously path parameters were constrained to be the only item in a particular path segment (a path segment being the characters between two forward slashes). So this /user/${x.userId}/settings
was ok but this /user-${x.userId}-settings
was not. Now both are supported. You still cannot include more than one path parameter per segment of the path but can achieve the functionality you're looking for there with a custom value serializer.
Adds trailing path parameters
There has been no first class support for catch all routes in Type Route to date. It was possible to hack around this limitation by doing some custom parsing of the url if no route was matched before falling back to a not found page. With the introduction of trailing path parameters, however, there is now support for this. Unlike other path parameters a trailing path parameter is not url encoded by default and so can be given characters such as forward slashes. A trailing path parameter must be the last one in the path.
Adds optional path parameters
Previously path parameters could not be optional. Now you can have up to one optional path parameter if it is the last in your path and has no leading or trailing text around the parameter within its path segment. Example:
defineRoute(
{
name: param.path.string,
version: param.path.optional.string,
},
(x) => `/software/${x.name}/${x.version}`
);
Adds state parameters
The history api exposes a way to pass along values embedded in state parameters as part of navigation (see https://developer.mozilla.org/en-US/docs/Web/API/History/state). Previously Type Route used this api internally but did not expose it for external use. Now declaring route parameters that reside in this state object is possible via the param.state.*
object. Note that while this parameter definition has the same semantics as the others for consistency (required by default) its likely that you would want state parameters to be optional in most cases (param.state.optional.*
) since simply copying/pasting a url would not pick up this special state object and therefore make that route unaccessible unless navigated to via application code or browser history.
Custom query string serializer support
Type Route now supports a pluggable query string serializer. Since parameters definitions take care of doing serialization of a parameter's value the query string serializer is only responsible for matching up keys with their values. The default query string serializer implementation looks like this:
export const defaultQueryStringSerializer: QueryStringSerializer = {
parse: (raw) => {
const queryParams: Record<string, string> = {};
for (const part of raw.split("&")) {
const [rawParamName, rawParamValue, ...rest] = part.split("=");
if (
rawParamName === undefined ||
rawParamValue === undefined ||
rest.length > 0
) {
continue;
}
queryParams[decodeURIComponent(rawParamName)] = rawParamValue;
}
return queryParams;
},
stringify: (queryParams) => {
return Object.keys(queryParams)
.map((name) => `${encodeURIComponent(name)}=${queryParams[name]}`)
.join("&");
},
};
It would be possible to support query strings that have alternate ways of representing array. For example ?letters[]=a&letters[]=b
could be parsed properly into {letters:["a","b"]}
using a combination of a custom query string serializer and a custom value type serializer for the parameter. This value can be passed in when the router is created in the second options argument like this:
import { createRouter, defineRoute } from "type-route";
const { routes } = createRouter(
{
home: defineRoute("/"),
},
{
queryStringSerializer: ...,
}
);
More permissive route matching
Previously any extraneous query string parameters would cause the route to not be a match. Now extraneous query and state parameters are allowed.
Fixes TypeScript "Go to definition"
Executing "Go to definition" on route names has always worked as expected but doing so on parameter names previously did not work. The TypeScript types have been adjusted to enable this behavior for parameter names as well as route names.
Better display of TypeScript types on hover
Hovering over parameters on the route object would previously show a long and confusing TypeScript type. Now hovering over those should display a much more readable type definition free from all of the internal Type Route stuff.
Renames "history" to "session" and revamps API
Several changes have been made in this area:
- Access to internal history object no longer given
- history renamed to session to avoid conflicting the with global namespace history object
getCurrentRoute()
replaced withgetInitialRoute()
in an effort to steer users towards the best practice of usinglisten
to sync route updates to their application.- The
session
object is now of this type:
export type RouterSessionHistory = {
push(url: string, state?: any): Promise<boolean>;
replace(url: string, state?: any): Promise<boolean>;
getInitialRoute(): Route;
back(amount?: number): void;
forward(amount?: number): void;
reset(options: RouterSessionHistoryOptions): void;
};
Route Builder Changes
Previously you would do something like this routes.home.link()
. Now route object are first built then properties are accessed on them. So now you'd do this instead: routes.home().link
. For simple routes with no parameters this doesn't make a huge difference but for more complex routes this subtle change makes building simple abstractions on top of the Type Route API for your application much more simple.
React Suspense Support
React's new concurrent mode has a useTransition
hook. This hook returns a startTransition
function which accepts a callback that does any state updates. Since these state updates are wrapped in startTransition
React can treat them specially. Type Route previously would have been incompatible with this pattern. You could call routes.user({ userId: "abc" }).push()
inside startTransition
but the call to your listener
would happen later so any setState
calls in there wouldn't have been captured inside startTransition
. Now they will be which should make Type Route compatible with this pattern.
Listen/Block
This release also upgrades Type Route to the latest version of the history
library. Part of this upgrade meant reconsidering the API for blocking navigation. Now returning false
in listen does ...