├── .gitignore
├── README.md
├── example
├── .npmignore
├── index.html
├── index.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
├── package.json
├── src
├── Router.tsx
├── index.ts
├── router-context.ts
├── types.ts
├── use-router.ts
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | .rts2_cache_cjs
6 | .rts2_cache_es
7 | .rts2_cache_umd
8 | .rts2_cache_esm
9 | dist
10 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## 🐁 react-tiniest-router
4 |
5 | For the times when you *really* need a simple router.
6 | Based on [mobx-router](https://github.com/kitze/mobx-router) and [rttr](https://github.com/kitze/rttr).
7 |
8 |
9 | ## Usage
10 |
11 | 1. Write the routes object.
12 |
13 | ```js
14 | const routes = {
15 | home: {
16 | id: 'home',
17 | path: '/',
18 | },
19 | about: {
20 | id: 'about',
21 | path: '/about',
22 | },
23 | gallery: {
24 | id: 'gallery',
25 | path: '/gallery/:imageId',
26 | },
27 | }
28 | ```
29 |
30 | 2. Wrap your app with the Router component
31 | ```js
32 |
33 |
34 |
35 | ```
36 |
37 |
38 | 3. Use the router using `useRouter`
39 |
40 | - Use the `goTo` function for navigating to a route
41 | - Use the `isRoute` function for checking if a route is currently active
42 |
43 |
44 | ```js
45 | const Root = () => {
46 | const {goTo, isRoute} = useRouter();
47 |
48 | return (
49 |
50 |
51 |
52 |
53 |
56 |
59 |
62 |
63 |
64 |
65 |
66 | {isRoute(routes.home) &&
Welcome home
}
67 | {isRoute(routes.about) &&
About us
}
68 | {isRoute(routes.gallery) &&
}
69 |
70 | );
71 | };
72 | ```
73 |
74 | 4. You also get `params`, `queryParams`, `routeId`, `path` in the router object.
75 |
76 | ```js
77 | const Gallery = () => {
78 | const { params } = useRouter();
79 | return Browsing picture {params.imageId}
;
80 | };
81 | ```
82 |
83 | ---
84 |
85 | ## FAQ
86 |
87 | - Does it support optional parameters in the path definition?
88 | Not yet, but it will as soon as I need them in a project.
89 |
90 | - Does it support SSR?
91 | No.
92 |
93 | - Will it ever support SSR?
94 | NO.
95 |
96 | - Does it have tests?
97 | TypeScript is poor man's tests.
98 |
99 | - Will it ever have tests?
100 | If you write them.
101 |
102 | - Does it support code splitting?
103 | Did you see which repo you're actually browsing?
104 | Does it say "facebook" in the url? No. So, no.
105 |
106 | - Does it support async routes?
107 | Please stop doing stupid stuff with your router.
108 |
109 | - Does it support protected routes?
110 | Please stop doing stupid stuff with your router.
111 |
112 | - I'm offended by this FAQ section, where can I complain?
113 | Yell @ me on [Twitter](https://twitter.com/thekitze)
114 |
115 | ### 🙋♂️ Made by [@thekitze](https://twitter.com/thekitze)
116 | - 🏫 [React Academy](https://reactacademy.io) - Interactive React and GraphQL workshops
117 | - 💌 [Twizzy](https://twizzy.app) - A standalone app for Twitter DM
118 | - 🤖 [JSUI](https://github.com/kitze/JSUI) - A powerful UI toolkit for managing JavaScript apps
119 | - 📹 [Vlog](https://youtube.com/kitze) - Watch my sad developer life
120 |
--------------------------------------------------------------------------------
/example/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import * as React from 'react';
3 | import * as ReactDOM from 'react-dom';
4 | import { useRouter, Router } from '../.';
5 |
6 | const routes = {
7 | home: {
8 | id: 'home',
9 | path: '/',
10 | },
11 | about: {
12 | id: 'about',
13 | path: '/about',
14 | },
15 | gallery: {
16 | id: 'gallery',
17 | path: '/gallery/:imageId',
18 | },
19 | };
20 |
21 | const App = () => (
22 |
23 |
24 |
25 | );
26 |
27 | const Gallery = () => {
28 | const { params } = useRouter();
29 | return Browsing picture {params.imageId}
;
30 | };
31 |
32 | const Root = () => {
33 | const {goTo, isRoute} = useRouter();
34 |
35 | return (
36 |
37 |
38 |
39 |
40 |
43 |
46 |
49 |
50 |
51 |
52 |
53 | {isRoute(routes.home) &&
Welcome home
}
54 | {isRoute(routes.about) &&
About us
}
55 | {isRoute(routes.gallery) &&
}
56 |
57 | );
58 | };
59 |
60 | ReactDOM.render(, document.getElementById('root'));
61 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "react-app-polyfill": "^1.0.0"
12 | },
13 | "alias": {
14 | "react": "../node_modules/react",
15 | "react-dom": "../node_modules/react-dom/profiling",
16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^16.8.15",
20 | "@types/react-dom": "^16.8.4",
21 | "parcel": "^1.12.3",
22 | "typescript": "^3.4.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "baseUrl": ".",
17 | "types": ["node"]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-tiniest-router",
3 | "version": "0.1.1",
4 | "main": "dist/index.js",
5 | "module": "dist/react-tiniest-router.esm.js",
6 | "typings": "dist/index.d.ts",
7 | "files": [
8 | "dist"
9 | ],
10 | "scripts": {
11 | "start": "tsdx watch",
12 | "build": "tsdx build",
13 | "test": "tsdx test --env=jsdom"
14 | },
15 | "peerDependencies": {
16 | "react": ">=16"
17 | },
18 | "husky": {
19 | "hooks": {
20 | "pre-commit": "pretty-quick --staged"
21 | }
22 | },
23 | "prettier": {
24 | "printWidth": 80,
25 | "semi": true,
26 | "singleQuote": true,
27 | "trailingComma": "es5"
28 | },
29 | "devDependencies": {
30 | "@types/jest": "^24.0.15",
31 | "@types/react": "^16.8.20",
32 | "@types/react-dom": "^16.8.4",
33 | "husky": "^2.4.1",
34 | "prettier": "^1.18.2",
35 | "pretty-quick": "^1.11.1",
36 | "react": "^16.8.6",
37 | "react-dom": "^16.8.6",
38 | "tsdx": "^0.7.1",
39 | "tslib": "^1.10.0",
40 | "typescript": "^3.5.2"
41 | },
42 | "dependencies": {
43 | "path-match": "^1.2.4",
44 | "query-string": "^6.7.0"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Router.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react';
2 | import { replaceUrlParams, createRouter } from './utils';
3 | import { mapObject } from './utils';
4 | import { RouterContext } from './router-context';
5 | import { RouterStateType, RouteType, RoutesType } from './types';
6 |
7 | export const Router: React.FC<{ routes: RoutesType }> = ({
8 | children,
9 | routes,
10 | }) => {
11 | const [state, setState] = useState({
12 | routeId: '',
13 | path: '/',
14 | params: {},
15 | queryParams: {},
16 | extra: {},
17 | options: {},
18 | });
19 |
20 | const router = useRef(null);
21 |
22 | const currentUrl: string = replaceUrlParams(
23 | state.path,
24 | state.params,
25 | state.queryParams
26 | );
27 |
28 | useEffect(() => {
29 | //create a router from the routes object
30 | router.current = createRouter(
31 | mapObject(routes, route => {
32 | return {
33 | key: route.path,
34 | value: params => {
35 | goTo(route, params);
36 | },
37 | };
38 | })
39 | );
40 |
41 | //initial location
42 | router.current(window.location.pathname);
43 |
44 | //on change route
45 | window.onpopstate = ev => {
46 | if (ev.type === 'popstate') {
47 | router.current(window.location.pathname);
48 | }
49 | };
50 | }, []);
51 |
52 | useEffect(() => {
53 | if (window.location.pathname !== currentUrl) {
54 | window.history.pushState(null, null, currentUrl);
55 | }
56 | }, [currentUrl]);
57 |
58 | const goTo = (
59 | route: RouteType,
60 | params = {},
61 | queryParams = {},
62 | extra = {}
63 | ) => {
64 | const { id, path, extra: routeExtra } = route;
65 | setState({
66 | ...state,
67 | routeId: id,
68 | path,
69 | params,
70 | queryParams,
71 | extra: { ...routeExtra, ...extra },
72 | });
73 | };
74 |
75 | const isRoute = route => route.id === state.routeId;
76 |
77 | return (
78 |
86 | {children}
87 |
88 | );
89 | };
90 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { Router } from './Router';
2 | export { useRouter } from './use-router';
3 | export { RouterContext } from './router-context';
4 |
--------------------------------------------------------------------------------
/src/router-context.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { RouterContextType } from './types';
3 |
4 | export const RouterContext = React.createContext(null);
5 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | type AnyObject = {[x: string]: any}
2 |
3 | export type RouteType = {
4 | id: string;
5 | path: string;
6 | extra?: object;
7 | };
8 |
9 | export interface RoutesType {
10 | [propertyName: string]: RouteType;
11 | }
12 |
13 | export type RouterStateType = {
14 | routeId: string;
15 | path: string;
16 | params: AnyObject;
17 | queryParams: AnyObject;
18 | extra?: AnyObject;
19 | options?: AnyObject;
20 | }
21 |
22 | export type RouterContextType = RouterStateType & {
23 | goTo: (route: RouteType, params?: AnyObject, queryParams?: AnyObject) => void;
24 | isRoute: (route: RouteType) => boolean;
25 | currentUrl: string;
26 | };
27 |
--------------------------------------------------------------------------------
/src/use-router.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { RouterContext } from './router-context';
3 |
4 | export const useRouter = () => useContext(RouterContext);
5 |
6 | export default useRouter;
7 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import queryString from 'query-string';
2 | import route from 'path-match';
3 |
4 | //regex
5 | export const paramRegex = /\/(:([^/?]*)\??)/g;
6 |
7 | //utils
8 | export const mapObject = (object, fn) => {
9 | return Object.keys(object).reduce((accum, objKey) => {
10 | const val = object[objKey];
11 | const {key, value} = fn(val, objKey);
12 | accum[key] = value;
13 | return accum;
14 | }, {});
15 | };
16 |
17 | export const getRegexMatches = (string, regexExpression, callback) => {
18 | let match;
19 | while ((match = regexExpression.exec(string)) !== null) {
20 | callback(match);
21 | }
22 | };
23 |
24 | export const replaceUrlParams = (path, params, queryParams = {}) => {
25 | const queryParamsString = queryString.stringify(queryParams).toString();
26 | const hasQueryParams = queryParamsString !== '';
27 | let newPath = path;
28 |
29 | getRegexMatches(
30 | path,
31 | paramRegex,
32 | ([_, paramKey, paramKeyWithoutColon]) => {
33 | const value = params[paramKeyWithoutColon];
34 | newPath = value
35 | ? newPath.replace(paramKey, value)
36 | : newPath.replace(`/${paramKey}`, '');
37 | }
38 | );
39 |
40 | return `${newPath}${hasQueryParams ? `?${queryParamsString}` : ''}`;
41 | };
42 |
43 |
44 | export const createRouter = routes => {
45 | const matchers = Object.keys(routes).map(path => [
46 | route()(path),
47 | routes[path]
48 | ]);
49 |
50 | return function(path) {
51 | return matchers.some(([matcher, fn]) => {
52 | const result = matcher(path);
53 | if (result === false) return false;
54 | fn(result);
55 | return true;
56 | });
57 | };
58 | };
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types"],
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "./",
11 | "strict": false,
12 | "noImplicitAny": false,
13 | "strictNullChecks": false,
14 | "strictFunctionTypes": true,
15 | "strictPropertyInitialization": false,
16 | "noImplicitThis": true,
17 | "alwaysStrict": false,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noImplicitReturns": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "moduleResolution": "node",
23 | "baseUrl": "src",
24 | "paths": {
25 | "*": ["src/*", "node_modules/*"]
26 | },
27 | "jsx": "react",
28 | "esModuleInterop": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------