Git Product home page Git Product logo

wouter's Introduction

Wouter β€” a super-tiny React router (logo by Katya Simacheva)

npm CI Coverage Coverage Edit in StackBlitz IDE
wouter is a tiny router for modern React and Preact apps that relies on Hooks.
A router you wanted so bad in your project!

Features

⚠️ These docs are for wouter v3 only. Please find the documentation for [email protected] here

by Katya Simacheva

developers πŸ’– wouter

... I love Wouter. It’s tiny, fully embraces hooks, and has an intuitive and barebones API. I can accomplish everything I could with react-router with Wouter, and it just feels more minimalist while not being inconvenient.

Matt Miller, An exhaustive React ecosystem for 2020

Wouter provides a simple API that many developers and library authors appreciate. Some notable projects that use wouter: Ultra, React-three-fiber, Sunmao UI, Million and many more.

Table of Contents

Getting Started

First, add wouter to your project.

npm i wouter

Or, if you're using Preact the use the following command npm i wouter-preact.

Check out this simple demo app below. It doesn't cover hooks and other features such as nested routing, but it's a good starting point for those who are migrating from React Router.

import { Link, Route, Switch } from "wouter";

const App = () => (
  <>
    <Link href="/users/1">Profile</Link>

    <Route path="/about">About Us</Route>

    {/* 
      Routes below are matched exclusively -
      the first matched route gets rendered
    */}
    <Switch>
      <Route path="/inbox" component={InboxPage} />

      <Route path="/users/:name">
        {(params) => <>Hello, {params.name}!</>}
      </Route>

      {/* Default route in a switch */}
      <Route>404: No such page!</Route>
    </Switch>
  </>
);

Browser Support

This library is designed for ES2020+ compatibility. If you need to support older browsers, make sure that you transpile node_modules. Additionally, the minimum supported TypeScript version is 4.1 in order to support route parameter inference.

Wouter API

Wouter comes with three kinds of APIs: low-level standalone location hooks, hooks for routing and pattern matching and more traditional component-based API similar to React Router's one.

You are free to choose whatever works for you: use location hooks when you want to keep your app as small as possible and don't need pattern matching; use routing hooks when you want to build custom routing components; or if you're building a traditional app with pages and navigation β€” components might come in handy.

Check out also FAQ and Code Recipes for more advanced things like active links, default routes, server-side rendering etc.

The list of methods available

Location Hooks

These can be used separately from the main module and have an interface similar to useState. These hooks don't support nesting, base path, route matching.

Routing Hooks

Import from wouter module.

  • useRoute β€” shows whether or not current page matches the pattern provided.
  • useLocation β€” allows to manipulate current router's location, by default subscribes to browser location. Note: this isn't the same as useBrowserLocation, read below.
  • useParams β€” returns an object with parameters matched from the closest route.
  • useSearch β€” returns a search string – everything that goes after the ?.
  • useRouter β€” returns a global router object that holds the configuration. Only use it if you want to customize the routing.

Components

Import from wouter module.

  • <Route /> β€” conditionally renders a component based on a pattern.
  • <Link /> β€” wraps <a>, allows to perfom a navigation.
  • <Switch /> β€” exclusive routing, only renders the first matched route.
  • <Redirect /> β€” when rendered, performs an immediate navigation.
  • <Router /> β€” an optional top-level component for advanced routing configuration.

Hooks API

useRoute: route matching and parameters

Checks if the current location matches the pattern provided and returns an object with parameters. This is powered by a wonderful regexparam library, so all its pattern syntax is fully supported.

You can use useRoute to perform manual routing or implement custom logic, such as route transitions, etc.

import { useRoute } from "wouter";

const Users = () => {
  // `match` is a boolean
  const [match, params] = useRoute("/users/:name");

  if (match) {
    return <>Hello, {params.name}!</>;
  } else {
    return null;
  }
};

A quick cheatsheet of what types of segments are supported:

useRoute("/app/:page");
useRoute("/app/:page/:section");

// optional parameter, matches "/en/home" and "/home"
useRoute("/:locale?/home");

// suffixes
useRoute("/movies/:title.(mp4|mov)");

// wildcards, matches "/app", "/app-1", "/app/home"
useRoute("/app*");

// optional wildcards, matches "/orders", "/orders/"
// and "/orders/completed/list"
useRoute("/orders/*?");

// regex for matching complex patterns,
// matches "/hello:123"
useRoute(/^[/]([a-z]+):([0-9]+)[/]?$/);
// and with named capture groups
useRoute(/^[/](?<word>[a-z]+):(?<num>[0-9]+)[/]?$/);

The second item in the pair params is an object with parameters or null if there was no match. For wildcard segments the parameter name is "*":

// wildcards, matches "/app", "/app-1", "/app/home"
const [match, params] = useRoute("/app*");

if (match) {
  // "/home" for "/app/home"
  const page = params["*"];
}

