Crossroad

A routing library for React with a familiar interface. It has some differences with React Router so you write cleaner code:

  • The links are plain <a> instead of custom components. Read more.
  • There are useful hooks like useUrl, useQuery, etc.
  • The <Route> path is exact by default and can match query parameters.
  • It's just ~1.5kb (min+gzip) instead of the 17kb of React Router(+Dom).

🔗 Demo on CodeSandbox

// App.js
import Router, { Switch, Route, Redirect } from "crossroad";

export default function App() {
  return (
    <Router>
      <nav>
        <a href="/">Home</a>
        <a href="/users">Users</a>
        ...
      </nav>
      <Switch>
        <Route path="/" component={Home} />
        <Route path="/users" component={Users} />
        <Route path="/users/:id" component={Profile} />
        <Redirect to="/" />
      </Switch>
    </Router>
  );
}

Getting Started

Create a React project (npx create-react-app demo) and install Crossroad:

npm i crossroad

Then import it on your App.js and define some routes:

import Router, { Switch, Route } from "crossroad";

export default function App() {
  return (
    <Router>
      <Switch>
        <Route path="/" component={Home} />
        <Route path="/users/:id" component={Profile} />
      </Switch>
    </Router>
  );
}

Then let's add some navigation and the actual pages:

import Router, { Switch, Route } from "crossroad";

const Home = () => <main>Home Page</main>;
const Profile = ({ id }) => <main>Hello {id.toUpperCase()}</main>;

export default function App() {
  return (
    <Router>
      <nav>
        <a href="/">Home</a>
        <a href="/users/a">User A</a>
        <a href="/users/b">User B</a>
      </nav>
      <Switch>
        <Route path="/" component={Home} />
        <Route path="/users/:id" component={Profile} />
      </Switch>
    </Router>
  );
}

Now you can start your project and test it by visiting http://localhost:3000/ and http://localhost:3000/login:

npm start

See the more complete working example in this CodeSandbox.

API

The API is composed of these parts:

  • <Router />: the top-level component that should wrap your whole app.
  • <Switch />: renders only the first child that matches the current url.
  • <Route />: filters whether the given component should be rendered or not for the current URL.
  • <Redirect />: takes the user to a different URL if it's rendered.
  • <a />: a plain HTML link, use it to navigate between pages.
  • useUrl(): a hook that returns the current URL and a setter to update it.
  • usePath(): a hook that returns the current path and a setter to update it.
  • useQuery(): a hook that returns the current query and a setter to update it.
  • useHash(): a hook that returns the current hash and a setter to update it.
  • useParams(): a hook that extracts params form the current path.

Router is the default export, <a> is not exported since it's just the plain link element, and everything else are named exports:

import Router, { Switch, Route, Redirect, useUrl, usePath } from "crossroad";

<Router />

The top-level component that has to wrap everything else. Internally it's used to handle clicks, history, etc. It's also the default export of the library:

// App.js
import Router from "crossroad";

export default function App() {
  return <Router>... Your normal App code ...</Router>;
}

You would normally setup this Router straight on your App, along things like Statux's or Redux's Store, error handling, translations, etc.

<Switch />

A component that will only render the first of its children that matches the current URL. This is very useful to handle 404s, multiple routes matching, etc. For example, if you have a username system like "/:username" but want to have a help page, you can make it work easily with the switch:

// In https://example.com/help, it'll render the Help component only
<Switch>
  <Route path="/help" component={Help} />
  <Route path="/:username" component={User} />
</Switch>

It is also very useful for 404s:

<Switch>
  <Route path="/path1" component={Comp1} />
  <Route path="/path2" component={Comp2} />
  <Route component={NotFound} />
</Switch>

The <Switch> component only accepts <Route> or <Redirect> as its children.

<Route />

