问题
I've got the following use case in a React component.
It is a search user input that uses React Autosuggest. Its value is always an ID, so I only have the user ID as a prop. Therefore at first load to show the username value, I need to fetch it at first mount.
EDIT: I don't want to fetch the value again when it changes later, because I already have the value from my suggestions request.
type InputUserProps = {
userID?: string;
onChange: (userID: string) => void;
};
// Input User is a controlled input
const InputUser: React.FC<InputUserProps> = (props) => {
const [username, setUsername] = useState<string | null>(null);
useEffect(() => {
if (props.userID && !username) {
fetchUsername(props.userID).then((username) => setUsername(username));
}
}, []);
async function loadSuggestions(inputValue: string): Suggestion[] {
return fetchSuggestions(inputValue);
}
function selectSuggestion(suggestion: Suggestion): void {
props.onChange(suggestion.userID); // I don't want to rerun the effect from that change...
setUsername(suggestion.username); // ...because I set the username here
}
return (
<InputSuggest
value={props.userID}
label={username}
onSuggestionsRequested={loadSuggestions}
onSuggestionSelected={selectSuggestion}
/>
);
};
export default InputUser;
(I'm adding a simplified way this component is called)
const App: React.FC<AppProps> = (props) => {
// simplified, I use react-hook-form in the real code
const [userID, setUserID] = useState<string?>(any_initial_value_or_null);
return <InputUser userID={userID} onChange={(newValue)=>setUserID(newValue)} />
};
export default App;
It works, but I have the following warning on my useEffect hook
React Hook useEffect has missing dependencies: 'props.userID' and 'username'. Either include them or remove the dependency array.eslint(react-hooks/exhaustive-deps)
But if I do so, I will run it more than once as the username value is changed by the hook itself! As it works without all the dependencies, I'm wondering:
- How can I solve my case cleanly?
- What are those dependencies for and why is it advised to be exhaustive with them?
回答1:
Looks like the userId
should indeed be a dependency, since if it changes you want to run your query again.
I think you can drop the check for username
and always fetch when, and only when the userId
changes:
useEffect(() => {
if(props.userID) {
fetchUsername(props.userID)
.then((username) => setUsername(username))
}
}, [props.userID])
Generally speaking, you want to list all closure variables in your effect to avoid stale references when the effect is executed.
-- EDIT to address OP questions: Since in your use case you know you only want to perform the action on the initial mount passing an empty dependency array is a valid approach.
Another option is to keep track of the fetched userID, e.g.
const fetchedIds = useRef(new Set())
whenever you fetch username for a new ID you can update the ref:
fetchedIds.current.add(newId)
and in your effect you can test:
if (props.userID && !fetchedIds.current.has(props.userID)) {
// do the fetch
}
回答2:
What are those depdendencies?
useEffect
takes an optional second argument which is an array of dependencies. Dependencies of the useEffect
hook tell it to run the effect whenever one if its dependency changes.
If you don't pass an optional second argument to useEffect hook, it will execute every time the component re-renders. Empty dependency array specifies that you want to run the effect only once, after the initial render of the component. In this case, useEffect
hook will almost behave like componentDidMount in class components.
why is it advised to be exhaustive on them ?
Effects see props
and state
from the render they were defined in. So when you use something from the scope of functional component that participates in react's data flow like props or state, inside the callback function of useEffect
hook, that callback function function will close over that data and unless new effect is defined with the new values of props and state, your effect will see the stale props and state values.
Following code snippet demonstrates what could go wrong if you lie about the dependencies of useEffect
hook.
In the following code snippet, there are two components, App
and User
. App
component has three buttons and maintains the id of the user which is displayed by the User
component. User id is passed as a prop from App
to User
component and User
component fetches the user with the id, passed as prop, from the jsonplaceholder
API.
Now the problem in the following code snippet is that it doesn't works correctly. Reason is that it lies about the dependency of the useEffect
hook. useEffect
hook depends on userID
prop to fetch the user from the API but as i skipped adding userID
as a dependency, useEffect
hook doesn't executes every time userID
prop changes.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, []);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<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="root"></div>
Above code snippet shows one of the few problems that could arise if you lie about the dependencies and this is why you must not skip or lie about the dependencies of the useEffect
hook or any other hook that has a dependency array, for example useCallback
hook.'
To fix the previous code snippet, you just have to add userID
as a dependency to the dependency array of useEffect
hook so that if executes whenever userID
prop changes and new user is fetched with the id equal to userID
prop.
function User({userID}) {
const [user, setUser] = React.useState(null);
React.useEffect(() => {
if (userID > 0) {
fetch(`https://jsonplaceholder.typicode.com/users/${userID}`)
.then(response => response.json())
.then(user => setUser(user))
.catch(error => console.log(error.message));
}
}, [userID]);
return (
<div>
{user ? <h1>{ user.name }</h1> : <p>No user to show</p>}
</div>
);
}
function App() {
const [userID, setUserID] = React.useState(0);
const handleClick = (id) => {
setUserID(id);
};
return(
<div>
<button onClick={() => handleClick(1)}>User with ID: 1</button>
<button onClick={() => handleClick(2)}>User with ID: 2</button>
<button onClick={() => handleClick(3)}>User with ID: 3</button>
<p>Current User ID: {userID}</p>
<hr/>
<User userID={userID} />
</div>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));
<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="root"></div>
In your case, if you skip adding props.userID
in the dependency array of useEffect
hook, your effect will not fetch the new data when the prop userID
changes.
To learn more about the negative impacts of omitting the dependencies of useEffect
hook, see:
- Don’t Lie to React About Dependencies
How can I solve my case cleanly ?
Since your effect depends on the prop value userID
, you should include it in the dependency array to always fetch the new data whenever the userID
changes.
Adding props.userID
as a dependency to useEffect
hook will trigger the effect every time props.userID
changes but the problem is that you are unnecessarily using username
inside the useEffect
. You should remove it because that is not needed since username
value doesn't and shouldn't decide when new user data should be fetched. You just want the effect to run whenever props.userID
changes.
You could also decouple the action from the state update by using the useReducer
hook to manage and update the state.
Edit
Since you only want to run the effect even when userID
is used by useEffect
hook, in your case it is ok to have an empty array as the second argument and ignore the eslint warning. You could also not omit any dependencies of the useEffect
hook and use some condition in useEffect
hook that evaluates to false after the effect runs for the first time and updates the state.
Personally i would suggest you to try to change how your components are structured so that you don't have to deal with this kind of problem in the first place.
回答3:
If you are sure that before mounting the InputUser component, all dependent props have been filled and they have the right value, then add eslint-disable-next-line react-hooks/exhaustive-deps
right before }, [])
line, but If depended props have no value at first component mounting, So you have to add them in useEffect dependencies.
来源:https://stackoverflow.com/questions/63216143/react-hooks-why-does-useeffect-need-an-exhaustive-array-of-dependencies