useLocation: working with the history

To get the current path and navigate between pages, call the useLocation hook. Similarly to useState, it returns a value and a setter: the component will re-render when the location changes and by calling navigate you can update this value and perform navigation.

By default, it uses useBrowserLocation under the hood, though you can configure this in a top-level Router component (for example, if you decide at some point to switch to a hash-based routing). useLocation will also return scoped path when used within nested routes or with base path setting.

import { useLocation } from "wouter";

const CurrentLocation = () => {
  const [location, setLocation] = useLocation();

  return (
    <div>
      {`The current page is: ${location}`}
      <a onClick={() => setLocation("/somewhere")}>Click to update</a>
    </div>
  );
};

All the components internally call the useLocation hook.

Additional navigation parameters

The setter method of useLocation can also accept an optional object with parameters to control how the navigation update will happen.

When browser location is used (default), useLocation hook accepts replace flag to tell the hook to modify the current history entry instead of adding a new one. It is the same as calling replaceState.

const [location, navigate] = useLocation();

navigate("/jobs"); // `pushState` is used
navigate("/home", { replace: true }); // `replaceState` is used

Additionally, you can provide a state option to update history.state while navigating:

navigate("/home", { state: { modal: "promo" } });

history.state; // { modal: "promo" }

Customizing the location hook

By default, wouter uses useLocation hook that reacts to pushState and replaceState navigation via useBrowserLocation.

To customize this, wrap your app in a Router component:

import { Router, Route } from "wouter";
import { useHashLocation } from "wouter/use-hash-location";

const App = () => (
  <Router hook={useHashLocation}>
    <Route path="/about" component={About} />
    ...
  </Router>
);

Because these hooks have return values similar to useState, it is easy and fun to build your own location hooks: useCrossTabLocation, useLocalStorage, useMicroFrontendLocation and whatever routing logic you want to support in the app. Give it a try!

useParams: extracting matched parameters

This hook allows you to access the parameters exposed through matching dynamic segments. Internally, we simply wrap your components in a context provider allowing you to access this data anywhere within the Route component.

This allows you to avoid "prop drilling" when dealing with deeply nested components within the route. Note: useParams will only extract parameters from the closest parent route.

import { Route, useParams } from "wouter";

const User = () => {
  const params = useParams();

  params.id; // "1"

  // alternatively, use the index to access the prop
  params[0]; // "1"
};

<Route path="/user/:id" component={User}> />

It is the same for regex paths. Capture groups can be accessed by their index, or if there is a named capture group, that can be used instead.

import { Route, useParams } from "wouter";

const User = () => {
  const params = useParams();

  params.id; // "1"
  params[0]; // "1"
};

<Route path={/^[/]user[/](?<id>[0-9]+)[/]?$/} component={User}> />

useSearch: query strings

Use this hook to get the current search (query) string value. It will cause your component to re-render only when the string itself and not the full location updates. The search string returned does not contain a ? character.

import { useSearch } from "wouter";

// returns "tab=settings&id=1"
// the hook for extracting search parameters is coming soon!
const searchString = useSearch();

For the SSR, use ssrSearch prop passed to the router.

<Router ssrSearch={request.search}>{/* SSR! */}</Router>

Refer to Server-Side Rendering for more info on rendering and hydration.

useRouter: accessing the router object

If you're building advanced integration, for example custom location hook, you might want to get access to the global router object. Router is a simple object that holds routing options that you configure in the Router component.

import { useRouter } from "wouter";

const Custom = () => {
  const router = useRouter();

  router.hook; // `useBrowserLocation` by default
  router.base; // "/app"
};

const App = () => (
  <Router base="/app">
    <Custom />
  </Router>
);

Component API

<Route path={pattern} />

Route represents a piece of the app that is rendered conditionally based on a pattern path. Pattern has the same syntax as the argument you pass to useRoute.

The library provides multiple ways to declare a route's body:

import { Route } from "wouter";

// simple form
<Route path="/home"><Home /></Route>

// render-prop style
<Route path="/users/:id">
  {params => <UserPage id={params.id} />}
</Route>

// the `params` prop will be passed down to <Orders />
<Route path="/orders/:status" component={Orders} />

A route with no path is considered to always match, and it is the same as <Route path="*" />. When developing your app, use this trick to peek at the route's content without navigation.

-<Route path="/some/page">
+<Route>
  {/* Strip out the `path` to make this visible */}
</Route>

Route Nesting

Nesting is a core feature of wouter and can be enabled on a route via the nest prop. When this prop is present, the route matches everything that starts with a given pattern and it creates a nested routing context. All child routes will receive location relative to that pattern.

Let's take a look at this example:

<Route path="/app" nest>
  <Route path="/users/:id" nest>
    <Route path="/orders" />
  </Route>
