ui icon indicating copy to clipboard operation
ui copied to clipboard

[bug]: Select Component Value Not Persisting After Page Reload

Open adilsonx opened this issue 1 year ago • 7 comments

Describe the bug

The Select component is not maintaining its selected value after a page reload, even when the value is being saved to and retrieved from localStorage.

Affected component/components

Select

How to reproduce

1- Create a new component using the Select component from shadcn/ui. 2- Implement localStorage to save and retrieve the selected value. 3- Select a value from the dropdown. 4- Reload the page.

Expected Behavior:

The Select component should display the previously selected value after page reload.

Actual Behavior:

The Select component resets to its default state (showing the placeholder) after page reload, even though the correct value is being retrieved from localStorage.

Code Example:

"use client";

import { useEffect, useState } from "react";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

const TestComp = () => {
  const [mode, setMode] = useState<string | undefined>(undefined);

  useEffect(() => {
    const keyMode = localStorage.getItem("keyMode");
    if (keyMode) {
      setMode(keyMode);
    }
  }, []);

  useEffect(() => {
    if (mode !== undefined) {
      localStorage.setItem("keyMode", mode);
    }
  }, [mode]);

  return (
    <div>
<form>
      <Select value={mode} onValueChange={(value) => setMode(value)}>
        <SelectTrigger className="w-[180px]">
          <SelectValue placeholder="Select mode" />
        </SelectTrigger>
        <SelectContent>
          <SelectItem value="fast">Fast ⚡</SelectItem>
          <SelectItem value="quality">Quality 💎</SelectItem>
        </SelectContent>
      </Select>

      <div>Mode: {mode || "Not selected"}</div>
    </div>
  <Button type="submit">Submit</Button>
      </form>
  );
};

export default TestComp;

Codesandbox/StackBlitz link

No response

Logs

No response

System Info

Next.js version: 14.2.3
React version: 18
Browser: Chrome

Before submitting

  • [X] I've made research efforts and searched the documentation
  • [X] I've searched for existing issues

adilsonx avatar Aug 18 '24 13:08 adilsonx

Try to set the initial state to an empty string instead of undefined

Dagas16 avatar Aug 19 '24 00:08 Dagas16

Update: I believe I've identified the source of the problem. It appears to be specific to the shadcn/ui Select component when used within a form element. Here's what I've found:

When the Select component is wrapped in a form, it doesn't retrieve the value from localStorage on page reload. Removing the form wrapper allows the localStorage value to be correctly retrieved.

This behavior seems to be unique to the shadcn/ui implementation, as it doesn't occur with standard HTML select elements :

Not working :

 <form>
        <Select value={mode} onValueChange={(value) => setMode(value)}>
          <SelectTrigger className="w-[180px]">
            <SelectValue placeholder="Select mode" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="fast">Fast ⚡</SelectItem>
            <SelectItem value="quality">Quality 💎</SelectItem>
          </SelectContent>
        </Select>

      
</form>

Working (without form wrapper) :

      <Select value={mode} onValueChange={(value) => setMode(value)}>
          <SelectTrigger className="w-[180px]">
            <SelectValue placeholder="Select mode" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="fast">Fast ⚡</SelectItem>
            <SelectItem value="quality">Quality 💎</SelectItem>
          </SelectContent>
        </Select>
        

Working (basic select element in form) :

<form>
        <select value={mode} onChange={(e) => setMode(e.target.value)}>
          <option value="fast">Fast ⚡</option>
          <option value="quality">Quality 💎</option>
        </select>
</form>

adilsonx avatar Aug 19 '24 10:08 adilsonx

Can confirm, in my setup the Select component also looses the value after a reload, but I get the value from a url query parameter instead. The component is also within a form, and the problem is not reproducible with an HTML select + option list elements.

edit: adding example as well (using Remix):

function MyComponent() {
  const [searchParams] useSearchParams()
  const options = [] /* some array of strings, also includes the default value from below */
  const value = searchParams("my-value) || "default"

  return (
    <Form method="get">
        <Select name="value" defaultValue={value}>
          <SelectTrigger className="text-sm">
              <SelectValue />
          </SelectTrigger>

          <SelectContent>
              {options.map((option) => (<SelectItem key={option} value={option}>{option}</SelectItem>))}
           </SelectContent>
         </Select>
    </Form>
  )
}

As mentioned, replacing it with an HTML select fixes the initial flickering of SelectValue.

reloadedhead avatar Aug 19 '24 11:08 reloadedhead

Thank you guys for debugging the problem. I am facing the same issue right now.

frontendjulien avatar Oct 30 '24 01:10 frontendjulien

I have a similar problem. When updating page, I get the data from sessionStorage. As well options are loaded from the server. When refreshing page, everything works correctly, but when switching from another page, the value in select is not substituted and, moreover, is transferred to sessionStorage.

{warehouses && (
    <FormField
      control={control}
      name="warehouse"
      render={({ field }) => {
        console.info('warehouse control', field.value)

        return (
          <div>
            <Select
              onValueChange={field.onChange}
              value={field.value?.toString()}
              label="Склад отгрузки"
              error={errors.warehouse?.message}
            >
              <SelectTrigger>
                <SelectValue placeholder="Выберите склад" />
              </SelectTrigger>
              <SelectContent>
                {warehouses?.warehouses.map(item => (
                  <SelectItem
                    key={item.id}
                    value={item.id.toString()}
                  >
                    {item.name}
                  </SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
        )
      }}
    />
  )}

For me helps small crutch

useEffect(() => {
   const val = getValues('warehouse')
   setTimeout(() => {
     setValue('warehouse', val)
   }, 0)
 }, [])

DEATHSTROUKE avatar Jan 30 '25 08:01 DEATHSTROUKE

Another solution I found is to add a key to the select-field and increase/change it whenever you need programmatic changes done:

const [mode, setMode] = useState<string | undefined>(undefined);
const [formKey, setFormKey] = useState<number>(0);

  useEffect(() => {
    if (mode !== undefined) {
      localStorage.setItem("keyMode", mode);
      setFormKey(k => k +1)
    }
  }, [mode]);

//and in the form

<Select key={`modeselect${formKey}`}>...</Select>

99power avatar Apr 25 '25 20:04 99power