skip to Main Content

I am currently using React and have run into a problem regarding states.

Consider a callback function testComponent, which, when a button is pressed, renders a new component each time.

Within this component, there will be select and input elements, and based on what is selected, the input element must also update.

The select element will display the name of the object, and the input element will display its ID.

All of the selectedOptions will start off with a default value of options[0] which happens in a useEffect.

This is a recreation of my problem just using the word test instead so that its easier to understand (less context needed). The idea is that every time the select element changes, it should also update the input value.

export default function CreateTestComponent() {
  const options: any[] = [
    {
      id: 1,
      name: "Test 1"
    },
    {
      id: 2,
      name: "Test 2",
    },
    {
      id: 3,
      name: "Test 3"
    }
  ]

  const [selectedOptions, setSelectedOptions] = useState<any[]>([options[0]]);
  const [testComponentIndex, setTestComponentIndex] = useState<number>(0);
  const [components, setComponents] = useState<any[]>([]);

  useEffect(() => {
    setComponents([testComponent(0)]);
    setTestComponentIndex((old) => old + 1);

    for (let i = 0; i < options.length; i++) {
      setSelectedOptions((old) => {
        const temp = [...old];
        temp[i] = options[0];
        return temp
      })
    }
  }, [])

  const testComponent = (index: number) => {
    return (
      <div className="flex flex-row gap-5" id={`${index}`}>
        <select
          onChange={((e) => {
            const id = e.target.value;
            setSelectedOptions((old) => {
              const temp = [...old]
              temp[index] = options.filter((option) => option.id == id)[0];
              return temp;
            })
          })}>
          {options.map((option, index: number) => {
            return (
              <option key={index} value={option.id}>
                {option.name}
              </option>
            );
          })}
        </select>
        <input readOnly value={selectedOptions[index].id} />
      </div>
    )
  }

  return (
    <>
      <button type="button" onClick={() => {
        setTestComponentIndex((old) => old + 1)
        setComponents([...components, testComponent(testComponentIndex)]);
      }} className="bg-black text-white rounded px-3 py-1">
        Add a Component
      </button>
      <div>
        <h1>Component testing!</h1>
        <div>
          <ul className="list-none">
            {components.map((component: any, index: number) => {
              return (
                <li key={index}>
                  <div className="flex flex-row gap-5">
                    {component}
                  </div>
                </li>
              )
            })}
          </ul>
        </div>
      </div>
    </>
  )
}

This code will work in a .tsx file.

As you can see, the state is updating, but the input element doesn’t update. I have done an annoying amount of research trying to figure out what is happening, and I am pretty sure that because it is inside a callback function in which the state is not continuously updated (which would, in turn, trigger a component rerender).

I tried doing a bunch of things to get around this obstacle. Namely, I tried useRef, but it doesn’t have the capability of rerendering, which only useState appears to do.

I went through a bunch of other things but none of them got around this problem because none of them combatted the problem that is that the state is not the same outside versus inside the callback function.

If there is no way around having an up-to-date state within a callback function, what are some other alternatives I could try so that I could still have the ability of pressing a button and generating a new instance of a component each time?

2

