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);
}, []);
}
Related
The code below is a simplified example of my problem. There is a lot more going on in the actual codebase, so let's just assume that my useHook function must be asynchronous and we cannot just fetch the data inside the useEffect hook.
It currently renders {}, but I want it to render "Data to be displayed"
const fetch = async () => {
/* This code can't be changed */
return "Data to be displayed"
}
const useFetch = async () => { // This function must be asynchronous
let data = await fetch();
return data;
};
const App = () => {
const data = useFetch();
const [state, setState] = useState(data);
useEffect(() => {
setState(data);
}, [data]);
return <h1>{JSON.stringify(state)}</h1>
}
export default App;
change
const data = useFetch(); to const data = await useFetch();
Move called inside the useEffect like this:
const App = () => {
const [state, setState] = useState({});
useEffect(() => {
const fetchData = async () => {
const data = await useFetch();
setState(data);
}
fetchData()
}, []); // [] load first time
return <h1>{JSON.stringify(state)}</h1>
}
Code:
import React, { useState, useEffect } from 'react';
export default function App() {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(() => {
getData();
}, []);
useEffect(() => {
const inter = setInterval(() => getData(), 1000);
}, []);
async function getData() {
if (!isLoading) {
return;
}
console.log(isLoading);
const data = await fetch('http://localhost:8000/api/todo/')
.then((res) => res.json())
.then((data) => data);
if (Array.isArray(data)) {
setData(data);
setLoading(false);
}
}
const tododata = data.map((ddd) => {
return <h1>{ddd.title}</h1>;
});
if (isLoading) {
return <h1>Loading</h1>;
} else {
return (
<>
{console.log(isLoading)}
{tododata}
</>
);
}
}
code summary
this is a simplified code from my real project. So I have 2 useState and the first one runs once and to try and get data for the first time, and the purpose of the second one is if the first one didn't get the data and the user is stuck in the loading the second one will repeatedly try to get the data and if the frontend gets the data it should stop fetching.
my problem
in this code somehow the getData function will keep fetching indicating that the isLoading is true but at the same time the component will render all of the title from the data state indicating that the isLoading is false. Why did this happened?
I change my answer to accommodate to setTimeout instead of setInterval:
import React, { useState, useEffect } from 'react';
export default function App() {
const [isLoading, setLoading] = useState(true);
const [data, setData] = useState([]);
useEffect(() => {
getData();
}, []);
async function getData() {
if (!isLoading) {
return;
}
console.log(isLoading);
try {
const data = await fetch('http://localhost:8000/api/todo/').then((res) => res.json());
if (Array.isArray(data)) {
setData(data);
setLoading(false);
} else {
setTimeout(() => {
getData();
}, 1000);
}
} catch (e) {
setTimeout(() => {
getData();
}, 1000);
}
}
const tododata = data.map((ddd) => {
return <h1>{ddd.title}</h1>;
});
if (isLoading) {
return <h1>Loading</h1>;
} else {
return (
<>
{console.log(isLoading)}
{tododata}
</>
);
}
}
I'm trying to implement a refresh button but can't get it done.
This is how my code looks like:
// ParentComponent.js
const ParentComponent = () => {
const { loading, error, data } = useItems();
return (
<ChildComponent items={data} />
);
... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items }) => {
return (
// Logic that renders the items in <li>s
<button onClick={() => console.log('Clicking this button should refresh parent component')}
)
};
// services/useItems.js
const useItems = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
axios
.get(API_URL + '/counter')
.then((response) => {
setItems(response.data);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(error.message);
});
}, []);
return { loading, error, data: counters };
}
I've tried several ways but none did the work. any helps would be truly appreciated :)
I don't think useEffect is the right mechanism here. Since it's an imperative call, nothing reactive about it, useState does the job just fine:
// ParentComponent.js
const ParentComponent = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const refresh = () => {
axios.get(API_URL + '/counter').then((response) => {
setItems(response.data);
setLoading(false);
}).catch((error) => {
setLoading(false);
setError(error.message);
});
};
useEffect(refresh, []);
return (
<ChildComponent items={items} refresh={refresh} />
);
// ... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items, refresh }) => {
return (
// Logic that renders the items in <li>s
<button onClick={refresh}>
Refresh
</button>
)
};
A very simple trick is to increase an integer state, let's just call it version, which would trigger a re-render of <ParentComponent /> and if useEffect depends on version, it'll re-execute the callback, so you get the "refresh" effect.
// ParentComponent.js
const ParentComponent = () => {
const [version, setVersion] = useState(0)
// when called, add 1 to "version"
const refresh = useCallback(() => {
setVersion(s => s + 1)
}, [])
const { loading, error, data } = useItems(version);
return (
<ChildComponent items={data} refresh={refresh} />
);
};
// ChildComponent.js
const ChildComponent = ({ items, refresh }) => {
return (
// Logic that renders the items in <li>s
<button onClick={refresh} />
)
};
// services/useItems.js
const useItems = (version) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
axios
.get(API_URL + '/counter')
.then((response) => {
setItems(response.data);
setLoading(false);
})
.catch((error) => {
setLoading(false);
setError(error.message);
});
}, [version]); // <-- depend on "version"
return { loading, error, data: counters };
}
There are couple fo small parts where you need to make changes to resolve issue.
You need to create a communication for refresh
Create a function to process any processing for refresh.
Pass this as a prop to child component
In child component, call it on necessary event, in this case click
Now since you are using hooks, you need to get it invoked.
You can add a function refreshData in your useItem hook and expose it
Call this function on click of button.
You will also have to add a flag in hooks and update useEffect to be triggered on its change
This function is necessary as setItems is only available inside hook.
Following is a working sample:
const { useState, useEffect } = React;
// ParentComponent.js
const ParentComponent = () => {
const { loading, error, data, refreshData } = useItems();
const refreshFn = () => {
refreshData()
}
return (
<ChildComponent
items={data}
onClick={refreshFn}/>
);
// ... rest of my code that shows the data
};
// ChildComponent.js
const ChildComponent = ({ items, onClick }) => {
const onClickFn = () => {
console.log('Clicking this button should refresh parent component')
if(!!onClick) {
onClick();
}
}
return (
// Logic that renders the items in <li>s
<div>
<button
onClick={ () => onClickFn() }
>Refresh</button>
<ul>
{
items.map((item) => <li key={item}>{item}</li>)
}
</ul>
</div>
)
};
// services/useItems.js
const useItems = () => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [refresh, setRefresh] = useState(false)
useEffect(() => {
if (refresh) {
setItems(Array.from({ length: 5 }, () => Math.random()));
setRefresh(false)
}
}, [ refresh ]);
return {
loading,
error,
data: items,
refreshData: () => setRefresh(true)
};
}
ReactDOM.render(<ParentComponent/>, document.querySelector('.content'))
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div class='content'></div>
As correctly commented by hackape, we need to add a check for refresh and fetch data only if its true
I have created a custom hook to fetch data and its useEffect gets called every time even there is no change in dependencies. I tried removing all the dependencies and tried to call it once by passing [] but it did not work. I am calling the hook from the same component every time.
custom hook:
import {useEffect, useState, useCallback} from 'react';
import {animecall} from '../apicalls';
export default function useFetchData(type, sort, format, page) {
console.log('hook called');
const [state, setState] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
console.log('customhook useeffects');
const fetchData = async () => {
setLoading(true);
const data= await animecall(type, sort, format, page);
setState((prevstate) => [...prevstate, ...data]);
setLoading(false);
};
fetchData();
}, [type, sort, format, page]);
console.log(state);
return {data: state, loading};
}
home.js:
const Home = React.memo(
({compProp, name}) => {
console.log('homeSlider');
const {data, loading} = useFetchData('Movie', 'TRENDING_DESC', 'TV', 1);
return (
<View style={styles.container}>
some jsx
</View>
);
},
(prevProps, nextProps) => {
if (prevProps.compProp !== nextProps.compProp) {
return false;
}
return true;
},
);
Try below code
useEffect(() => {
console.log('customhook useeffects');
const fetchData = async () => {
setLoading(true);
const data= await animecall(type, sort, format, page);
setState((prevstate) => [...prevstate, ...data]);
setLoading(false);
};
}, []);
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]);