drop-down-menusignalspreactpreact-signal

How to set the default value of a signal as the default value of an option element in an HTML select element?


I'm using Fresh/Preact. In an HTML select element, I want 2 things:

Attempt 1: simple setup

import { signal } from "@preact/signals";

const pet = signal(undefined)
function Pets() {
  return (
    <>
      <select value={pet.value} onChange={e => pet.value = e.target.value} >
        <option selected value="dog">Dog</option>
        <option value="cat">Cat</option>
        <option value="hamster">Hamster</option>
      </select>
      {pet.value} 
    </>
  );
}

render(<Pets />, document.getElementById("app"));

The default selected option isn't got initially. As the options are generated, I cannot specify the default value of the signal. Playground

Attempt 2: generating the options and select default value via a loop

import { signal } from "@preact/signals";

const pet = signal(undefined)
const arr = ['Dog', 'Cat', 'Hamster'] 

function Pets() {
    const options = [] 
    for (const item of arr) {
        if (arr.indexOf(item) === 0){
            options.push(<option selected value={item}>{item}</option>)
            pet.value = item
        }  else {
            options.push(<option value={item}>{item}</option>)
        } 
    } 
    return (
    <>
      <select value={pet.value} onChange={e => pet.value = e.target.value} >
        {options}
      </select>
      {pet.value} 
    </>
  );
}

render(<Pets />, document.getElementById("app"));

The select element freezes, I can't select other options. Playground

I think it's because the code is inside the component, so every time it renders the default value is reset.

Attempt 3: Moving the loop outside of the component

import { signal } from "@preact/signals";

const pet = signal(undefined)
const arr = ['Dog', 'Cat', 'Hamster'] 
const options = [] 
for (const item of arr) {
    if (arr.indexOf(item) === 0){
        options.push(<option selected value={item}>{item}</option>)
        pet.value = item
    }  else {
        options.push(<option value={item}>{item}</option>)
    } 
} 
function Pets() {

    return (
    <>
      <select value={pet.value} onChange={e => pet.value = e.target.value} >
                {options}
      </select>
      {pet.value} 
    </>
  );
}

render(<Pets />, document.getElementById("app"));

This fixes the problem. Nevertheless it still leaves me two unsolved questions:


Solution

  • I think it's because the code is inside the component, so every time it renders the default value is reset.

    No, the issue is that you skipped a rather key part of my comment (and therefore have a fundamental rendering flaw): you set pet.value to the first item upon every rerender. Once you rerender the component, you're setting it again. That's why we need the if (typeof pet.value == 'undefined'), we only set the value if this is the first run and the value is undefined still. On subsequent runs, we skip this.

    Here's how you could make that work:

    import { signal } from "@preact/signals";
    
    const pet = signal(undefined)
    const arr = ['Dog', 'Cat', 'Hamster'] 
    
    function Pets() {
      const options = arr.map((item, idx) => {
        if (typeof pet.value == 'undefined' && idx == 0) pet.value = item;
        return <option selected={pet.value == item} value={item}>{item}</option>;
      });
    
      return (
        <>
          <select value={pet.value} onChange={e => pet.value = e.target.value} >
            {options}
          </select>
          {pet.value} 
        </>
      );
    }
    
    render(<Pets />, document.getElementById("app"));
    

    In my understanding the signal won't re-render the whole component but just the text.

    Signals can skip rerendering, but not in your case. You're accessing the signal within the loop (and the component), so upon signal change, your component must rerender. Additionally, with your API (generating a list of options, one with selected), that has to run every time the signal value changes. There's simply no way around that.

    Inspecting the result I see that there is no selected option

    Preact sets the property, not the attribute. In Firefox, you can right click on a DOM element and select "Show DOM properties" to get the full read out, and in this, you'll see selected: true. I'm sure Chrome has some sort of equivilant.

    DOM properties and HTML attributes are related, but ultimately, two different things.


    You didn't ask, but I'd try instead use a computed in your third example, like this:

    import { signal, computed } from "@preact/signals";
    
    const pet = signal(undefined)
    const arr = ['Dog', 'Cat', 'Hamster']
    
    const options = computed(() =>
      arr.map((item, idx) => {
        if (typeof pet.value == 'undefined' && idx == 0) pet.value = item;
        return <option selected={pet.value == item} value={item}>{item}</option>;
      })
    );
    
    function Pets() {
      return (
        <>
          <select value={pet.value} onChange={e => pet.value = e.target.value} >
            {options}
          </select>
          {pet.value} 
        </>
      );
    }
    
    render(<Pets />, document.getElementById("app"));