Skip to content

Instantly share code, notes, and snippets.

@vasilionjea
Forked from alexanderson1993/README.md
Created May 30, 2024 21:20
Show Gist options
  • Save vasilionjea/30e6f3d3a4bcbad5b159d1f04a53bdf0 to your computer and use it in GitHub Desktop.
Save vasilionjea/30e6f3d3a4bcbad5b159d1f04a53bdf0 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
Author

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

import {
  createContext,
  useContext,
  useState,
  PropsWithChildren,
  useEffect,
  useMemo,
} 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);
}

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

  const deviceTests = useMemo(() => {
    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),
    };
  }, [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]);

  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]
  );
};

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