This component defines a conditional path that, when strictly matched, renders the given component. Its props are:

  • path: the path to match to the current browser's URL. It can have parameters /:id and a wildcard at the end * to make it a partial route.
  • component: the component that will be rendered if the browser's URL matches the path parameter.
  • render: a function that will be called with the params if the browser's URL matches the path parameter.
  • children: the children to render if the browser's URL matches the path parameter.

So for example if the path prop is "/user" and you visit the page "/user", then the component is rendered; it is ignored otherwise:

// In https://example.com/
<Route path="/" component={Home} /> // Rendered
<Route path="/*" component={Any} />  // Rendered
<Route path="/user" component={User} />  // Not rendered
<Route path="/:page" component={Page} />  // Not rendered

// In https://example.com/user/
<Route path="/" component={Home} /> // Not Rendered
<Route path="/*" component={Any} />  // Rendered
<Route path="/user" component={User} />  // Rendered
<Route path="/:page" component={Page} />  // Rendered

When matching a path with a parameter (a part of the url that starts with :) it will be passed as a prop straight to the children:

// In https://example.com/user/abc
const User = ({ id }) => <div>Hello {id}</div>;
const UserList = () => <div>List here</div>;

<Route path="/user/:id" component={User} />;
// <div>Hello abc</div>

<Route path="/user/:id" render={({ id }) => <User id={id} />} />;
// <div>Hello abc</div>

// Avoid when you need the params, since they cannot be passed
<Route path="/user/">
  <UserList />
</Route>;
// <div>List here</div>

NOTE: the parameter is passed straight to the component instead of wrapped like in React Router.

The path can also include a wildcard *, in which case it will perform a partial match of everything before itself. It can only be at the end of the path:

// In https://example.com/user/abc

// All of these match the current route
<Route path="/*" component={User} />
<Route path="/user/*" component={User} />
<Route path="/user/abc/*" component={User} />
<Route path="/user/:id/*" component={User} />

NOTE: in Crossroad the paths are exact by default, and with the wildcard you can make them partial matches. So the wildcard is the opposite of adding exact to React Router.

It can also match query parameters:

// In /profile?page=settings&filter=abc

// All of these match the current route
<Route path="/profile" component={User} />
<Route path="/profile?page" component={User} />
<Route path="/profile?page=settings" component={User} />
<Route path="/profile/*?page=settings" component={User} />
<Route path="/:id?page=settings" component={User} />
<Route path="/:id/*?page=settings" component={User} />

// These shall not match:
<Route path="/?page" component={User} />  // Wrong path
<Route path="/profile?page2" component={User} />  // Wrong key
<Route path="/profile?page=options" component={User} />  // Wrong value

<Redirect />

Go to the given url when this component is rendered. It should be used with a Switch:

<Switch>
  <Route ... />
  <Route ... />
  ...
  <Redirect to="/" />
</Switch>

This way, when nothing else is matched in the Switch, then the redirect will be triggered and go to the homepage (/) or any other url we want.

<a>

Links with Crossroad are just traditional plain <a>. You write the URL and a relative path, and Crossroad handles all the history, routing, etc:

export default () => (
  <nav>
    <a href="/">Home</a>
    <a href="/users">Users</a>
    <a href="/settings">Settings</a>
  </nav>
);

An important concept to understand is where links open, whether it's a react navigation or a browser page change:

  • /: plain paths will navigate within React
  • /?abc=def: queries, hashtags, etc. will also perform a navigation in React
  • https://example.com/: full URLs will trigger a browser page change
  • target="_self": will trigger a browser page change, in the same tab
  • target="_blank": will open a new tab

Some examples:

// In https://example.com/users/25

// React navigation:
<a href="/">Home</a>

// React navigation:
<a href="/users?filter=new">New users</a>

// Page refresh (since it's a full URL)
<a href="https://google.com/">Google it</a>

// Page refresh (a full URL, even in the same domain)
<a href="https://example.com/">Home</a>

// Page refresh (it has a target="_self")
<a href="/update" target="_self">Update</a>

