Skip to content

Commit d1ed2da

Browse files
authored
Merge pull request #7 from atomic-router/feat/create-route-view
feat: Upgraded `createRouteView` & `createRoutesView`
2 parents 8417266 + 299d7e1 commit d1ed2da

File tree

5 files changed

+169
-72
lines changed

5 files changed

+169
-72
lines changed

README.md

Lines changed: 94 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,17 @@ npm i effector effector-react react
2020

2121
## Usage
2222

23-
### `RouterProvider` - provides router instance
23+
### Component API
24+
25+
#### `RouterProvider` - provides router instance
2426

2527
Wrap your app into this:
2628

2729
```tsx
28-
import { createHistoryRouter } from 'atomic-router'
29-
import { RouterProvider, Route } from 'atomic-router-react'
30+
import { createHistoryRouter } from "atomic-router";
31+
import { RouterProvider, Route } from "atomic-router-react";
3032

31-
import { HomePage } from './routes'
33+
import { HomePage } from "./routes";
3234

3335
const router = createHistoryRouter({ routes });
3436

@@ -41,15 +43,15 @@ const App = () => {
4143
};
4244
```
4345

44-
### `Link` - render a link
46+
#### `Link` - render a link
4547

4648
Simple usage:
4749

4850
```tsx
4951
import { createRoute } from 'atomic-router'
5052
import { Link } from 'atomic-router-react'
5153

52-
const homeRoute = createRoute<{postId:string}>()
54+
const homeRoute = createRoute()
5355
const postRoute = createRoute<{postId:string}>()
5456

5557
<Link to={homeRoute}>Route link</Link>
@@ -60,23 +62,102 @@ const postRoute = createRoute<{postId:string}>()
6062
All params:
6163

6264
```tsx
63-
import { Link } from 'atomic-router-react'
65+
import { Link } from "atomic-router-react";
6466

6567
<Link
6668
to={route}
67-
params={{ foo: 'bar' }}
68-
query={{ bar: 'baz' }}
69+
params={{ foo: "bar" }}
70+
query={{ bar: "baz" }}
6971
activeClassName="font-semibold text-red-400"
7072
inactiveClassName="opacity-80"
71-
/>
73+
/>;
74+
```
75+
76+
#### `Route` - render route
77+
78+
```tsx
79+
import { Route } from "atomic-router-react";
80+
81+
<Route route={homeRoute} view={HomePage} />;
82+
```
83+
84+
### Declarative API
85+
86+
87+
88+
#### `createRouteView` - render view if route is opened
89+
90+
```tsx
91+
import { createRoute } from "atomic-router";
92+
import { createRouteView } from "atomic-router-react";
93+
import { restore, createEffect } from "effector";
94+
95+
const homeRoute = createRoute();
96+
97+
const getPostsFx = createEffect(/* ... */);
98+
99+
const $posts = restore(getPostsFx, []);
100+
101+
const postsLoadedRoute = chainRoute({
102+
route,
103+
beforeOpen: getPostsFx,
104+
});
105+
106+
const PostsList = createRouteView({
107+
route: postsLoadedRoute,
108+
view: () => {
109+
const posts = useStore($posts);
110+
111+
return; /* ... */
112+
},
113+
otherwise: () => {
114+
return <div>Loading...</div>;
115+
},
116+
});
117+
```
118+
119+
You can also set only a part of `createRouteView` config on create and pass the rest of it via props.
120+
121+
#### `createRoutesView` - render routes
122+
123+
```tsx
124+
import { createRouteView, RouterProvider } from "atomic-router-react";
125+
126+
// { route: RouteInstance<...>, view: FC<...> }
127+
import * as Home from "@/pages/home";
128+
import * as Post from "@/pages/post";
129+
130+
import { router } from "@/app/routing";
131+
132+
const RoutesView = createRoutesView({
133+
routes: [
134+
{ route: Home.route, view: Home.Page },
135+
{ route: Post.route, view: Post.Page },
136+
],
137+
otherwise: () => {
138+
return <div>Page not found!</div>;
139+
},
140+
});
141+
142+
const App = () => {
143+
return (
144+
<RouterProvider router={router}>
145+
<RoutesView />
146+
</RouterProvider>
147+
);
148+
};
72149
```
73150

74-
### `Route` - render route
151+
Like in `createRouteView`, you can set only a part of `createRoutesView` config on create and pass the rest of it via props:
75152

76153
```tsx
77-
import { Route } from 'atomic-router-react'
154+
// Set specific otherwise view
155+
const RoutesView = createRoutesView({
156+
otherwise: SpecificNotFound,
157+
});
78158

