Code Monkey home page Code Monkey logo

hookrouter's Introduction

React Hook Router

The modern alternative to react-router.

Tested from React 16.8.1 upwards.

How to install

Well, this is straightforward:

npm i hookrouter

Typescript

This project is not and will not be written in typescript.

Thanks to the github user @mcaneris, you can install types via:

npm i @types/hookrouter

I did not check if those types are correct nor will I keep them up to date with future releases.

Documentation

Detailed documentation about how to use hookrouter can be found here

A quick example

import {useRoutes} from 'hookrouter';

const routes = {
    '/': () => <HomePage />,
    '/about': () => <AboutPage />,
    '/products': () => <ProductOverview />,
    '/products/:id': ({id}) => <ProductDetails id={id} />
};
	
const MyApp = () => {
    const routeResult = useRoutes(routes);
    
    return routeResult || <NotFoundPage />;
}

Routes are defined as an object. Keys are the routes, which are matched against the URL, the values need to be functions that are called when a route matches. You may define placeholders in your routes with :something which will be forwarded as props to your function calls so you can distribute them to your components.

The hook will return whatever the route function returned, so you may also return strings, arrays, React fragments, null - whatever you like.

hookrouter's People

Contributors

csantos42 avatar dy avatar gudleik avatar guotie avatar philip-peterson avatar pickypg avatar tugceakin avatar xuo avatar yuanchenxi95 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

hookrouter's Issues

Repeated unknown route does not trigger changes

While writing up a standard 404 page, I noticed an interesting behavior where if I navigate to a 404 page, it works as you would expect. However, if you try to navigate in-app from there to another page that also triggers the 404 route, then nothing is triggered.

This causes confusing state, especially if you use window.location.pathname in the 404 component.

Fortunately this is not a very realistic scenario in most webapps, but it's very easy to notice while building out a new one.

usePath alongside useRoutes

This is an example of my router code, which is pretty basic:

  // If no route is found, always send them to UnknownPage
  return useRoutes(routes) || <UnknownPage />;

The routes object is currently rather trivial. The problem occurs with the new usePath hook used within the UnknownPage component.

Within the render method of that component I fetch and display the path:

// UnknownPage
render() {
  const path = usePath();

  // rendered a little prettier, but logically:
  return (
    <>404 Unknown page: {path}</>
  );
}

This triggers this error:

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

Of interest, if I move the usePath hook usage outside of the useRoutes code path, then everything is happy, but it also means every sub-component is getting rerendered unnecessarily.

Proposal for Redirect component

Nice library. I have migrated to this from react-router. One issue I encountered is there is no analogy of react-router Redirect component. This component replaces the current item in history by the provided one and navigates the page to the provided path.

  • I have created one myself like the following below, but I am not sure if it correct way of doing this. Could you please comment?
  • I think this could be included to your library. What do you think?
import React from 'react';
import { navigate } from 'hookrouter';

export const Redirect = (props: { href: string }) => {
    navigate(props.href, true);
    return <></>;
};

Navigate back

Is there a way to navigate back one place in the browser's history?

Is there any way to detect which route has matched?

I have conditional rendering case like

props => {
let match = useRouter({
'/sign-in': () => <SignInScreen/>,
'/dashboard': () => <DashboardScreen/>
})

return <main>
{isSignInScreen && <NavBar/>}
{match || <>Not Fount</>}
</main>
}

What's the best way to detect isSignInScreen flag?

Create git tags for releases

This is more of a suggestion, but it would be helpful if you created git tags to mark the npm release versions of code that you release in the future.

Understanding passing additional data to route functions

Picking up where convo started to get off topic in #27

I tried/failed to understand this pattern. I think what's happening is the route triggers a function which returns another function which then returns a component with new props. If that's correct, I think I got lost by not understanding where the variable 'product' was coming from.

