Home > Enterprise >  React js I have to click two times on search button to get result from api and my UI get changed
React js I have to click two times on search button to get result from api and my UI get changed

Time:11-30

When I click on the search button I send a POST request and get data from the server. The thing is the first time it returns an empty array based on my console log report, but when I click on the button for the second time it has the values from the server and my UI changes. Why does this happen? is it because async-await process? what should I do to solve this problem?

export default function Home() {
  const [searchTerm, setSearchTerm] = useState('');
  const obj = { searchTerm };
  const [receivedData, setReceivedData] = useState([]);
  const [episList, setEpisList] = useState([]);
  const [isPlaying, setIsPlaying] = useState(true);
  const [currentUrlIndex, setCurrentUrlIndex] = useState(0);

  const options = {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(obj),
  };

  const submitSearch = async () => {
    const response = await fetch('http://localhost:3001/', options);
    const data = await response.json();

    //COMMENT--- Manipulating and using the result I get from server
    setReceivedData(data.body.result);

    const epName = receivedData.map((x) => {
      return x.name;
    });

    setEpisList(epName);
    console.log(epName);
  };

  return (
    <>
      <main>
           <div>
                <ReactPlayer
                  controls={true}
                  url={`videos/${episList[currentUrlIndex]}.mkv`}
                />
              </div>
            )}
          </div>
        </div>
        <div>
          <SearchBox
            value={searchTerm}
            onChange={(e) => {
              setSearchTerm(e.target.value);
            }}
          />
          <SearchButton onClick={submitSearch} />
        </div>
      </main>
    </>
  );
}

CodePudding user response:

Your issue lies in the following code:

//COMMENT--- Manipulating and using the result I get from server
setReceivedData(data.body.result);

const epName = receivedData.map((x) => {
  return x.name;
});

setReceivedData(data.body.result) will update receivedData on the next render. The current execution context still uses the old (stale) value. Meaning that epName will lack behind, because it is based on the old receivedData.

So how do you solve this? Quite simple, store the new value in a variable.

// create a shadow variable that holds the new value
const receivedData = data.body.result;
setReceivedData(receivedData);

const epName = receivedData.map((x) => {
  return x.name;
});

The above creates a new variable receivedData and assigns it the new value. This does not update the state, so we still need to call setReceivedData(receivedData) which update the component state. After that we use this variable instead of the state.


If you don't quite understand shadow variables, imagine the above code like this:

// create a variable that holds the new value
const newReceivedData = data.body.result;
setReceivedData(newReceivedData);

const epName = newReceivedData.map((x) => {
  return x.name;
});

A shadow variable does the exact same, but uses a name that is already defined in an outer scope.

CodePudding user response:

So, when you set a react state, you schedule a rerender. When that happens, the body of your function(-al component) is going to run again, recreating all variables, including submitSearch where you store your function.

submitSearch closes over several references to variables who's "content" changes every time that function is redefined (every render), and makes several calls to set state. I think stuff like this is best avoided, since it can be a little bit annoying to debug.

Consider letting submitSearch have a single responsibility: to retrieve backend data to a state.

const submitSearch = async () => {
    const response = await fetch('http://localhost:3001/', options);
    const data = await response.json();
    setReceivedData(data.body.result);
}

Then you can generate side effects on that state

React.useEffect(() => {
    setEpisList(receivedData?.map((x) => x.name)
},[receivedData, setEpisList])

But, episList is really just a nice-to-have subset of receivedData, you could redefine it per render, without having a state for it.

const episList = receivedData?.map((x) => x.name)

If you want to take it a step further, consider relocating things out of the component, and consider errors


const myLocalhostApi = {
    get: (options) => fetch('http://localhost:3001/', options).then(r => r.json()
}

const MyComponent() {
    const [receivedData, setReceivedData] = React.useState([])

    const options = // options

    const handleSubmit = myLocalhostApi.get(options)
        .then(setReceivedData)
        .catch(myErrorHandler)
    
    const episList = receivedData.map((x) => x.name)

...

CodePudding user response:

Essentially you are trying to update in synchronous time a variable with asynchronous data. To solve you problem you have to use then after the fetch statement. You have to rewrite your submitSearch function like that:

const submitSearch = () => {
    fetch('http://localhost:3001/', options)
    .then(response => response.json())
    .then(result => {
            setReceivedData(result);
            const epName = receivedData.map((x) => {
               return x.name;
            });

            setEpisList(epName);
            console.log(epName);
         })    
};

In this way after the fetch, if everything is ok, you enter in the then and there you update your state.

  • Related