</Route>
  1. This first route will be active for all paths that start with /app, this is equivalent to having a base path in your app.

  2. The second one uses dynamic pattern to match paths like /app/user/1, /app/user/1/anything and so on.

  3. Finally, the inner-most route will only work for paths that look like /app/users/1/orders. The match is strict, since that route does not have a nest prop and it works as usual.

If you call useLocation() inside the last route, it will return /orders and not /app/users/1/orders. This creates a nice isolation and it makes it easier to make changes to parent route without worrying that the rest of the app will stop working. If you need to navigate to a top-level page however, you can use a prefix ~ to refer to an absolute path:

<Route path="/payments" nest>
  <Route path="/all">
    <Link to="~/home">Back to Home</Link>
  </Route>
</Route>

Note: The nest prop does not alter the regex passed into regex paths. Instead, the nest prop will only determine if nested routes will match against the rest of path or the same path. To make a strict path regex, use a regex pattern like /^[/](your pattern)[/]?$/ (this matches an optional end slash and the end of the string). To make a nestable regex, use a regex pattern like /^[/](your pattern)(?=$|[/])/ (this matches either the end of the string or a slash for future segments).

<Link href={path} />

Link component renders an <a /> element that, when clicked, performs a navigation.

import { Link } from "wouter"

<Link href="/">Home</Link>

// `to` is an alias for `href`
<Link to="/">Home</Link>

// all standard `a` props are proxied
<Link href="/" className="link" aria-label="Go to homepage">Home</Link>

// all location hook options are supported
<Link href="/" replace state={{ animate: true }} />

Link will always wrap its children in an <a /> tag, unless asChild prop is provided. Use this when you need to have a custom component that renders an <a /> under the hood.

// use this instead
<Link to="/" asChild>
  <UIKitLink />
</Link>

// Remember, `UIKitLink` must implement an `onClick` handler
// in order for navigation to work!

When you pass a function as a className prop, it will be called with a boolean value indicating whether the link is active for the current route. You can use this to style active links (e.g. for links in navigation menu)

<Link className={(active) => (active ? "active" : "")}>Nav</Link>

Read more about active links here.

<Switch />

There are cases when you want to have an exclusive routing: to make sure that only one route is rendered at the time, even if the routes have patterns that overlap. That's what Switch does: it only renders the first matching route.

import { Route, Switch } from "wouter";

<Switch>
  <Route path="/orders/all" component={AllOrders} />
  <Route path="/orders/:status" component={Orders} />

  {/* 
     in wouter, any Route with empty path is considered always active. 
     This can be used to achieve "default" route behaviour within Switch. 
     Note: the order matters! See examples below.
  */}
  <Route>This is rendered when nothing above has matched</Route>
</Switch>;

When no route in switch matches, the last empty Route will be used as a fallback. See FAQ and Code Recipes section to read about default routes.

<Redirect to={path} />

When mounted performs a redirect to a path provided. Uses useLocation hook internally to trigger the navigation inside of a useEffect block.

Redirect can also accept props for customizing how navigation will be performed, for example for setting history state when navigating. These options are specific to the currently used location hook.

<Redirect to="/" />

// arbitrary state object
<Redirect to="/" state={{ modal: true }} />

// use `replaceState`
<Redirect to="/" replace />

If you need more advanced logic for navigation, for example, to trigger the redirect inside of an event handler, consider using useLocation hook instead:

import { useLocation } from "wouter";

const [location, setLocation] = useLocation();

fetchOrders().then((orders) => {
  setOrders(orders);
  setLocation("/app/orders");
});

<Router hook={hook} parser={fn} base={basepath} hrefs={fn} />

Unlike React Router, routes in wouter don't have to be wrapped in a top-level component. An internal router object will be constructed on demand, so you can start writing your app without polluting it with a cascade of top-level providers. There are cases however, when the routing behaviour needs to be customized.

These cases include hash-based routing, basepath support, custom matcher function etc.

import { useHashLocation } from "wouter/use-hash-location";

<Router hook={useHashLocation} base="/app">
  {/* Your app goes here */}
</Router>;

A router is a simple object that holds the routing configuration options. You can always obtain this object using a useRouter hook. The list of currently available options:

  • hook: () => [location: string, setLocation: fn] β€” is a React Hook function that subscribes to location changes. It returns a pair of current location string e.g. /app/users and a setLocation function for navigation. You can use this hook from any component of your app by calling useLocation() hook. See Customizing the location hook.

  • searchHook: () => [search: string, setSearch: fn] β€” similar to hook, but for obtaining the current search string.

  • base: string β€” an optional setting that allows to specify a base path, such as /app. All application routes will be relative to that path. To navigate out to an absolute path, prefix your path with an ~. See the FAQ.

  • parser: (path: string, loose?: boolean) => { pattern, keys } β€” a pattern parsing function. Produces a RegExp for matching the current location against the user-defined patterns like /app/users/:id. Has the same interface as the parse function from regexparam. See this example that demonstrates custom parser feature.

  • ssrPath: string and ssrSearch: string use these when rendering your app on the server.

  • hrefs: (href: boolean) => string β€” a function for transforming href attribute of an <a /> element rendered by Link. It is used to support hash-based routing. By default, href attribute is the same as the href or to prop of a Link. A location hook can also define a hook.hrefs property, in this case the href will be inferred.

