Enter input data to api's endpoint query in react hooks - reactjs

I am new to React. I want to integrate weatherstack API with my project. Problem is I do not understand how can I input country's capital's name into API's endpoint.
const App = () => {
const [ countries, setCountries ] = useState([])
const [ searchingItem, setSearchingItem ] = useState('')
const [ showAll, setShowAll ] = useState(true)
const [ weatherData, setWeatherData ] = useState([])
const [ capital, setCapital ] = useState('')
const api_key = process.env.REACT_APP_WEATHER_API
useEffect(() => {
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => {
setCountries(countries.concat(response.data))
})
}, [])
useEffect(() => {
console.log(capital, searchingItem)
axios
.get(`http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`)
.then( response => {
console.log(searchingItem)
setWeatherData(weatherData.concat(response.data))
})
.catch((err) => console.log(err))
}, [])
const handleChangeSearch = (event) => {
setSearchingItem(event.target.value)
setShowAll(false)
}
let countriesToShow = showAll
? countries
: countries.filter(country => country.name.indexOf(searchingItem) !== -1);
const handleClick = (country) => {
setCapital(country.capital)
setSearchingItem(country.name)
}
return (
<div id='App'>
<h2>Phonebook</h2>
<div>
Search a name <Filter value={searchingItem} onChange={handleChangeSearch} />
<div>
{ countriesToShow.length === 250
? ''
: countriesToShow.length >= 10
? <div><p>Too many matches, specify another filter</p></div>
: countriesToShow.length > 1
? countriesToShow.map(country => <div>
<li key={nanoid()}>{country.name}
<button type='button' onClick={() => handleClick(country)}>Show</button></li></div>)
: countriesToShow.length === 1
? <div>
<View country={countriesToShow[0]} key={nanoid()}/>
</div>
: 'No results occured'}
</div>
<div>
<div>{console.log(weatherData)}</div>
</div>
</div>
</div>
)
I need to display weather data below the View component if I pressed button 'Show'.
I have tried to create state variable capital and set it to country's name whenever onClick event will happen:
const handleChangeSearch = (event) => {
setSearchingItem(event.target.value)
setShowAll(false)
}
But it seems like I can't access to capital variable in the useState hook:
useEffect(() => {
console.log(capital, searchingItem)
axios
.get(`http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`)
.then( response => {
console.log(capital)
setWeatherData(weatherData.concat(response.data))
})
.catch((err) => console.log(err))
}, [])
because this is the only thing that I am getting when hook makes a call:
error:
code: 601
info: "Please specify a valid location identifier using the query parameter."
type: "missing_query"
__proto__: Object
success: false
Any hints about how to display weather info whenever user clicks Show button?
Here is repo of the project if it is needed: https://github.com/rd9911/UniHelsinkiFullStack/tree/main/part2/part2c
If more info needed please ask.

When the hook runs initially, capital is empty ''. You want to call the API when capital changes using dep array ([capital]) and avoid calling the API when it is empty. Try this:
useEffect(() => {
if(! capital) { return;}
console.log(capital, searchingItem)
axios
.get(`http://api.weatherstack.com/current?access_key=${api_key}&query=${capital}`)
.then( response => {
console.log(searchingItem)
setWeatherData(weatherData.concat(response.data))
})
.catch((err) => console.log(err))
}, [capital])

When you provide no values inside the second argument(which is an array) of useEffect hook, it will run only once in the beginning. So in your case the call to the API goes even before the capital variable is set. To solve this you can just add capital to the array [capital] as a second argument to useEffect.

Related

Trying to get data from api and map to another component in React

