├── .babelrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json └── src ├── dom.js ├── index.js └── react.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env", { "useBuiltIns": false }], "@babel/react"], 3 | "plugins": [ 4 | [ 5 | "@babel/transform-runtime", 6 | { 7 | "regenerator": true 8 | } 9 | ] 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | .cache/ 3 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Effector Routing 2 | 3 | > ### **DEPRECATED: Consider using [atomic-router](https://atomic-router.github.io) instead** 4 | 5 | Simple abstact router on top of Effector. Also has React bindings and DOM adapter 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i effector-routing 11 | ``` 12 | 13 | ## Usage 14 | 15 | ### Init router 16 | 17 | ```js 18 | import { addRoutes } from "effector-routing" 19 | import { initDomRouter } from "effector-routing/dist/dom" 20 | 21 | const routes = { 22 | home: { 23 | view: HomePage, 24 | meta: { 25 | path: "/" 26 | } 27 | }, 28 | posts: { 29 | view: PostsList, 30 | meta: { 31 | path: "/posts" 32 | } 33 | }, 34 | singlePost: { 35 | view: SinglePost, 36 | meta: { 37 | path: "/posts/:id" 38 | } 39 | } 40 | } 41 | 42 | addRoutes(routes) 43 | initDomRouter({ 44 | defaultRoute: { name: "home" } 45 | }) 46 | ``` 47 | 48 | ### Navigation, stores and events 49 | 50 | ```js 51 | import { 52 | $route, 53 | goTo, 54 | historyBack, 55 | beforeRouteEnter, 56 | routeChanged, 57 | onRouteChanged 58 | } from "effector-routing" 59 | 60 | // Imperative navigation 61 | // Both functions are Effects 62 | goTo({ name: "posts" }) 63 | goTo({ name: "singlePost", params: { id: 1 } }) 64 | historyBack() 65 | 66 | // $route is a Store which contains current { name, params } 67 | // So you can .map, .watch, combine etc 68 | const $postId = $route.map(({ name, params }) => 69 | name === "singlePost" ? params.id : null 70 | ) 71 | 72 | // Add a middleware 73 | beforeRouteEnter(({ name, params }) => { 74 | // Navigate "as is" 75 | if (name !== "singlePost") { 76 | return 77 | } 78 | 79 | // Change route 80 | if (params.id === 2) { 81 | return { 82 | name: "singlePost", 83 | params: { id: 1 } 84 | } 85 | } 86 | 87 | // Undo navigation 88 | if (params.id === 3) { 89 | return false 90 | } 91 | }) 92 | 93 | // Call something on route change 94 | onRouteChanged(({ name, params }) => { 95 | console.log({ name, params }) 96 | }) 97 | 98 | // Also available as an Event 99 | routeChanged.watch(({ name, params }) => { 100 | console.log({ name, params }) 101 | }) 102 | ``` 103 | 104 | ### Use with React 105 | 106 | #### Components 107 | 108 | ```js 109 | import React from 'react' 110 | import { RouteLink, RouterView } from "effector-routing/dist/react" 111 | 112 | const Menu = () => { 113 | return 118 | } 119 | 120 | const App = () => { 121 | return ( 122 |
123 | 124 | 125 |
126 | ) 127 | } 128 | ``` 129 | 130 | #### Example with useStore 131 | 132 | ```js 133 | import React from "react" 134 | import { combine } from "effector" 135 | import { useStore } from "effector-react" 136 | 137 | import { $route } from "effector-routing" 138 | import { $postsList } from "./stores" 139 | 140 | const $postId = $route.map(({ params }) => params.id) 141 | 142 | const $currentPost = combine($postId, $postsList, (id, list) => 143 | list.find(post => post.id === id) 144 | ) 145 | 146 | const Post = () => { 147 | const currentPost = useStore($currentPost) 148 | 149 | return ( 150 |
151 |

{currentPost.title}

152 |

{currentPost.description}

153 |
154 | ) 155 | } 156 | ``` 157 | 158 | ### Writing your own adapter 159 | 160 | If you want to write your own adapter 161 | Here's an example of adapter which stores last route in LocalStorage (e.g. for Electron) 162 | 163 | ```js 164 | import { initFirstRoute, onRouteChanged } from "effector-routing" 165 | 166 | export const initLsRouter = ({ defaultRoute }) => { 167 | let lastRoute = defaultRoute 168 | try { 169 | lastRoute = JSON.parse(localStorage.lastRoute) 170 | } catch (err) { 171 | lastRoute = defaultRoute 172 | } 173 | 174 | initFirstRoute(defaultRoute) 175 | onRouteChanged(newRoute => { 176 | localStorage.lastRoute = JSON.stringify({ 177 | name: newRoute.name, 178 | params: newRoute.params 179 | }) 180 | }) 181 | } 182 | ``` 183 | 184 | And just use it then 185 | 186 | ```js 187 | import { addRoutes } from "effector-routing" 188 | import { initLsRouter } from "./adapter" 189 | 190 | const routes = { 191 | /* ... */ 192 | } 193 | 194 | addRoutes(routes) 195 | initLsRouter() 196 | ``` 197 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "effector-routing", 3 | "version": "0.1.4", 4 | "main": "dist/index.js", 5 | "module": "index.js", 6 | "author": { 7 | "name": "Anton Kosykh", 8 | "email": "kelin2025@yandex.ru" 9 | }, 10 | "description": "Simple effector router", 11 | "dependencies": { 12 | "@babel/cli": "^7.4.4", 13 | "@babel/core": "^7.4.5", 14 | "@babel/plugin-transform-runtime": "^7.5.0", 15 | "@babel/preset-env": "^7.4.5", 16 | "@babel/preset-react": "^7.0.0", 17 | "effector": "^19.1.0", 18 | "effector-react": "^19.1.2", 19 | "path-to-regexp": "^3.0.0" 20 | }, 21 | "scripts": { 22 | "build": "babel src --out-dir dist", 23 | "dev": "babel --watch src --out-dir dist" 24 | }, 25 | "devDependencies": { 26 | "parcel": "^1.12.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/dom.js: -------------------------------------------------------------------------------- 1 | import { initFirstRoute, onRouteChanged } from "." 2 | 3 | export const initDomRouter = ({ defaultRoute }) => { 4 | let route = defaultRoute 5 | 6 | for (const [name, route] of Object.entries(routes)) { 7 | if (pathToRegexp(route.meta.path).exec(location.pathname)) { 8 | route = { name, params: history.state && history.state.params } 9 | break 10 | } 11 | } 12 | 13 | initFirstRoute(route) 14 | 15 | onRouteChanged(newRoute => { 16 | history.pushState( 17 | { name: newRoute.name, params: newRoute.params }, 18 | null, 19 | pathToRegexp.compile(newRoute.routeInfo.meta.path)(newRoute.params) 20 | ) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | createStore, 3 | createEvent, 4 | createStoreObject, 5 | combine, 6 | createEffect 7 | } from "effector" 8 | 9 | export const $name = createStore(null) 10 | export const $params = createStore({}) 11 | export const $routes = createStore({}) 12 | export const $middlewares = createStore([]) 13 | export const $history = createStore([]) 14 | 15 | export const addRoutes = createEvent("Add routes") 16 | export const addMiddleware = createEvent("Add middleware") 17 | export const goTo = createEffect("Go to") 18 | export const initFirstRoute = createEvent("Init first route") 19 | export const historyBack = createEffect("History back") 20 | 21 | export const $routeInfo = combine( 22 | $name, 23 | $routes, 24 | (name, routes) => routes[name] 25 | ) 26 | export const $routeMeta = $routeInfo.map(route => (route && route.meta) || {}) 27 | 28 | export const $historyItem = createStoreObject({ 29 | name: $name, 30 | params: $params 31 | }) 32 | 33 | export const $route = createStoreObject({ 34 | name: $name, 35 | params: $params, 36 | routeInfo: $routeInfo 37 | }) 38 | 39 | export const beforeRouteEnter = addMiddleware 40 | export const routeChanged = goTo.done.map(({ result }) => result) 41 | export const onRouteChanged = routeChanged.watch 42 | 43 | $name 44 | .on(goTo.done, (state, { result }) => result.name) 45 | .on(historyBack.done.map(p => p.result), (state, route) => route.name) 46 | 47 | $params 48 | .on(goTo.done, (state, { result }) => result.params || {}) 49 | .on(historyBack.done.map(p => p.result), (state, route) => route.params) 50 | 51 | $history 52 | .on($historyItem, (state, item) => [...state, item]) 53 | .on(historyBack.done, state => state.slice(0, -1)) 54 | 55 | $routes.on(addRoutes, (state, routes) => ({ ...state, ...routes })) 56 | 57 | $middlewares.on(addMiddleware, (state, middleware) => [...state, middleware]) 58 | 59 | const navigate = async ({ name, params = {} }) => { 60 | let res = { name, params } 61 | 62 | for (const middleware of $middlewares.getState()) { 63 | const nextRes = await middleware({ name, params }) 64 | 65 | if (nextRes === false) { 66 | return Promise.reject("REJECTED") 67 | } 68 | if (typeof nextRes === "object") { 69 | res = { name: nextRes.name, params: nextRes.params || {} } 70 | } 71 | } 72 | 73 | const routeInfo = $routes.getState()[name] 74 | 75 | if (!routeInfo) { 76 | return Promise.reject("NOT_FOUND") 77 | } 78 | 79 | if (routeInfo.redirect) { 80 | return navigate({ 81 | name: routeInfo.redirect.name, 82 | params: routeInfo.redirect.params 83 | }) 84 | } 85 | 86 | return { 87 | name: res.name, 88 | params: res.params, 89 | routeInfo: routeInfo 90 | } 91 | } 92 | 93 | goTo.use(navigate) 94 | 95 | initFirstRoute.watch(goTo) 96 | 97 | historyBack.use(() => { 98 | if (!$history.getState().length) { 99 | return Promise.reject("EMPTY") 100 | } 101 | return $history.getState().slice(-1)[0] 102 | }) 103 | -------------------------------------------------------------------------------- /src/react.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { equals } from "ramda" 3 | import { useStore } from "effector-react" 4 | 5 | import { $route, goTo, $routes } from "." 6 | 7 | export const useRoute = () => { 8 | return useStore($route) 9 | } 10 | 11 | export const RouterView = () => { 12 | const route = useRoute() 13 | if (!route || !route.routeInfo) { 14 | return null 15 | } 16 | const View = route.routeInfo.view 17 | 18 | return View && 19 | } 20 | 21 | export const RouteLink = ({ 22 | name, 23 | params = {}, 24 | children, 25 | className = "", 26 | activeClassName = "active", 27 | onClick, 28 | ...props 29 | }) => { 30 | const currentRoute = useStore($route) 31 | const allRoutes = useStore($routes) 32 | const thisRoute = allRoutes[name] 33 | 34 | const isActive = React.useMemo(() => { 35 | if (!name) { 36 | return false 37 | } 38 | if (thisRoute && thisRoute.isActive) { 39 | return thisRoute.isActive(currentRoute) 40 | } 41 | return name === currentRoute.name && equals(params, currentRoute.params) 42 | }, [name, params, currentRoute, thisRoute]) 43 | 44 | const handleClick = React.useCallback( 45 | evt => { 46 | evt.preventDefault() 47 | if (onClick) { 48 | onClick() 49 | } 50 | if (name) { 51 | goTo({ 52 | name: name, 53 | params: params 54 | }) 55 | } 56 | }, 57 | [name, params] 58 | ) 59 | 60 | const realClassName = `${className}${isActive ? ` ${activeClassName}` : ""}` 61 | 62 | return ( 63 | 64 | {children} 65 | 66 | ) 67 | } 68 | --------------------------------------------------------------------------------