FAQ and Code Recipes

I deploy my app to the subfolder. Can I specify a base path?

You can! Wrap your app with <Router base="/app" /> component and that should do the trick:

import { Router, Route, Link } from "wouter";

const App = () => (
  <Router base="/app">
    {/* the link's href attribute will be "/app/users" */}
    <Link href="/users">Users</Link>

    <Route path="/users">The current path is /app/users!</Route>
  </Router>
);

Calling useLocation() within a route in an app with base path will return a path scoped to the base. Meaning that when base is "/app" and pathname is "/app/users" the returned string is "/users". Accordingly, calling navigate will automatically append the base to the path argument for you.

When you have multiple nested routers, base paths are inherited and stack up.

<Router base="/app">
  <Router base="/cms">
    <Route path="/users">Path is /app/cms/users!</Route>
  </Router>
</Router>

How do I make a default route?

One of the common patterns in application routing is having a default route that will be shown as a fallback, in case no other route matches (for example, if you need to render 404 message). In wouter this can easily be done as a combination of <Switch /> component and a default route:

import { Switch, Route } from "wouter";

<Switch>
  <Route path="/about">...</Route>
  <Route>404, Not Found!</Route>
</Switch>;

Note: the order of switch children matters, default route should always come last.

If you want to have access to the matched segment of the path you can use wildcard parameters:

<Switch>
  <Route path="/users">...</Route>

  {/* will match anything that starts with /users/, e.g. /users/foo, /users/1/edit etc. */}
  <Route path="/users/*">...</Route>

  {/* will match everything else */}
  <Route path="*">
    {(params) => `404, Sorry the page ${params["*"]} does not exist!`}
  </Route>
</Switch>

β–Ά Demo Sandbox

How do I make a link active for the current route?

Instead of a regular className string, provide a function to use custom class when this link matches the current route. Note that it will always perform an exact match (i.e. /users will not be active for /users/1).

<Link className={(active) => (active ? "active" : "")}>Nav link</Link>

If you need to control other props, such as aria-current or style, you can write your own <Link /> wrapper and detect if the path is active by using the useRoute hook.

const [isActive] = useRoute(props.href);

return (
  <Link {...props} asChild>
    <a style={isActive ? { color: "red" } : {}}>{props.children}</a>
  </Link>
);

β–Ά Demo Sandbox

Are strict routes supported?

If a trailing slash is important for your app's routing, you could specify a custom parser. Parser is a method that takes a pattern string and returns a RegExp and an array of parsed key. It uses the signature of a parse function from regexparam.

Let's write a custom parser based on a popular path-to-regexp package that does support strict routes option.

import { pathToRegexp } from "path-to-regexp";

/**
 * Custom parser based on `pathToRegexp` with strict route option
 */
const strictParser = (path, loose) => {
  const keys = [];
  const pattern = pathToRegexp(path, keys, { strict: true, end: !loose });

  return {
    pattern,
    // `pathToRegexp` returns some metadata about the keys,
    // we want to strip it to just an array of keys
    keys: keys.map((k) => k.name),
  };
};

const App = () => (
  <Router parser={strictParser}>
    <Route path="/foo">...</Route>
    <Route path="/foo/">...</Route>
  </Router>
);

β–Ά Demo Sandbox

Are relative routes and links supported?

Yes! Any route with nest prop present creates a nesting context. Keep in mind, that the location inside a nested route will be scoped.

const App = () => (
  <Router base="/app">
    <Route path="/dashboard" nest>
      {/* the href is "/app/dashboard/users" */}
      <Link to="/users" />

      <Route path="/users">
        {/* Here `useLocation()` returns "/users"! */}
      </Route>
    </Route>
  </Router>
);

β–Ά Demo Sandbox

Can I initiate navigation from outside a component?

Yes, the navigate function is exposed from the "wouter/use-browser-location" module:

import { navigate } from "wouter/use-browser-location";

navigate("/", { replace: true });

It's the same function that is used internally.

Can I use wouter in my TypeScript project?

Yes! Although the project isn't written in TypeScript, the type definition files are bundled with the package.

How can add animated route transitions?

Let's take look at how wouter routes can be animated with framer-motion. Animating enter transitions is easy, but exit transitions require a bit more work. We'll use the AnimatePresence component that will keep the page in the DOM until the exit animation is complete.

Unfortunately, AnimatePresence only animates its direct children, so this won't work:

import { motion, AnimatePresence } from "framer-motion";