// New tab (it has a target="_blank")
<a href="/terms-of-service" target="_blank">Read terms of service</a>

useUrl()

NOTE: within Crossroad's and for lack of a better name, "URL" refers to the combination of path + search query + hash.

Read and set the full URL:

export default function Login() {
  const [url, setUrl] = useUrl();

  const login = async () => {
    // ... do some stuff ...
    setUrl("/welcome");
  };

  return <Button onClick={login}>Login</Button>;
}

These are the structures of each:

  • url: an object with the properties, it's similar to the native URL:
    • url.href: a string with the full URL (path + query + hash)
    • url.path: a string with the current pathname
    • url.query: an object with the keys and values. Example: { q: 'hello' }, { q: 'hello', s: 'world' }.
    • url.hash: the hashtag, without the "#"
  • setUrl(): a setter in the React Hooks style
    • setUrl("/newpath?search=hello"): a shortcut with the string
    • setUrl({ path: '/newpath' }): set the path (and delete anything else if any)
    • setUrl({ path: '/newpath', query: { hello: 'world' } }): update the path and query (and delete the hash if any)
    • setUrl(prev => ...): use the previous url (object)

The resulting url is an object containing each of the parts of the URL:

// In /whatever?filter=hello#world
const [url, setUrl] = useUrl();
console.log(url.path); // /whatever
console.log(url.query); // { filter: hello }
console.log(url.hash); // world

You can also set it fully or partially:

const [url, setUrl] = useUrl();

setUrl("/#firsttime"); // [Shorthand] Redirect to home with a hashtag
setUrl({ path: "/", hash: "firsttime" }); // Same as above
setUrl({ ...url, path: "/" }); // Keep everything the same except the path
setUrl({ ...url, query: { search: myQuery } }); // Set a full search query
setUrl({ ...url, query: { ...url.query, safe: 0 } }); // Modify only one query param

useUrl() is powerful enough for all of your needs, but you might still be interested in other hooks to simplify situations where you do e.g. heavy query manipulation with useQuery.

By default setUrl() will create a new entry in the browser history. If you want to instead replace the current entry you can pass a second parameter with { mode: 'replace' }:

setUrl("/newurl", { mode: "replace" });

usePath()

Read and set only the path(name) part of the URL:

const Login = () => {
  const [path, setPath] = usePath();

  const login = async () => {
    // ...
    setPath("/welcome");
  };

  return <Button onClick={login}>Login</Button>;
};

Note: this only modifies the path(name) and keeps the search query and hash the same, so if you want to modify the full URL you should instead utilize useUrl() and setUrl('/welcome')

By default setPath() will create a new entry in the browser history. If you want to instead replace the current entry you can pass a second parameter with { mode: 'replace' }:

setPath("/newurl", { mode: "replace" });

useQuery()

Read and set only the search query parameters from the URL:

// In /users?search=name&filter=new
const [query, setQuery] = useQuery();
// { search: 'name', filter: 'new' }

setQuery({ search: "myname" }); // Remove the other query params
// Goto /users?search=myname

setQuery({ ...query, search: "myname" }); // Keep the other query params
// Goto /users?search=myname&filter=new

If you pass a parameter, it can read and modify that parameter while keeping the others the same. This is specially useful in e.g. a search form:

// In /users?search=name&filter=new
const [search, setSearch] = useQuery("search");
// 'name'

setQuery("myname");
// Goto /users?search=myname&filter=new

When you update it, it will clean any parameter not passed, so make sure to pass the old ones if you want to keep them or a new object if you want to scrub them:

// In /users?search=name&filter=new
const [query, setQuery] = useQuery();

setQuery({ search: "myname" }); // Goto /users?q=myname  (removes the filter)
setQuery({ ...query, search: "myname" }); // Goto /users?q=myname&filter=new
setQuery(prev => ({ ...prev, search: "myname" })); // Goto /users?q=myname&filter=new

