how to implement a refresh button with React Hooks? - javascript

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

Related

How to change a snippets of code from Class Component to Functional Component

Here is the code of the snippet I want to change to a Functional component, I write almost my code here now please check.
import _ from 'lodash';
import { ListItem, SearchBar, Avatar } from 'react-native-elements';
import { getUsers, contains } from './api/index';
function App(props) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [fullData, setFullData] = useState([]);
const [query, setQuery] = useState();
useEffect(() => {
makeRemoteRequest();
},[query]);
const makeRemoteRequest = _.debounce(() => {
setLoading(true);
getUsers(20, query)
.then((users) => {
setLoading(false);
setData(users);
setFullData(users);
})
.catch((error) => {
setLoading(false);
});
}, 250);
const handleSearch = (text) => {
const formattedQuery = text.toLowerCase();
const data = _.filter(fullData, (user) => {
return contains(user, formattedQuery);
});
// I want to change the below code to work on Functioanl component
// this.setState({ data, query: text }, () => //this.makeRemoteRequest());
// New code here.....
};
I implemented it in a different way but not work.
You can have something like the following.
const [query, setQuery] = useState();
const [data, setData] = useState();
useEffect(() => {
makeRemoteRequest();
}, [query])
Read more about useEffect here
You're trying to make a set of data and text, then call a callback after the set.
There are several ways to obtain this behaviour.
What I would suggest you is to have a state (useState) which include data and text and then listen for the changes of this stage through a useEffect.
export default function App() {
const [request, setRequest] = useState({data: {}, text: ''});
const makeRemoteRequest = useCallback(() => console.log({request}),[request]);
useEffect(() => {
//on mount
setRequest({data: {obj:'with data'}, text: 'text'})
},[])
useEffect(() => {
makeRemoteRequest()
},[request,makeRemoteRequest])
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
What you can see here, it's a functional component which is:
setting a state on mount (read comment)
define a function makeRemoteRequest every time the state request changes through the useCallback hook
call the function makeRemoteRequest every time the state request or the callback makeRemoteRequest changes through the useEffect hook
EDIT:
import _ from 'lodash';
import { ListItem, SearchBar, Avatar } from 'react-native-elements';
import { getUsers, contains } from './api/index';
function App(props) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [fullData, setFullData] = useState([]);
const [query, setQuery] = useState();
useEffect(() => {
makeRemoteRequest();
},[query]);
const makeRemoteRequest = _.debounce(() => {
setLoading(true);
getUsers(20, query)
.then((users) => {
setLoading(false);
setData(users);
setFullData(users);
})
.catch((error) => {
setLoading(false);
});
}, 250);
const handleSearch = (text) => {
const formattedQuery = text.toLowerCase();
const data = _.filter(fullData, (user) => {
return contains(user, formattedQuery);
});
setData(data);
setQuery(text);
}
};
Actually what you want is to trigger the function makeRemoteRequest, right now that you have to do in order to get it is to make the proper set (which means setQuery), which is going to trigger the useEffect

Testing React component with data provided by custom hook

