Fetch data with a custom React hook - javascript

I'm newbie in React but I'm developing an app which loads some data from the server when user open the app. App.js render this AllEvents.js component:
const AllEvents = function ({ id, go, fetchedUser }) {
const [popout, setPopout] = useState(<ScreenSpinner className="preloader" size="large" />)
const [events, setEvents] = useState([])
const [searchQuery, setSearchQuery] = useState('')
const [pageNumber, setPageNumber] = useState(1)
useEvents(setEvents, setPopout) // get events on the main page
useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber)
// for ajax pagination
const handleSearch = (searchQuery) => {
setSearchQuery(searchQuery)
setPageNumber(1)
}
return(
<Panel id={id}>
<PanelHeader>Events around you</PanelHeader>
<FixedLayout vertical="top">
<Search onChange={handleSearch} />
</FixedLayout>
{popout}
{
<List id="event-list">
{
events.length > 0
?
events.map((event, i) => <EventListItem key={event.id} id={event.id} title={event.title} />)
:
<InfoMessages type="no-events" />
}
</List>
}
</Panel>
)
}
export default AllEvents
useEvents() is a custom hook in EventServerHooks.js file. EventServerHooks is designed for incapsulating different ajax requests. (Like a helper file to make AllEvents.js cleaner) Here it is:
function useEvents(setEvents, setPopout) {
useEffect(() => {
axios.get("https://server.ru/events")
.then(
(response) => {
console.log(response)
console.log(new Date())
setEvents(response.data.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
)
}, [])
return null
}
function useSearchedEvents(setEvents, setPopout, searchQuery, pageNumber) {
useEffect(() => {
setPopout(<ScreenSpinner className="preloader" size="large" />)
let cancel
axios({
method: 'GET',
url: "https://server.ru/events",
params: {q: searchQuery, page: pageNumber},
cancelToken: new axios.CancelToken(c => cancel = c)
}).then(
(response) => {
setEvents(response.data)
setPopout(null)
},
(error) => {
console.log('Error while getting events: ' + error)
}
).catch(
e => {
if (axios.isCancel(e)) return
}
)
return () => cancel()
}, [searchQuery, pageNumber])
return null
}
export { useEvents, useSearchedEvents }
And here is the small component InfoMessages from the first code listing, which display message "No results" if events array is empty:
const InfoMessages = props => {
switch (props.type) {
case 'no-events':
{console.log(new Date())}
return <Div className="no-events">No results :(</Div>
default:
return ''
}
}
export default InfoMessages
So my problem is that events periodically loads and periodically don't after app opened. As you can see in the code I put console log in useEvents() and in InfoMessages so when it's displayed it looks like this:
logs if events are displayed, and the app itself
And if it's not displayed it looks like this: logs if events are not displayed, and the app itself
I must note that data from the server is loaded perfectly in both cases, so I have totally no idea why it behaves differently with the same code. What am I missing?

Do not pass a hook to a custom hook: custom hooks are supposed to be decoupled from a specific component and possibly reused. In addition, your custom hooks return always null and that's wrong. But your code is pretty easy to fix.
In your main component you can fetch data with a custom hook and also get the loading state like this, for example:
function Events () {
const [events, loadingEvents] = useEvents([])
return loadingEvents ? <EventsSpinner /> : <div>{events.map(e => <Event key={e.id} title={e.title} />}</div>
}
In your custom hook you should return the internal state. For example:
function useEvents(initialState) {
const [events, setEvents] = useState(initialState)
const [loading, setLoading] = useState(true)
useEffect(function() {
axios.get("https://server.ru/events")
.then(
(res) => {
setEvents(res.data)
setLoading(false)
}
)
}, [])
return [events, loading]
}
In this example, the custom hook returns an array because we need two values, but you could also return an object with two key/value pairs. Or a simple variable (for example only the events array, if you didn't want the loading state), then use it like this:
const events = useEvents([])

This is another example that you can use, creating a custom hook that performs the task of fetching the information
export const useFetch = (_url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(true);
useEffect(function() {
setLoading('procesando...');
setData(null);
setError(null);
const source = axios.CancelToken.source();
setTimeout( () => {
axios.get( _url,{cancelToken: source.token})
.then(
(res) => {
setLoading(false);
console.log(res.data);
//setData(res);
res.data && setData(res.data);
// res.content && setData(res.content);
})
.catch(err =>{
setLoading(false);
setError('si un error ocurre...');
})
},1000)
return ()=>{
source.cancel();
}
}, [_url])

Related

ReactJS : Invalid hook call. Hooks can only be called inside of the body of a function component

While calling useEffect function on one of my submethod result in
ReactJS : Invalid hook call. Hooks can only be called inside of the
body of a function component.
The Flow
onClick(message) --> call CallGetAMIDetails(message) --> Call Loaddata(message) --> Perform REST Call and -->Returns Array of String
But my class is already a function component
import React, {useEffect, useState} from 'react';
import {DashboardLayout} from '../components/Layout';
import Select from 'react-select'
const options = [
{value: 'ami-abc*', label: 'ami-abc'},
{value: 'ami-xyz*', label: 'ami-xyz'},
]
const DiscoverAMIPage = () => {
function Loaddata() {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);
useEffect(() => {
fetch("http://localhost:10000/connections")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, [])
if (error) {
return []
} else if (!isLoaded) {
return []
} else {
return (
items
);
}
}
function CallGetAMIDetails(message) {
return Loaddata(message)
}
const [message, setMessage] = useState('');
const [items, setItems] = useState([]);
return (
<DashboardLayout>
<h2>Discovered AMI</h2>
<Select
onChange={e => {
setMessage(e.value);
setItems(CallGetAMIDetails(e.value));
}}
options={options}
/>
{console.log("----")}
<h2>{items}</h2>
{console.log("----")}
</DashboardLayout>
)
}
export default DiscoverAMIPage;
What I'm doing wrong here ?
Get rid of Loaddata. You're confusing components with normal functions and trying to use them interchangeably, which is leading to a drastically over-engineered and broken structure.
DiscoverAMIPage is your component. Inside that component should be your calls to useState and useEffect. The useState calls define your component's state values and the useEffect call invokes an operation when dependencies change on a re-render. Simplify what you're doing.
For example:
const DiscoverAMIPage = () => {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [items, setItems] = useState([]);
const [message, setMessage] = useState('');
useEffect(() => {
fetch("http://localhost:10000/connections")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
setIsLoaded(true);
setError(error);
}
)
}, [])
return (
<DashboardLayout>
<h2>Discovered AMI</h2>
<Select
onChange={e => setMessage(e.value)}
options={options}
/>
<h2>{items}</h2>
</DashboardLayout>
);
}
Now you have a component with 4 state values and 1 effect. That effect is that an AJAX operation is performed when the component is first loaded (and never again afterward), and that operation updates the state when it's complete. That state update will re-render the component with its new data. From this starting point you can continue to develop your features.
Edit: Based on comments below it sounds like you also want to manually invoke the AJAX function in an event handler. For that you would extract the AJAX logic into a function and invoke that function in both the event handler and in useEffect, the same way you would extract functionality into a function to call from multiple places anywhere else.
For example:
const loadData = () =>
fetch("http://localhost:10000/connections")
.then(res => res.json())
.then(
(result) => {
setIsLoaded(true);
setItems(result);
},
// Note: it's important to handle errors here
// instead of a catch() block so that we don't swallow
// exceptions from actual bugs in components.
(error) => {
setIsLoaded(true);
setError(error);
}
);
useEffect(loadData, []);
Now that you have a function which independently performs the AJAX operation (and useEffect is just calling that function), you can call that function in your event handler:
<Select
onChange={e => {
setMessage(e.value);
loadData();
}}
options={options}
/>
If you need to pass the selected value to loadData then you can pass e.value just like you do to setMessage. Then in loadData you'd add a function argument and use it however you need to.

access to RESTful API in react

I have laravel in server side, that can show api with entering this url: http://localhost:8000/api/cabangs, and show this (the data as example):
[
{
"id":2,
"nm_cabang":"zxcvb",
"deskripsi":"poiuyt",
"created_at":"2020-08-08T05:25:31.000000Z",
"updated_at":"2020-08-08T05:29:23.000000Z"
},
{
"id":3,
"nm_cabang":"asdfg",
"deskripsi":"qwerty",
"created_at":"2020-08-08T05:28:26.000000Z",
"updated_at":"2020-08-08T05:28:26.000000Z"
}
]
I want to only display nm_cabang and deskripsi. if it's possible, using react hooks. thanks
You could start with something like this.
I'm assuming this is for the web so I used span and div.
const App: React.FC = () => {
const [data, setData] = React.useState();
const [isLoading, setLoading] = React.useState(true);
React.useEffect(() => {
const fetchData = async () => {
try {
const result = await fetch('http://localhost:8000/api/cabangs');
if (result.ok)
{
let json = await result.json();
setData(json);
}
} catch (e) {
//error
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (isLoading) {
return <span>Loading...</span>;
}
return (
<div>
{
data ? (
data.map((item) => {
return <span>{`${item.nm_cabang} ${item.deskripsi}`}</span>;
})
) : (
<></>
)
}
</div>
);
};
Use a library like axios or fetch, you only need to make the http request to the url and use the data you need.
https://github.com/axios/axios
https://javascript.info/fetch
I like axios

Get from api in useEffect and render components accordingly

Im having troubles rendering components based on api calls in React. I fetch my data in useEffect hook update a state with the data. The state is null for a while before the api get all the data but by that time, the components are rendering with null values. This is what I have:
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
//if found is 0 not loaded, 1 is found, 2 is not found err
const [found, setFound] = useState(0);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
setFound(1);
})
.catch(err => {
console.log(err.message);
setFound(2);
});
}, [])
if(found===2) {
return(
<Redirect to="/" push />
)
}else{
console.log(poll)
return (
<div>
</div>
)
}
}
export default Poll
That is my workaround but it doesnt feel like thats the way it should be done. How can I set it so that I wait for my api data to get back then render components accordingly?
You don't need to track the state of the API call like const [found, setFound] = useState(1). Just check if poll exists and also you can create a new state variable for tracking the error.
For example if (!poll) { return <div>Loading...</div>} this will render a div with 'loading...' when there is no data. See the code below, for complete solution,
import React, { useEffect, useState } from 'react'
import axios from 'axios';
import { Redirect } from 'react-router-dom';
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [hasError, setHasError] = useState(false);
useEffect(() => {
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setHasError(true)
});
}, [])
if(!poll) {
console.log('data is still loading')
return(
<div>Loading....</div>
)
}
if (hasError) {
console.log('error when fetching data');
return (
<Redirect to="/" push />
)
}
return (
<div>
{
poll && <div>/* The JSX you want to display for the poll*/</div>
}
</div>
);
}
export default Poll
In your than, try to use a filter:
setPoll(poll.filter(poll => poll.id !== id));
Make sure to replace id by your identificator
The standard way is to have other variables for the loading and error states like this
const Poll = (props) => {
const [poll, setPoll] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
useEffect(() => {
setLoading(true);
axios.get(`api/poll/${props.match.params.id}`)
.then(res => {
console.log(res.data);
setPoll(res.data);
})
.catch(err => {
console.log(err.message);
setError(true);
})
.finally(()=> {
setLoading(false);
};
}, [])
if(error) return <span>error<span/>
if(loading) return <span>loading<span/>
return (
<div>
// your poll data
</div>
)
}

State update not finised before other update starts

I have this object:
{
"Widget1": "notes",
"Widget2": "clock",
"Widget3": "weather",
}
It is transferred to my react app via a rest API.
const Widgets: React.FC = () => {
interface DataType {
[key: string]: string;
}
const [data, setData] = React.useState<DataType>();
const [isLoaded, setIsLoaded] = React.useState<boolean>(false);
React.useEffect(() => {
const fetchData = async () => {
fetch("http://localhost:1202/data")
.then((res) => res.json())
.then((json) => {
setData(json);
console.log("json", json);
});
};
fetchData();
setIsLoaded(true);
}, []);
const [currentWidget1, setCurrentWidget1] = React.useState<string>("wetter");
const widget1Handler = (widget: string) => {};
if (!isLoaded) {
return (
<div>loading...<div>
);
} else {
return (
<component data={data}></component>
);
}
};
export default Widgets;
The problem is that apparently it takes longer for setData to be updated than for setIsLoaded to update.
So component is loaded before the data is stored in the data object.
Is there a quick fix for that?
You can check if there is data then set state: data && setData(json)
Yeah, you can use something like
if (Object.keys(data) > 0) {
<Component data={data}></Component>
}
else {
<div>loading...<div>
}
No Need to use isLoaded
FYI: React Components are always called with first character in uppercase Component

How to use a custom hook with event handler?

I have created a custom Hook which fetches data from the server, sends dispatch to the store and returns data. It is usable if I want to list all comments in my app, however, I wanted to reuse it in the component where I need to fetch all comment replies, and that should happen only when certain button is clicked.
This is the hook down below.
import { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
const useFetch = (url, options, actionType, dataType) => {
const [response, setResponse] = useState([]);
const dispatch = useDispatch();
useEffect(() => {
(async () => {
const res = await fetch(url);
const json = await res.json();
setResponse(json);
})();
}, []);
useEffect(() => {
dispatch({ payload: response, type: actionType });
}, [response]);
const data = useSelector(state => state[dataType]);
return data;
};
export default useFetch;
Inside of my component I need to fetch replies when a button is clicked
const ParentComment = ({ comment }) => {
const handleShowMoreReplies = (e) => {
e.preventDefault();
}
let replies = useFetch(
`/api/comment_replies?comment_id=${comment.id}`,
null,
"REPLIES_RECEIVED",
"replies"
);
return (
<div>
<Comment comment={comment} />
<div className="replies">
{replies.map(reply => (
<Comment key={reply.id} comment={reply} />
))}
<a href="#" className="show_more" onClick={handleShowMoreReplies}>
Show More Replies ({comment.replies_count - 1})
</a>
</div>
</div>
);
};
If I put useFetch call inside of the handler I hget an error that Hooks can't be called there, but I need to call it only when the button is clicked so I don't know if there is a way to implement that.
I think you have subtle problems in your useFetch hook
1.your useEffect is having dep of ${url} and ${actionType } which you need to define.
2.In order to call this hook by clicking the button, you need to expose the setUrl as follows
const useFetch = ( initialUrl, options, actionType, dataType) => {
const [url, setUrl ] = useState(initialUrl);
const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(url);
const data = await res.json();
dispatch({ payload: data, type: actionType });
} catch (error) {
console.log(error);
}
};
fetchData();
}, [url, actionType]);
const data = useSelector(state => state[dataType]);
return [ data, setUrl ];
};
export default useFetch;
Then when you are trying to use this hook, you can
const [data, fetchUrl] = useFetch(
`/api/comment_replies?comment_id=${comment.id}`,
null,
"REPLIES_RECEIVED",
"replies"
);
Then every time you have a button you can simply call
fetchUrl(${yourUrl}).
your hook will receive the new URL, which is the dep of your hook and rerender it.
Here is an related article
https://www.robinwieruch.de/react-hooks-fetch-data

Categories