diff --git a/README.md b/README.md index d0e9f2d..af1fedb 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,23 @@ export default function HomeLoading() { } ``` +#### **7. Loader Support** + +You can add a `loader.ts` file inside the folder to handle **loader** to that path.
+For more information, see [Loader in React Router](https://reactrouter.com/en/main/route/loader) + +```tsx +// src/pages/loader.ts +export default async function rootLoader() { + const res = await fetch('https://swapi.dev/api/people'); + + return await res.json(); +} + +// src/pages/layout.tsx +const data = useLoaderData(); +``` + --- ### **📄 How to Contribute** diff --git a/docs/README.md b/docs/README.md index 99c8b04..11c0e64 100644 --- a/docs/README.md +++ b/docs/README.md @@ -177,6 +177,23 @@ export default function HomeLoading() { } ``` +#### **7. loader 지원** + +폴더 내에 `loader.ts` 파일을 추가하여 해당 경로에 **loader**에 대한 처리를 수행할 수 있습니다.
+자세한 내용은 [React Router의 loader](https://reactrouter.com/en/main/route/loader)를 참고해주세요 + +```tsx +// src/pages/loader.ts +export default async function rootLoader() { + const res = await fetch('https://swapi.dev/api/people'); + + return await res.json(); +} + +// src/pages/layout.tsx +const data = useLoaderData(); +``` + ### **📄 기여 방법** 이 프로젝트에 기여하고 싶으시다면, 다음 절차를 따라주세요: diff --git a/package.json b/package.json index 4af1bd6..39879ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-router-file-routing", - "version": "0.1.4", + "version": "0.1.5", "description": "A library to support folder-based routing of next.js to react-router-dom", "main": "dist/index.js", "files": [ diff --git a/src/file-router.tsx b/src/file-router.tsx index 1af122e..6860e6d 100644 --- a/src/file-router.tsx +++ b/src/file-router.tsx @@ -18,9 +18,5 @@ export function FileRouter() { const router = React.useMemo(() => createBrowserRouter(routes), [routes]); - return ( - Loading,,,}> - - - ); + return ; } diff --git a/src/types/index.ts b/src/types/index.ts index 89a47df..65c8918 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,21 +1,25 @@ +import { LoaderFunction } from 'react-router-dom'; + export type RouteNode = { /** 라우터 path */ path?: string; /** 라우터 component */ - element?: React.LazyExoticComponent; + element?: React.LazyExoticComponent>; /** layout component */ - layoutElement?: React.LazyExoticComponent; + layoutElement?: React.LazyExoticComponent>; /** page component */ - pageElement?: React.LazyExoticComponent; + pageElement?: React.LazyExoticComponent>; /** error component */ - errorElement?: React.LazyExoticComponent; + errorElement?: React.LazyExoticComponent>; /** loading component */ - loadingElement?: React.LazyExoticComponent; + loadingElement?: React.LazyExoticComponent>; + /** loader function */ + loaderFn?: LoaderFunction; /** children 라우트 */ children?: Record; }; export type NodeData = { - type: 'page' | 'layout' | 'error' | 'loading'; - element: React.LazyExoticComponent; + type: 'page' | 'layout' | 'error' | 'loading' | 'loader'; + element: React.LazyExoticComponent> | LoaderFunction; }; diff --git a/src/utils/add-tree.ts b/src/utils/add-tree.ts index 1ab994f..e8d3c67 100644 --- a/src/utils/add-tree.ts +++ b/src/utils/add-tree.ts @@ -1,3 +1,4 @@ +import { LoaderFunction } from 'react-router-dom'; import { NodeData, RouteNode } from '../types'; /** @@ -19,17 +20,28 @@ export function addToRouteTree( /** 루트 레이아웃이면 트리의 최상위에 layoutElement를 설정 */ if (isRootLayout && nodeData.type === 'layout') { - tree.layoutElement = nodeData.element; + tree.layoutElement = nodeData.element as React.LazyExoticComponent< + React.ComponentType + >; return; } /** 루트 에러면 트리의 최상위에 errorElement를 설정 */ if (isRootLayout && nodeData.type === 'error') { - tree.errorElement = nodeData.element; + tree.errorElement = nodeData.element as React.LazyExoticComponent< + React.ComponentType + >; return; } /** 루트 로딩이면 트리의 최상위에 loadingElement를 설정 */ if (isRootLayout && nodeData.type === 'loading') { - tree.loadingElement = nodeData.element; + tree.loadingElement = nodeData.element as React.LazyExoticComponent< + React.ComponentType + >; + return; + } + /** 루트 loader면 트리의 최상위에 loaderFn를 설정 */ + if (isRootLayout && nodeData.type === 'loader') { + tree.loaderFn = nodeData.element as LoaderFunction; return; } @@ -75,14 +87,31 @@ export function addToRouteTree( /** 마지막 세그먼트인 경우 요소 할당 */ if (index === segments.length - 1) { - if (nodeData.type === 'page') { - current.pageElement = nodeData.element; - } else if (nodeData.type === 'layout') { - current.layoutElement = nodeData.element; - } else if (nodeData.type === 'error') { - current.errorElement = nodeData.element; - } else if (nodeData.type === 'loading') { - current.loadingElement = nodeData.element; + switch (nodeData.type) { + case 'page': + current.pageElement = nodeData.element as React.LazyExoticComponent< + React.ComponentType + >; + break; + case 'layout': + current.layoutElement = nodeData.element as React.LazyExoticComponent< + React.ComponentType + >; + break; + case 'error': + current.errorElement = nodeData.element as React.LazyExoticComponent< + React.ComponentType + >; + break; + case 'loading': + current.loadingElement = + nodeData.element as React.LazyExoticComponent< + React.ComponentType + >; + break; + case 'loader': + current.loaderFn = nodeData.element as LoaderFunction; + break; } } } diff --git a/src/utils/build-tree.ts b/src/utils/build-tree.ts index 10c9972..6366301 100644 --- a/src/utils/build-tree.ts +++ b/src/utils/build-tree.ts @@ -24,6 +24,8 @@ export function buildRoutesFromTree( ? React.createElement(node.loadingElement) : undefined; + const loaderFn = node.loaderFn ? node.loaderFn : undefined; + if (node.pageElement) { childRoutes.push({ index: true, @@ -31,6 +33,7 @@ export function buildRoutesFromTree( errorElement: node.errorElement ? wrapWithSuspense(node.errorElement) : undefined, + loader: loaderFn, }); } diff --git a/src/utils/create-route-object.ts b/src/utils/create-route-object.ts index dc2d45e..a7004f8 100644 --- a/src/utils/create-route-object.ts +++ b/src/utils/create-route-object.ts @@ -30,6 +30,11 @@ export function createRouteObject(node: RouteNode, path?: string): RouteObject { route.errorElement = wrapWithSuspense(node.errorElement, loadingComponent); } + /** loader 처리 */ + if (node.loaderFn) { + route.loader = node.loaderFn; + } + /** page 처리 */ if (node.pageElement) { const pageElement = wrapWithSuspense(node.pageElement, loadingComponent); @@ -40,6 +45,7 @@ export function createRouteObject(node: RouteNode, path?: string): RouteObject { index: true, element: pageElement, errorElement: route.errorElement, + loader: route.loader, }, ]; } else { diff --git a/src/utils/get-path-segments.ts b/src/utils/get-path-segments.ts index d7cf391..4f9c665 100644 --- a/src/utils/get-path-segments.ts +++ b/src/utils/get-path-segments.ts @@ -25,7 +25,8 @@ export function getPathSegments(filePath: string): string[] { segments[0].startsWith('page.') || segments[0].startsWith('layout.') || segments[0].startsWith('error.') || - segments[0].startsWith('loading.') + segments[0].startsWith('loading.') || + segments[0].startsWith('loader.') ) { segments[0] = '/'; } @@ -35,7 +36,8 @@ export function getPathSegments(filePath: string): string[] { segments[segments.length - 1].startsWith('page.') || segments[segments.length - 1].startsWith('layout.') || segments[segments.length - 1].startsWith('error.') || - segments[segments.length - 1].startsWith('loading.') + segments[segments.length - 1].startsWith('loading.') || + segments[segments.length - 1].startsWith('loader.') ) { segments.pop(); } diff --git a/src/utils/get-routes.ts b/src/utils/get-routes.ts index 2f2eeee..97c4c8d 100644 --- a/src/utils/get-routes.ts +++ b/src/utils/get-routes.ts @@ -1,5 +1,9 @@ import React from 'react'; -import { RouteObject } from 'react-router-dom'; +import { + LoaderFunction, + LoaderFunctionArgs, + RouteObject, +} from 'react-router-dom'; import { sortRoutes } from './sort-routes'; import { RouteNode } from '../types'; import { getPathSegments } from './get-path-segments'; @@ -26,6 +30,10 @@ export function getRoutes(): RouteObject[] { * pages 디렉토리의 loading.tsx 파일을 모두 가져옵니다. */ const loadings = import.meta.glob('/src/pages/**/loading.{jsx,tsx}'); + /** + * pages 디렉토리의 loader.ts 파일을 모두 가져옵니다. + */ + const loaders = import.meta.glob('/src/pages/**/loader.{js,ts}'); /** 트리 구조를 생성하기 위한 루트 노드 */ const routeTree: RouteNode = {}; @@ -33,11 +41,30 @@ export function getRoutes(): RouteObject[] { /** 모든 {page | layout | error | loading} 파일을 순회하여 트리 구조에 추가합니다 */ const addElementsToTree = ( glob: Record Promise>, - type: 'page' | 'layout' | 'error' | 'loading', + type: 'page' | 'layout' | 'error' | 'loading' | 'loader', ) => { Object.keys(glob).forEach((filePath) => { const pathSegments = getPathSegments(filePath); - const element = React.lazy(glob[filePath] as any); + + let element: + | React.LazyExoticComponent> + | LoaderFunction; + + if (type === 'loader') { + const moduleLoader = glob[filePath] as () => Promise<{ + default: LoaderFunction; + }>; + element = async (args: LoaderFunctionArgs) => { + const module = await moduleLoader(); + return module.default(args); + }; + } else { + element = React.lazy( + glob[filePath] as unknown as () => Promise<{ + default: React.ComponentType; + }>, + ); + } addToRouteTree(routeTree, pathSegments, { type, @@ -50,6 +77,7 @@ export function getRoutes(): RouteObject[] { addElementsToTree(layouts, 'layout'); addElementsToTree(errors, 'error'); addElementsToTree(loadings, 'loading'); + addElementsToTree(loaders, 'loader'); /** 트리 구조를 기반으로 RouteObject 배열 생성 */ const routeObjects = buildRoutesFromTree(routeTree);