Skip to content

Commit

Permalink
DEV restructure hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
cdcabrera committed Sep 28, 2024
1 parent a139cf2 commit 859c846
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 233 deletions.
4 changes: 2 additions & 2 deletions src/components/productView/productView.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { ProductViewMissing } from './productViewMissing';
* @returns {React.ReactNode}
*/
const ProductView = ({ t, useRouteDetail: useAliasRouteDetail }) => {
const { disableIsClosest, firstMatch, productGroup } = useAliasRouteDetail();
const { disableIsClosestMatch, firstMatch, productGroup } = useAliasRouteDetail();

const renderProduct = useCallback(() => {
const updated = config => {
Expand Down Expand Up @@ -86,7 +86,7 @@ const ProductView = ({ t, useRouteDetail: useAliasRouteDetail }) => {
return updated(firstMatch);
}, [firstMatch, t]);

if (disableIsClosest) {
if (disableIsClosestMatch) {
return <ProductViewMissing />;
}

Expand Down
259 changes: 67 additions & 192 deletions src/components/router/routerContext.js
Original file line number Diff line number Diff line change
@@ -1,86 +1,38 @@
import { useCallback, useEffect, useState } from 'react';
import { useLocation as useLocationRU } from 'react-use';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import { useShallowCompareEffect, useLocation } from 'react-use';
import { routerHelpers } from './routerHelpers';
import { helpers } from '../../common/helpers';
import { storeHooks, reduxTypes } from '../../redux';
import { storeHooks } from '../../redux';
import { translate } from '../i18n/i18n';

/**
* @memberof Router
* @module RouterContext
*/

/**
* Combine react-use useLocation with actual window location.
* Focused on exposing replace and href.
*
* @param {object} options
* @param {Function} options.useLocation
* @param {*} options.windowLocation
* @returns {{_id, search, hash}}
*/
const useLocation = ({
useLocation: useAliasLocation = useLocationRU,
windowLocation: aliasWindowLocation = window.location
} = {}) => {
useAliasLocation();
const windowLocation = aliasWindowLocation;
const [updatedLocation, setUpdatedLocation] = useState({});
const forceUpdateLocation = useCallback(() => {
const _id = helpers.generateHash(windowLocation);
if (updatedLocation?._id !== _id) {
setUpdatedLocation({
...windowLocation,
_id
});
}
}, [updatedLocation?._id, windowLocation]);

useEffect(() => {
const _id = helpers.generateHash(windowLocation);
if (updatedLocation?._id !== _id) {
setUpdatedLocation({
...windowLocation,
_id,
updateLocation: forceUpdateLocation
});
}
}, [forceUpdateLocation, updatedLocation?._id, windowLocation]);

return updatedLocation;
};

/**
* useNavigate wrapper. Leverage useNavigate for a modified router with parallel "state"
* update. Dispatches the same type leveraged by the initialize hook, useSetRouteDetail.
*
* @param {object} options
* @param {Function} options.useDispatch
* @param {Function} options.useLocation
* @param {*} options.windowHistory
* @returns {Function}
*/
const useNavigate = ({
useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch,
useLocation: useAliasLocation = useLocation,
windowHistory: aliasWindowHistory = window.history
} = {}) => {
const windowHistory = aliasWindowHistory;
const { search = '', hash = '' } = useAliasLocation();
const dispatch = useAliasDispatch();

return useCallback(
(pathLocation, options) => {
const pathName = (typeof pathLocation === 'string' && pathLocation) || pathLocation?.pathname;
const { firstMatch } = routerHelpers.getRouteConfigByPath({ pathName });

if (firstMatch?.productPath) {
dispatch({
type: reduxTypes.app.SET_PRODUCT,
config: firstMatch?.productPath
});

return windowHistory.pushState(
{},
'',
Expand All @@ -91,181 +43,104 @@ const useNavigate = ({

return windowHistory.pushState({}, '', (pathName && `${pathName}${search}${hash}`) || pathLocation, options);
},
[dispatch, hash, search, windowHistory]
[hash, search, windowHistory]
);
};

/**
* Initialize and store product path, parameter, in a "state" update parallel to routing.
* We're opting to use "window.location.pathname" directly since it appears to be quicker,
* Initialize and store a product path, parameter, in a "state" update parallel to variant detail.
* We're opting to use "window.location.pathname" directly because its faster.
* and returns a similar structured value as useParam.
*
* @param {object} options
* @param {Function} options.useSelector
* @param {Function} options.useDispatch
* @param {boolean} options.disableIsClosestMatch
* @param {Function} options.useLocation
* @param {*} options.windowLocation
* @returns {*|string}
* @param {Function} options.useSelector
* @returns {{ firstMatch: unknown, isClosest: boolean, productGroup: string, productConfig: Array,
* productPath: string, productVariant: string, disableIsClosestMatch:boolean }}
*/
const useSetRouteDetail = ({
useSelector: useAliasSelector = storeHooks.reactRedux.useSelectors,
useDispatch: useAliasDispatch = storeHooks.reactRedux.useDispatch,
const useSetRouteProduct = ({
disableIsClosestMatch = helpers.DEV_MODE === true,
useLocation: useAliasLocation = useLocation,
windowLocation: aliasWindowLocation = window.location
useSelector: useAliasSelector = storeHooks.reactRedux.useSelector
} = {}) => {
useAliasLocation();
const dispatch = useAliasDispatch();
const [updatedPath] = useAliasSelector([({ view }) => view?.product?.config]);
const { pathname: productPath } = aliasWindowLocation;

useEffect(() => {
if (productPath && productPath !== updatedPath) {
dispatch({
type: reduxTypes.app.SET_PRODUCT,
config: productPath
});
const [product, setProduct] = useState({});
const { pathname: productPath } = useAliasLocation();
const productVariant = useAliasSelector(({ view }) => view?.product?.variant, {});

useShallowCompareEffect(() => {
let routeConfig = routerHelpers.getRouteConfigByPath({
pathName: productPath,
isIgnoreClosest: disableIsClosestMatch
});

if (productVariant) {
const selectedVariant = productVariant?.[routeConfig?.firstMatch?.productGroup];
if (selectedVariant) {
routeConfig = routerHelpers.getRouteConfigByPath({
pathName: selectedVariant,
isIgnoreClosest: disableIsClosestMatch
});
}
}
}, [updatedPath, dispatch, productPath]);

return updatedPath;
const { configs, firstMatch, isClosest, ...config } = routeConfig;

setProduct(() => ({
...config,
firstMatch,
isClosest,
productGroup: firstMatch?.productGroup,
productConfig: (configs?.length && configs) || [],
productPath,
productVariant,
disableIsClosestMatch:
(disableIsClosestMatch && isClosest) || (disableIsClosestMatch && routerHelpers.dynamicPath() === '/')
}));
}, [disableIsClosestMatch, productPath, productVariant]);

return product;
};

/**
* Get a route detail from "state". Consume useSetRouteDetail and set basis for product
* configuration context.
* Aggregate display settings and configuration. Get a product route detail.
* Consumes useSetRouteProduct to return a display configuration for use in productView context.
*
* @param {object} options
* @param {boolean} options.disableIsClosest
* @param {Function} options.t
* @param {Function} options.useChrome
* @param {Function} options.useSelectors
* @param {Function} options.useSetRouteDetail
* @returns {{baseName: string, errorRoute: object}}
* @param {useSetRouteProduct} options.useSetRouteProduct
* @returns {{firstMatch: *, isClosest: boolean, productGroup: string, productConfig: Array, productPath: string,
* productVariant: string, disableIsClosestMatch: boolean}}}
*/
const useRouteDetail = ({
disableIsClosest = helpers.DEV_MODE === true,
t = translate,
useChrome: useAliasChrome = useChrome,
useSelectors: useAliasSelectors = storeHooks.reactRedux.useSelectors,
useSetRouteDetail: useAliasSetRouteDetail = useSetRouteDetail
useSetRouteProduct: useAliasSetRouteProduct = useSetRouteProduct
} = {}) => {
useAliasSetRouteDetail();
const product = useAliasSetRouteProduct();
const { getBundleData = helpers.noop, updateDocumentTitle = helpers.noop } = useAliasChrome();
const bundleData = getBundleData();
const [productPath, productVariant] = useAliasSelectors([
({ view }) => view?.product?.config,
({ view }) => view?.product?.variant
]);
const [detail, setDetail] = useState({});
const productGroup = product?.productGroup;

useEffect(() => {
const updatedVariantPath = productPath;
const hashPath = helpers.generateHash({ productPath, productVariant });

if (updatedVariantPath && detail?._passed !== hashPath) {
// Get base configuration match
let routeConfig = routerHelpers.getRouteConfigByPath({
pathName: updatedVariantPath
});

// Determine variant to display, if any
if (productVariant) {
const selectedVariant = productVariant?.[routeConfig?.firstMatch?.productGroup];

if (selectedVariant) {
routeConfig = routerHelpers.getRouteConfigByPath({
pathName: selectedVariant
});
}
}

const { allConfigs, availableVariants, configs, firstMatch, isClosest } = routeConfig;

// Set document title, remove pre-baked suffix
updateDocumentTitle(
`${t(`curiosity-view.title`, {
appName: helpers.UI_DISPLAY_NAME,
context: firstMatch?.productGroup
})} - ${helpers.UI_DISPLAY_NAME}${(bundleData?.bundleTitle && ` | ${bundleData?.bundleTitle}`) || ''}`,
true
);

// Set route detail
setDetail({
_passed: hashPath,
allConfigs,
availableVariants,
firstMatch,
errorRoute: routerHelpers.errorRoute,
isClosest,
productGroup: firstMatch?.productGroup,
productConfig: (configs?.length && configs) || [],
productPath,
productVariant,
disableIsClosest: disableIsClosest && isClosest
});
}
}, [bundleData?.bundleTitle, detail?._passed, disableIsClosest, productPath, productVariant, t, updateDocumentTitle]);

return detail;
};

/**
* Search parameter, return
*
* @param {object} options
* @param {Function} options.useLocation
* @param {*} options.windowHistory
* @returns {Array}
*/
const useSearchParams = ({
useLocation: useAliasLocation = useLocation,
windowHistory: aliasWindowHistory = window.history
} = {}) => {
const windowHistory = aliasWindowHistory;
const { updateLocation, search } = useAliasLocation();

/**
* Alias returned React Router Dom useSearchParams hook to something expected.
* This hook defaults to merging search objects instead of overwriting them.
*
* @param {object} updatedQuery
* @param {object} options
* @param {boolean} options.isMerged Merge search with existing search, or don't
* @param {string|*} options.currentSearch search returned from useLocation
*/
const setSearchParams = useCallback(
(updatedQuery, { isMerged = true, currentSearch = search } = {}) => {
let updatedSearch = {};

if (isMerged) {
Object.assign(updatedSearch, routerHelpers.parseSearchParams(currentSearch), updatedQuery);
} else {
updatedSearch = updatedQuery;
}

windowHistory.pushState(
{},
'',
`?${Object.entries(updatedSearch)
.map(([key, value]) => `${key}=${value}`)
.join('&')}`
);

updateLocation();
},
[search, updateLocation, windowHistory]
);

return [routerHelpers.parseSearchParams(search), setSearchParams];
// Set platform document title, remove pre-baked suffix
updateDocumentTitle(
`${t(`curiosity-view.title`, {
appName: helpers.UI_DISPLAY_NAME,
context: productGroup
})} - ${helpers.UI_DISPLAY_NAME}${(bundleData?.bundleTitle && ` | ${bundleData?.bundleTitle}`) || ''}`,
true
);
}, [bundleData?.bundleTitle, productGroup, t, updateDocumentTitle]);

return product;
};

const context = {
useLocation,
useNavigate,
useRouteDetail,
useSearchParams,
useSetRouteDetail
useSetRouteProduct
};

export { context as default, context, useLocation, useNavigate, useRouteDetail, useSearchParams, useSetRouteDetail };
export { context as default, context, useNavigate, useRouteDetail, useSetRouteProduct };
Loading

0 comments on commit 859c846

Please sign in to comment.