From 263da0505d898ee50e460ad19f9ee53055e6a439 Mon Sep 17 00:00:00 2001 From: Chris Herman Date: Tue, 12 Nov 2024 13:32:55 -0500 Subject: [PATCH] chore(hooks): move hooks to more readible files Here's a brief overview of each hook: 1. `useComponentId` - This hook allows you to get the unique component ID from the current route. 2. `useLinking` - This hook provides functionality for linking between routes in your application. 3. `useModal` - This hook manages modals within your application, allowing you to open and close them as needed. 4. `useNavigator` - This hook provides a way to navigate through the application's history. 5. `usePathParams` - This hook allows you to access the dynamic path parameters from the current route. 6. `useQueryParams` - This hook enables you to access the query parameters from the current URL. 7. `useRoute` - This hook retrieves the current router state from the RouterContext. 8. `useRouteData` - This hook allows you to retrieve the router data associated with the current route. --- .../app-router/src/hooks/useComponentId.ts | 36 +++ packages/app-router/src/hooks/useLinking.ts | 66 ++++++ packages/app-router/src/hooks/useModal.ts | 53 +++++ .../src/{hooks.tsx => hooks/useNavigator.tsx} | 224 +----------------- .../app-router/src/hooks/usePathParams.ts | 52 ++++ .../app-router/src/hooks/useQueryParams.ts | 42 ++++ packages/app-router/src/hooks/useRoute.ts | 36 +++ packages/app-router/src/hooks/useRouteData.ts | 39 +++ packages/app-router/src/index.ts | 10 +- 9 files changed, 345 insertions(+), 213 deletions(-) create mode 100644 packages/app-router/src/hooks/useComponentId.ts create mode 100644 packages/app-router/src/hooks/useLinking.ts create mode 100644 packages/app-router/src/hooks/useModal.ts rename packages/app-router/src/{hooks.tsx => hooks/useNavigator.tsx} (63%) create mode 100644 packages/app-router/src/hooks/usePathParams.ts create mode 100644 packages/app-router/src/hooks/useQueryParams.ts create mode 100644 packages/app-router/src/hooks/useRoute.ts create mode 100644 packages/app-router/src/hooks/useRouteData.ts diff --git a/packages/app-router/src/hooks/useComponentId.ts b/packages/app-router/src/hooks/useComponentId.ts new file mode 100644 index 0000000000..2ad2d50a96 --- /dev/null +++ b/packages/app-router/src/hooks/useComponentId.ts @@ -0,0 +1,36 @@ +import {useContext} from 'react'; + +import {ComponentIdContext} from '../context'; + +/** + * Custom hook to access the Component ID context. + * + * @returns {string} The current component ID from the ComponentIdContext. + * @throws Will throw an error if the hook is used outside of a ComponentIdContext.Provider. + * + * @example + * ```tsx + * import { useComponentId } from 'path-to-hooks/useComponentId'; + * + * function MyComponent() { + * const componentId = useComponentId(); + * + * return ( + *
+ *

Component ID: {componentId}

+ *
+ * ); + * } + * ``` + */ +export function useComponentId(): string { + const state = useContext(ComponentIdContext); + + if (!state) { + throw new Error( + 'useComponentId must be used within a ComponentIdContext.Provider', + ); + } + + return state; +} diff --git a/packages/app-router/src/hooks/useLinking.ts b/packages/app-router/src/hooks/useLinking.ts new file mode 100644 index 0000000000..9f1dbfcf7f --- /dev/null +++ b/packages/app-router/src/hooks/useLinking.ts @@ -0,0 +1,66 @@ +import {useEffect} from 'react'; +import {Linking} from 'react-native'; +import urlParse from 'url-parse'; + +import {useNavigator} from './useNavigator'; + +/** + * A custom hook that manages deep linking by listening for URL changes + * and navigating accordingly using a navigator. + * + * @example + * function App() { + * useLinking(); + * + * return ; + * } + * + * // This hook will automatically handle incoming deep links and navigate + * // based on the URL, such as `yourapp://path?query=param`. + */ +export function useLinking() { + const navigator = useNavigator(); + + /** + * Handles the URL passed from deep linking and navigates to the correct screen. + * + * @param {Object} params - Parameters object. + * @param {string | null} params.url - The URL to be processed. + * + * @example + * callback({ url: 'yourapp://profile?user=123' }); + */ + function callback({url}: {url: string | null}) { + if (!url) return; + + try { + const {pathname, query} = urlParse(url); + + // Navigates to the parsed URL's pathname and search params + navigator.open(pathname + query); + } catch (e) { + // Handle the error (e.g., log it) + } + } + + useEffect(() => { + // Check the initial URL when the app is launched + (async function () { + try { + const url = await Linking.getInitialURL(); + + callback({url}); + } catch (e) { + // Handle the error (e.g., log it) + } + })(); + + // Listen for any URL events and handle them with the callback + const subscription = Linking.addEventListener('url', callback); + + // Cleanup the event listener when the component unmounts + return () => { + subscription.remove(); + }; + }, []); +} diff --git a/packages/app-router/src/hooks/useModal.ts b/packages/app-router/src/hooks/useModal.ts new file mode 100644 index 0000000000..736d23574c --- /dev/null +++ b/packages/app-router/src/hooks/useModal.ts @@ -0,0 +1,53 @@ +import {useContext} from 'react'; + +import {ModalContext} from '../context'; + +import {useComponentId} from './useComponentId'; + +/** + * Hook to interact with a modal's state within the context of a `ModalContext.Provider`. + * Provides access to the modal's data and functions to resolve or reject the modal. + * + * @template T - The type of the data passed to the modal. + * @template U - The type of the result returned when the modal is resolved. + * + * @throws {Error} If the hook is used outside of a `ModalContext.Provider`. + * + * @returns {{ resolve: (result: U) => void; reject: () => void }} An object containing the modal's data, a `resolve` function to return a result, and a `reject` function to close the modal without returning a result. + * + * @example + * ```typescript + * // Using useModal in a component + * const { resolve, reject } = useModal(); + * + * // Resolve the modal with a result + * const handleConfirm = () => { + * resolve({ success: true }); + * }; + * + * // Reject the modal without returning a result + * const handleCancel = () => { + * reject(); + * }; + * ``` + */ +export function useModal() { + // Access the current modal state from the ModalContext + const state = useContext(ModalContext); + + // Get the unique component ID of the modal + const componentId = useComponentId(); + + // Ensure the hook is used within a ModalContext.Provider + if (!state) { + throw new Error('useModal must be used inside a ModalContext.Provider'); + } + + return { + // A function to resolve the modal with a result of type U + resolve: state.resolve(componentId) as (result: U) => void, + + // A function to reject the modal, closing it without returning a result + reject: state.reject(componentId) as () => void, + }; +} diff --git a/packages/app-router/src/hooks.tsx b/packages/app-router/src/hooks/useNavigator.tsx similarity index 63% rename from packages/app-router/src/hooks.tsx rename to packages/app-router/src/hooks/useNavigator.tsx index dd409afde9..6284613e28 100644 --- a/packages/app-router/src/hooks.tsx +++ b/packages/app-router/src/hooks/useNavigator.tsx @@ -1,11 +1,12 @@ +import URLParse from 'url-parse'; import {match} from 'path-to-regexp'; -import {useContext, useEffect} from 'react'; import {Layout, Navigation, Options} from 'react-native-navigation'; -import {Linking} from 'react-native'; -import urlParse from 'url-parse'; -import {ComponentIdContext, ModalContext, RouteContext} from './context'; -import {ActionRoute, Guard, RouteMatchRoute} from './types'; +import {ComponentIdContext, ModalContext} from '../context'; +import type {ActionRoute, Guard, RouteMatchRoute} from '../types'; + +import {useComponentId} from './useComponentId'; +import {useRoute} from './useRoute'; /** * Counter used for generating unique IDs. @@ -17,147 +18,7 @@ import {ActionRoute, Guard, RouteMatchRoute} from './types'; * @type {number} * @default 0 */ -let idCounter = 0; - -/** - * Custom hook to access the Router context. - * - * @returns {RouterContextType} The current router state from the RouterContext. - * @throws Will throw an error if the hook is used outside of a RouterContext.Provider. - */ -export function useRoute() { - const state = useContext(RouteContext); - - if (!state) { - throw new Error( - 'useRoute must be used inside a AppRouterURLContext.Provider', - ); - } - - return state; -} - -/** - * Hook to interact with a modal's state within the context of a `ModalContext.Provider`. - * Provides access to the modal's data and functions to resolve or reject the modal. - * - * @template T - The type of the data passed to the modal. - * @template U - The type of the result returned when the modal is resolved. - * - * @throws {Error} If the hook is used outside of a `ModalContext.Provider`. - * - * @returns {{ data: T; resolve: (result: U) => void; reject: () => void }} An object containing the modal's data, a `resolve` function to return a result, and a `reject` function to close the modal without returning a result. - * - * @example - * ```typescript - * // Using useModal in a component - * const { data, resolve, reject } = useModal(); - * - * // Access the data passed to the modal - * console.log(data); - * - * // Resolve the modal with a result - * const handleConfirm = () => { - * resolve({ success: true }); - * }; - * - * // Reject the modal without returning a result - * const handleCancel = () => { - * reject(); - * }; - * ``` - */ -export function useModal() { - // Access the current modal state from the ModalContext - const state = useContext(ModalContext); - - // Get the unique component ID of the modal - const componentId = useComponentId(); - - // Ensure the hook is used within a ModalContext.Provider - if (!state) { - throw new Error('useModal must be used inside a ModalContext.Provider'); - } - - return { - // A function to resolve the modal with a result of type U - resolve: state.resolve(componentId) as (result: U) => void, - - // A function to reject the modal, closing it without returning a result - reject: state.reject(componentId) as () => void, - }; -} - -/** - * Custom hook to access the Component ID context. - * - * @returns {ComponentIdContextType} The current component ID from the ComponentIdContext. - * @throws Will throw an error if the hook is used outside of a ComponentIdContext.Provider. - */ -export function useComponentId() { - const state = useContext(ComponentIdContext); - - if (!state) { - throw new Error( - 'useAppRouterURLContext must be used inside a ComponentIdContext.Provider', - ); - } - - return state; -} - -/** - * Custom hook to retrieve URL parameters matched by the route's path. - * - * @returns {Record} An object containing the matched URL parameters. - * @throws Will throw an error if no matches are found for the path parameters. - */ -export function usePathParams() { - const route = useRoute(); - - if (!route.url) return {}; - - // Match the current URL pathname against the router's path pattern - try { - const {pathname} = urlParse(route.url, true); - const matches = match(route.path)(pathname); - - if (!matches) { - throw new Error('no matches for path params'); - } - - // Return the matched parameters - return matches.params; - } catch (e) { - return {}; - } -} - -/** - * Custom hook to retrieve search parameters from the URL. - * - * @returns {Record} An object containing key-value pairs of search parameters. - */ -export function useQueryParams() { - const route = useRoute(); - - if (!route.url) return {}; - - const {query} = urlParse(route.url, true); - - return query; -} - -/** - * Custom hook to retrieve the router data. - * - * @returns {T} The current router data. - */ -export function useRouteData(): T { - const route = useRoute(); - - return route.data as T; -} +let idCounter: number = 0; /** * Custom hook to provide navigation functions for managing the component stack. @@ -217,8 +78,8 @@ export function useNavigator() { try { // Call the guard function with the parsed paths and control functions await guard( - urlParse(toPath, true), - fromPath ? urlParse(fromPath, true) : undefined, + URLParse(toPath, true), + fromPath ? URLParse(fromPath, true) : undefined, {cancel, redirect, showModal}, ); } catch (e) { @@ -276,7 +137,7 @@ export function useNavigator() { * open('/home'); */ async function open(path: string, passProps = {}, options?: Options) { - const url = urlParse(path, true); + const url = URLParse(path, true); const matchedRoute = route.routes.find(it => match(it.path)(url.pathname)); if (!matchedRoute) { @@ -286,7 +147,7 @@ export function useNavigator() { try { const redirect = await runGuards( url.href, - route.url ? urlParse(route.url, true).href : null, + route.url ? URLParse(route.url, true).href : null, matchedRoute.guards, ); @@ -294,7 +155,7 @@ export function useNavigator() { if (redirect === false) return; const res = match(matchedRoute.path)(url.pathname); - const {query} = urlParse(url.href, true); + const {query} = URLParse(url.href, true); if (matchedRoute.type === 'action') { await (matchedRoute as unknown as ActionRoute).action( @@ -499,64 +360,3 @@ export function useNavigator() { showModal, }; } - -/** - * A custom hook that manages deep linking by listening for URL changes - * and navigating accordingly using a navigator. - * - * @example - * function App() { - * useLinking(); - * - * return ; - * } - * - * // This hook will automatically handle incoming deep links and navigate - * // based on the URL, such as `yourapp://path?query=param`. - */ -export function useLinking() { - const navigator = useNavigator(); - - /** - * Handles the URL passed from deep linking and navigates to the correct screen. - * - * @param {Object} params - Parameters object. - * @param {string | null} params.url - The URL to be processed. - * - * @example - * callback({ url: 'yourapp://profile?user=123' }); - */ - function callback({url}: {url: string | null}) { - if (!url) return; - - try { - const {pathname, query} = urlParse(url); - - // Navigates to the parsed URL's pathname and search params - navigator.open(pathname + query); - } catch (e) { - // Handle the error (e.g., log it) - } - } - - useEffect(() => { - // Check the initial URL when the app is launched - (async function () { - try { - const url = await Linking.getInitialURL(); - - callback({url}); - } catch (e) { - // Handle the error (e.g., log it) - } - })(); - - // Listen for any URL events and handle them with the callback - const subscription = Linking.addEventListener('url', callback); - - // Cleanup the event listener when the component unmounts - return () => { - subscription.remove(); - }; - }, []); -} diff --git a/packages/app-router/src/hooks/usePathParams.ts b/packages/app-router/src/hooks/usePathParams.ts new file mode 100644 index 0000000000..d511d8b0dd --- /dev/null +++ b/packages/app-router/src/hooks/usePathParams.ts @@ -0,0 +1,52 @@ +import {match} from 'path-to-regexp'; +import URLParse from 'url-parse'; + +import {useRoute} from './useRoute'; + +/** + * Custom hook to retrieve URL parameters matched by the route's path. + * + * @returns {Partial>} An object containing the matched URL parameters. + * @throws Will throw an error if no matches are found for the path parameters. + * + * @example + * ```tsx + * import { usePathParams } from 'path-to-hooks/usePathParams'; + * + * function UserProfile() { + * const params = usePathParams(); + * const userId = params.userId; + * + * return ( + *
+ *

User Profile

+ *

User ID: {userId}

+ *
+ * ); + * } + * + * // Assuming the route path is defined as `/user/:userId` + * // If the current URL is `/user/123`, this component will display: + * // User ID: 123 + * ``` + */ +export function usePathParams(): Partial> { + const route = useRoute(); + + if (!route.url) return {}; + + // Match the current URL pathname against the router's path pattern + try { + const {pathname} = URLParse(route.url, true); + const matches = match(route.path)(pathname); + + if (!matches) { + throw new Error('no matches for path params'); + } + + // Return the matched parameters + return matches.params; + } catch (e) { + return {}; + } +} diff --git a/packages/app-router/src/hooks/useQueryParams.ts b/packages/app-router/src/hooks/useQueryParams.ts new file mode 100644 index 0000000000..85514c2e7c --- /dev/null +++ b/packages/app-router/src/hooks/useQueryParams.ts @@ -0,0 +1,42 @@ +import URLParse from 'url-parse'; + +import {useRoute} from './useRoute'; + +/** + * Custom hook to retrieve search parameters from the URL. + * + * @returns {Partial>} An object containing key-value pairs of search parameters. + * + * @example + * ```tsx + * import { useQueryParams } from 'path-to-hooks/useQueryParams'; + * + * function SearchResults() { + * const queryParams = useQueryParams(); + * const searchTerm = queryParams.q; + * const page = queryParams.page; + * + * return ( + *
+ *

Search Results

+ *

Search Term: {searchTerm}

+ *

Page: {page}

+ *
+ * ); + * } + * + * // Assuming the current URL is `/search?q=books&page=2` + * // This component will display: + * // Search Term: books + * // Page: 2 + * ``` + */ +export function useQueryParams(): Partial> { + const route = useRoute(); + + if (!route.url) return {}; + + const {query} = URLParse(route.url, true); + + return query; +} diff --git a/packages/app-router/src/hooks/useRoute.ts b/packages/app-router/src/hooks/useRoute.ts new file mode 100644 index 0000000000..de8db29cb7 --- /dev/null +++ b/packages/app-router/src/hooks/useRoute.ts @@ -0,0 +1,36 @@ +import {useContext} from 'react'; + +import {RouteContext} from '../context'; +import type {RouteMatch} from '../types'; + +/** + * Custom hook to access the Router context. + * + * @returns {RouteMatch} The current router state from the RouterContext. + * @throws Will throw an error if the hook is used outside of a RouterContext.Provider. + * + * @example + * ```tsx + * import { useRoute } from 'path-to-hooks/useRoute'; + * + * function MyComponent() { + * const route = useRoute(); + * + * return ( + *
+ *

