1

I need to show the props value (which is a simple string). Each time I get new search results, I'm sending in the props.
At the very first render the props will always be undefined.

Edit:
Header.jsx

function Header() {
  const [searchString, setString] = useState('');
      
  const onChangHandler = (e) => {
    setString(e.target.value);
  };
  
  const activeSearch = () => {
    if (searchString.length > 0) {
      <Home searchResults={searchString} />;
    }
  };

  return (
    <div>
        <input
          placeholder='Search here'
          value={searchString}
          onChange={(e) => onChangHandler(e)}
        />
        <button onClick={activeSearch}>Search</button>
      </header>
    </div>
  );
}

I searched for previous stackoverflow questions and reactjs.org but found no answer.

Home.jsx

import React, { useEffect, useState } from 'react';

function Home({ searchResults }) {
  const [itemSearchResults, setResults] = useState([]);
  const [previousValue, setPreviousValue] = useState();

  // What function will re-render when the props are first defined or changed ? 
   useEffect(() => { // Doesn't work
    setResults(searchResults);
   }, [searchResults]);
           
  return (
    <div>
      <h3>Home</h3>
      <h1>{itemSearchResults}</h1>
    </div>
  );
}

export default Home;

App.js

function App() {
  return (
    <div className='App'>
      <Header />
      <Home />
      <Footer />
    </div>
  );
}

I'm sending the input string only to check if the props will change at the child component ("Home").

Any experts here know what's the problem?

ExtraSun
  • 528
  • 2
  • 11
  • 31
  • 1
    Are your props named `props`? If not, then you should not be destructuring it in the function arguments. – Areeb Khan Jul 04 '21 at 19:15
  • @AreebKhan My props are named "searchResults". – ExtraSun Jul 04 '21 at 19:34
  • you don't have to do anything, React will re-render on its own when props change. Though if you're [mutating the props or state (anti-pattern)](https://stackoverflow.com/a/40309023/1218980), React may be failing to see any changes when comparing previous props or state. – Emile Bergeron Jul 04 '21 at 20:05
  • That said, it's unclear what you're trying to achieve, what is not working, and how it's implemented on your side right now ([symptoms of the XY problem](https://meta.stackexchange.com/q/66377/254800)). Please include a [mcve]. – Emile Bergeron Jul 04 '21 at 20:08
  • 1
    @EmileBergeron I fixed my question, I think it's much clearer now. – ExtraSun Jul 04 '21 at 20:21
  • Thanks for taking the time to update your question, I was able to write an answer now that we have enough information, and in fact, the problem was inside `Header` rather than inside `Home`. – Emile Bergeron Jul 04 '21 at 21:41

2 Answers2

1

Why it doesn't work?

It's because the Home component is never used, even if it's included in the following snippet:

const activeSearch = () => {
  if (searchString.length > 0) {
    <Home searchResults={searchString} />;
  }
};

The activeSearch function has a couple problems:

  • it is used as an event handler though it uses JSX (outside the render phase)
  • it doesn't return the JSX (would still fail inside the render phase)

JSX should only be used within the render phase of React's lifecycle. Any event handler exists outside this phase, so any JSX it might use won't end up in the final tree.

The data dictates what to render

That said, the solution is to use the state in order to know what to render during the render phase.

function Header() {
  const [searchString, setString] = useState('');
  const [showResults, setShowResults] = useState(false);

  const onChangHandler = (e) => {
    // to avoid fetching results for every character change.
    setShowResults(false);
    setString(e.target.value);
  };

  const activeSearch = () => setShowResults(searchString.length > 0);

  return (
    <div>
        <input
          value={searchString}
          onChange={(e) => onChangHandler(e)}
        />
        <button onClick={activeSearch}>Search</button>
        {showResults && <Home query={searchString} />}
    </div>
  );
}

useEffect to trigger effects based on changing props

And then, the Home component can trigger a new search request to some service through useEffect.

function Home({ query }) {
  const [results, setResults] = useState(null);

  useEffect(() => {
    let discardResult = false;

    fetchResults(query).then((response) => !discardResult && setResults(response));

    // This returned function will run before the query changes and on unmount.
    return () => {
      // Prevents a race-condition where the results from a previous slow
      // request could override the loading state or the latest results from
      // a faster request.
      discardResult = true;

      // Reset the results state whenever the query changes.
      setResults(null);
    }
  }, [query]);

  return results ? (
    <ul>{results.map((result) => <li>{result}</li>))}</ul>
  ) : `Loading...`;
}

It's true that it's not optimal to sync some state with props through useEffect like the article highlights:

useEffect(() => {
  setInternalState(externalState);
}, [externalState]);

...but in our case, we're not syncing state, we're literally triggering an effect (fetching results), the very reason why useEffect even exists.

const { useState, useEffect } = React;

const FAKE_DELAY = 5; // seconds

function Home({ query }) {
  const [results, setResults] = useState(null);
  
  useEffect(() => {
    let queryChanged = false;
    
    console.log('Fetch search results for', query);
    
    setTimeout(() => {
      
      if (queryChanged) {
        console.log('Query changed since last fetch, results discarded for', query);
        return;
      }
      setResults(['example', 'result', 'for', query])
    }, FAKE_DELAY * 1000);
    
    return () => {
      // Prevent race-condition
      queryChanged = true;
      setResults(null);
    };
  }, [query]);
  
  return (
    <div>
      {results ? (
        <ul>
          {results.map((result) => (
            <li>{result}</li>
          ))}
        </ul>
      ) : `Loading... (${FAKE_DELAY} seconds)`}
    </div>
  );
}