Anyway, I think I've solved my original issue (routeResult rendered child component not updating) by adding the menu state as a new route. So now it's /product/menu when the menu is open and closing it triggers plain old /product. The downside is some potentially counter-intuitive things with the browser's back button, but passing true as second argument to navigate() I think has smoothed that out.

Feel free to just close this issue if there's nothing more specific to hook router to say on the matter :)

[Feature]: Wildcard Redirect

How difficult would it be to implement a wildcard redirect?

So instead of having to write 6 redirects, it would be similar to this:

function Component() {
  useRedirect('*', '/login')
  //...
}

Some problems I can think of is that this might overwrite routing that is already mapped, so some route checking will be required.

CodeSandbox Example?

It would be great to see the example highlighted in your blog post and documentation in a CodeSandbox. I would be happy to contribute and create one if you don't have time!

Feature: Use IDs for paths and navigation

In one router there were feature to set each route an ID, so later you can use <A to="some-id">Go to ID</A>, So, if topology of routes changed, you no need to change links all over the code.
Every time I started new project, thinking about this great feature. What do you think?

Make routes object updateable

Right now, useRoutes() consumes a route object on first call, then ignores subsequently passed objects for better caching.

There should at least be a shallow comparsion if a new routes object has been passed since available routes may change with application state.

Update docs

The docs need to mention that route result functions need to be pure.

The docs also need to advise that route objects should be defined outside the component who uses the route hook so route objects won't get destroyed and renewed on every re-render.

Add getWorkingPath to exports

I have a case where I need to know the route I'm on from within a component down the tree. I see that getWorkingPath is exported in the router but not in the exports from hookrouter. Could we add this to the exports?

Support for URL hash?

Is there a way to support URL hash change instead of route change like React Router? I have a need to use hash instead of path for routing because I'm building an electron application which is served at /index.html and isn't served by a web server, so route change doesn't work properly. Is this possible to support as a configuration?

Feature request - SSR

The library is really cool. It doesn't work with server side rendering. Digging through the source code, I see that there are a lot fo window api's being called. I'm not sure how I would get this to work on SSR.

Documentation

I need to create proper documentation - at best using a tool like docusaurus.

Feature: Relative navigation

There is a long-standing discussion in react-router about this. I haven't read a whole thing, it's back and forth in there. So I am wondering if some thinking out of the box can be applied to the hooks solution.

The basic idea is that you can call eg. navigate('../') to simply move higher in a hierarchy relative to a current path.

Relative routes are a different story and I wouldn't worry about that right now.

Provide metadata for SSR when redirecting

use cases:

  • Differentiate between temporary 302 (e.g. after completing a form or changing the language) and permanent 301 redirects (paths that no longer exist)
  • Be able to tell the SSR to return 404 but still render content, not redirecting on the client side

Securing routes

This is more of a question but what I want to secure a route? F.ex check if the user has a certain claim or something.

Would the following be a good approach:

import React, { useState, useEffect } from 'react';
import {useRoutes, A, navigate } from 'hookrouter';

const HomePage = () => {
  return (
    <div>
      HomePage
    </div>
  );
};

const AboutPage = () => {
  return <div>About</div>
}

const NotFoundPage = () => {
  return <div>Not found</div>
}

const UnAuthorized = () => {
  return <div>Unauthorized</div>
}

const Secure = ({children}) => {
  const isAuthorized = false; // imagine checking an Auth token.
  useEffect(() => {
    if(!isAuthorized){
      navigate(`/unauthorized`)
    }
  });

  return children
}

const routes = {
    '/': () => <HomePage />,
    '/about': () => <Secure><AboutPage /></Secure>,
    '/unauthorized': () => <UnAuthorized />
  };

