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>
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…