reactjstypescriptvisual-studio-codenext.js

NextJS "is not a function" error when trying to import custom hook


I'm new to NextJS and I'm trying to create a website for which I've started with a navigation bar component, which I want to make sure it's responsive based on the viewport dimensions. Currently I have this folder structure: enter image description here

Where I'm attaching a context from src/context/ViewportContext.tsx into the main layout at src/app/layout.tsx, as follows:

import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { ViewportProvider } from '@/context/ViewportContext';

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <ViewportProvider>
          {children}
        </ViewportProvider>
      </body>
    </html>
  );
}

The ViewportContext.tsx file looks like this:

'use client';

import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';

type ViewportContextType = {
  width: number;
  height: number;
};

const ViewportContext = createContext<ViewportContextType | undefined>(undefined);

export const ViewportProvider = ({ children }: { children: ReactNode }) => {
  const [size, setSize] = useState<ViewportContextType>({
    width: 0,  // Initialize with 0, because the actual value will be set on the client
    height: 0,
  });

  useEffect(() => {
    if (typeof window !== 'undefined') {
      const handleResize = () => {
        setSize({
          width: window.innerWidth,
          height: window.innerHeight,
        });
      };

      // Sets initial size
      handleResize();

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

  return (
    <ViewportContext.Provider value={size}>
      {children}
    </ViewportContext.Provider>
  );
}

export const useViewport = () => {
  const context = useContext(ViewportContext);
  if (!context) {
    throw new Error('useViewport must be used within a ViewportProvider');
  }
  return context;
}

so as you see, it also has a useViewport hook which I'm planning to use to obtain the viewport dimensions and work with them elsewhere. My main page file, at src/app/page.tsx currently looks like this:

import NavBar from '@/components/NavBar';

export default function Home() {
  return (
    <>
      <NavBar />
    </>
  );
}

and the NavBar component I'm specifying which is at src/components/NavBar/index.tsx looks like this:

import Image from 'next/image';
import styles from './NavBar.module.css';
import useDeviceType from '@/utils/useDeviceType';

const NavBar: React.FC = () => {
  const { isTabletOrSmaller, isDesktopOrLarger } = useDeviceType();

  console.log(isTabletOrSmaller);

  return (
    <div className={styles.container}>
      <Image
        alt='GreenDream logo'
        src='/images/logo.svg'
        width={162}
        height={46}
      />
      <Image
        alt='Sandwhich'
        className={styles.sandwhich}
        src='/images/sandwhich.svg'
        width={36}
        height={32}
      />
    </div>
  )
}

export default NavBar;

For now, I'm trying to test the functionality of the hook, useDeviceType which is at src/utils/useDeviceType.tsx:

import { useViewport } from '@/context/ViewportContext';

const useDeviceType = () => {
  const { width } = useViewport();

  const isMobile = width <= 768;
  const isTablet = width > 768 && width <= 1024;
  const isDesktop = width > 1024 && width <= 1440;
  const isLargeDesktop = width > 1440;
  const isTabletOrSmaller = width <= 1024;
  const isDesktopOrLarger = width > 1024;

  return { 
    isMobile,
    isTablet,
    isDesktop,
    isLargeDesktop,
    isTabletOrSmaller,
    isDesktopOrLarger,
  };
}

export default useDeviceType;

If all worked correctly, it should simply print true if my viewport is smaller than 1024px, but as I run I get this error, useViewport is not a function:

enter image description here

I'm not really sure why it seems that the import doesn't work for the useViewport hook, as another function in that file imports correctly in layout.tsx. Also, my tsconfig.json currently looks like this:

{
  "compilerOptions": {
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

As far as I understand, the line "@/*": ["./src/*"] should allow to import anything from the src folder so long as I define its import directory as @/ right? So what could be causing this issue? Is there a configuration I forgot to add? If anyone knows or has a insight on this please let me know.


Solution

  • React hooks must be used within client components. Converting the Nav component to a client component using the "use client" directive at the top of src/components/NavBar/index.tsx worked for me:

    working code