reactjsnext.jsreact-contextcreatecontext

createContext took default value instead of passed value


I'm creating a context that allows child components to setTheme. But the context when imported and used by useContext returned the default value instead of the passed value. How do I fix this? What this I do wrong? What is going on?

Bad English, really sorry if this sounds rude.

_document.tsx

type ThemeContextType = [string, React.Dispatch<React.SetStateAction<string>>]

export const ThemeContext: React.Context<ThemeContextType> = createContext([
    "light",
    (value: string | ((prevState: string) => string)): void => {
        console.log("default")
    },
])

/** create a context for theme and set theme */
export default function Document(): JSX.Element {
    const [theme, setTheme] = useState("light")

    return (
        <Html lang="vi">
            <Head>
                <meta charSet="utf-8" />
                <meta
                    name="viewport"
                    content="width=device-width, initial-scale=1"
                />
                <meta
                    name="theme-color"
                    content="#000000"
                />
                <link
                    rel="manifest"
                    href="/manifest.json"
                />
                <link
                    rel="shortcut icon"
                    href="/favicon.ico"
                />
            </Head>

            <body data-theme={theme}>
                <ThemeContext.Provider value={[theme, setTheme]}>
                    <Main />
                </ThemeContext.Provider>
                <NextScript />
            </body>
        </Html>
    )
}

students-info.tsx

export default function StudentsInfo({
    capturing,
    setCapturing,
}: {
    capturing: boolean
    setCapturing: React.Dispatch<React.SetStateAction<boolean>>
}): JSX.Element {
    const [theme, setTheme] = useContext(ThemeContext)
    const data: Student[] = useMemo((): Student[] => students, [])
    const table: Table<Student> = useReactTable({
        data,
        columns,
        getCoreRowModel: getCoreRowModel(),
    })

    useRenderEffect((): void => {
        if (!capturing) return

        const dom: HTMLElement | null = document.getElementById("students")
        if (!dom) return

        /** current theme */
        const theme_: string = theme
        setTheme("light")

        /** fit dom into jspdf and save */
        const pdf: jsPDF = new jsPDF("p", "pt", "a4")

        pdf.html(dom, {
            callback: (pdf: jsPDF): void => {
                pdf.save("students.pdf")

                setTheme(theme_)

                setCapturing(false)
            },
        })
    }, [capturing])

    return (
        <table id="students">
            <TableHead table={table} />
            <TableBody table={table} />
        </table>
    )
}

Solution

  • You should not use the context provider in the _document.tsx. This file is only used by your server to serialize the HTML file and should contain only static content. In order to have the above context provided to all of your pages, you have to wrap your pages using the _app.tsx, which is used on server and client side.

    The rest of your code can stay in the _document.tsx, because it is static.

    So you would create instead an _app.tsx with content:

    import type { AppProps } from 'next/app';
    
    type ThemeContextType = [string, React.Dispatch<React.SetStateAction<string>>]
    
    export const ThemeContext: React.Context<ThemeContextType> = createContext([
        "light",
        (value: string | ((prevState: string) => string)): void => {
            console.log("default")
        },
    ])
    
    const CustomApp = ({ Component, pageProps }: AppProps) => {
      const [theme, setTheme] = useState("light")
    
      return (
        <ThemeContext.Provider value={[theme, setTheme]}>
          <Component {...pageProps} />
        </ThemeContext.Provider>
      );
    }
    
    export default CustomApp;