79-
<Route route={homeRoute} view={HomePage} />
159+
// Pass the routes as a prop
160+
<RoutesView routes={routes} />;
80161
```
81162

82163
### `useLink` — resolve path from route

src/create-route-view.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
11
import React from "react";
2-
import { combine } from "effector";
3-
import { useUnit } from "effector-react";
42
import { RouteInstance } from "atomic-router";
53

6-
export const createRouteView = <Props,>(
7-
route: RouteInstance<any> | RouteInstance<any>[],
8-
View: React.FC<Props>
9-
) => {
10-
const $isOpened = Array.isArray(route)
11-
? combine(combine(route.map((r) => r.$isOpened)), (isOpened) => isOpened.includes(true))
12-
: route.$isOpened;
4+
import { useIsOpened } from "./use-is-opened";
5+
6+
export type RouteViewConfig<Props, Params> = {
7+
route: RouteInstance<Params> | RouteInstance<Params>[];
8+
view: React.FC<Props>;
9+
otherwise?: React.FC<Props>;
10+
};
1311

14-
function RouteView(props: Props) {
15-
const isOpened = useUnit($isOpened);
12+
export const createRouteView = <
13+
Props,
14+
Params,
15+
Config extends {
16+
[key in keyof RouteViewConfig<Props, Params>]?: RouteViewConfig<Props, Params>[key];
17+
}
18+
>(
19+
config: Config
20+
) => {
21+
return (props: Props & Omit<RouteViewConfig<Props, Params>, keyof Config>) => {
22+
const mergedConfig = { ...config, ...props } as RouteViewConfig<Props, Params>;
23+
const isOpened = useIsOpened(mergedConfig.route);
1624

1725
if (isOpened) {
26+
const View = mergedConfig.view;
27+
1828
return <View {...props} />;
1929
}
2030

21-
return null;
22-
}
31+
if (mergedConfig.otherwise) {
32+
const Otherwise = mergedConfig.otherwise;
2333

24-
return RouteView;
34+
return <Otherwise {...props} />;
35+
}
36+
37+
return null;
38+
};
2539
};

src/create-routes-view.tsx

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,40 @@
1-
import { combine } from "effector";
2-
import { useUnit } from "effector-react";
31
import React, { FC } from "react";
42
import { RouteInstance } from "atomic-router";
5-
import { createRouteView } from "./create-route-view";
6-
7-
export const createRoutesView = (config: {
8-
routes: { route: RouteInstance<any> | RouteInstance<any>[]; view: FC<any> }[];
9-
notFound?: FC<any>;
10-
}) => {
11-
const views = config.routes.map(({ route, view }) => createRouteView(route, view));
12-
const $isSomeOpened = combine(
13-
...config.routes
14-
.map(({ route }) => route)
15-
.flat()
16-
.map((route) => route.$isOpened),
17-
// @ts-expect-error
18-
(...isOpened) => isOpened.some(Boolean)
19-
);
20-
21-
const NotFound = config.notFound;
22-
23-
return () => {
24-
const isSomeOpened = useUnit($isSomeOpened);
25-
26-
if (!isSomeOpened && NotFound) {
27-
return <NotFound />;
3+
4+
import { useIsOpened } from "./use-is-opened";
5+
6+
type RouteRecord<Props, Params> = {
7+
route: RouteInstance<Params> | RouteInstance<Params>[];
8+
view: FC<Props>;
9+
};
10+
11+
export type RoutesViewConfig = {
12+
routes: RouteRecord<unknown, unknown>[];
13+
otherwise?: React.FC<unknown>;
14+
};
15+
16+
export const createRoutesView = <Config extends RoutesViewConfig>(config: Config) => {
17+
return (props: Omit<Config, keyof Config>) => {
18+
const mergedConfig = { ...config, ...props };
19+
const routes = mergedConfig.routes.map((routeRecord) => {
20+
const isOpened = useIsOpened(routeRecord.route);
21+
return { ...routeRecord, isOpened };
22+
});
23+
24+
for (const route of routes) {
25+
if (route.isOpened) {
26+
const View = route.view;
27+
28+
return <View />;
29+
}
30+
}
31+
32+
if (mergedConfig.otherwise) {
33+
const Otherwise = mergedConfig.otherwise;
34+
35+
return <Otherwise />;
2836
}
29-
return (
30-
<>
31-
{views.map((View, idx) => (
32-
<View key={idx} />
33-
))}
34-
</>
35-
);
37+
38+
return null;
3639
};
3740
};

src/route.tsx

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
import React, { FC } from 'react';
2-
import { useStoreMap } from 'effector-react';
32
import { RouteInstance, RouteParams } from 'atomic-router';
43

5-
import { useRouter } from './router-provider';
4+
import { useIsOpened } from './use-is-opened';
65

76
type Props<Params extends RouteParams> = {
87
route: RouteInstance<Params> | RouteInstance<Params>[];
98
view: FC;
109
};
1110

1211
export function Route<Params>({ route, view: Component }: Props<Params>) {
13-
const router = useRouter();
14-
/* eslint-disable */
15-
const isOpened = useStoreMap({
16-
store: router.$activeRoutes,
17-
keys: [route],
18-
fn: (activeRoutes, [route]) => {
19-
return Array.isArray(route)
20-
? route.some(route => activeRoutes.includes(route))
21-
: activeRoutes.includes(route);
22-
},
23-
});
12+
const isOpened = useIsOpened(route);
2413

2514
if (isOpened) {
2615
return <Component />;

src/use-is-opened.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useUnit } from 'effector-react';
2+
import { RouteInstance } from 'atomic-router';
3+
4+
export const useIsOpened = (
5+
route: RouteInstance<any> | RouteInstance<any>[]
6+
) => {
7+
return Array.isArray(route)
8+
? useUnit(route.map((route) => route.$isOpened)).some(Boolean)
9+
: useUnit(route.$isOpened);
10+
};

0 commit comments

Comments
 (0)