Search code examples
javascriptreactjstypescriptnext.jsscope

Next.JS ReferenceError variable is not defined


Inside server-side page.tsx I have client-side component SelectType.tsx. And should work like this:

  1. User changes value of select component
  2. It triggers function inside page.tsx
  3. The function should change object called downloadParams, but it doesn't...

Here is code page.tsx

import {SelectType} from "@/components/SelectType";

export default async function DownloadWithParams ({ params }: { params: { slug: string } }) {

    let downloadParams: DlParameters = {}

    const updateDlParams = async (newParams: DlParameters) => {
        "use server"
        downloadParams = newParams
        console.log(downloadParams)
    }

    return(
        <div className="bg-white w-[80%] min-h-[500px] rounded-2xl shadow-lg p-10 text-zinc-800">
            <div className="parameters-container w-full px-[100px]">
                <form className="flex">
                    <div className="choosetype-container mr-10 grow-[1] flex flex-col">
                        <label className="font-semibold block mb-2">Choose a type:</label>
                        <SelectType dlParams={downloadParams}
                                    updateDlParams={updateDlParams}
                        />
                    </div>
                </form>
            </div>
        </div>
    );
}

SelectType.tsx

'use client'

import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select";
import {DlParameters} from "@/types";

type HandleFnc = (newParams: DlParameters) => void

export function SelectType({dlParams, updateDlParams} : {dlParams : DlParameters, updateDlParams : HandleFnc}) {
    return (
        <Select defaultValue={dlParams?.type || ""}
                onValueChange={async (value: "video" | "audio") => {
                    dlParams.type = value || undefined;
                    await updateDlParams(dlParams);
                }}>
            <SelectTrigger className="grow border-2 border-gray-300 rounded-2xl font-semibold">
                <SelectValue placeholder="Type" />
            </SelectTrigger>
            <SelectContent className="font-semibold" >
                <SelectItem value="video">Video</SelectItem>
                <SelectItem value="audio">Audio</SelectItem>
            </SelectContent>
        </Select>
    );
}

Error message


Error: downloadParams is not defined

Source
src\app\[slug]\page.tsx (25:9) @ downloadParams

  23 |     const updateDlParams = async (newParams: DlParameters) => {
  24 |         "use server"
> 25 |         downloadParams = newParams
     |         ^
  26 |         console.log(downloadParams)
  27 |     }
  28 |

I just wanna understand why variable is out of the function`s scope and how to make it works


Solution

  • First of all, you can't change a variable value this way, declaring ordinary variables inside a react component and trying to change their value isn't possible, but you can use react state hook useState to create a variable that can be changed by calling some events, for example in your case you'll need to inlude "use client" (opt-in to using client-side rendering) to use react state: PS: Since your page.tsx is a client component you don't really need to specify use client in the SelectType.tsx component as it will definitely be a client component.

    // page.tsx
    "use client";
    import {SelectType} from "@/components/SelectType";
    
    interface DownloadPageProps {
       params: {
          slug: string
       }
    }
    
    export default async function DownloadWithParams ({ params }: DownloadPageProps) {
    
        const [downloadParams,setDownloadParams] = useState<DlParameters>({})
    
        // You should not use server action here, this is a very basic use case that does not require server actions at all.
        const updateDlParams = (newParams: DlParameters) => {
            // this will update the state
            setDownloadParams(newParams)
        }
    
        return(
            <div className="bg-white w-[80%] min-h-[500px] rounded-2xl shadow-lg p-10 text-zinc-800">
                <div className="parameters-container w-full px-[100px]">
                    <form className="flex">
                        <div className="choosetype-container mr-10 grow-[1] flex flex-col">
                            <label className="font-semibold block mb-2">Choose a type:</label>
                            <SelectType dlParams={downloadParams}
                                        updateDlParams={updateDlParams}
                            />
                        </div>
                    </form>
                </div>
            </div>
        );
    }
    
    // SelectType.tsx
    
    /* ...importing modules */
    
    interface SelectTypeProps {
       dlParams : DlParameters, 
       updateDlParams : (newParams: DlParameters) => void
    }
    
    export function SelectType({dlParams, updateDlParams} : SelectTypeProps) {
        return (
            <Select defaultValue={dlParams?.type || ""}
                    onValueChange={(value: "video" | "audio") => {
                        updateDlParams({ ...dlParams, type: value });
                    }}>
                <SelectTrigger className="grow border-2 border-gray-300 rounded-2xl font-semibold">
                    <SelectValue placeholder="Type" />
                </SelectTrigger>
                <SelectContent className="font-semibold" >
                    <SelectItem value="video">Video</SelectItem>
                    <SelectItem value="audio">Audio</SelectItem>
                </SelectContent>
            </Select>
        );
    }
    

    PS: If you don't want your whole page to be rendered client side, you can create a wrapper for your form (e.g. DlParamsForm.tsx) and include "use client" there along with state logic, i'd highly recommend this approach.