Current Route

+ *
{JSON.stringify(route, null, 2)}
+ *
+ * ); + * } + * ``` + */ +export function useRoute(): RouteMatch { + const state = useContext(RouteContext); + + if (!state) { + throw new Error('useRoute must be used within a RouteContext.Provider.'); + } + + return state; +} diff --git a/packages/app-router/src/hooks/useRouteData.ts b/packages/app-router/src/hooks/useRouteData.ts new file mode 100644 index 0000000000..373e1f010e --- /dev/null +++ b/packages/app-router/src/hooks/useRouteData.ts @@ -0,0 +1,39 @@ +import {useRoute} from './useRoute'; + +/** + * Custom hook to retrieve the router data. + * + * @returns {T} The current router data. + * + * @example + * ```tsx + * import { useRouteData } from 'path-to-hooks/useRouteData'; + * + * type RouteData = { + * userId: string; + * isAdmin: boolean; + * }; + * + * function UserProfile() { + * const data = useRouteData(); + * + * return ( + *
+ *

User Profile

+ *

User ID: {data.userId}

+ *

Admin Status: {data.isAdmin ? 'Yes' : 'No'}

+ *
+ * ); + * } + * + * // Assuming the route's data includes { userId: '123', isAdmin: true } + * // This component will display: + * // User ID: 123 + * // Admin Status: Yes + * ``` + */ +export function useRouteData(): T { + const route = useRoute(); + + return route.data as T; +} diff --git a/packages/app-router/src/index.ts b/packages/app-router/src/index.ts index 796ce4dea6..fcf4087ed5 100644 --- a/packages/app-router/src/index.ts +++ b/packages/app-router/src/index.ts @@ -1,3 +1,11 @@ -export * from './hooks'; export * from './router'; export * from './types'; + +export * from './hooks/useComponentId'; +export * from './hooks/useLinking'; +export * from './hooks/useModal'; +export * from './hooks/useNavigator'; +export * from './hooks/usePathParams'; +export * from './hooks/useQueryParams'; +export * from './hooks/useRoute'; +export * from './hooks/useRouteData';