export const MyComponent = () => (
  <AnimatePresence>
    {/* This will not work! `motion.div` is not a direct child */}
    <Route path="/">
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
      />
    </Route>
  </AnimatePresence>
);

The workaround is to match this route manually with useRoute:

export const MyComponent = ({ isVisible }) => {
  const [isMatch] = useRoute("/");

  return (
    <AnimatePresence>
      {isMatch && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        />
      )}
    </AnimatePresence>
  );
};

More complex examples involve using useRoutes hook (similar to how React Router does it), but wouter does not ship it out-of-the-box. Please refer to this issue for the workaround.

Preact support?

Preact exports are available through a separate package named wouter-preact (or within the wouter/preact namespace, however this method isn't recommended as it requires React as a peer dependency):

- import { useRoute, Route, Switch } from "wouter";
+ import { useRoute, Route, Switch } from "wouter-preact";

You might need to ensure you have the latest version of Preact X with support for hooks.

β–Ά Demo Sandbox

Server-side Rendering support (SSR)?

In order to render your app on the server, you'll need to wrap your app with top-level Router and specify ssrPath prop (usually, derived from current request). Optionally, Router accepts ssrSearch parameter if need to have access to a search string on a server.

import { renderToString } from "react-dom/server";
import { Router } from "wouter";

const handleRequest = (req, res) => {
  // top-level Router is mandatory in SSR mode
  const prerendered = renderToString(
    <Router ssrPath={req.path} ssrSearch={req.search}>
      <App />
    </Router>
  );

  // respond with prerendered html
};

Tip: wouter can pre-fill ssrSearch, if ssrPath contains the ? character. So these are equivalent:

<Router ssrPath="/goods?sort=asc" />;

// is the same as
<Router ssrPath="/goods" ssrSearch="sort=asc" />;

On the client, the static markup must be hydrated in order for your app to become interactive. Note that to avoid having hydration warnings, the JSX rendered on the client must match the one used by the server, so the Router component must be present.

import { hydrateRoot } from "react-dom/client";

const root = hydrateRoot(
  domNode,
  // during hydration, `ssrPath` is set to `location.pathname`,
  // `ssrSearch` set to `location.search` accordingly
  // so there is no need to explicitly specify them
  <Router>
    <App />
  </Router>
);

β–Ά Demo

How do I configure the router to render a specific route in tests?

Testing with wouter is no different from testing regular React apps. You often need a way to provide a fixture for the current location to render a specific route. This can be easily done by swapping the normal location hook with memoryLocation. It is an initializer function that returns a hook that you can then specify in a top-level Router.

import { render } from "@testing-library/react";
import { memoryLocation } from "wouter/memory-location";

it("renders a user page", () => {
  // `static` option makes it immutable
  // even if you call `navigate` somewhere in the app location won't change
  const { hook } = memoryLocation({ path: "/user/2", static: true });

  const { container } = render(
    <Router hook={hook}>
      <Route path="/user/:id">{(params) => <>User ID: {params.id}</>}</Route>
    </Router>
  );

  expect(container.innerHTML).toBe("User ID: 2");
});

The hook can be configured to record navigation history. Additionally, it comes with a navigate function for external navigation.

it("performs a redirect", () => {
  const { hook, history, navigate } = memoryLocation({
    path: "/",
    // will store navigation history in `history`
    record: true,
  });

  const { container } = render(
    <Router hook={hook}>
      <Switch>
        <Route path="/">Index</Route>
        <Route path="/orders">Orders</Route>

        <Route>
          <Redirect to="/orders" />
        </Route>
      </Switch>
    </Router>
  );

  expect(history).toStrictEqual(["/"]);

  navigate("/unknown/route");

  expect(container.innerHTML).toBe("Orders");
  expect(history).toStrictEqual(["/", "/unknown/route", "/orders"]);
});

1KB is too much, I can't afford it!

We've got some great news for you! If you're a minimalist bundle-size nomad and you need a damn simple routing in your app, you can just use bare location hooks. For example, useBrowserLocation hook which is only 650 bytes gzipped and manually match the current location with it:

import { useBrowserLocation } from "wouter/use-browser-location";

const UsersRoute = () => {
  const [location] = useBrowserLocation();

  if (location !== "/users") return null;

  // render the route
};

Wouter's motto is "Minimalist-friendly".

Acknowledgements

Wouter illustrations and logos were made by Katya Simacheva and Katya Vakulenko. Thank you to @jeetiss and all the amazing contributors for helping with the development.

wouter's People

Contributors

arnaudbarre avatar cbbfcd avatar cedeber avatar davidje13 avatar dependabot[bot] avatar developit avatar devrnt avatar fgnass avatar graphman65 avatar gravitytwog avatar hansbrende avatar helkyle avatar itsmapleleaf avatar jacobbuck avatar jeetiss avatar jonahplusplus avatar jvdsande avatar mjfwebb avatar molefrog avatar o-alexandrov avatar omgovich avatar rojvv avatar shannonrothe avatar silverwind avatar timmak avatar tomas2d avatar trymoto avatar ty3uk avatar yaroslavbkh avatar ziinc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wouter's Issues

Support for commonjs output - wouter-preact blocks ts-jest on import statements

Hello! :)
I've just added wouter-preact to my preact + typescript boilerplate. I'm using ts-jest for tests with ts-jest preset. Unfortunately I cannot test app.tsx component which contains
import { Link, Route } from 'wouter-preact';

