# Crossroad [![crossroad](https://img.shields.io/npm/v/crossroad?label=crossroad&color=greenlime)](https://www.npmjs.com/package/crossroad) [![tests](https://github.com/franciscop/crossroad/workflows/tests/badge.svg)](https://github.com/franciscop/crossroad/actions) [![gzip size](https://img.badgesize.io/franciscop/crossroad/master/index.min.js.svg?label=gzip&logo=&compression=gzip)](https://github.com/franciscop/crossroad/blob/master/index.min.js) [![dependencies](https://img.shields.io/badge/dependencies-0-limegreen.svg)](https://github.com/franciscop/crossroad/blob/master/package.json) A React library to handle navigation in your WebApp. Built with simple components and React Hooks so you write cleaner code: - ``, `` and `` inspired by React Router so it's easy to get started. - Very useful hooks like [`useUrl`](#useurl), [`useQuery`](#usequery), etc. Follow [the rules of hooks](https://reactjs.org/docs/hooks-rules.html). - Links are plain `` instead of custom components. [Read more](#a). - The `` path is `exact` by default and can match query parameters. - It's [just ~1.9kb](https://bundlephobia.com/package/crossroad) (min+gzip) instead of the 17kb of React Router(+Dom). - Add `scrollUp` to `` o `` to automatically scroll up on a route change. [**🔗 Demo on CodeSandbox**](https://codesandbox.io/s/recursing-wozniak-uftyo?file=/src/App.js) ```js // App.js import Router, { Switch, Route } from "crossroad"; export default function App() { return ( ); } ``` ## Getting Started Create a React project (`npx create-react-app demo`) and install Crossroad: ```js npm i crossroad ``` Then import it on your App.js and define some routes: ```js import Router, { Switch, Route } from "crossroad"; export default function App() { return ( ); } ``` Then let's add some navigation and the actual pages: ```js import Router, { Switch, Route } from "crossroad"; const Home = () =>
Home Page
; const Profile = ({ id }) =>
Hello {id.toUpperCase()}
; export default function App() { return ( ); } ``` Now you can start your project and test it by visiting `http://localhost:3000/` and `http://localhost:3000/login`: ```bash npm start ``` See the more complete working example [in this CodeSandbox](https://codesandbox.io/s/recursing-wozniak-uftyo?file=/src/App.js). ## 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. - [``](#a): a plain HTML link, use it to navigate between pages. - [`useUrl()`](#useurl): a hook that returns the current URL and a setter to update it. - [`usePath()`](#usepath): a hook that returns the current path and a setter to update it. - [`useQuery()`](#usequery): a hook that returns the current query and a setter to update it. - [`useHash()`](#usehash): a hook that returns the current hash and a setter to update it. - [`useParams()`](#useparams): a hook that extracts params form the current path. `Router` is the default export, `` is not exported since it's just the plain link element, and everything else are named exports: ```js import Router, { Switch, Route, useUrl, usePath } from "crossroad"; ``` ### `` 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: ```js // App.js import Router from "crossroad"; export default function App() { return ... Your normal App code ...; } ``` Add the prop `scrollUp` to automatically scroll up the browser window when _any_ route changes. In contrast, you could also add it only to a single or multiple ``. Add the prop `url` to simulate a fake URL instead of the current `window.location`, useful specially for testing. You would normally setup this Router straight on your App, along things like [Statux](https://statux.dev/)'s or [Redux](https://redux.js.org/)'s Store, error handling, translations, etc. An example for a simple app: ```js // App.js import Router, { Switch, Route } from "crossroad"; import Home from "./pages/Home"; import Dashboard from "./pages/Dashboard"; import Profile from "./pages/Profile"; export default function App() { return ( ); } ``` ### `` 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: ```js // In https://example.com/help, it'll render the Help component only ``` You might want to redirect the user to a specific route (like `/notfound`) when none of the given routes matches the current URL. You can then use the attribute "redirect": ```js ``` The redirect parameter can be a plain string, an url-like object or a callback that returns any of the previous: ```js "/gohere"}> ({ ...url, path: "/gohere" })}> ``` Or to keep it in the current route, whatever it is, you can render a component with no path (no path === `*`): ```js ``` The `` component only accepts `` as its children. ### `` 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`, with optional types like `/: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. - `scrollUp`: automatically scroll up the browser window when this route/component/etc is matched. > Exactly one of "component | render | children" props must be defined, not 0, not multiple. So for example if the `path` prop is `"/user"` and you visit the page `"/user"`, then the component is rendered; it is ignored otherwise: ```js // In https://example.com/ // Rendered // Rendered // Not rendered // Not rendered // In https://example.com/user/ // Not Rendered // Rendered // Rendered // 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: ```js // In https://example.com/user/25 const User = ({ id }) =>
Hello {id} ({typeof id})
; const UserList = () =>
List here
; ; //
Hello 25 (string)
; //
Hello 25 (number)
} />; //
Hello 25 (string)
} />; //
Hello 25 (number)
// Avoid when you need the params, since they cannot be passed easily ; //
List here
``` > [!WARNING] > The parameter is passed straight to the component instead of wrapped like in React Router, see the examples above. 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: ```js // In https://example.com/user/abc // All of these match the current route ``` > [!TIP] > 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: ```js // In /profile?page=settings&filter=abc // All of these match the current route // These shall not match: // Wrong path // Wrong key // Wrong value ``` Finally, it can also attempt to convert the types to the specified format. It does _not_ run validation. The only really useful type right now is `number` and `date` (`string` is the default so no need for it): ```jsx { console.log(id, typeof id); // 25 number }} />; { console.log(time, typeof time); // Jan 25th, 2055 Date }} />; ```` ### `
` Links with Crossroad are just traditional plain ``. You write the URL and a relative path, and Crossroad handles all the history, routing, etc: ```js export default () => ( ); ``` 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 - `tel:+1234567890`, `mailto:test@example.com`: non-HTTP protocol links will trigger a browser action (call, email, etc.) - `target="_self"`: will trigger a browser page change, in the same tab - `target="_blank"`: will open a new tab Some examples: ```js // In https://example.com/users/25 // React navigation: Home // React navigation: New users // Page refresh (since it's a full URL) Google it // Page refresh (a full URL, even in the same domain) Home // Page refresh (it has a target="_self") Update // New tab (it has a target="_blank") Read terms of service ``` ### `useUrl()` Read and set the full URL (path + search query + hash): ```js import { useUrl } from "crossroad"; export default function Login() { const [url, setUrl] = useUrl(); const login = async () => { // ... do some stuff ... setUrl("/welcome"); }; return ; } ``` These are the structures of each: - `url`: an object with the properties, it's similar to the native URL: - `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) `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`](#usequery). #### url The resulting `url` is an object containing each of the parts of the URL: ```js // 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 ``` It is memoized, so that if the url doesn't change then the object will remain the same. The same of course applies to the subelements like `url.path`. It will however change when the url changes, so you want to put it in your dependencies as usual: ```js // You can put the whole thing if you want to listen to // ANY change on the url useEffect(() => { // ... }, [url]); // Or only a part of it. This is useful becase it WON'T trigger // when the query or hashtag change useEffect(() => { // ... }, [url.path]); ``` #### Setter The setter can be invoked directly, or with a callback: ```js const [url, setUrl] = useUrl(); // [Shorthand] Redirect to home with a hashtag setUrl("/#firsttime"); // Same as above, but specifying the parts setUrl({ path: "/", hash: "firsttime" }); // Keep everything the same except the path setUrl({ ...url, path: "/" }); // Set a full search query setUrl({ ...url, query: { search: "hello" } }); // Modify only one query param setUrl({ ...url, query: { ...url.query, safe: "no" } }); ``` The function `setUrl` is _always_ the same, so it doesn't matter whether you put it as a dependency or not. However the `path` can be updated and change, so you want to depend on it: ```js const [url, setUrl] = useUrl(); useEffect(() => { if (url.path === "/base") { setUrl("/base/deeper"); } }, [url.path, setUrl]); ``` If you update the url with the current url, it won't trigger a rerender. So the above can also be written as this, removing all dependencies: ```js const [url, setUrl] = useUrl(); useEffect(() => { setUrl((old) => { if (old.path === "/base") return "/base/deeper"; return old; }); }, []); ``` #### New history entry By default `setUrl()` will create a new entry in the browser history. If you want to instead replace the current url you can pass a second parameter with `{ mode: 'replace' }`: ```js setUrl("/newurl"); // Default: "push" setUrl("/newurl", { mode: "replace" }); ``` - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/c` and then click on the back button, the browser will go back to `/b`. This is because `/b` and `/c` are both independent entries in your history. - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/c` and then click on the back button, it'll go back to `/a`. This is because `/c` is overwriting `/b`, instead of adding a new entry. ### `usePath()` Read and set only the path(name) part of the URL: ```js const Login = () => { const [path, setPath] = usePath(); const login = async () => { // ... setPath("/welcome"); }; return ; }; ``` The path is always a string equivalent to `window.location.pathname`. Why not use `window.location.pathname` then? Because usePath() is a hook that will trigger a re-render when the path changes! `setPath` _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')` #### Setter The setter can be invoked directly, or with a callback: ```js setPath("/newpath"); setPath((oldPath) => "/newpath"); ``` The function `setPath` is _always_ stable, so it doesn't matter whether you put it as a dependency or not. However the `path` can be updated, so you might want to put that: ```js const [path, setPath] = usePath(); useEffect(() => { if (path === "/base") { setPath("/base/deeper"); } }, [path, setPath]); ``` If you update the path with the current path, it won't trigger a rerender. So the above can also be written as this, removing all dependencies: ```js const [path, setPath] = usePath(); useEffect(() => { setPath((old) => { if (old === "/base") return "/base/deeper"; return old; }); }, []); ``` #### New history entry By default `setPath()` will create a new entry in the browser history. If you want to instead replace the current url you can pass a second parameter with `{ mode: 'replace' }`: ```js setPath("/newpath"); // Default: "push" setPath("/newpath", { mode: "replace" }); ``` - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/c` and then click on the back button, the browser will go back to `/b`. This is because `/b` and `/b?q=c` are both independent entries in your history. - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/c` and then click on the back button, it'll go back to `/a`. This is because `/c` is overwriting `/b`, instead of adding a new entry. ### `useQuery()` Read and set only the search query parameters from the URL: ```js import { useQuery } from "crossroad"; export default function SearchInput() { // In /users?search= const [query, setQuery] = useQuery(); // [{ search: "" }, fn] // Goes to /users?search={value} const onChange = (e) => setQuery({ search: e.target.value }); return ; } ``` If you pass a key, it can read and modify that parameter while keeping the others the same. This is specially useful in e.g. a search form: ```js // In /users?search=name&filter=new const [search, setSearch] = useQuery("search"); // 'name' setSearch("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: ```js // In /users?search=name&filter=new const [query, setQuery] = useQuery(); setQuery({ search: "myname" }); // Goto /users?search=myname (removes the filter) setQuery({ ...query, search: "myname" }); // Goto /users?search=myname&filter=new setQuery((prev) => ({ ...prev, search: "myname" })); // Goto /users?search=myname&filter=new ``` `setQuery` only modifies the query string part of the URL, keeping the `path` and `hash` the same as they were previously. When you set a search query to `null` it will be removed from the URL. However, empty strings `""`, zero `0` or boolean `false` are not removed. So if you want falsy values to also remove the parameter in the URL, please do this: ```js const [myname, setMyname] = useQuery("myname"); // ... setMyname(newName || null); ``` If you are using `react-query` and already have a bunch of `useQuery()` in your code and prefer to use other name, you can rename this method when importing it: ```js import { useQuery as useSearch } from 'crossroad'; ... ``` #### New history entry 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' }`: ```js setQuery({ search: "abc" }); // Default: "push" setQuery({ search: "abc" }, { mode: "replace" }); ``` - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b?q=c` and then click on the back button, the browser will go back to `/b`. This is because `/b` and `/b?q=c` are both independent entries in your history. - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/b?q=c` and then click on the back button, it'll go back to `/a`. This is because `/b?q=c` is overwriting `/b`, instead of adding a new entry. ### `useHash()` Read and set only the hash part of the URL (without the `"#"`): ```js // 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' }`: ```js setHash("newhash", { mode: "replace" }); ``` If you want to remove the hash, pass a `null` or `undefined` to the setter. #### New history entry By default `setHash()` 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' }`: ```js setHash("newhash"); // Default: "push" setHash("newhash", { mode: "replace" }); ``` - `push` (default): creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(push)> `/b#c` and then click on the back button, the browser will go back to `/b`. This is because `/b` and `/b?q=c` are both independent entries in your history. - `replace`: creates a new entry in the history. E.g. if you navigate `/a` => `/b` =(replace)> `/b#c` and then click on the back button, it'll go back to `/a`. This is because `/b#c` is overwriting `/b`, instead of adding a new entry. ### `useParams()` Get the parameters from the matched URL, already parsed as an object, or pass an argument to just get the key: ```ts function Profile() { const { username } = useParams(); // or const username = useParams('username'); return
Hello {username}
; } ``` The path in the [Route](#route) can also specify the type. Whenever possible it's preferable to use the props (since those can be type-checked automatically): ```js function Profile({ id }: { id: number }) { return
Hello {id}
; } ```` Because with React Context we cannot infer the types properly, so if you want to differentiate between string | number you can do so with: ```js // const userId = useParams("id"); // "25" const bookId = useParams("bookId"); // "55" // const userId = useParams("id"); // 25 const bookId = useParams("bookId"); // 55 ``` You can also type the whole list of params, but we recommend using useParams() with the key argument: ```js // const params = useParams<{ id: string, bookId: string }>(); // { id: "25", bookId: "55" } // const params = useParams<{ id: number, bookId: number }>(); // { id: 25, bookId: 55 } ```` ## Examples ### Static routes Let's see a traditional company website, where you have a homepage, some specific pages and a PDF: [**Codesandbox example**](https://codesandbox.io/s/loving-joana-jikne) https://user-images.githubusercontent.com/2801252/131257834-bfd9b6c6-f22e-46f2-9d06-8c14ac7f2708.mp4 ```js // App.js import Router, { Switch, Route } from "crossroad"; import Nav from "./Nav"; import Pages from "./Pages"; export default function App() { return (