Is it safe to do something like the following?
const [foo, setFoo] = useState(undefined)
useEffect(() => {
dispatch(someFunc()).then(response => {
let { someFoo } = response
setFoo(someFoo)
})
}, []) // or }, [bar])
useEffect(() => {
dispatch(anotherFunc()).then(response => {
let { anotherFoo } = response
setFoo(anotherFoo)
})
}, [bar])
The effects are executed in the given order and only the "foo" from the last effect setter will be visible in the UI. For instance, the following component will output bar - 1:
const Component = ({ bar }) => {
const [foo, setFoo] = useState(undefined);
console.log("render", bar, foo);
useEffect(() => {
console.log("effect 1");
let someFoo = bar + 1;
setFoo(someFoo);
}, [bar]);
useEffect(() => {
console.log("effect 2");
let anotherFoo = bar - 1;
setFoo(anotherFoo);
}, [bar]);
return (
<div>
{bar} sets {foo}
</div>
);
};
https://codesandbox.io/s/proud-leaf-g55n9?file=/src/App.js:76-482
EDIT: If you use [] as the dependency array, it will only execute once. If there's an async function inside the effect like fetch, the last executed setFoo will prevail. The following example will display random results in each click:
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(1);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
useEffect(() => {
if (disabled) {
const random = 500 * Math.random();
const handle = setTimeout(() => {
setFoo(2);
}, random);
return () => clearTimeout(handle);
}
}, [disabled]);
Example 2:
https://codesandbox.io/s/jovial-architecture-0ybcz?file=/src/App.js
Related
I'm implementing infinite scrolling with search bar.
So I need to bring 10 items each time I call API, incrementing page number by 1.
I used useState to increase pageNumber and called setPageNum before I call 'fetchMore' function in the child component.
The problem is that it always initialized to 1 and it returns the same state, 2 only.
I've been trying every way that I know for about 5hrs but got stucked.
What am I doing wrong and how would I be able to fix this?
const InputTodo = ({ setTodos }) => {
const [inputText, setInputText] = useState("");
const [suggestionList, setSuggestionList] = useState([]);
const [params, setParams] = useState(null);
const [pageNum, setPageNum] = useState(1);
const debouncedText = useDebounce(inputText, 500);
useEffect(() => {
if (!debouncedText) {
return;
}
async function fetchData() {
const { data } = await getSearchSuggestion(debouncedText, 1);
const { result, total, limit, page } = data;
setPageNum(page);
setSuggestionList(result || []);
setParams({
total,
limit
});
}
fetchData();
}, [debouncedText]);
const handleFetchMore = useCallback(
async (query) => {
if (params.total < params.limit * (pageNum - 1)) {
return;
}
try {
const { data } = await getSearchSuggestion(query, pageNum + 1);
setSuggestionList((prev) => [...prev, ...data.result]);
setPageNum(data.page);
} catch (error) {
console.log(error);
}
},
[pageNum, params]
);
//child component
const InputDropdown = ({
query,
suggestions,
pageNum,
setPageNum,
limit,
total,
onFetchMore
}) => {
const [isBottom, setIsBottom] = useState(null);
const bottomObserver = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
if (total < limit * pageNum) {
return;
}
setPageNum((prev) => prev + 1);
onFetchMore(query);
}
},
{ threshold: 0.25 }
);
bottomObserver.current = observer;
}, []);
The problem might be that you are changing the state in the useEffect hook. Since the dependency array of useEffect does not have pageNum in it, it wont run again.
I think it's because the prev value in setPageNum((prev) => prev + 1) is initialised only when the component mount and it's not refreshed.
To solve this problem you can call a handleTrigerObserver() function outside of your useEffect :
const observer = new IntersectionObserver(
entries => {
if (entries[0].isIntersecting) {
handleObserverTrigger();
}
},
{ threshold: 0.01 },
);
you can store this function in a useCallback if you want to optimise the recalculate of this function on each render :
const handleObserverTrigger = useCallback( async () => {
const newData = await getPagedData(intersectionCounter * 50, 50, filters)
setData([ ...newData]);
setPageNum(pageNum + 1);
}, [ getPagedData, pageNum, filters]);
Here pageNum value will refresh on each render. in your observale it's set one time
That's the warning in the console,
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
Here is my code
const [index, setIndex] = useState(0);
const [refreshing, setRefreshing] = useState(false);
const refContainer: any = useRef();
const [selectedIndex, setSelectedIndex] = useState(0);
const navigation = useNavigation();
useEffect(() => {
refContainer.current.scrollToIndex({animated: true, index});
}, [index]);
const theNext = (index: number) => {
if (index < departments.length - 1) {
setIndex(index + 1);
setSelectedIndex(index + 1);
}
};
setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
const onRefresh = () => {
if (refreshing === false) {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}
};
What should I do to make clean up?
I tried to do many things but the warning doesn't disappear
setTimeout need to use in useEffect instead. And add clear timeout in return
useEffect(() => {
const timeOut = setTimeout(() => {
theNext(index);
if (index === departments.length - 1) {
setIndex(0);
setSelectedIndex(0);
}
}, 4000);
return () => {
if (timeOut) {
clearTimeout(timeOut);
}
};
}, []);
Here is a simple solution. first of all, you have to remove all the timers like this.
useEffect(() => {
return () => remover timers here ;
},[])
and put this
import React, { useEffect,useRef, useState } from 'react'
const Example = () => {
const isScreenMounted = useRef(true)
useEffect(() => {
isScreenMounted.current = true
return () => isScreenMounted.current = false
},[])
const somefunction = () => {
// put this statement before every state update and you will never get that earrning
if(!isScreenMounted.current) return;
/// put here state update function
}
return null
}
export default Example;
I have hook useInterval which download data every 10 seconds automaticaly, however I have also button which can manually download data in every moment. I'm struggling to restart interval timer when I click button. So basically if interval counts to 5, but I click button meantime, interval should restart and starts counting to 10 again before downloading data
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};
export default useInterval;
APP PART:
useInterval(() => {
getMessage();
}, 10000)
const getMessage = async () => {
setProcessing(true)
try {
const res = await fetch('url')
const response = await res.json();
setRecievedData(response)
}
catch (e) {
console.log(e)
}
finally {
setProcessing(false)
}
}
const getMessageManually = () => {
getMessage()
RESTART INTERVAL
}
You can add a reset function in the hook and return that function. The reset function should clear the existing interval and start a new one.
Here is the code for the hook which can be reset and stopped.
const useInterval = (callback, delay) => {
const savedCallback = useRef(callback);
const intervalRef = useRef(null);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (delay !== null) {
const id = setInterval(savedCallback.current, delay);
intervalRef.current = id;
return () => clearInterval(id);
}
}, [delay]);
useEffect(()=>{
// clear interval on when component gets removed to avoid memory leaks
return () => clearInterval(intervalRef.current);
},[])
const reset = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
intervalRef.current = setInterval(savedCallback.current,delay)
}
});
const stop = useCallback(() => {
if(intervalRef.current!==null){
clearInterval(intervalRef.current);
}
})
return {
reset,
stop
};
};
// usage
const {reset,stop} = useInterval(()=>{},10000);
reset();
stop();
You should add a reset function as returning a value from the hook.
I also fixed few issues and added an unmount handler:
// Usage
const resetInterval = useInterval(() => ..., DELAY);
resetInterval();
// Implementation
const useInterval = (callback, delay) => {
const savedCallbackRef = useRef(callback);
const intervalIdRef = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// handle tick
useEffect(() => {
const tick = () => {
savedCallback.current();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
const resetInterval = useCallback(() => {
clearInterval(intervalIdRef.current);
intervalIdRef.current = setInterval(savedCallback.current, delay)
}, [delay]);
return resetInterval;
};
Another solution is to remove the ref on the callback making the hook restart the count on every change to the callback
so updating the above solution
// Implementation
const useInterval = (callback, delay) => {
const intervalIdRef = useRef();
// handle tick
useEffect(() => {
const tick = () => {
callback();
};
if (delay !== null) {
intervalIdRef.current = setInterval(tick, delay);
}
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, [delay]);
// handle unmount
useEffect(() => {
const id = intervalIdRef.current;
return () => {
clearInterval(id);
};
}, []);
};
And then you can use it like this
const [counter, setCounter] = useState[0]
const onTimerFinish = useCallback(() => {
setCounter(counter + 1)
// setCounter will reset the interval
}, [counter])
useResetInterval(() => {
onTimerFinish()
}, 5000)
I'm not understanding why the following code, the callback onSocketMessage is not using the new acquisition state. inside the useEffect the state is correctly updated, but the function is not evaluated again...i've also tryed using useCallback with acquisition as dependency but nothing changed.
const Ac = () => {
const [acquisition, setAcquisition] = useState({ data: {} })
const [loading, setLoading] = useState(true)
const socket = useRef(null);
const onSocketMessage = (message) => {
console.log(acquisition) // this is always initial state
let { data } = acquisition
data.input[message.index] = message.input
setAcquisition(prevState => ({ ...prevState, data }));
}
useEffect(() => {
fetchCurrentAcquisition(acquisition => {
setAcquisition(acquisition)
setLoading(false)
socket.current = newSocket('/acquisition', () => console.log('connected'), onSocketMessage);
})
return () => socket.current.disconnect()
}, [])
console.log(acquisition)
You are logging a stale closure you should try the following instead:
const onSocketMessage = useCallback((message) => {
setAcquisition((acquisition) => {
//use acquisition in the callback
console.log(acquisition);
//you were mutating state here before
return {
...acquisition,
data: {
...acquisition.data,
input: {
//not sure if this is an array or not
//assimung it is an object
...acquisition.data.input,
[message.index]: message.input,
},
},
};
});
}, []); //only created on mount
useEffect(() => {
fetchCurrentAcquisition((acquisition) => {
setAcquisition(acquisition);
setLoading(false);
socket.current = newSocket(
'/acquisition',
() => console.log('connected'),
onSocketMessage
);
});
return () => socket.current.disconnect();
//onSocketMessage is a dependency of the effect
}, [onSocketMessage]);
I'm learning to React Hooks.
And I'm struggling initialize data that I fetched from a server using a custom hook.
I think I'm using hooks wrong.
My code is below.
const useFetchLocation = () => {
const [currentLocation, setCurrentLocation] = useState([]);
const getCurrentLocation = (ignore) => {
...
};
useEffect(() => {
let ignore = false;
getCurrentLocation(ignore);
return () => { ignore = true; }
}, []);
return {currentLocation};
};
const useFetch = (coords) => {
console.log(coords);
const [stores, setStores] = useState([]);
const fetchData = (coords, ignore) => {
axios.get(`${URL}`)
.then(res => {
if (!ignore) {
setStores(res.data.results);
}
})
.catch(e => {
console.log(e);
});
};
useEffect(() => {
let ignore = false;
fetchData(ignore);
return () => {
ignore = true;
};
}, [coords]);
return {stores};
}
const App = () => {
const {currentLocation} = useFetchLocation();
const {stores} = useFetch(currentLocation); // it doesn't know what currentLocation is.
...
Obviously, it doesn't work synchronously.
However, I believe there's the correct way to do so.
In this case, what should I do?
I would appreciate if you give me any ideas.
Thank you.
Not sure what all the ignore variables are about, but you can just check in your effect if coords is set. Only when coords is set you should make the axios request.
const useFetchLocation = () => {
// Start out with null instead of an empty array, this makes is easier to check later on
const [currentLocation, setCurrentLocation] = useState(null);
const getCurrentLocation = () => {
// Somehow figure out the current location and store it in the state
setTimeout(() => {
setCurrentLocation({ lat: 1, lng: 2 });
}, 500);
};
useEffect(() => {
getCurrentLocation();
}, []);
return { currentLocation };
};
const useFetch = coords => {
const [stores, setStores] = useState([]);
const fetchData = coords => {
console.log("make some HTTP request using coords:", coords);
setTimeout(() => {
console.log("pretending to receive data");
setStores([{ id: 1, name: "Store 1" }]);
}, 500);
};
useEffect(() => {
/*
* When the location is set from useFetchLocation the useFetch code is
* also triggered again. The first time coords is null so the fetchData code
* will not be executed. Then, when the coords is set to an actual object
* containing coordinates, the fetchData code will execute.
*/
if (coords) {
fetchData(coords);
}
}, [coords]);
return { stores };
};
function App() {
const { currentLocation } = useFetchLocation();
const { stores } = useFetch(currentLocation);
return (
<div className="App">
<ul>
{stores.map(store => (
<li key={store.id}>{store.name}</li>
))}
</ul>
</div>
);
}
Working sandbox (without the comments) https://codesandbox.io/embed/eager-elion-0ki0v