Atomic-Router Chains
Table of contents
Отделение роутов от URL
// src/shared/routing/routes.ts
export const routes = {
home: createRoute(),
users: {
list: createRoute(),
view: createRoute<{userId: string}>(),
tickets: createRoute<{userId: string}>(),
},
posts: {
list: createRoute(),
view: createRoute<{postId: string}>(),
},
};
export const routesMap = [
{
path: '/',
route: routes.home,
},
{
path: '/users',
route: routes.users.list,
},
{
path: '/users/:userId',
route: routes.users.view,
},
{
path: '/users/:userId/tickets',
route: routes.users.tickets,
},
{
path: '/explore',
route: routes.posts.list,
},
{
path: '/posts/:postId',
route: routes.posts.view,
},
];
Это позволяет прицепить к одному пути несколько роутов
// src/shared/routing/routes.ts
export const sections = {
users: createRoute(),
posts: createRoute(),
};
export const routesMap = [
{
path: '/',
route: routes.home,
},
{
path: '/users',
route: [routes.users.list, sections.users],
},
{
path: '/users/:userId',
route: [routes.users.view, sections.users],
},
{
path: '/users/:userId/tickets',
route: [routes.users.tickets, sections.users],
},
{
path: '/explore',
route: [routes.posts.list, sections.posts],
},
{
path: '/posts/:postId',
route: [routes.posts.view, sections.posts],
},
];
Так мы можем не завязывать на конкретные урлы
// src/layouts/navigation/index.tsx
const $links = createStore<Link[]>([
{
label: "Users",
route: routes.users.list,
active: sections.users,
icon: IconUsers,
},
{
label: "Posts",
route: routes.posts.list,
active: sections.participants,
icon: IconUserCheck,
},
]);
export function LayoutNavigate({ children }: { children: ReactNode }) {
const links = useList($links, {
getKey: (link) => link.label,
fn(link) {
const isActive = useUnit(link.active?.$isOpened ?? link.route.$isOpened);
const classNames = isActive ? classes.linkActive : "";
return (
<Link
key={link.label}
to={link.route}
className={clsx(classes.link, classNames)}
activeClassName={classes.linkActive}
>
<link.icon className={classes.linkIcon} stroke={1.5} />
<span>{link.label}</span>
</Link>
);
},
});
return (/* ... */)
}
теперь можно реагировать на изменение роутов, а не рендер компонентов
// src/pages/posts-list/model.ts
export const currentRoute = routes.posts.list;
sample({
// Когда текущий роут откроется
clock: currentRoute.opened,
// Начать загружать список постов
target: postsLoadFx,
});
Также можно реагировать на изменение параметров роута. То есть можем отличить первый заход на страницу от обновления только параметра.
// src/pages/posts-view/model.ts
export const currentRoute = routes.posts.view;
//
sample({
// Когда текущий роут обновился
clock: currentRoute.updated,
// Взять параметры из урла
source: currentRoute.$params,
// Извлекаем только параметры. В наличии еще query
fn: ({params}) => ({postId: params.postId}),
// Начать загружать пост с новым postId
target: postGetFx,
});
На самом деле, можно добавить в выражение выше и .opened
, чтобы загружать пост и при первом открытии страницы тоже:
// Когда текущий роут открылся или обновился
sample({
clock: [currentRoute.opened, currentRoute.updated],
source: currentRoute.$params,
fn: ({params}) => params,
target: postGetFx,
});
chainRoute создает последовательные цепочки для получения данных
далеко не всегда можно начать загружать инфу для отображения страницы параллельно. ну а если можно, то цепочки будут короче.
// src/pages/posts-view/model.ts
export const currentRoute = routes.posts.view;
// Эффект postGetFx будет выполняться когда currentRoute откроется
// или обновится с новым postId
export const postRoute = chainRoute({
route: currentRoute,
beforeOpen: postGetFx,
});
// Когда postGetFx завершится с любым результатом, postRoute будет открыт
// postCommentsGetFx будет вызван ПОСЛЕ загрузки postGetFx
// то есть, когда откроется postRoute
export const commentsRoute = chainRoute({
route: postRoute,
beforeOpen: postCommentsGetFx,
});
можно реагировать на события каждого роута по отдельности, у каждого из них есть свои .opened
и .$isOpened
:
sample({
clock: postRoute.opened,
fn: ({params}) => ({postId: params.postId, active: true}),
target: analytics.reportFx,
});
все чейнеры выполняют последовательные задачи, к ним можно привязывать свой view
// src/pages/posts-view/index.ts
import {LayoutNavigation} from '~/layouts/navigation';
import {currentRoute, postRoute} from './model';
import {LoaderFullPage, PostViewPage} from './view';
const PostView = createRouteView({
route: postRoute,
view: PostViewPage,
otherwise: LoaderFullPage,
});
export default {
route: currentRoute,
view: PostView,
layout: LayoutNavigation,
};
Для каждого роута будет отрендерен свой компонент. Компоненты можно вкладывать друг в друга: например, после загрузки поста отображать комменты в виде skeleton-загрузки. При этом в компонентах не будет кучи условий и опциональных рендеров.
добавление редиректов, но с сохранением ссылок
Есть случаи, когда пользователи могут добавить ссылки на страницы в нашем приложении в закладки. Это значит, что если мы поменяем урлы, то такие закладки сломаются. Поэтому хорошо было бы или не ломать урлы, или выполнять плавную миграцию.
Есть два пути:
- открывать страницу по двум урлам сразу, но в интерфейсе ссылки давать только на новый урл
для этого можно назначить одному роуту несколько урлов
// src/shared/routing/routes.ts
export const routesMap = [
// ...
{
path: '/explore',
route: routes.posts.list,
},
{
path: '/posts',
route: routes.posts.list,
},
];
Путь с роутом расположенный выше будет использоваться для ссылок.
- добавить редирект, чтобы старые ссылки всегда редиректили на новые урлы
заводим список легаси роутов, назначаем им свои урлы и добавляем редиректы прям тут
// src/shared/routing/routes.ts
const legacyRoutes = {
posts: {list: createRoute()},
};
export const routesMap = [
// ...
{
path: '/explore',
route: routes.posts.list,
},
{
path: '/posts',
route: legacyRoutes.posts.list,
},
];
redirect({
clock: legacyRoutes.posts.list.opened,
route: routes.posts.list,
});
Таким образом роуты не будут смешиваться, legacyRoutes
всегда видны, если их кто-то будет использовать.
- всегда можно смешивать два подхода, чтобы дать пользователям лучший ux