I'm trying to map an array of movies which I get from an API.
The data is fetched successfully but when I try to map the values and display, it becomes undefined and does not show anything.
I'm new to React so any help and advice would be helpful.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios
.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
})
.catch((error) => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return (
<div>
{items.map((item) => {
<p>{item.title}</p>;
})}
</div>
);
The data is stored like this:
0: {
adult: false,
backdrop_path: '/9eAn20y26wtB3aet7w9lHjuSgZ3.jpg',
id: 507086,
title: 'Jurassic World Dominion',
original_language: 'en',
...
}
You're not returning anything from your map
{
items.map((item) => {
// Add a return
return <p>{item.title}</p>
})
}
First, your items value is an empty array[] as you have initialized with setState([]) and your useEffect() runs only after your component is rendered which means even before you could do your data fetching, your HTML is being displayed inside which you are trying to get {item.title} where your items is an empty array currently and hence undefined. You will face this issue often as you learn along. So if you want to populate paragraph tag with item.title you should fast check if your items is an empty array or not and only after that you can do the mapping as follow and also you need to return the element from the map callback. If it takes some time to fetch the data, you can choose to display a loading indicator as well.
const [items, setItems] = useState([]);
const getMovieData = () => {
axios.get(api_url)
.then((response) => {
const allMovies = response.data;
console.log(allMovies);
setItems(allMovies);
}).catch(error => console.error(`Error: ${error}`));
};
useEffect(() => {
getMovieData();
}, []);
return ( < div > {
items.length !== 0 ? items.map((item) => {
return <p > {
item.title
} < /p>
}) : < LoadingComponent / >
}
<
/div>
);
Good catch by Ryan Zeelie, I did not see it.
Another thing, since you're using promises and waiting for data to retrieve, a good practice is to check if data is present before mapping.
Something like :
return (
<div>
{ (items.length === 0) ? <p>Loading...</p> : items.map( (item)=>{
<p>{item.title}</p>
})}
</div>
);
Basically, if the array is empty (data is not retrieved or data is empty), display a loading instead of mapping the empty array.

query.onSnapshot() runs twice every time text box changes text on the page - Firebase and react

I have an input tag: <input type="text" onChange={(e) => setMessage(e.target.value)} /> (message and setMessage are state variables).
I also have a Firebase query: firebase.firestore().collection('messages').where('users', 'array-contains', uid)
I set up a query.onSnapshot listener to listen for collection updates, and put a console.log inside of it.
It triggers twice every time the text changes in the text box, and I included the entire tag because it doesn't trigger when another input tag, with an onChange attribute (but doesn't change a state variable) is changed, so it seems that the problem is somewhere with the state variable.
Does anyone know what might be triggering the onSnapshot event?
function Chatroom(props) {
const [ messages, setMessages ] = useState([])
const [ chatWithUser, setChatWithUser ] = useState("")
const [ chatWithUserTemp, setChatWithUserTemp ] = useState("")
const [ message, setMessage ] = useState("")
const { uid, photoURL } = auth.currentUser
const mref = firestore.collection('messages')
const query = mref.where('users', 'array-contains', uid).orderBy('time')
const getContent = async() => {
let content = []
await query.get().then((docs) => {
docs.forEach(doc => {
if(doc.data().users.includes(chatWithUser)) {
content.push(<li key={doc.id}>From: {doc.data().sender}, message: {doc.data().message}</li>)
}
})
})
setMessages(content)
}
const updateMessages = (data) => {
}
query.onSnapshot((snapshot) => {
getContent()
})
useEffect(() => {
getContent()
}, [])
const sendMessage = () => {
mref.add({
message: message,
sender: auth.currentUser.uid,
time: firebase.firestore.FieldValue.serverTimestamp(),
users: [auth.currentUser.uid, chatWithUser]
})
setMessage("")
}
return (
<div>
<div className="sidenav">
<h3>Chat with Users</h3>
<input type="text" className="form-control" placeholder="Enter UID" onChange={(e) => setChatWithUserTemp(e.target.value)}></input>
<Button onClick={() => setChatWithUser(chatWithUserTemp)}>Chat</Button>
<p>Your UID: {auth.currentUser.uid}</p>
<Logout />
</div>
<div className="main">
<p>Chatting with {chatWithUser}</p>
<ul>
{messages}
</ul>
<input type="text" value={message} className="form-control" placeholder="Message..." onChange={(e) => setMessage(e.target.value)} />
<Button onClick={sendMessage}>Send</Button>
</div>
</div>
)
}
This method call is in the body of the component:
query.onSnapshot((snapshot) => {
getContent()
})
The component body gets called every time the component rerenders, so this is creating a new subscription to the query every time the component renders.
Since subscribing to a query is a side effect, it should be called inside a useEffect callback:
function Chatroom(props) {
// ...
// mref is used both inside and outside the effect. useMemo ensures it's only
// called once so we can add it to the effect's dependency array
const mref = useMemo(() => firestore.collection("messages"), []);
useEffect(() => {
// since query and getContent are only used by this effect, we should
// define them inside the effect so we don't have to worry about
// adding them to the dependency array
const query = mref.where("users", "array-contains", uid).orderBy("time");
const getContent = async () => {
let content = [];
await query.get().then((docs) => {
docs.forEach((doc) => {
if (doc.data().users.includes(chatWithUser)) {
content.push(
<li key={doc.id}>
From: {doc.data().sender}, message: {doc.data().message}
</li>
);
}
});
});
setMessages(content);
};
const unsubscribe = query.onSnapshot((snapshot) => {
getContent();
});
// Firebase will call the onSnapshot callback once automatically, so there
// is no need to call getContent outside of onSnapshot
// When the component is unmounted, we need to unsubscribe from the
// query so we don't keep getting updates
return () => unsubscribe();
}, [mref]);
const sendMessage = () => {
mref.add({
message: message,
sender: auth.currentUser.uid,
time: firebase.firestore.FieldValue.serverTimestamp(),
users: [auth.currentUser.uid, chatWithUser],
});
setMessage("");
};
...
}
I think that your query.onSnapshot function is being triggered in every state update. The general approach with listeners is to put them in lifecycle hooks and then clean them
something like this:
useEffect(() => {
const unsubscribe = query.onSnapshot((snapshot) => {
getContent()
})
return () => unsubscribe()
}, [])
The return of an useEffect will unsubscribe the listener
you only will call getContent in the onSnapshot , also the snapshot will have your latest messages, so not need to query them again in getContent