After running yarn test I've got:

FAIL src/app/app.spec.tsx
● Test suite failed to run

/home/mk/Documents/repos/preact-ts-parcel/node_modules/wouter-preact/index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import locationHook from "./use-location.js";
                                                                                                ^^^^^^^^^^^^

SyntaxError: Unexpected identifier

  1 | import { h } from 'preact';
> 2 | import { Link, Route } from 'wouter-preact';
    | ^
  3 | import { TypedComponent } from '../shared/typings/prop-types';
  4 | import { actions, StoreState } from '../store';
  5 | import { useAction, useSelector } from '@preact-hooks/unistore';

  at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
  at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)

I'm not saying that this is a problem with wouter-preact itself, because it may be e.g. ts-jest problem or maybe even I did something wrong in terms of configuration (see below) but could you add also commonjs output?

jest.config.js

module.exports = {
    preset: 'ts-jest',
    roots: ['<rootDir>/src'],
    verbose: true,
    transform: {
        '^.+\\.(t|j)sx?$': 'ts-jest',
    },
    testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
};

tsconfig.json:


{
    "compilerOptions": {
        "outDir": "./dist",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "strict": true,
        "moduleResolution": "node",
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "target": "es6",
        "jsx": "react",
        "jsxFactory": "h",
        "allowJs": true
    },
    "include": ["./src/**/*"],
    "exclude": ["node_modules", "**/*.spec.ts", "**/*.spec.tsx"]
}

I've been trying with different tsconfig.json configurations (also without "include" & || "exclude" fields) but without success.

Related issue (?): #70

Minor type tweaks to cover edge cases

Hey @StrayFromThePath @StrayFromThePath I'm super excited to have types finally in the project! I managed to spent some time reading the sources and although I'm not a TS user I've got some feedback I thought I need to share with you.

Proper Params type

The current Params type is a union of hash or null. This works fine for the Match type which can represent either [true, { ... }] or [false, null], however this also assumes that the render prop can contain the null as a first argument here. I think we can narrow the type even further, because the render prop always guarantees that the argument is a valid object.

So how can this be solved? I suggest we make the Params type to always be an object and mark that the match result could contain null:

type Params = { [paramName: string]: string }

// it this a real TypeScript code? :) I'm not sure, but you get the idea
type Match = [boolean, Params | null];

// or is this even possible:
type Match = [true, Params] | [false, null];

Remove match props from Route

This prop is an internal thing used by the switch, so it's not part of the public API. I suggest we either remove it or change the type, since it's not a boolean anymore, but Match. But I think it's easier to just remove this.

Proper type of children in Router

As @StrayFromThePath pointed out in #54, some components need proper types to support mixed React nodes. The current type of the children prop in Router is ReactElement | ReactElement[];, but this makes it impossible to use something like:

<Router>
  Hello, this is an <Application />
  We also have <Footer /> and some {1337} numbers here.
</Router>

Same applies to Link. Generally speaking, anything that React.Provider accepts should work here as well.

Let me know what you guys think and keep up with the good work!

Preact support

Since the library was created to be as small as possible it would make sense to add support for Preact. It should only support the version 10 of Preact, which isn't fully released yet, but supports hooks and fragments.

There are few interested problems that we will need to solve in order to release this. Here is how I see the final API:

// regular imports
import { Route, Switch, Link } from "wouter/preact";

// additional modules
import staticHistory from "wouter/preact/extra/static-history.js"

We can do that by simply creating a new folder preact in the repo with one file called react-deps.js with all react/preact exports such as useState, createElement and so on. The folder can be prepopulated with the sources from the root folder on prepublish npm script (might need to use this package https://www.npmjs.com/package/copyfiles for cross-platfom support). We shouldn't commit anything except for preact/react-deps.js however, the files should be gitginored!

The implementation plan:

  • Remove incompatible methods isValidElement and Children.map. Merge #27 and check that it doesn't break anything.
  • Create a project structure for Preact /preact/react-deps.js and /react-deps.js and write a prepublish script.
  • Test the implementation with Preact. Basic rendering, make sure Route, Link and Switch work. A 2-3 test cases should be enough. One thing that isn't clear yet is how to require right react-deps.js in tests without having sources in preact folder. Maybe Jest mocks?
  • Describe the Preact feature in the README.

ctrl + click to open link in new tab not working

For desktop browsers there are typical navigation patterns to open the link in a new tab instead of the current one. The most common one is ctrl + click or clicking with the middle mouse button. Currently this doesn't work because the event will always be prevented:

wouter/index.js

Lines 91 to 98 in c9c7900

const handleClick = useCallback(
event => {
event.preventDefault();
navigate(href);
onClick && onClick(event);
},
[href, onClick, navigate]
);

In preact-router we have this line which explicitly checks for common key combinations and lets the browser handle the event instead:

// ignore events the browser takes care of already:
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button!==0) return;

