├── .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 | Zero To Shipped 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 | --------------------------------------------------------------------------------