Setting and updating form inputs with firebase database and react hooks

In my app I have profile section with a form. When the component mounts I want to fetch user data from firebase, and display it in the form, with the current values of the user profile. Either using the "value" prop or the "placeholder" prop.
When the user makes changes in the form inputs and submit the changes, I want the database to update and the form to update with the new data.
Currently I can make the database value appear in the form input field, or I can make the form input field empty, but update the database. But not both.
The following code makes the database data render in the form input, but it cant be changed.
I know it could be something with the second useEffect() and the getUserData() function, that I cant seem to figure out.
const UserEdit = (props) => {
const [currentUser, setCurrentUser] = useState('');
const [forening, setForening] = useState('');
useEffect(() => {
firebase_app.auth().onAuthStateChanged(setCurrentUser);
}, [])
const getUserData = async () => {
await dbRef.ref('/' + currentUser.uid + '/profil/' ).once('value', snapshot => {
const value = snapshot.val();
setForening(value)
})
}
useEffect(() => {
getUserData()
},[] )
const handleInput = (event) => {
setForening(event.target.value)
}
const updateUserData = () => {
dbRef.ref('/' + currentUser.uid + '/profil/' ).set({foreningsnavn: forening}, function(error) {
if(error) {
console.log("update failed")
} else {
alert(forening)
}
})
}
const handleClick = () => {
updateUserData()
}
return (
<>
<div className="card-body">
<div className="row">
<div className="col-md-5">
<div className="form-group">
<label className="form-label">{Forening}</label>
<input className="form-control" type="text" value={forening} onChange={handleInput}/>
</div>
</div>
</div>
</div>
</>
)
}
Your second useEffect will run only one time because the second argument array [] of dependencies is empty:
useEffect(() => {
getUserData()
},[] )
You can add foreign dependency to make useEffect run with input change
useEffect(() => {
getUserData()
},[foreign] )
or you can use polling to sync database state

Can't Update State After Deleting Item from Backend with Axios and React