https://github.com/preactjs/preact-router/blob/3eb5b31fe75c34672eeb13f2d310dee6d51b1f97/src/index.js#L112

Intercept the location change

Hi,

I could not see on the docs how to intercept the location change from a component.

Let's say I have a dirty state at the current page and want to use a custom modal component to alert the user if they want to move forward and lose data.

Thanks

Wouter isn't compatible with TypeScript and Preact at the same time

Wouter can't be used with TypeScript and Preact at the same time.

If you install @types/wouter, that also installs @types/react, which conflicts with and breaks type definitions for preact.

It (probably) works if you use preact-compat, but at that point you're not really "compatible with preact" in my opinion.

A possible fix would be to push an alternative @types/wouter-preact that worked with Preact's type definitions.

CC @StrayFromThePath - Thought this would be relevant for you.

Allow passing state to link/setLocation

Would it be possible to support the following using the browser's history.state API, optionally allowing history[method](stateObj, 0, to), instead of always using history.[method](0, 0, to)?

e..g.

<Link href="/" state={{ exampleState: true }}>Link</Link>

and

const [location, setLocation] = useLocation();

setLocation("/", { exampleState: true });

Not sure where the accessor makes the most sense, maybe something like:

import { useRouterState } from 'wouter';

const routerState = useRouterState();

Thanks!

Getting active path

Hello, wouter seems great so far!

I would like to highlight the current page's link on my navbar, is there a better way than the following?

const [, params] = useRoute("/:page");
const currentPage = params ? `/${params.page}` : "/"; //params will be null on /

Router push with Switch

I made a <Switch> component like this:

import { useRouter } from 'wouter'

const Switch = ({ children }) => {
    const router = useRouter()
    const path = router.history.path()
    const childPaths = children.map(c => c.props.path)
    let matchedIndex
    childPaths.forEach((childPath, idx) => {
        const match = router.matcher(childPath, path)[0]
        if (isNaN(matchedIndex) && match) {
            matchedIndex = idx
        }
    })
    return children[matchedIndex]
}

export default Switch

This just checks which of the children's path matches first and renders that Route. This sorta works, but when I navigate using router.history.push() afterwards, I get blank screens and nothing renders until I refresh. Any ideas?

Uncaught RangeError: Maximum call stack size exceeded

I'm getting Uncaught RangeError: Maximum call stack size exceeded while using useLocation right from 'wouter' package

import { useLocation } from 'wouter';

this works fine

import useLocation from 'wouter/use-location';

Url Params decode

Hello,

Been having some issues with params values in the sense that with long text params the space symbols still stays in the props value extracted and i have to use decodeURIComponent.

Is there anyway props value could be formatted by default?

How to add an 404 route?

Hello. Nice routing library you got here. But I can't find a way to do a "default route" eg to create a 404 error route.

Can you shed some info on this?

Thanks.

Support onClick funtion on Link component

Hi @molefrog, nice plugin, i love the Hooks approace!

I have discovered that the Link component didn't accept a onClick prop to do something other then the normal behavior of navigate.

I have implemented the code to support this functionality locally, can i make a PR to merge with your package?

Thanks in advance!

Discussion about default routes

i wrote a demo:

<Switch>
    <Route path="/:anything*">Default Route: nothing found!</Route>
    <Route path="/users/one">Users One</Route>
    <Route path="/">First Route</Route>
     //...
    {/* <Route path="/:anything*">Default Route: nothing found!</Route> */}
</Switch>

If we accidentally put the default route in front, it will cause problems.

I think there are two ways:

  1. The problem is explained in the documentation, which draws the attention of developers⚠️
  2. If you use <Route default component={AnyComponent}/> and the position is irrelevant, is it a good idea?

@omgovich @molefrog Looking forward to your reply.

New tabs

Hello,

Is there any way to use the component to open a new tab at that location?

Thanks

Add coverage

My recommendation is to add coverage badge, example codecov.

The regex routes cause errors

If we will try to use path-to-regexp package it will break the router just because it uses another semantics. I have written a custom makeMatcher function to make it possible to use it. It is not perfect but at least doesn't break the system.

The getRegexp(pattern); function doesn't return tuple but just regex in the path-to-regexp package

Also, this workaround allows creating i18n routes like /:lang(en|es|pt)/posts/:id.

