I'm new to react and I've just started learning about hooks and context.
I am getting some data from an API with the following code:
const getScreen = async uuid => {
const res = await axios.get(
`${url}/api/screen/${uuid}`
);
dispatch({
type: GET_SCREEN,
payload: res.data
});
};
Which goes on to use a reducer.
case GET_SCREEN:
return {
...state,
screen: action.payload,
};
In Screen.js, I am calling getScreen and sending the UUID to show the exact screen. Works great. The issue I am having is when I am trying to fetch the API (every 3 seconds for testing) and update the state of nodeupdated based on what it retrieves from the API. The issue is, screen.data is always undefined (due to it being asynchronous?)
import React, {
useState,
useEffect,
useContext,
} from 'react';
import SignageContext from '../../context/signage/signageContext';
const Screen = ({ match }) => {
const signageContext = useContext(SignageContext);
const { getScreen, screen } = signageContext;
const [nodeupdated, setNodeupdated] = useState('null');
const foo = async () => {
getScreen(match.params.id);
setTimeout(foo, 3000);
};
useEffect(() => {
foo();
setNodeupdated(screen.data)
}, []);
If I remove the [] is does actually get the data from the api ... but in an infinate loop.
The thing is this seemed to work perfectly before I converted it to hooks:
componentDidMount() {
// Get screen from UUID in url
this.props.getScreen(this.props.match.params.id);
// Get screen every 30.5 seconds
setInterval(() => {
this.props.getScreen(this.props.match.params.id);
this.setState({
nodeUpdated: this.props.screen.data.changed
});
}, 3000);
}
Use a custom hook like useInterval
function useInterval(callback, delay) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
Then in your component
useInterval(() => {
setCount(count + 1);
}, delay);
Dan Abramov has a great blog post about this
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
You can use some thing like this. Just replace the console.log with your API request.
useEffect(() => {
const interval = setInterval(() => {
console.log("Making request");
}, 3000);
return () => clearInterval(interval);
}, []);
Alternatively, Replace foo, useEffect and add requestId
const [requestId, setRequestId] = useState(0);
const foo = async () => {
getScreen(match.params.id);
setTimeout(setRequestId(requestId+1), 3000);
};
useEffect(() => {
foo();
setNodeupdated(screen.data)
}, [requestId]);
Related
I'm fetching Dogs from my API through a JavaScript timeout. It works fine, except it fails to clear the timeout sometimes:
import { useState, useEffect, useCallback } from 'react';
const DogsPage = () => {
const [dogs, setDogs] = useRef([]);
const timeoutId = useRef();
const fetchDogs = useCallback(
async () => {
const response = await fetch('/dogs');
const { dogs } = await response.json();
setDogs(dogs);
timeoutId.current = setTimeout(fetchDogs, 1000);
},
[]
);
useEffect(
() => {
fetchDogs();
return () => clearTimeout(timeoutId.current);
},
[fetchDogs]
);
return <b>Whatever</b>;
};
It looks like the problem is that sometimes I unmount first, while the code is still awaiting for the Dogs to be fetched. Is this a common issue and if so, how would I prevent this problem?
One idea would be to use additional useRef() to keep track of whether the component has been unmounted in between fetch:
const DogsPage = () => {
const isMounted = useRef(true);
const fetchDogs = useCallback(
async () => {
// My fetching code
if (isMounted.current) {
timeoutId.current = setTimeout(fetchDogs, 1000);
}
},
[]
);
useEffect(
() => {
return () => isMounted.current = false;
},
[]
);
// The rest of the code
};
But perhaps there is a cleaner way?
You can assign a sentinel value for timeoutId.current after clearing it, then check for that value before starting a new timer:
import { useState, useEffect, useCallback } from 'react';
const DogsPage = () => {
const [dogs, setDogs] = useRef([]);
const timeoutId = useRef();
const fetchDogs = useCallback(
async () => {
const response = await fetch('/dogs');
const { dogs } = await response.json();
setDogs(dogs);
if (timeoutId.current !== -1)
timeoutId.current = setTimeout(fetchDogs, 1000);
},
[]
);
useEffect(
() => {
fetchDogs();
return () => void (clearTimeout(timeoutId.current), timeoutId.current = -1);
},
[fetchDogs]
);
return <b>Whatever</b>;
};
I want to increment the number of users after each 200ms till 5000 with the below code. But it doesn't clear the interval when the number of users greater than 5000.
const Cards = () => {
const [users, setUsers] = useState(40);
useEffect(() => {
const setIntervalUsers = setInterval(() => {
setUsers((prevUsers) => prevUsers = prevUsers + 100)
}, 200);
if (users >= 5000) {
console.log('ok');
clearInterval(setIntervalUsers)
}
}, []);
return (<div>number of users {users} </div>)}
I would suggest you to return a clean up function so you don't register the interval twice in case you are in StrictMode with React 18, and also to remove it from the memory when the component gets unmounted.
Also use a ref set with useRef and a separate useEffect that would watch changes in users and clear the interval there. Like so:
import { useEffect, useRef, useState } from "react";
const Cards = () => {
const [users, setUsers] = useState(40);
const intervalRef = useRef();
useEffect(() => {
if (users >= 5000) {
console.log("ok");
clearInterval(intervalRef.current);
}
}, [users]);
useEffect(() => {
intervalRef.current = setInterval(() => {
setUsers((prevUsers) => (prevUsers = prevUsers + 100));
}, 200);
return () => clearInterval(intervalRef.current);
}, []);
return <div>number of users {users} </div>;
};
This doesnt work because:
you never call the useEffect again to check if the condition is met
the interval ref is lost
I made a working sample of your code here : https://codepen.io/aSH-uncover/pen/wvmYdNy
Addintionnaly you should clean the interval when the component is destroyed by returning the cleanInterval call in the hook that created the inteerval
const Card = ({ step }) => {
const intervals = useRef({})
const [users, setUsers] = useState(40)
useEffect(() => {
intervals.users = setInterval(() => {
setUsers((prevUsers) => prevUsers = prevUsers + step)
}, 200)
return () => clearInterval(intervals.users)
}, [])
useEffect(() => {
if (users >= 5000) {
clearInterval(intervals.users)
}
}, [users])
return (<div>number of users {users} </div>)
}
I came up with this. You can try it out. Although there are many ways suggested above
const [users, setUsers] = useState(40);
const [max_user, setMaxUser] = useState(true);
let setIntervalUsers: any;
let sprevUsers = 0;
useEffect(() => {
if (max_user) {
setIntervalUsers = setInterval(() => {
sprevUsers += 100;
if (sprevUsers >= 5000) {
setMaxUser(false);
clearInterval(setIntervalUsers);
} else {
setUsers(sprevUsers);
}
}, 200);
}
}, []);
The way how you check for your condition users >= 5000 is not working because users is not listed as a dependency in your useEffect hook. Therefore the hook only runs once but doesnt run again when users change. Because of that you only check for 40 >= 5000 once at the beginning.
An easier way to handle that is without a setInterval way.
export const Cards = () => {
const [users, setUsers] = useState(40);
useEffect(() => {
// your break condition
if (users >= 5000) return;
const increment = async () => {
// your interval
await new Promise((resolve) => setTimeout(resolve, 200));
setUsers((prevState) => prevState + 100);
}
// call your callback
increment();
// make the function run when users change.
}, [users]);
return <p>current number of users {users}</p>
}
currently, I'm using axios and i try to get request and set data then abort request using useState with interval.
however, it seems like the request does not keep abort request as soon as get the request.
I want to know if it's possible to abort request !
my code is like this .
const controller = new AbortController();
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
const getAPI = useCallback(async () => {
await axios
.get(convert.GET_API_URL, {
headers: convert.Header,
cancelToken: source.token,
signal: controller.signal,
})
.then(response => {
setAPI_DATA(response.data);
setSpinner(false);
source.cancel();
controller.abort();
})
.catch(error => {
setSpinner(false);
});
}, [API_DATA, convert, spinner]);
useInterval(
() => {
getAPI();
},
apiStart ? convert?.API_Read_Speed : null
);
useInterval
import { useEffect } from "react";
import { useRef } from "react";
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;
https://codesandbox.io/s/react-hooks-usefetch-cniul
Please see above url for a very simplified version of my code.
I want to be able to refetch data from an API with my hook, within an interval (basically poll an endpoint for data).
What I want is to be able to just call something like refetch (as I've shown in the code as a comment), which would essentially just call fetchData again and update state with the response accordingly.
What's the best way to go about this? The only way I can think of is to add a checker variable in the hook which would be some sort of uuid (Math.random() maybe), return setChecker as what is refetch and just add checker to the array as 2nd useEffect argument to control rerendering. So whenever you call refetch it calls setChecker which updates the random number (checker) and then the function runs again.
Obviously this sounds "hacky", there must be a nicer way of doing it - any ideas?
If you want to have a constant poll going, I think you can move the setInterval() into the hook like so:
function useFetch() {
const [data, setDataState] = useState(null);
const [loading, setLoadingState] = useState(true);
useEffect(() => {
function fetchData() {
setLoadingState(true);
fetch(url)
.then(j => j.json())
.then(data => {
setDataState(data);
setLoadingState(false);
});
}
const interval = setInterval(() => {
fetchData();
}, 5000);
fetchData();
return () => clearInterval(interval);
}, []);
return [
{
data,
loading
}
];
}
Remember to include the return () => clearInterval(interval); so the hook is cleaned up correctly.
import React, { useEffect, useState, useCallback } from "react";
import ReactDOM from "react-dom";
const url = "https://api.etilbudsavis.dk/v2/dealerfront?country_id=DK";
function useFetch() {
const [data, setDataState] = useState(null);
const [loading, setLoadingState] = useState(true);
const refetch = useCallback(() => {
function fetchData() {
console.log("fetch");
setLoadingState(true);
fetch(url)
.then(j => j.json())
.then(data => {
setDataState(data);
setLoadingState(false);
});
}
fetchData();
}, []);
return [
{
data,
loading
},
refetch
// fetchData <- somehow return ability to call fetchData function...
];
}
function App() {
const [
{ data, loading },
refetch
// refetch
] = useFetch();
useEffect(() => {
const id = setInterval(() => {
// Use the refetch here...
refetch();
}, 5000);
return () => {
clearInterval(id);
};
}, [refetch]);
if (loading) return <h1>Loading</h1>;
return (
<>
<button onClick={refetch}>Refetch</button>
<code style={{ display: "block" }}>
<pre>{JSON.stringify(data[0], null, 2)}</pre>
</code>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Maybe the following will work, it needs some adjustments to useFetch but you can still call it normally in other places.
//maybe you can pass url as well so you can use
// it with other components and urls
function useFetch(refresh) {
//code removed
useEffect(() => {
//code removed
}, [refresh]);
//code removed
}
const [refresh, setRefresh] = useState({});
const [{ data, loading }] = useFetch(refresh);
useEffect(() => {
const interval = setInterval(
() => setRefresh({}), //forces re render
5000
);
return () => clearInterval(interval); //clean up
});
Simple answer to question:
export default function App() {
const [entities, setEntities] = useState();
const [loading, setLoadingState] = useState(true);
const getEntities = () => {
setLoadingState(true);
//Changet the URL with your own
fetch("http://google.com", {
method: "GET",
})
.then((data) => data.json())
.then((resp) => {
setEntities(resp);
setLoadingState(false);
});
};
useEffect(() => {
const interval = setInterval(() => {
getEntities();
}, 5000);
return () => clearInterval(interval);
}, []);
}
I have something like:
const [loading, setLoading] = useState(false);
...
setLoading(true);
doSomething(); // <--- when here, loading is still false.
Setting state is still async, so what's the best way to wait for this setLoading() call to be finished?
The setLoading() doesn't seem to accept a callback like setState() used to.
an example
class-based
getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (this.state.pagesSeen.includes(this.state.page + 1)) {
return this.setState({
page: this.state.page + 1,
});
}
if (this.state.prefetchedOrders) {
const allOrders = this.state.orders.concat(this.state.prefetchedOrders);
return this.setState({
orders: allOrders,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
prefetchedOrders: null,
});
}
this.setState(
{
isLoading: true,
},
() => {
getOrders({
page: this.state.page + 1,
query: this.state.query,
held: this.state.holdMode,
statuses: filterMap[this.state.filterBy],
})
.then((o) => {
const { orders } = o.data;
const allOrders = this.state.orders.concat(orders);
this.setState({
orders: allOrders,
isLoading: false,
page: this.state.page + 1,
pagesSeen: [...this.state.pagesSeen, this.state.page + 1],
// Just in case we're in the middle of a prefetch.
prefetchedOrders: null,
});
})
.catch(e => console.error(e.message));
},
);
};
convert to function-based
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
};
In the above, we want to run each setWhatever call sequentially. Does this mean we need to set up many different useEffect hooks to replicate this behavior?
useState setter doesn't provide a callback after state update is done like setState does in React class components. In order to replicate the same behaviour, you can make use of the a similar pattern like componentDidUpdate lifecycle method in React class components with useEffect using Hooks
useEffect hooks takes the second parameter as an array of values which React needs to monitor for change after the render cycle is complete.
const [loading, setLoading] = useState(false);
...
useEffect(() => {
doSomething(); // This is be executed when `loading` state changes
}, [loading])
setLoading(true);
EDIT
Unlike setState, the updater for useState hook doesn't have a callback, but you can always use a useEffect to replicate the above behaviour. However you need to determine the loading change
The functional approach to your code would look like
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
const prevLoading = usePrevious(isLoading);
useEffect(() => {
if (!prevLoading && isLoading) {
getOrders({
page: page + 1,
query: localQuery,
held: localHoldMode,
statuses: filterMap[filterBy],
})
.then((o) => {
const { orders: fetchedOrders } = o.data;
const allOrders = orders.concat(fetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
setIsLoading(false);
})
.catch(e => console.error(e.message));
}
}, [isLoading, preFetchedOrders, orders, page, pagesSeen]);
const getNextPage = () => {
// This will scroll back to the top, and also trigger the prefetch for the next page on the way up.
goToTop();
if (pagesSeen.includes(page + 1)) {
return setPage(page + 1);
}
if (prefetchedOrders) {
const allOrders = orders.concat(prefetchedOrders);
setOrders(allOrders);
setPage(page + 1);
setPagesSeen([...pagesSeen, page + 1]);
setPrefetchedOrders(null);
return;
}
setIsLoading(true);
};
Wait until your component re-render.
const [loading, setLoading] = useState(false);
useEffect(() => {
if (loading) {
doSomething();
}
}, [loading]);
setLoading(true);
You can improve clarity with something like:
function doSomething() {
// your side effects
// return () => { }
}
function useEffectIf(condition, fn) {
useEffect(() => condition && fn(), [condition])
}
function App() {
const [loading, setLoading] = useState(false);
useEffectIf(loading, doSomething)
return (
<>
<div>{loading}</div>
<button onClick={() => setLoading(true)}>Click Me</button>
</>
);
}
Created a custom useState hook which works similar to the normal useState hook except that the state updater function for this custom hook takes a callback that will be executed after the state is updated and component rerendered.
Typescript Solution
import { useEffect, useRef, useState } from 'react';
type OnUpdateCallback<T> = (s: T) => void;
type SetStateUpdaterCallback<T> = (s: T) => T;
type SetStateAction<T> = (newState: T | SetStateUpdaterCallback<T>, callback?: OnUpdateCallback<T>) => void;
export function useCustomState<T>(init: T): [T, SetStateAction<T>];
export function useCustomState<T = undefined>(init?: T): [T | undefined, SetStateAction<T | undefined>];
export function useCustomState<T>(init: T): [T, SetStateAction<T>] {
const [state, setState] = useState<T>(init);
const cbRef = useRef<OnUpdateCallback<T>>();
const setCustomState: SetStateAction<T> = (newState, callback?): void => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Javascript solution
import { useEffect, useRef, useState } from 'react';
export function useCustomState(init) {
const [state, setState] = useState(init);
const cbRef = useRef();
const setCustomState = (newState, callback) => {
cbRef.current = callback;
setState(newState);
};
useEffect(() => {
if (cbRef.current) {
cbRef.current(state);
}
cbRef.current = undefined;
}, [state]);
return [state, setCustomState];
}
Usage
const [state, setState] = useCustomState(myInitialValue);
...
setState(myNewValueOrStateUpdaterCallback, () => {
// Function called after state update and component rerender
})
you can create a async state hooks
const useAsyncState = initialState => {
const [state, setState] = useState(initialState);
const asyncSetState = value => {
return new Promise(resolve => {
setState(value);
setState((current) => {
resolve(current);
return current;
});
});
};
return [state, asyncSetState];
};
then
const [loading, setLoading] = useAsyncState(false)
const submit = async () => {
await setLoading(true)
dosomething()
}
I have a suggestion for this.
You could possibly use a React Ref to store the state of the state variable. Then update the state variable with the react ref. This will render a page refresh, and then use the React Ref in the async function.
const stateRef = React.useRef().current
const [state,setState] = useState(stateRef);
async function some() {
stateRef = { some: 'value' }
setState(stateRef) // Triggers re-render
await some2();
}
async function some2() {
await someHTTPFunctionCall(stateRef.some)
stateRef = null;
setState(stateRef) // Triggers re-render
}
Pass a function to the setter instead of value!
instead of giving a new value to the setter directly, pass it an arrow function that takes the current state value and returns the new value.
it will force it to chain the state updates and after it's done with all of them, it will rerender the component.
const [counter, setCounter] = useState(0);
const incrementCount = () => {
setCounter( (counter) => { return counter + 1 } )
}
now every time incrementCount is called, it will increase the count by one and it will no longer be stuck at 1.