perf upgrade
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { AppShell } from '@mantine/core';
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import Header from './header';
|
||||
import Navbar from './navbar';
|
||||
import Pullable from './pullable';
|
||||
@@ -8,10 +8,8 @@ import useRouterConfig from '../hooks/use-router-config';
|
||||
import Page from '@/components/page';
|
||||
|
||||
const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { header } = useRouterConfig();
|
||||
const { header, withPadding, fullWidth } = useRouterConfig();
|
||||
const viewport = useVisualViewportSize();
|
||||
const [scrollPosition, setScrollPosition] = useState({ x: 0, y: 0 });
|
||||
const { withPadding, fullWidth } = useRouterConfig();
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
@@ -47,7 +45,7 @@ const Layout: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
maw='100dvw'
|
||||
style={{ transition: 'none', overflow: 'hidden' }}
|
||||
>
|
||||
<Pullable scrollPosition={scrollPosition} onScrollPositionChange={setScrollPosition}>
|
||||
<Pullable>
|
||||
<Page noPadding={!withPadding} fullWidth={fullWidth}>
|
||||
{children}
|
||||
</Page>
|
||||
|
||||
@@ -19,14 +19,13 @@ export const NavLink = ({
|
||||
include,
|
||||
exclude,
|
||||
}: NavLinkProps) => {
|
||||
const router = useRouterState();
|
||||
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
||||
const isActive = useMemo(
|
||||
() =>
|
||||
(!exclude?.some((e) => router.location.pathname.includes(e)) &&
|
||||
(router.location.pathname === href ||
|
||||
(router.location.pathname.includes(href) && href !== "/"))) ||
|
||||
include?.includes(router.location.pathname),
|
||||
[router.location.pathname, href]
|
||||
(!exclude?.some((e) => pathname.includes(e)) &&
|
||||
(pathname === href || (pathname.includes(href) && href !== "/"))) ||
|
||||
include?.includes(pathname),
|
||||
[pathname, href]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,15 +8,10 @@ import { useLocation } from "@tanstack/react-router";
|
||||
|
||||
const THRESHOLD = 80;
|
||||
|
||||
interface PullableProps extends PropsWithChildren {
|
||||
scrollPosition: { x: number, y: number };
|
||||
onScrollPositionChange: (position: { x: number, y: number }) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pullable is a component that allows the user to pull down to refresh the page
|
||||
*/
|
||||
const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollPositionChange }) => {
|
||||
const Pullable: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const height = useAppShellHeight();
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [scrolling, setScrolling] = useState(false);
|
||||
@@ -25,7 +20,14 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
|
||||
const location = useLocation();
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollY = useMemo(() => scrollPosition.y < 0 && scrolling ? Math.abs(scrollPosition.y) : 0, [scrollPosition.y, scrolling]);
|
||||
const [pullDistance, setPullDistance] = useState(0);
|
||||
|
||||
const handleScrollPositionChange = useCallback((position: { x: number, y: number }) => {
|
||||
const next = position.y < 0 ? Math.abs(position.y) : 0;
|
||||
setPullDistance((prev) => (prev === next ? prev : next));
|
||||
}, []);
|
||||
|
||||
const scrollY = scrolling ? pullDistance : 0;
|
||||
|
||||
const onTrigger = useCallback(async () => {
|
||||
setIsRefreshing(true);
|
||||
@@ -143,11 +145,11 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
|
||||
viewport.scrollLeft = 0;
|
||||
}
|
||||
}
|
||||
onScrollPositionChange({ x: 0, y: 0 });
|
||||
setPullDistance(0);
|
||||
}, 10);
|
||||
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [location.pathname, onScrollPositionChange]);
|
||||
}, [location.pathname]);
|
||||
|
||||
|
||||
return (
|
||||
@@ -175,7 +177,7 @@ const Pullable: React.FC<PullableProps> = ({ children, scrollPosition, onScrollP
|
||||
<ScrollArea
|
||||
ref={scrollAreaRef}
|
||||
id='scroll-wrapper'
|
||||
onScrollPositionChange={onScrollPositionChange}
|
||||
onScrollPositionChange={handleScrollPositionChange}
|
||||
type='never' mah='100%' h='100%'
|
||||
pt={(scrolling || scrollY > 40) || !isRefreshing ? 0 : 40 - scrollY}
|
||||
styles={{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMatches } from "@tanstack/react-router";
|
||||
import { useMemo } from "react";
|
||||
import { HeaderConfig } from "../types/header-config";
|
||||
|
||||
export const defaultHeaderConfig: HeaderConfig = {
|
||||
@@ -10,32 +11,34 @@ export const defaultHeaderConfig: HeaderConfig = {
|
||||
const useRouterConfig = () => {
|
||||
const matches = useMatches();
|
||||
|
||||
const matchesWithHeader = matches.filter((match) =>
|
||||
match?.loaderData && 'header' in match.loaderData
|
||||
);
|
||||
return useMemo(() => {
|
||||
const matchesWithHeader = matches.filter((match) =>
|
||||
match?.loaderData && 'header' in match.loaderData
|
||||
);
|
||||
|
||||
const headerConfig = matchesWithHeader.reduce((acc, match) => {
|
||||
const loaderData = match?.loaderData;
|
||||
if (loaderData && typeof loaderData === 'object' && 'header' in loaderData) {
|
||||
const header = loaderData.header;
|
||||
if (header && typeof header === 'object') {
|
||||
return {
|
||||
...acc,
|
||||
...header,
|
||||
const headerConfig = matchesWithHeader.reduce((acc, match) => {
|
||||
const loaderData = match?.loaderData;
|
||||
if (loaderData && typeof loaderData === 'object' && 'header' in loaderData) {
|
||||
const header = loaderData.header;
|
||||
if (header && typeof header === 'object') {
|
||||
return {
|
||||
...acc,
|
||||
...header,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, defaultHeaderConfig);
|
||||
return acc;
|
||||
}, defaultHeaderConfig);
|
||||
|
||||
const current = matches[matches.length - 1]?.loaderData;
|
||||
const current = matches[matches.length - 1]?.loaderData;
|
||||
|
||||
return {
|
||||
header: headerConfig,
|
||||
refresh: current && typeof current === 'object' && 'refresh' in current ? current.refresh : [],
|
||||
withPadding: current && typeof current === 'object' && 'withPadding' in current ? current.withPadding : true,
|
||||
fullWidth: current && typeof current === 'object' && 'fullWidth' in current ? current.fullWidth : false,
|
||||
};
|
||||
return {
|
||||
header: headerConfig,
|
||||
refresh: current && typeof current === 'object' && 'refresh' in current ? current.refresh : [],
|
||||
withPadding: current && typeof current === 'object' && 'withPadding' in current ? current.withPadding : true,
|
||||
fullWidth: current && typeof current === 'object' && 'fullWidth' in current ? current.fullWidth : false,
|
||||
};
|
||||
}, [matches]);
|
||||
}
|
||||
|
||||
export default useRouterConfig;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const eventListerOptions = {
|
||||
passive: true,
|
||||
@@ -11,25 +11,49 @@ const useVisualViewportSize = () => {
|
||||
height: windowExists ? window.visualViewport?.height || 0 : 0,
|
||||
top: windowExists ? window.visualViewport?.offsetTop || 0 : 0,
|
||||
});
|
||||
|
||||
const setSize = useCallback(() => {
|
||||
if (!windowExists) return;
|
||||
setWindowSize({ width: window.visualViewport?.width || 0, height: window.visualViewport?.height || 0, top: window.visualViewport?.offsetTop || 0 });
|
||||
}, []);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!windowExists) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const setSize = () => {
|
||||
setWindowSize((prev) => {
|
||||
const next = {
|
||||
width: window.visualViewport?.width || 0,
|
||||
height: window.visualViewport?.height || 0,
|
||||
top: window.visualViewport?.offsetTop || 0,
|
||||
};
|
||||
if (
|
||||
prev.width === next.width &&
|
||||
prev.height === next.height &&
|
||||
prev.top === next.top
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const scheduleSetSize = () => {
|
||||
if (rafRef.current !== null) return;
|
||||
rafRef.current = window.requestAnimationFrame(() => {
|
||||
rafRef.current = null;
|
||||
setSize();
|
||||
});
|
||||
};
|
||||
|
||||
setSize();
|
||||
|
||||
window.visualViewport?.addEventListener('resize', setSize, eventListerOptions);
|
||||
window.visualViewport?.addEventListener('scroll', setSize, eventListerOptions);
|
||||
window.visualViewport?.addEventListener('resize', scheduleSetSize, eventListerOptions);
|
||||
|
||||
return () => {
|
||||
window.visualViewport?.removeEventListener('resize', setSize);
|
||||
window.visualViewport?.removeEventListener('scroll', setSize);
|
||||
}
|
||||
}, [setSize]);
|
||||
window.visualViewport?.removeEventListener('resize', scheduleSetSize);
|
||||
if (rafRef.current !== null) {
|
||||
window.cancelAnimationFrame(rafRef.current);
|
||||
rafRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return windowSize;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user