Answers


  1. I did some changes and it should work fine. Changes are :-

    • separated TestComponent as a child component with props for index, selectedOption, and a handleSelectChange function.

    • handleSelectChange updates the correct option in the state by mapping through the current options.

    • AddComponent adds a new instance of TestComponent to the list, maintaining state synchronization.

    try it and let me know if it works for you 🙂

    import React, { useState, useEffect } from 'react';
    
    interface Option {
      id: number;
      name: string;
    }
    
    const options: Option[] = [
      { id: 1, name: 'Test 1' },
      { id: 2, name: 'Test 2' },
      { id: 3, name: 'Test 3' },
    ];
    
    const TestComponent = ({ index, selectedOption, handleSelectChange }: { index: number; selectedOption: Option; handleSelectChange: (index: number, id: number) => void }) => {
      return (
        <div className="flex flex-row gap-5" id={`${index}`}>
          <select
            value={selectedOption.id}
            onChange={(e) => handleSelectChange(index, parseInt(e.target.value))}
          >
            {options.map((option, idx) => (
              <option key={idx} value={option.id}>
                {option.name}
              </option>
            ))}
          </select>
          <input readOnly value={selectedOption.id} />
        </div>
      );
    };
    
    export default function CreateTestComponent() {
      const [selectedOptions, setSelectedOptions] = useState<Option[]>([options[0]]);
      const [components, setComponents] = useState<JSX.Element[]>([]);
    
      useEffect(() => {
        setComponents([<TestComponent key={0} index={0} selectedOption={options[0]} handleSelectChange={handleSelectChange} />]);
      }, []);
    
      const handleSelectChange = (index: number, id: number) => {
        setSelectedOptions((old) =>
          old.map((opt, idx) => (idx === index ? options.find((option) => option.id === id)! : opt))
        );
      };
    
      const addComponent = () => {
        setSelectedOptions([...selectedOptions, options[0]]);
        setComponents((old) => [
          ...old,
          <TestComponent key={old.length} index={old.length} selectedOption={options[0]} handleSelectChange={handleSelectChange} />,
        ]);
      };
    
      return (
        <>
          <button type="button" onClick={addComponent} className="bg-black text-white rounded px-3 py-1">
            Add a Component
          </button>
          <div>
            <h1>Component testing!</h1>
            <div>
              <ul className="list-none">
                {components.map((component, index) => (
                  <li key={index}>
                    <div className="flex flex-row gap-5">{component}</div>
                  </li>
                ))}
              </ul>
            </div>
          </div>
        </>
      );
    }
    
    Login or Signup to reply.
  2. You are storing JSX into your React state which leads to stale Javascript Closures. You should only store your data into state then map it to JSX when rendering. You are also over-complicating the logic a bit with the multiple states to represent the "data" and what is added, etc. It can be reduced to a single state.

    Example Refactor:

    • Move the static options data outside the React tree
    • You are using Typescript, so you should really actually use it and create usefule types/interfaces
    • Use a single selectedOptions state that is the array of objects with id and name properties you are rendering.
    • Add a GUID property to the data so they are always uniquely identifiable
    • Use the state updates to shallow copy the current state that you are updating and returning for the next state value
    • When adding new "components" create a new data object that represents the component/selected option you want to render
    import { useState } from "react";
    import { nanoid } from "nanoid";
    
    interface Option {
      id: number;
      name: string;
    }
    
    interface SelectedOption extends Option {
      _id: string;
    }
    
    const options: Option[] = [
      { id: 1, name: "Test 1" },
      { id: 2, name: "Test 2" },
      { id: 3, name: "Test 3" },
    ];
    
    const createOption = (): SelectedOption => ({
      _id: nanoid(),
      ...options[0],
    });
    
    function CreateTestComponent() {
      const [selectedOptions, setSelectedOptions] = useState<SelectedOption[]>([
        createOption(),
      ]);
    
      const renderOptions = (option: SelectedOption) => {
        return (
          <div className="flex flex-row gap-5" id={`${option._id}`}>
            <select
              onChange={(e) => {
                const id = Number(e.target.value);
                setSelectedOptions((selectedOptions) =>
                  selectedOptions.map((selectedOption) =>
                    selectedOption._id === option._id
                      ? { ...selectedOption, id }
                      : selectedOption
                  )
                );
              }}
            >
              {options.map((option) => (
                <option key={option.id} value={option.id}>
                  {option.name}
                </option>
              ))}
            </select>
            <input readOnly value={option.id} />
          </div>
        );
      };
    
      return (
        <>
          <button
            type="button"
            onClick={() => {
              setSelectedOptions((selectedOptions) =>
                selectedOptions.concat(createOption())
              );
            }}
            className="bg-black text-white rounded px-3 py-1"
          >
            Add a Component
          </button>
          <div>
            <h1>Component testing!</h1>
            <div>
              <ul className="list-none">
                {selectedOptions.map((option: SelectedOption) => {
                  return (
                    <li key={option._id}>
                      <div className="flex flex-row gap-5">
                        {renderOptions(option)}
                      </div>
                    </li>
                  );
                })}
              </ul>
            </div>
          </div>
        </>
      );
    }
    

    Edit modest-edison

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search