Working on a practice phonebook project where the visitor can enter a name and phone number. Utilizing json-server for the backend and React for front end.
The full code is here Phonebook Github Code
The functionality of adding a number works fine, but I'm having issues with a button which allows the visitor to delete a number. When a user clicks on the 'delete' button, it is successfully removed from the backend (file is db.json). However on the frontend, the deleted number isn't removed, and I can see that the state isn't changing.
Any help is appreciated.
Here's my delete function for removing the number from backend
const deletePerson = id => {
const request = axios.delete(baseUrl + `/` + id);
return request.then(response => response.data);
};
and this function is being called from a button onClick method
const deleteNum = event => {
let personID = event.target.value;
if (window.confirm("Do you really want to delete?")) {
personService
.deletePerson(personID)
.then(() => {
setPersons(persons.filter(item => item.id !== personID));
})
.catch(error => {
console.log("Error", error);
});
}
};
and the rest of the relevant code to give this context
const App = () => {
const [persons, setPersons] = useState([]);
const [newName, setNewName] = useState("");
const [newNumber, setNewNumber] = useState("");
const [filter, setFiltered] = useState("");
useEffect(() => {
personService.getAll().then(initialPersons => setPersons(initialPersons));
}, []);
console.log("Persons", persons);
const peopleToShow =
filter === ""
? persons
: persons.filter(person =>
person.name.toLowerCase().includes(filter.toLowerCase())
);
const rows = () =>
peopleToShow.map(p => (
<p key={p.name}>
{p.name} {p.number}{" "}
<span>
<button value={p.id} onClick={deleteNum}>
delete
</button>
</span>
</p>
));
item.id is stored as a number, whereas the personID is taken as a string. Hence, try changing !== to !=.

Setting state with React Hooks + Fetch API does not trigger re-render

I'm trying to learn React Hooks by building a simple Domain Availability checker. Specifically, I'm playing with useState()
The aim is just to have an input field where the user types a keyword, hits Enter, and then the app will run a fetch request for that keyword with a number of different domain endings.
Here is my App component (or check codesandbox here: https://codesandbox.io/s/5410rkrq1p)
const App = () => {
const domainEndings = [
".ws",
".ga",
".cf",
".tk",
".ml",
".gq",
".kz",
".st",
".fm",
".je"
];
const [domainString, setDomainString] = useState("");
const [domainsArray, setDomainsArray] = useState([]);
const [lookedUpDomainsArray, setLookedUpDomainsArray] = useState([]);
const handleDomainChange = event => {
setDomainString(event.target.value);
setDomainsArray(
domainEndings.map(ending => event.target.value.trim() + ending)
);
};
let testArray = [];
const runDomainLookup = url => {
return fetch(
`https://domainr.p.rapidapi.com/v2/status?domain=${url}&mashape-key=${myAPIKEY}`
)
.then(res => res.json())
.then(data => {
testArray.push({
url: data.status[0].domain,
status: data.status[0].status
});
setLookedUpDomainsArray(testArray);
});
};
const handleSubmit = e => {
e.preventDefault();
setLookedUpDomainsArray([]);
testArray = [];
domainsArray.map(eachDomain => runDomainLookup(eachDomain));
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
value={domainString}
placeholder="type keyword then hit enter"
onChange={e => handleDomainChange(e)}
/>
</form>
{lookedUpDomainsArray &&
lookedUpDomainsArray.map((domain, index) => {
return (
<div key={index}>
{domain.url} is {domain.status}
</div>
);
})}
</div>
);
};
The bug that I'm experiencing is:
the state seems to be being set correctly (checked in React Dev Tools).
the first response from the mapped fetch requests is rendered correctly to the DOM
a re-render for the newly added state (from fetch request) is not triggered until the user presses a key in the input field
Here is a video demonstrating it : https://streamable.com/klshu
You will notice that the rest of the results don't appear until I start typing other characters into the input
Thanks in advance 🙏
Thanks! Yes I had tried Promise.all() however I wanted each fetch request to run separately (for example, if one fetch request times out, it would hold up ALL of the others).
Found a solution on Reddit
Essentially, when setting the state via Hooks, I should have passed in the previous state as so:
const runDomainLookup = url => {
return fetch(
`https://domainr.p.rapidapi.com/v2/status?domain=${url}&mashape-key=${myAPIKEY}`
)
.then(res => res.json())
.then(data => {
setLookedUpDomainsArray(prevArray => [
...prevArray,
{
url: data.status[0].domain,
status: data.status[0].status
}
]);
});
};
Working solution on CodeSandbox: https://codesandbox.io/s/51ky3r63x

Resources