i have a function that use in useEffect and click event, then throw warning "React Hook useEffect has a missing dependency", how should i remove the warning?
// location is react router location
const Component = ({ location }) => {
const [data, setData] = useState(null);
const fetchData = () => {
const { id } = parseLocation(location);
fetchDataFromServer(id).then(data => setData(data));
}
useEffect(() => {
fetchData();
}, [location]);
return (
<div>
{data}
<button onClick={fetchData)}>reload</button>
</div>
);
}
then i try this, but the warning still exist
// location is react router location
const Component = ({ location }) => {
const [data, setData] = useState(null);
const fetchData = (l) => {
// l is location
const { id } = parseLocation(l);
fetchDataFromServer(id).then(data => setData(data));
}
useEffect(() => {
fetchData(location);
}, [location]);
return (
<div>
{data}
<button onClick={() => fetchData(location)}>reload</button>
</div>
);
}
The point of the exhaustive-deps rule is to prevent hooks from reading stale props or state. The issue is that since fetchData is defined within the component, as far as the linter is concerned, it could be accessing stale props or state (via closure).
One solution is to pull fetchData out of the component and pass it everything it needs: (it's already being passed the location):
const fetchData = (l, setData) => {
// l is location
const { id } = parseLocation(l);
fetchDataFromServer(id).then(data => setData(data));
}
const Component = ({ location }) => {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(location, setData);
}, [location]);
return (
<div>
{data}
<button onClick={() => fetchData(location, setData)}>reload</button>
</div>
);
}
Since fetchData isn't defined outside the component, the linter knows that it won't access state or props, so it isn't an issue for stale data.
To be clear, though, your original solution is correct, from a runtime perspective, since fetchData doesn't read state or props - but the linter doesn't know that.
You could simply disable the linter, but it'd be easy to accidentally introduce bugs later on (if fetchData is ever modified) that way. It's better to have the linter rule verifying correctness, even if it means some slight restructuring of code.
Alternative solution
An alternative solution which leverages closure instead of passing the location into fetchData:
const Component = ({ location }) => {
const [data, setData] = useState(null);
const fetchData = useCallback(() => {
// Uses location from closure
const { id } = parseLocation(location);
fetchDataFromServer(id).then(data => setData(data));
// Ensure that location isn't stale
}, [location]);
useEffect(() => {
fetchData();
// Ensure that fetchData isn't stale
}, [fetchData]);
return (
<div>
{data}
<button onClick={fetchData}>reload</button>
</div>
);
}
This approach lets you avoid passing location into fetchData every time you call it. However, with this approach it's important to make sure to avoid stale state and props.
If you omitted the [fetchData] dep to useEffect, the effect would only run once, and new data wouldn't be fetched when location changed.
But if you have fetchData in the deps for useEffect but don't wrap fetchData in useCallback, the fetchData function is a new function every render, which would cause the useEffect to run every render (which would be bad).
By wrapping in useCallback, the fetchData function is only a new function whenever the location changes, which causes the useEffect to run at the appropriate point.
Related
I am way too irritated with the warning: cannot update state on unmounted component. And I have been using logic like:
const [mounted,setMounted] = useState(true);
useEffect(() => {
return () => setMounted(false);
},[]);
if(mounted) setState(<newValue>);
Now these if statements are increasing in lots of places and so I decided to go with a custom hook as shown below:
import { useState, useEffect, useCallback } from 'react';
const useAbortableState = <T,>(initialValue: T): [T, (value: T) => void] => {
const [value, setValue] = useState(initialValue);
const [mounted, setMounted] = useState(true);
useEffect(() => {
return () => setMounted(false);
}, []);
const setNewValue = useCallback(
(newValue: any) => {
if (mounted) {
setValue(newValue);
}
},
[mounted]
);
return [value, setNewValue];
};
export default useAbortableState;
I am missing any performance stuff or some side effects which could be dangerous in this case. Can any expert confirm if I could use useAbortableState instead of useState everywhere in my components for state updates or should I go with useState with the mounted logic to play safe?
Also even though I am using the custom logic, I still get the cannot update state on unmounted component warning. I am not sure what I could be missing here?
I want to understand the utility of useCallback in ReactJs. I read that useCallback is used to memoise the function inside it, and to trigger the callback depending by dependecies. How i notice we should use this hook when pass a function as a prop. In the same time i found an example on the internet and i can't figure out why the hook is used.
const useAsync = () => {
const [data, setData] = useState(null)
const execute = useCallback(() => {
setLoading(true)
return asyncFunc()
.then(res => {
setData(res)
return res
})
}, [])
}
Why execute function is wrapped by this hook in this example? And in general should we use useCallback if we don't pass a function as a parameter in a compoenent?
Definition:
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
So yes it returns a memoized callback, but is basically used, in general, to factorize some redundant operations (like a call to an API).
In your case, suppose you have a useCallback like this:
const useAsync = (asyncFunc) => {
const [data, setData] = useState(null)
const execute = useCallback(() => {
return asyncFunc()
.then(res => {
setData(res)
return res
})
}, [asyncFunc])
return { execute, data };
}
Now let's use it in a component:
import React, { useEffect } from 'react';
function App() {
const { execute, data } = useAsync(myFunction);
useEffect(() => {
execute();
}, [execute]);
return (
<div>
{data.map(el => ...)}
</div>
);
}
Where myFunction is:
function myFunction() {
return fetch('http://localhost:3001/users/')
.then((response) => {
return response.json().then((data) => {
return data;
}).catch((err) => {
console.log(err);
})
});
}
Well, the result is that, data now are filled with the response coming from 'http://localhost:3001/users/' route.
Ok so now you could say "Yes but what's the difference between this verbose code and just a direct call to myFunction somewhere in the code?" and the answer is "this is a better approach because the callback is memoized (= will be taken in care by React that caches some operation to increase performances) and will change only if myFunction changes (I mean if you use another function because you have to fetch from another route)".
useCallback is used to prevent useless re-rendering of components or its child. If you know about React.memo(), useCallback is its functional equivalent.
Consider this:
const Foo = () => {
const handleClick = () => {
console.log('Clicked');
}
return <button onClick={handleClick}>Click Me</button>;
}
This will re-render the Foo component again and again even when it's not necessary.
Now consider this:
const Foo = () => {
const memoizedHandleClick =
useCallback(
() => console.log('Click happened')
,[]); // Tells React to memoize regardless of arguments.
return <Button onClick={memoizedHandleClick}>Click Me</Button>;
}
In this code, React will memoize the callback function and the callback will not be created multiple times hence no more useless re-renders
So I've read these blog posts about using custom hooks to fetch data, so for instance we have a custom hook doing the API call, setting the data, possible errors as well as the spinny isFetching boolean:
export const useFetchTodos = () => {
const [data, setData] = useState();
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
useEffect(() => {
setIsFetching(true);
axios.get('api/todos')
.then(response => setData(response.data)
.catch(error => setError(error.response.data)
.finally(() => setFetching(false);
}, []);
return {data, isFetching, error};
}
And then at the top level of our component we would just call const { data, error, fetching } = useFetchTodos(); and all great we render our component with all the todos fetched.
The thing I don't understand is how would we send dynamic data / parameters to the hook based on the internal state of the component, without breaking the rules of hooks?
For instance, imagine we have a useFetchTodoById(id) hook defined the same way as the above one, how would we pass that id around? Let's say our TodoList component which renders our Todos is the following:
export const TodoList = (props) => {
const [selectedTodo, setSelectedTodo] = useState();
useEffect(() => {
useFetchTodoById(selectedTodo.id) --> INVALID HOOK CALL, cannot call custom hooks from useEffect,
and also need to call our custom hooks at the "top level" of our component
}, [selectedTodo]);
return (<ul>{props.todos.map(todo => (
<li onClick={() => setSelectedTodo(todo.id)}>{todo.name}</li>)}
</ul>);
}
I know for this specific usecase we could pass our selectedTodo through props and call our useFetchTodoById(props.selectedTodo.id) at the top of our component, but I'm just illustrating the issue with this pattern I ran into, we won't always have the luxury of receiving the dynamic data that we need in the props.
Also -- how would we apply this pattern for POST/PUT/PATCH requests which take dynamic data properties?
You should have a basic useFetch hook the accepts a url, and fetches whenever the url changes:
const useFetch = (url) => {
const [data, setData] = useState();
const [isFetching, setIsFetching] = useState(false);
const [error, setError] = useState();
useEffect(() => {
if(!url) return;
setIsFetching(true);
axios.get(url)
.then(response => setData(response.data))
.catch(error => setError(error.response.data))
.finally(() => setFetching(false));
}, [url]);
return { data, isFetching, error };
};
Now you can create other custom hook from this basic hook:
const useFetchTodos = () => useFetch('api/todos');
And you can also make it respond to dynamic changes:
const useFetchTodoById = id => useFetch(`api/todos/${id}`);
And you can use it in the component, without wrapping it in useEffect:
export const TodoList = (props) => {
const [selectedTodo, setSelectedTodo] = useState();
const { data, isFetching, error } = useFetchTodoById(selectedTodo.id);
return (
<ul>{props.todos.map(todo => (
<li onClick={() => setSelectedTodo(todo.id)}>{todo.name}</li>)}
</ul>
);
};
I'm trying to fetch data from an API and set a state with the data, but when I use the data in a child component, I get an [Unhandled promise rejection: TypeError: null is not an object (evaluating 'data.name')] warning.
Here is a gist of what I'm trying to do. Does anyone know why this might be occurring? I assume it's because the data isn't received from the API. I have tried adding an "isLoading" state and only returning the ChildComponent if it's false, but I still get the same warning (this might be because setNewProp in useEffect isn't updating when it receives the data from the API).
const ParentComponent = (props) => {
const [data, setData] = useState(null);
const [newProp, setNewProp] = useState();
const fetchData = async () => {
new DataService.retrieveData().then((response) => {
setData(response);
}
}
useEffect(() => {
fetchData();
setNewProp({ data, ...props });
}, []);
return (
<ChildComponent newProp={newProp} />
);
}
You cannot use an async function inside an useEffect lifecycle event. As a good solution i would recommend to fully utilize the useEffect hook and use it as an effect to the updated data.
const ParentComponent = (props) => {
const [data, setData] = useState(null);
const [newProp, setNewProp] = useState();
const fetchData = async () => {
new DataService.retrieveData().then((response) => {
setData(response);
}
}
useEffect(() => {
fetchData();
}, []);
useEffect(() => {
setNewProp({ data, ...props });
}, [data]);
return (
<ChildComponent newProp={newProp} />
);
}
I also want to point out that useEffect runs AFTER the first render. That means your ChildComponent will always receive "undefined" as first props, since there is no initial value set at:
const [newProps, setNewProp] = useState(); // initial value comes here to prevent errors
Looks like maybe you have missed the await that is needed in useEffect() to make the code wait until that fetch has finished:
Before:
useEffect(() => {
fetchData();
setNewProp({ data, ...props });
}, []);
After:
useEffect(() => {
(async () => {
await fetchData();
setNewProp({ data, ...props });
})();
}, []);
Note that useEffect() doesn't support async functions (because it needs the return value to be a cleanup function, or undefined. For example, see this article.
BUT even better might be something like:
const [data, setData] = useState(null);
const fetchData = async () => {
new DataService.retrieveData().then((response) => {
setData(response);
}
}
fetchData();
if (data) {
const newProp = { data, ...props };
}
In your code, you first call fetchData function, which calls a useState hook when it's done. Since useState hook works asynchronously, the state data will not be changed right after.
useEffect(() => {
fetchData(); // Called setData()
setNewProp({ data, ...props }); // At this point, data hasn't changed yet.
}, []);
So you can use useEffect hook again to watch for changes in your data state. Then you should set the new state of your newProp.
useEffect(() => {
(async () => {
await fetchData();
})();
}, []);
useEffect(() => {
setNewProp({...props, data });
}, [data]);
I'm trying to load some data which I get from an API in a form, but I seem to be doing something wrong with my state hook.
In the code below I'm using hooks to define an employee and employeeId.
After that I'm trying to use useEffect to mimic the componentDidMount function from a class component.
Once in here I check if there are params in the url and I update the employeeId state with setEmployeeId(props.match.params.employeeId).
The issue is, my state value didn't update and my whole flow collapses.
Try to keep in mind that I rather use function components for this.
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
}
// This remains -1, while it should be the params.employeeId
if (employeeId) {
getEmployee();
}
}
const getEmployee = () => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId) // --> This will return an invalid employee
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The new value from setEmployeeId will be available probably in the next render.
The code you're running is part of the same render so the value won't be set yet.
Since you're in the same function, use the value you already have: props.match.params.employeeId.
Remember, when you call set* you're instructing React to queue an update. The update may happen when React decides.
If you'd prefer your getEmployee to only run once currentEmployeeId changes, consider putting that in its own effect:
useEffect(() => {
getEmployee(currentEmployeeId);
}, [currentEmployeeId])
The problem seems to be that you are trying to use the "updated" state before it is updated. I suggest you to use something like
export default function EmployeeDetail(props) {
const [employeeId, setEmployeeId] = useState<number>(-1);
const [isLoading, setIsLoading] = useState(false);
const [employee, setEmployee] = useState<IEmployee>();
useEffect(() => componentDidMount(), []);
const componentDidMount = () => {
// --> I get the correct id from the params
let currentEmployeeId
if (props.match.params && props.match.params.employeeId) {
currentEmployeeId = props.match.params.employeeId
setEmployeeId(currentEmployeeId)
}
// This was remaining -1, because state wasn't updated
if (currentEmployeeId) {
getEmployee(currentEmployeeId);
//It's a good practice to only change the value gotten from a
//function by changing its parameter
}
}
const getEmployee = (id: number) => {
setIsLoading(true);
EmployeeService.getEmployee(id)
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}
return (
<div>
...
</div>
)
}
The function returned from useEffect will be called on onmount. Since you're using implicit return, that's what happens in your case. If you need it to be called on mount, you need to call it instead of returning.
Edit: since you also set employee id, you need to track in the dependency array. This is due to the fact that setting state is async in React and the updated state value will be available only on the next render.
useEffect(() => {
componentDidMount()
}, [employeeId]);
An alternative would be to use the data from props directly in the getEmployee method:
useEffect(() => {
componentDidMount()
}, []);
const componentDidMount = () => {
if (props.match.params && props.match.params.employeeId) {
setEmployeeId(props.match.params.employeeId)
getEmployee(props.match.params.employeeId);
}
}
const getEmployee = (employeeId) => {
setIsLoading(true);
EmployeeService.getEmployee(employeeId);
.then((response) => setEmployee(response.data))
.catch((err: any) => console.log(err))
.finally(() => setIsLoading(false))
}