├── .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 |
--------------------------------------------------------------------------------