function Header() {
  const [searchString, setString] = useState('');
  const [showResults, setShowResults] = useState(false);

  const onChangHandler = (e) => {
    // to avoid fetching results for every character change.
    setShowResults(false);
    setString(e.target.value);
  };

  const activeSearch = () => setShowResults(searchString.length > 0);

  return (
    <div>
        <input
          placeholder='Search here'
          value={searchString}
          onChange={(e) => onChangHandler(e)}
        />
        <button onClick={activeSearch}>Search</button>
        {showResults && <Home query={searchString} />}
    </div>
  );
}

ReactDOM.render(<Header />, document.querySelector("#app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

<div id="app"></div>

Better solution: Uncontrolled inputs

Another technique in your case would be to use an uncontrolled <input> by using a ref and only updating the search string on click of the button instead of on change of the input value.

function Header() {
  const [searchString, setString] = useState('');
  const inputRef = useRef();

  const activeSearch = () => {
    setString(inputRef.current.value);
  }

  return (
    <div>
        <input ref={inputRef} />
        <button onClick={activeSearch}>Search</button>
        {searchString.length > 0 && <Home query={searchString} />}
    </div>
  );
}

const { useState, useEffect, useRef } = React;

const FAKE_DELAY = 5; // seconds

function Home({ query }) {
  const [results, setResults] = useState(null);
  
  useEffect(() => {
    let queryChanged = false;
    
    console.log('Fetch search results for', query);
    
    setTimeout(() => {
      
      if (queryChanged) {
        console.log('Query changed since last fetch, results discarded for', query);
        return;
      }
      setResults(['example', 'result', 'for', query])
    }, FAKE_DELAY * 1000);
    
    return () => {
      // Prevent race-condition
      queryChanged = true;
      setResults(null);
    };
  }, [query]);
  
  return (
    <div>
      {results ? (
        <ul>
          {results.map((result) => (
            <li>{result}</li>
          ))}
        </ul>
      ) : `Loading... (${FAKE_DELAY} seconds)`}
    </div>
  );
}

function Header() {
  const [searchString, setString] = useState('');
  const inputRef = useRef();

  const activeSearch = () => {
    setString(inputRef.current.value);
  }

  return (
    <div>
        <input
          placeholder='Search here'
          ref={inputRef}
        />
        <button onClick={activeSearch}>Search</button>
        {searchString.length > 0 && <Home query={searchString} />}
    </div>
  );
}

ReactDOM.render(<Header />, document.querySelector("#app"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>

<div id="app"></div>

Passing the state around

[The following line] brings the Home component inside the Header component, which makes duplicate

 {searchString.length > 0 && <Home query={searchString} />}

In order to make the Header component reusable, the quickest way would be to lift the state up.

// No state needed in this component, we now receive
// a callback function instead.
function Header({ onSubmit }) {
  const inputRef = useRef();

  const activeSearch = () => {
    // Uses the callback function instead of a state setter.
    onSubmit(inputRef.current.value);
  }

  return (
    <div>
        <input ref={inputRef} />
        <button onClick={activeSearch}>Search</button>
    </div>
  );
}

function App() {
  // State lifted up to the parent (App) component.
  const [searchString, setString] = useState('');

  return (
    <div className='App'>
       <Header onSubmit={setString} />
       {searchString.length > 0 && <Home query={searchString} />}
       <Footer />
    </div> 
  );
}

If that solution is still too limited, there are other ways to pass data around which would be off-topic to bring them all up in this answer, so I'll link some more information instead:

Emile Bergeron
  • 17,074
  • 5
  • 83
  • 129
  • Hi Emile, thanks for your response and your will to help. This line code - `{searchString.length > 0 && }` brings the `Home` component inside the `Header` component, which makes duplicate, I don't need that. Using ` const inputRef = useRef();` is excellent. I want to send props not to show other components inside `Header`. – ExtraSun Jul 05 '21 at 04:51
  • @Sunny In this case, you'd need to [lift the state up](https://reactjs.org/docs/lifting-state-up.html). – Emile Bergeron Jul 05 '21 at 05:17
  • I meant the page will look like `https://www.iherb.com/`. Is it possible to send props that are not inside the `return{}` ? To explain better as I eddited my question - My App.js looks like this - `function App() { return (
    ); }`
    – ExtraSun Jul 05 '21 at 05:17
  • @Sunny how best to pass the data around in a React's app is a whole other subject, though there are already [great answers](https://stackoverflow.com/q/24147331/1218980) to help you choose what's best. (Lifting the state up, context API, state management like Redux, etc) – Emile Bergeron Jul 05 '21 at 05:22
  • 1
    I've also added an example with your code into my answer so that it's clearer what I mean. – Emile Bergeron Jul 05 '21 at 05:33
  • I discovered that `- onSubmit(inputRef.current.value);` is lagging! – ExtraSun Jul 05 '21 at 10:45
  • 1
    @Sunny Note that these new issues are unrelated to the question thread, so if you're having any new issues, you should go through the usual dev process and if it fails, open a new question. That said, [state isn't updated synchronously](https://stackoverflow.com/q/54069253/1218980), so it's possibly what you're experiencing. – Emile Bergeron Jul 05 '21 at 14:11
0

if your props are passed as searchResults, then change the props to,

function Home({ searchResults}) {...}

and use

useEffect(() => { // code, function },[searchResults]) ).
D.Waasala
  • 145
  • 6
  • I changed: `function Home({ searchResults}) {...}` and did - `useEffect(() => { setResults(searchResults); },[searchResults]) ) ` but nothing happened, it didn't render the new props after they changed. – ExtraSun Jul 04 '21 at 19:56