Skip to content

Instantly share code, notes, and snippets.

@alexanderson1993
Created May 30, 2024 13:02
Show Gist options
  • Save alexanderson1993/3841debccfcf0aef883fa129edb5596c to your computer and use it in GitHub Desktop.
Save alexanderson1993/3841debccfcf0aef883fa129edb5596c to your computer and use it in GitHub Desktop.
Refactor this custom React Hook

React Hook Refactor

I can think of at least three ways this hook can be refactored which makes it easier to read and more performant. This can be done without adding new dependencies or changing the signature of the context provider or hook, so it only refactors the internal behavior.

I'd love to see how anyone else would refactor this.

import {
createContext,
useContext,
useState,
ReactNode,
useEffect,
useMemo,
} from "react";
import { throttle } from "lodash";
const breakpoints = {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
}
type ViewportContextType = {
width: number;
height: number;
isMobile: boolean;
isTablet: boolean;
isDesktop: boolean;
isLessThanMedium: boolean;
};
const ViewportContext = createContext<ViewportContextType>({
width: 0,
height: 0,
isMobile: true,
isTablet: false,
isDesktop: false,
isLessThanMedium: false,
});
// The result of this function will only be useful if it's in pixels. It would
// not be a useful value if the unit is e.g. `%`, `em`, etc.
function stripUnit(value: string | number): number {
if (typeof value === "number") {
return value;
}
return parseFloat(value);
}
function getIsMobile(width: number) {
return width <= stripUnit(breakpoints.sm);
}
function getIsTablet(width: number) {
return (
width > stripUnit(breakpoints.sm) && width <= stripUnit(breakpoints.lg)
);
}
function getIsDesktop(width: number) {
return width > stripUnit(breakpoints.lg);
}
function getIsLessThanMedium(width: number) {
return width < stripUnit(breakpoints.md);
}
export const ViewportProvider = ({ children }: { children: ReactNode }) => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
const [isMobile, setIsMobile] = useState(getIsMobile(width));
const [isDesktop, setIsDesktop] = useState(getIsDesktop(width));
const [isTablet, setIsTablet] = useState(getIsTablet(width));
const [isLessThanMedium, setIsLessThanMedium] = useState(
getIsLessThanMedium(width)
);
useEffect(() => {
// Set the size on the initial load. We need to do this here because we only
// want it to run on the client.
setWidth(window.innerWidth);
setHeight(window.innerHeight);
const handleWindowResize = throttle(
() => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
},
250,
{ trailing: true }
);
window.addEventListener("resize", handleWindowResize);
return () => window.removeEventListener("resize", handleWindowResize);
}, [setWidth, setHeight]);
useEffect(() => {
setIsMobile(getIsMobile(width));
setIsTablet(getIsTablet(width));
setIsDesktop(getIsDesktop(width));
setIsLessThanMedium(getIsLessThanMedium(width));
}, [width]);
return (
<ViewportContext.Provider
value={{
width,
height,
isMobile,
isTablet,
isDesktop,
isLessThanMedium,
}}
>
{children}
</ViewportContext.Provider>
);
};
export const useViewport = () => {
const { width, height, isMobile, isTablet, isDesktop, isLessThanMedium } =
useContext(ViewportContext);
return useMemo(
() => ({ width, height, isMobile, isTablet, isDesktop, isLessThanMedium }),
[width, height, isMobile, isTablet, isDesktop, isLessThanMedium]
);
};
@vasilionjea
Copy link

vasilionjea commented May 30, 2024

https://stackblitz.com/edit/vitejs-vite-db9krs?file=src%2FViewportProvider.tsx

import {
  createContext,
  useContext,
  useState,
  useEffect,
  useMemo,
  PropsWithChildren,
} from 'react';
import throttle from 'lodash.throttle';

type ViewportContextType = {
  width: number;
  height: number;
  isMobile: boolean;
  isTablet: boolean;
  isDesktop: boolean;
  isLessThanMedium: boolean;
};

const ViewportContext = createContext<ViewportContextType>({
  width: 0,
  height: 0,
  isMobile: true,
  isTablet: false,
  isDesktop: false,
  isLessThanMedium: false,
});

const breakpoints = {
  sm: '640px',
  md: '768px',
  lg: '1024px',
  xl: '1280px',
};

// The result of this function will only be useful if it's in pixels. It would
// not be a useful value if the unit is e.g. `%`, `em`, etc.
function stripUnit(value: string) {
  if (!Number.isNaN(value) && typeof value === 'number') {
    return value;
  }
  return parseFloat(value);
}

function getDeviceTests(width: number) {
  const { sm, md, lg } = breakpoints;
  return {
    isMobile: width <= stripUnit(sm),
    isDesktop: width > stripUnit(lg),
    isTablet: width > stripUnit(sm) && width <= stripUnit(lg),
    isLessThanMedium: width < stripUnit(md),
  };
}

export const ViewportProvider = ({ children }: PropsWithChildren) => {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const deviceTests = useMemo(() => getDeviceTests(width), [width]);

  useEffect(() => {
    // Set the size on the initial load. We need to do this here because we only
    // want it to run on the client.
    setWidth(window.innerWidth);
    setHeight(window.innerHeight);

    const handleWindowResize = throttle(
      () => {
        setWidth(window.innerWidth);
        setHeight(window.innerHeight);
      },
      250,
      { trailing: true }
    );

    window.addEventListener('resize', handleWindowResize);
    return () => window.removeEventListener('resize', handleWindowResize);
  }, []);

  return (
    <ViewportContext.Provider value={{ width, height, ...deviceTests }}>
      {children}
    </ViewportContext.Provider>
  );
};

export const useViewport = () => {
  const { width, height, isMobile, isTablet, isDesktop, isLessThanMedium } =
    useContext(ViewportContext);

  return useMemo(
    () => ({ width, height, isMobile, isTablet, isDesktop, isLessThanMedium }),
    [width, height, isMobile, isTablet, isDesktop, isLessThanMedium]
  );
};

@vasilionjea
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment