Search code examples
javascriptreactjsreact-hooksreact-propsuse-state

useState sending props only in the second onClick, with a string at the first onClick, why?


When setting useState const [searchString, setString] = useState([]) with a string setString("some string") it will be sent it immediately and shown at the other component <Home/>,
BUT when I send an array it will be stated settingResults(resultArray) only after the second onClick here.
I tried settingResults([...resultArray])- its the same.

All functions checked with console.log().

Header.jsx

function Header({ settingResults }) {
  const [productsObj, setObjs] = useState([]);
  const [searchString, setString] = useState('');
  let resultArray = [];


  const onChangHandler = (e) => {
    setString(e.target.value);
  };

  const activeSearch = () => {
    if (searchString.length > 0) {
      resultArray = productsObj.filter((obj) =>
        obj.productName.toLowerCase().includes(searchString.toLowerCase())
      );
      if (resultArray.length !== 0) {

        settingResults(resultArray);
      }
    }
    resultArray = [];
  };

  return (
    <div>
      <header className='header-shop'>
        Welcome to Vitamins Store
        <br />
        <input
          placeholder='Search here'
          value={searchString}
          onChange={(e) => onChangHandler(e)}
        />
        <button onClick={activeSearch}>Search</button>
      </header>
    </div>
  );
}

App.js

function App() {
  const [searchString, setString] = useState([]);

  return (
    <div className='App'>
      <Header settingResults={setString} />
      <Home searchResults={searchString} />
      <Footer />
    </div>
  );
}

Home.jsx

function Home({ searchResults }) {
  const [itemSearchResults, setResults] = useState([]);
  const [viewResults, setViewResult] = useState(null);
  let itemsFound = [];

  useEffect(() => {
    setResults(searchResults);
    itemsFound = itemSearchResults.map((productObj) => {
      return (
        <div key={productObj.id}>
          {productObj.productName} <br />
          {productObj.price}
          <br />
          <img src={productObj.image} />
        </div>
      );
    });
    setViewResult(itemsFound);
  }, [searchResults]);

  return (
    <div>
      <h3>Home</h3>
      <h1>{viewResults}</h1>
    </div>
  );
}

Anyone know why useState won't work first time with an object-array ?


Solution

  • The issue with your code are these two lines:

        setResults(searchResults);
        itemsFound = itemSearchResults...
    

    Calling setResults does not immediately change itemSearchResults. Instead, it queues a change to happen after the current render. But this code won't get called again until searchResults changes again.

    Why are you using itemSearchResults at all? It seems like you could just use searchResults, like the following minimal change: (but see below for better answers)

    function Home({ searchResults }) {
      const [viewResults, setViewResult] = useState(null);
    
      useEffect(() => {
        const itemsFound = searchResults.map((productObj) => {
          return (
            <div key={productObj.id}>
              {productObj.productName} <br />
              {productObj.price}
              <br />
              <img src={productObj.image} />
            </div>
          );
        });
        setViewResult(itemsFound);
      }, [searchResults]);
    
      return (
        <div>
          <h3>Home</h3>
          <h1>{viewResults}</h1>
        </div>
      );
    }
    

    This code would also be better written with useMemo:

    function Home({ searchResults }) {
      const viewResults = useMemo(() => {
        return searchResults.map((productObj) => {
          return (
            <div key={productObj.id}>
              {productObj.productName} <br />
              {productObj.price}
              <br />
              <img src={productObj.image} />
            </div>
          );
        });
      }, [searchResults]);
    
      return (
        <div>
          <h3>Home</h3>
          <h1>{viewResults}</h1>
        </div>
      );
    }
    

    As pointed out in the comments, it's dangerous to memoize in this way, and you'd be better off directly rendering like so:

    function Home({ searchResults }) {
      return (
        <div>
          <h3>Home</h3>
          <h1>
            {searchResults.map((productObj) =>
              <div key={productObj.id}>
                {productObj.productName} <br />
                {productObj.price}
                <br />
                <img src={productObj.image} />
              </div>
            )}
          </h1>
        </div>
      );
    }
    
    // This will guarantee re-rendering only when the props change.
    Memo = React.memo(Memo);