Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify state handling #5

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# React Awesome Router

A simple, lightweight, middleware oriented router for react applications.
A simple, lightweight, middleware oriented router for React applications.

## Motivation

Comming from non-react world, routing throgh JSX components feels annoying to me. I don't like to spread the routing logic between different react components or write JSX components to extend router capabilities (like auth). I also missed other features I enjoy and was used to like [Angular guards](https://angular.io/api/router/CanActivate) and [Koa](https://github.com/koajs/koa) middleware based architecture.
Comming from non-React world, routing throgh JSX components feels annoying to me. I don't like to spread the routing logic between different react components or write JSX components to extend router capabilities (like auth). I also missed other features I enjoy and was used to like [Angular guards](https://angular.io/api/router/CanActivate) and [Koa](https://github.com/koajs/koa) middleware based architecture.

When starting with react hooks, I realized how simple it will be to write a react router with hooks, [history.js](https://github.com/ReactTraining/history) and [path-to-regexp](https://github.com/pillarjs/path-to-regexp); indeed I think the whole module is far below 200 lines of code. This module provides basic routing features to small applications, and allows more advanced features on bigger applications through the use of custom ad-hoc middlewares.
When starting with React hooks, I realized how simple it will be to write a React router with hooks, [history.js](https://github.com/ReactTraining/history) and [path-to-regexp](https://github.com/pillarjs/path-to-regexp); indeed I think the whole module is far below 200 lines of code. This module provides basic routing features to small applications, and allows more advanced features on bigger applications through the use of custom ad-hoc middlewares.

## Installation

Expand Down Expand Up @@ -163,7 +163,7 @@ const authGuard = (router, next) => {
};
```

## Running for developement
## Running for development

To run both the router module and the example together with live reloading, first clone the repository:

Expand Down
4 changes: 1 addition & 3 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import logo from './logo.svg';
import {Routes, useLocation} from 'react-awesome-router';

const App: React.FC = () => {
const {location, context, setLocation, setContext} = useLocation();
const {context, setLocation, setContext} = useLocation();

const login = () => {
setContext({auth: {logued: true, username: 'notadmin'}});
//Optional, force refresh on login
setLocation(location);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer necessary to do manual refresh, since we are now fully reactive to changes in context.

};

const logout = () => {
Expand Down
16 changes: 8 additions & 8 deletions example/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import { Router } from 'react-awesome-router';
import { createRoot } from 'react-dom/client';
import App from './App';
import './index.css';
import { routes } from './routes';
import * as serviceWorker from './serviceWorker';

import {Router} from 'react-awesome-router';
import {routes} from './routes';

ReactDOM.render(
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(
<Router routes={routes}>
<App />
</Router>,
document.getElementById('root')
</Router>
);

// If you want your app to work offline and load faster, you can change
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-awesome-router",
"version": "2.0.1",
"version": "2.0.2",
"description": "Lightweight middleware-based react router",
"repository": {
"type": "git",
Expand Down
134 changes: 39 additions & 95 deletions src/Router/index.tsx
Original file line number Diff line number Diff line change
@@ -1,123 +1,67 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { createBrowserHistory, History } from 'history';
import { RouterContext } from '../Context';

import { Route } from '../types';
import { Route, Router as IRouter } from '../types';
import Path from '../PathUtils';

export interface RouterState {
location: string;
params: Object;
routes: Array<Route>;
context: Object;
forceRefresh: number;
routedElement: React.ReactNode | undefined;
}

export interface IRouterProps {
routes: Array<Route>;
children: React.ReactNode
}

export const Router: React.FC<IRouterProps> = props => {
const history = useRef<History | undefined>(undefined);
const initialState: RouterState = {
location: '',
params: {},
context: {},
routes: props.routes,
routedElement: undefined,
forceRefresh: 0
};
const browserHistory = createBrowserHistory()
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now lives in a constant since the only reason for it being a ref was because it lived inside the component so it needed to mutate once. Now it's a singleton for the life of the module.


const [state, setState] = useState(initialState);
const getComponentFromRoute = (route: Route, router: IRouter) => {
let guardReturn: React.ReactNode = undefined;

const setLocation = (location: string) => {
if (state.location !== location) {
history.current?.push(location);
} else {
setState({
...state,
forceRefresh: state.forceRefresh + 1
});
if (route.guards) {
let nexted = false;
for (const guard of route.guards) {
guardReturn = guard(router, () => {
nexted = true
return undefined
})
if (!nexted && guardReturn) break
}
};

const setContext = (context: Object) => {
setState({
...state,
context: Object.assign(state.context, context)
});
};
}

useEffect(() => {
history.current = createBrowserHistory();
setState({
...state,
location: history.current.location.pathname
});

const unlisten = history.current.listen(update => {
setState({
...state,
location: update.location.pathname
});
});
return guardReturn ?? route.component
}

return () => {
unlisten();
};
}, []);
export const Router = ({ routes, children }: IRouterProps) => {
const [location, setLocation] = useState<string>(browserHistory.location.pathname)
const [context, setContext] = useState<object>({})
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I minimized the state necessary to run the router.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! With this every change in context will trigger the router logic. The only concern I have is if it could be useful to change the router context without triggering a reroute?


// Listen to location changes
useEffect(() => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effect is now the getComponentFromRoute method.

let route = state.routes.find(route => {
return Path.match(route.path, state.location);
browserHistory.listen(update => {
setLocation(update.location.pathname);
});
}, []);

if (route) {
let guardReturn: React.ReactNode = undefined;
let nexted = false;

if (route.guards) {
for (const guard of route.guards) {
guardReturn = guard({
location: state.location,
setLocation,
setContext,
context: state.context,
params: state.params,
}, () => {
nexted = true
return undefined
});
if (!nexted && guardReturn) break;
}
}

setState({
...state,
params: Path.parse(route.path, state.location),
routedElement: guardReturn ?? route.component
});

}

const route = routes.find(route => Path.match(route.path, location));

if (!route) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will help catch bugs in client applications.

console.error(`Current location ${location} did not match any defined route`)
return null;
}

}, [state.location, state.forceRefresh]);
const params = Path.parse(route.path, location)
const component = getComponentFromRoute(route, { location, setLocation, context, setContext, params })
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this and the route computation could be memoized but I think every re-render will need to recompute it anyway so I think it's safe to just roll with it.

On very heavy routings (hundreds of thousands of routes times hundreds of thousands of guards per route, this could be slow) but if the case ever comes up we could just optimize it.


return (
<RouterContext.Provider
value={{
location: state.location,
context: state.context,
setLocation: setLocation,
setContext: setContext,
params: state.params,
component: state.routedElement
params,
location,
setLocation,
context,
setContext,
component
}}
>
{props.children}
{children}
</RouterContext.Provider>
);
};
)
}