setQuery only modifies the query string part of the URL, keeping the path and hash the same as they were previously.

By default setQuery() will create a new entry in the browser history. If you want to instead replace the current entry, so that the "Back" button goes to the previous page, you can pass a second parameter with { mode: 'replace' }:

setQuery({ search: "abc" }, { mode: "replace" });

useHash()

Read and set only the hash part of the URL:

// In /login#welcome
const [hash, setHash] = useHash();
// welcome

setHash("bye");
// Goto /login#bye

By default setHash() will create a new entry in the browser history. If you want to instead replace the current entry you can pass a second parameter with { mode: 'replace' }:

setHash("newhash", { mode: "replace" });

useParams()

Parse the current URL against the given reference:

// In /users/2
const params = useParams("/users/:id");
// { id: '2' }

It's not this method responsibility to match the url, just to attempt to parse it, so if there's no good match it'll just return an empty object (use a <Route /> for path matching):

// In /pages/settings
const params = useParams("/users/:id");
// {}

Examples

Static routes

Vanity URLs

Search page

Query routing

Not found

Github hosting

NOTE: this is a bad idea for SEO, but if that doesn't matter much for you...

React Router diff

This part of the documentation tries to explain in detail the differences between Crossroad and React Router (Dom). Crossroad goal is to build a modern Router API from scratch, removing the legacy code and using Hooks natively.

Remove imperative API

With React Router your component receives the props history. This is no longer needed with Crossroad; instead of handling the history details, we provide a hook useUrl() with the setter setUrl() where you can set the new URL straight away:

import { useUrl } from "crossroad";

export default function LoginButton() {
  const [url, setUrl] = useUrl();
  const login = async e => {
    // ...
    setUrl("/welcome");
  };
  return <button onClick={login}>Login</button>;
}

The other hooks, like useQuery(), behave in a similar way so you don't need to be concerned about the history API.

Useful Hooks

I've seen in multiple codebases people end up creating a useQuery() hook wrapping useLocation and useHistory to work with query parameters. Fear no more, this and some other useful hooks are there already on Crossroad and you can use them straight away:

// setUrl() is quite flexible:
const [url, setUrl] = useUrl();
setUrl("/#firsttime"); // [Shorthand] Redirect to home with a hashtag
setUrl({ path: "/", hash: "firsttime" }); // Same as above
setUrl({ ...url, path: "/" }); // Keep everything the same except the path
setUrl({ ...url, query: { search: myQuery } }); // Set a full search query
setUrl({ ...url, query: { ...url.query, safe: 0 } }); // Modify only one query param
// In /?search=myname&filter=new

// Manipulate the whole query object
const [query, setQuery] = useQuery();
setQuery({ ...query, search: "myname2" });

// Manipulate _only_ the query parameter "search"
const [search, setSearch] = useQuery("search");
setSearch("myname2");

To add a link in your application, you use the native <a> element instead of having to import a different component. What's more, this makes links a lot more consistent than in React Router. Some examples:

// Crossroad

// Normal link
<a href="/">Hello</a>

// Open in same page with refresh
<a href="https://example.com/">Hello</a> // http(s): links open with refresh
<a href="/" target="_self">Hello</a>  // self to open with refresh

// Open in new page
<a href="/myfile.pdf" target="_blank">Hello</a>  // Traditional target blank
<a href="https://example.com/" target="_blank">Hello</a>

The same in React Router are like this, note the inconsistencies of some times using <Link> and some times using <a>

// React Router

// Normal link
<Link to="/">Hello</Link>

// Open in same page with refresh
<a href="https://example.com/">Hello</Link>
<a href="/">Hello</Link>

// Open in new page
<a href="https://example.com/" target="_blank">Hello</Link>
<Link to="https://example.com/">Hello</Link>  // Broken

GitHub

https://github.com/franciscop/crossroad