const App = () => {
  const routeResult = useRoutes(routes);
  
  return {routeResult || <NotFoundPage />
}

export default App;

Is this approach a good practise? Or is there something better out of the box for this?

History integration

Definitely digging the idea, awesome work! What about the ability to navigate back or even further in history? Or being able to block navigation to eg. show question about saving a form. I've actually made hook version of a Prompt component which turned out to be fairly easy.

function usePrompt(when, message) {
  const { history } = React.useContext(Router.__RouterContext);

  React.useEffect(() => {
    if (when && !context.staticContext) {
      return history.block(message)
    }
  }, [when, message])
}

Would you be interested in history integration of this project or is that out of the scope?

Feature: Basepath for non-root apps

It's a bit unrealistic to refactor a bigger application routing at once. It is partially related to a propose static route #15 for testing. It would make sense if we could set an path prefix to specify where is that routing table sitting right now. Something like this.

const routeResult = useRoutes({
  '/category': () => <MenuCategoryListPage />
}, '/settings/menu')

I made myself a simple function that adds that prefix to those routes. Only later I've realized it's not enough because navigate and useRedirect won't take that into account. It needs to be in the context to be really useful.

Take the Router Challenge

It's not healthy for the Router market to be so dominated by the React Router. I want to level the playing field so that your Hook Router gets a fair crack of the whip. That's why I've written the Router Challenge. It aims to do for Routers what TodoMVC did for UI frameworks by offering the same SPA built in React using different Routers. For it to be successful I need your help. Will you take the Router Challenge and implement the SPA using your Hook Router, please?

Controlled Interceptor inserting its interceptorFunction into the address bar on navigate

I'm trying to apply useControlledInterceptor to manage an unmount navigation on a generic component.

const [className, setClassName] = useState(classes.incoming);
const [nextPath, confirmNavigation] = useControlledInterceptor();

useEffect(() => {
	if (!nextPath) {
		return;
	}

	setClassName(classes.out);
	setTimeout(confirmNavigation, 200);
}, [nextPath]);

Outside of this component I have <A href="/">...</A>, and when I click this link (or perform any other behavior in the app that causes navigation) I get this url in my address bar http://localhost:3000/function%20(nextPath,%20currentPath)%20%7B%20%20%20%20%20%20setInterceptedPath(nextPath);%20%20%20%20%20%20return%20currentPath;%20%20%20%20%7D and my 404 page displays.

I assume that this is because the interceptors don't link in with the router context so it only works with navigation caused from within the same component, but I haven't dug into it too far yet.

Rerender onpopstate

Could you pls showcase example how to rerender screen when user navigates browser history? Would be really nice use-case coverage. For now browser history does not seem to affect hookrouter.

Provide href / onClick Property values as object

The current approach is to use A to do this directly.

import { A } from 'hookrouter';

export const Link = () => (
  <A href="/">Root link</A>
);

This works great until you have a framework that wants to be given href / onClick properties for navigation. It would be nice if the internals of A were exported separately so that they could be reused, something like:

export const getLinkProperties = (props) => {
  const onClick = (e) => {
    e.preventDefault();

    navigate(e.currentTarget.href);

    if (props.onClick) {
      props.onClick(e);
    }
  };

  const href = props.href.substr(0, 1) === '/'
               ? getBasepath() + props.href : props.href;

  return { href, onClick };
}

Then callers would be free to use it as:

<MyLink whatever="x" {...getLinkProperties({ href: '/' })}>
  My link's text
</MyLink>

Then A becomes a simple class wrapper:

const A = (props) => (
  <a {...props} {...getLinkProperties(props)} />
);

Feature: Replace instead of push

I see that navigate will always push, but sometimes it's required to navigate by replacing.

For example route /product/new, when the form is saved, I would like to replace history with eg. /product/15 and then navigate with a push to /products. In case the user goes back, he should go to the created product page and not to create a new one again.

To make this happen it's either about introducing navigateReplace as a separate function or perhaps wrapping the second argument of navigate to some options. Honestly, I don't think that query params are that common to have such a prominent place there.

navigate('path', { replace: true, queryParams: {...}})

[Feature Request] Regex route match

It would be lovely to have more power in the routing path. Mainly I'm thinking regex, to acheive effects similar to express-router.

Here's a few examples:

const routes = {
    '/': () => <HomePage />,
    '/ab+cd': () => <Alphabet />,
    '/ab(cd)?e': () => <ProductOverview path={$1} />,
    /a/: () => <ProductDetails id={$1} />
};

Confirm before navigating away from unsaved data

Just finishing integrating hookrouter into my current project. I like it a lot so far! Very easy to implement and reason around.

On a certain page my user is filling out a form, and I want to confirm before they can leave, to prevent loss of unsaved data.

I think it would be better if this was baked into the package somehow. I'd want something simple where you could set it for a whole page and it applies to children and parents. I also would prefer to be able to pass any function, in case window.confirm() isn't good enough.

Route state

Sometimes it's useful to have an additional state within the navigation intent, but it might be bulky and more structured than what URI is providing.

window.history.pushState(null, null, url);

Would be lovely if we could specify state object instead of having a fixed null there.

Then there is a matter of how to consume it. I think it can be the second argument to the route callback. Some contrived example follows.

const routes = {
    '/': () => <HomePage />,
    '/products/:id': ({id}, { type }) => <ProductDetails id={id} type={type}  />
};
	
const handleClick = () => {
    navigate('/products/12', { type: 'main' });
};

Optional parameter support

I have a component where you can set one id as active and navigate to it without a url parameter from the nav, but there is an optional url parameter that acts as an override.

As a workaround, when I was converting my application over from React Router, I made two routes, one with the param and one without, but it would be convenient to consolidate with an optional param.

For reference here's the before and after for that component.

// react-router (I made a custom HOC to clean up the API)
export const routes = [{
	path: '/',
	exact: true,
	component: List,
}, {
	path: '/create',
	component: Create,
}, {
	path: 'edit/:id?',
	component: Edit,
}, {
	path: '/view/:id?',
	render(props) {
		return <Edit readOnly {...props} />;
	},
}];
// hookrouter
const routes = {
	'/': () => <List />,
	'/create': () => <Create />,
	'/edit': () => <Edit />,
	'/edit/:id': ({ id }) => <Edit id={id} />,
	'/view': () => <Edit readOnly />,
	'/view/:id': ({ id }) => <Edit id={id} readOnly />,
};

This is definitely a nice to have, and as you can see from this example it's already cleaner using hookrouter, but if we could accommodate for optional parameters I could get rid of a couple extra lines of code.

Write Tests

I definitely need to create tests before this can be moved out of beta. I used it in a couple of customer projects right now and it seems to work fine, but tests are mandatory ๐Ÿ’ช

'routeResult' component not updating when props change

I've spoke about this before but still having an issue. Going to try a different approach to explaining it.

function ParentComponent () {
    const [value, setValue] = useState(1)
    const routeResult = useRoutes({
        '/': () => <ChildComponent value={value} setValue={setValue} />
    })

    return (
        <div>
            {routeResult}
        </div>
    )
}

and separately,

function ChildComponent (props) {

    function changeToTwo() {
        props.setValue(2)
    }

    return (
        <div onClick={changeToTwo}>
            {props.value}
        </div>
    )
}

In this simplified example, the value rendered on screen via ChildComponent does not update from 1 to 2, even though the ParentComponent is aware of the change. Without hook router in the picture, the child component updates because its props do. However with hook router, it does not. Is this intentional or a bug?

Typescript

@Paratron I assume you are not TypeScript user, but many TS users would appreciate having types available.

The best course of action is, of course, have the source in TypeScript. It can compile to clean JS a similar way as Babel can. Or you can even use Babel to compile and TypeScript to extract typings only.

Otherwise, it means to manually synchronize types for every API change.

I can help with either way.

Allow setting query parsing lib

Current implementation of query parsing is verry limited and does not suport query sorting or even arrays. I would like to use query-string for query parsing.

customPath should not be a singleton

By making the customPath be a singleton in module scope, SSR can not be asynchronous and detecting redirects is harder.

I think it would be prudent to add the path to the Context and inject it at the top of the tree when doing SSR. That way each tree has its own private path and keeping track of redirects is easy

navigate() ignores query params

According to the documentation:

// These are the same
navigate('/test?a=hello&b=world');
navigate('/test', false, {a: 'hello', b: 'world'});

However the above is not true. First example navigates to '/test' and the second example navigates to '/test?a=hello&b=world'

Feature: Generated navigation functions

Just an idea, needs some thinking through. What I've always missed with react-router is some DRYness in route paths and navigation. We have various paths floating all around and it's easy to miss one in case of some change because it's just a string.

For example, when I specify /products/:id in routes, it would be great to somehow get a function bound to such route. Instead of repeating that string, I would like to call something like navigateProducts({ id: 11 }).

Perhaps it's more of a job for some code generator tool than a runtime solution. Mainly because it would be lovely to have types for such calls (sorry @Paratron).

Using object-hash to create internal id

hookrouter/src/router.js

Lines 204 to 209 in c1871c3

export const useRoutes = (routeObj) => {
// Each router gets an internal id to look them up again.
const [[routerId], setUpdate] = React.useState(['rtr_' + Math.random().toString().split('.')[1], 0]);
// Needed to create nested routers which use only a subset of the URL.
const parentRouterId = React.useContext(ParentContext);

at useRoute(routeObj) internal id created by random function.
I think it is good to use https://github.com/puleos/object-hash on routeObj.
This will guarantees the uniqueness of the id of each route object.

Extend interceptors

Should be chained when multiple interceptors are registered. The evaluation order is LIFO. If an interceptor in the chain returns currentPath the chain evaluation will stop.

Interceptor functions need a third argument: the parent match path in a nested scenario.

queryParams does not work for SSR

queryParams uses a singleton to manage listeners, so this will easily leak if not unlistened, and it will cause query params of two different in-progress SSR runs to be mixed up

(also, the URLSearchParams it uses is not supported in IE at all, probably worth documenting that it needs a polyfill there)

Feature: Static route for testing purposes

In a couple of automated tests, we need to set the current route so it renders what's expected based on the current route.

I suppose it's as easy as exposing a function that modifies closest context value. It could be tricky in regards to nested routing though because tests don't include the whole App, but only a portion of it.

React Native Support?

I am asking because I don't see any open/closed tickets for this, and I suspect other people would want to know too.

Is Native planned to be supported? How hard would it be to support Native?

Minor - router.js:257 Uncaught wth

There is one routing path in my application that hits the throw 'wth'; line that runs when stackEntry is falsy. Outside of my routing logic I render a navigation menu with <A href="/" />. There's only one page where clicking that link causes it to throw this error. It doesn't break anything, but I thought I'd mention it since it's kind of an interesting case. Also because it's just a beautiful error.

When it happens, I see

> JSON.stringify(stack, null, 2)
"{
  "1": {
    "routerId": 1,
    "originalRouteObj": {},
    "routes": [
      [
        "/",
        null
      ],
      [
        "/campaigns*",
        null
      ],
      [
        "/utilities*",
        null
      ],
      [
        "/characters*",
        null
      ]
    ],
    "parentRouterId": null,
    "matchedRoute": "/",
    "reducedPath": "",
    "passContext": false,
    "result": {
      "key": null,
      "ref": null,
      "props": {},
      "_owner": null,
      "_store": {}
    }
  }
}"
> parentRouterId
8

Proposed improvements for setting query params

  1. Ideally, setQueryParams would merge rather than replace existing query params:

Current:
start URL: /?animal=dog
action: setQueryParams({ name: 'fido' })
end URL: /?name=fido

Proposed:
start URL: /?animal=dog
action: setQueryParams({ name: 'fido' })
end URL: /?animal=dog&name=fido

  1. An empty query object might trim the ? from the URL:

Current:
start URL: /
action: setQueryParams({})
end URL: /?

Proposed:
start URL: /
action: setQueryParams({})
end URL: /

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.