I have created this custom hook to fetch data:
const useSuggestionsApi = () => {
const [data, setData] = useState({ suggestions: [] });
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
const fetchData = () => {
setError(false);
setLoading(true);
if(url) {
fetch(url).then((res) => {
if (res.status !== 200) {
console.error(`It seems there was an problem fetching the result. Status Code: ${res.status}`)
return;
}
res.json().then((fetchedData) => {
setData(fetchedData)
})
}).catch(() => {
setError(true)
})
setLoading(false);
};
}
fetchData();
}, [url]);
return [{ data, loading, error }, setUrl];
}
export default useSuggestionsApi;
It used used in this component to render the response (suggestions).
const SearchSuggestions = ({ query, setQuery}) => {
const [{ data }, doFetch] = useSuggestionsApi();
const { suggestions } = data;
useEffect(() => {
const encodedURI = encodeURI(`http://localhost:3000/search?q=${query}`);
doFetch(encodedURI);
}, [doFetch, query]);
return (
<div className="search-suggestions__container">
<ul className="search-suggestions__list">
{suggestions.map((suggestion) => {
return (
<li className="search-suggestions__list-item" key={uuid()}>
<span>
{suggestion.searchterm}
</span>
</li>
)
})}
</ul>
</div>
);
};
export default SearchSuggestions;
Now I would like to write some unit test for the SearchSuggestions component but I am lost on how to mock the returned data from useSuggestionApi. I tried importing useSuggestionApi as a module and then mocking the response like this but with no success:
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions/>)
it('test if correct amount of list-item elements are rendered', () => {
jest.mock("../hooks/useSuggestionsApi", () => ({
useSuggestionsApi: () => mockResponse
}));
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.data.suggestions.length);
});
})
I am new to testing React components so very grateful for any input!
This works:
jest.mock('../hooks/useSuggestionsApi', () => {
return jest.fn(() => [{data: mockResponse}, jest.fn()]
)
})
describe('SearchSuggestions', () => {
const wrapper = shallow(<SearchSuggestions query="jas"/>)
it('correct amount of list-items gets rendered according to fetched data', () => {
expect(wrapper.find('.search-suggestions__list').children()).toHaveLength(mockResponse.suggestions.length);
});
})

Custom hook runs on every render even there is no change in dependecies in React Native

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);
};
}, []);

React Hook : Correct way of using custom hook to handle onClick Event?

As the title said, what is the correct way of using custom hook to handle onClick Event?
This codesandbox application will display a new quote on the screen when user clicks the search button.
function App() {
const [{ data, isLoading, isError }, doFetch] = useDataApi(
"https://api.quotable.io/random"
);
return (
<Fragment>
<button disabled={isLoading} onClick={doFetch}>
Search
</button>
{isError && <div>Something went wrong ...</div>}
{isLoading ? <div>Loading ...</div> : <div>{data.content}</div>}
</Fragment>
);
}
I created a custom hook called useDataApi() which would fetch a new quote from an API. In order to update the quote when the user clicks the button, inside the useDataApi(), I created a handleClick() which will change the value of a click value to trigger re-render. And this handleClick() function will be return back to App()
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [click, setClick] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const handleClick = () => {
setClick(!click);
};
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [initialUrl, click]);
return [{ data, isLoading, isError }, handleClick];
};
This is working, however, I don't feel this is the correct solution.
I also tried moving the fetchData() out of useEffect and return the fetchData(), and it works too. But according to the React Doc, it says it is recommended to move functions inside the useEffect.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, []);
return [{ data, isLoading, isError }, fetchData];
};
In addition, for creating these kinds of application, is the way that I am using is fine or there is another correct solution such as not using any useEffects or not create any custom Hook?
Thanks
Not sure if this is correct, but here is my solution.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const doFetch = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
return [{ data, isLoading, isError }, doFetch];
};
Btw, don't mutate state directly.
const handleClick = () => {
setClick(!click); // don't do this
setClick(prev => !prev); // use this
};
Your implementation is fine. We are also using something similar. Hope you find it useful.
function useApi(promiseFunction, deps, shouldRun=true){
// promisFunction returns promise
const [loading, setLoading] = useState(false)
const [data, setData] = useState(false)
const [error, setError] = useState(false)
const dependencies: any[] = useMemo(()=>{
return [...dependencyArray, shouldRun]
},[...dependencyArray, shouldRun])
const reload = () => {
async function call() {
try {
setError(null)
setLoading(true)
const res = await promiseFunction();
}
catch (error) {
setError(error)
}
finally {
setLoading(false)
}
}
call();
}
useEffect(() => {
if(!shouldRun) return
setResult(null) //no stale data
reload()
}, dependencies)
return {loading, error, data, reload, setState: setData}
}
Below code will provide some idea about how to use it.
function getUsersList(){
return fetch('/users')
}
function getUserDetail(id){
return fetch(`/user/${id}`)
}
const {loading, error, data } = useApi(getUsersList, [], true)
const {loading: userLoading, error: userError, data: userData}
= useApi(()=>getUserDetail(id), [id], true)

React programmatically recall a hook

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);
}, []);
}

Categories