function makeMatcher(makeRegexpFn = pathToRegexp) {
  const cache: { [key: string]: RegExp } = {};

  // obtains a cached regexp version of the pattern
  const getRegexp = (pattern: string) =>
    (cache[pattern]) || (cache[pattern] = makeRegexpFn(pattern));

  return (pattern: Path, path: Path) => {
    const regexp = getRegexp(pattern);
    const found = pattern.match(/:(\w+)/ig);
    const keys = found ? found.map(el => el.substr(1)) : [];
    const out = regexp.exec(path);

    if (!out) return [false, null];

    // formats an object with matched params
    const params = keys.reduce((params, key, i) => {
      params[key] = out[i + 1];
      return params;
    }, {});

    return [true, params];
  };
}
...

<Router matcher={makeMatcher(pathToRegexp)}>

Support for search

In the latest version of wouter, when the location is somewebsite.com/users?name=john

const [location] = useLocation();

location only retrieves /users emitting ?name=john. And even though I push new query params with other methods, re-rendering is not triggered. But sometimes a view needs to depend on search params.

I think we have two options in a broad sense

  • add useSearch thing

    const [search, setSearch] = useSearch();
    search // "?name=john"

    or we can let wouter give searchParams as a URLSearchParams object, the parsed one.
    or how about useURL which gives current location as URL object?

  • let location be location.pathname + location.search.

    breaking change!

    In this case, we got a new problem to redefine routing rules. Should useRoute provide matching for search parameters? I don't think so. Routing rules can go the same just ignoring search.
    And the parsing for location.pathname + location.search is annoying. AFAIK, there is no builtin function to parse it. And even if there is, the code to parse would be repeated in many components.

404 on direct request to route

Routes only work with <Link> but when you go directly to a route it just throws 404. What's about adding support for direct requests like in React Router?

Auto cast numeric parameters as integers

Here's an idea:

Cast parameters as integers when it can, leave it otherwise.

Use case: I'm comparing match.id with object.id to apply a selected class and it would be great to not have to use == and not have to cast it myself.

Why: I don't see casting parameter types inside the scope of my application, but rather belonging to the routing mechanism.

How would I do: test each parameter value for a string of numbers and apply parseInt accordingly.

I can hack it together and send a PR if you like it. Also, let me know if think another approach would be best.

Cheers! 🍺

Use with React Native?

Has anyone used this library with React Native?

Any interesting experiences to share?

Package is not compatible with Create React App and it's testing setup

I see the published package contains import statements. This produce the following error in default create react app setup after running npm test

Test suite failed to run

    /home/igor/react-hooks-beer-example/node_modules/wouter/index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import locationHook from "./use-location.js";
                                                                                                    ^^^^^^^^^^^^

    SyntaxError: Unexpected identifier

Can we publish a transpiled version containing require statements so no babel transpilation would be required inside node_modules?

`Redirect` component is not documented

I just saw the Redirect component exported in the source code, but I couldn't find it documented anywhere, this is just a heads up but if I find time over the weekend I'll try to document it myself πŸ˜‰

Incorrect Link href with basepath

Hello, I'm trying to implement basepath into my application. I followed the README's instruction. Everything works well, except <Link /> component.

For example,

<Link href="/path">link</Link>

It's fine that clicking on it will navigate to :basepath/path, but the href prop is still /path.

Now I had to use <Link /> in this way,

<Link href="/path">
  <span><a href={`${basepath}/path`}>link</a></span>
</Link>

Any idea on it?

Support for basepath

Hello again πŸ™ŒπŸ»

I'm made demo on gh-pages and I can't specify base path for router (in my case it wouter-async-routes)

need this feature πŸ’πŸ»β€β™‚οΈ

Get down to 1KB. Reimplement a subset of path-to-regexp

Right now the gzipped library size is 2KB, 70% of which is path-to-regexp dependency. While path-to-regexp provides a well-known way for developer to describe routes, it seems like we could shrink it's functionality to cover the 99% of use cases.

It would be cool to reimplement a limited subset of the library, so that:

  1. We drop that dependency and make it a zero-dependency library 😎
  2. We reduce the size to be less than 1KB (current bundle size without a matcher is 686B).

For users who still want to use the path-to-regexp functionality and get an advanced syntax we could provide an optional entry point:

import { Router } from "wouter";
import makeMatcher from "wouter/matcher"; 
import pathToRegexp from "path-to-regexp";

<Router matchFn={makeMatcher(pathToRegexp)}>
  ...
</Router>

So ideally it would be nice to define a subset of features we would like to support. Here are my ideas:

  • Always case-insensitive
  • Ignores a slash at the end
  • Matches an entire path
  • Supports named segments /:foo/:bar
  • Supports modifiers: :foo?, :foo* and :foo+
  • No support for unnamed params. All params should have a name.
  • No segment constraints e.g. /:foo(\d+)/

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.