I'd like make an API call, which user input makes part of the API URL. Data is only fetched on demand after user submit.
My problem is: after first time input and submit, input is processed as an empty string, constructed wrong URL and made API call. (still loads data but the wrong data)
Only after second submit does it get actual user input, construct the correct URL and display the right data.
monitering network:
User input is stored in enteredWallet, Console.log(enteredWallet) prints the input, but setOwner(enteredWallet) doesn't change owner to be enteredWallet.
import { useState } from "react";
// example input: 0x147412d494731cbb91dbb5d7019464a536de04dc
function App() {
const [data, setData] = useState([]);
const [enteredWallet, setEnteredWallet] = useState("");
const [owner, setOwner] = useState("");
const walletChangeHandler = (event) => {
setEnteredWallet(event.target.value);
};
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
fetchNFTHandler();
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
};
function fetchNFTHandler() {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${owner}&order_direction=desc&offset=0&limit=10`
)
.then((res) => {
return res.json();
})
.then((data) => {
const transformedData = data.assets.map((element, index) => {
return {
title: element.name,
id: index,
};
});
setData(transformedData);
console.log("fetched");
});
}
return (
<div className="App">
<header className="App-header">
<h3>Show me assets in this wallet</h3>
<form onSubmit={submittedHandler}>
<input
placeholder="wallet address"
value={enteredWallet}
onChange={walletChangeHandler}
/>
<button>Submit</button>
</form>
<div>
{data.map((element) => (
<li key={element.id}>{element.title}</li>
))}
</div>
</header>
</div>
);
}
export default App;
Because owner in fetchNFTHandler doesn't update immediately after call setOwner.
Why don't use onwer as a param.
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
fetchNFTHandler(enteredWallet); //here
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
};
function fetchNFTHandler(owner) {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${owner}&order_direction=desc&offset=0&limit=10`
)...
or if you need use it as state indeed.
use useEffect to call fetchNFTHandler
useEffect(() => {
fetchNFTHandler();
}, [owner]) // when owner change, fetchNFTHandler will be call
If you want use a variable, it can take effect at once. you can try useRef.
const ownerRef = useRef("");
const submittedHandler = (event) => {
event.preventDefault();
ownerRef.current = enteredWallet;
fetchNFTHandler();
};
function fetchNFTHandler(owner) {
fetch(
`https://api.opensea.io/api/v1/assets?owner=${ownerRef.current}&order_direction=desc&offset=0&limit=10`
)...
The function returned by useState (in your case, setEnteredWallet or setOwner) is not synchronous. The state is not immediately changed after calling either it. If you want to call fetchNFTHandler every time enteredWallet changes, you can use useEffect. Or simply, you can pass enteredWallet to fetchNFTHandler as a parameter. An example usage of useEffect:
useEffect(() => {
fetchNFTHandler();
console.log("enteredWallet:", enteredWallet);
console.log("owner:", owner);
}, [owner, enteredWallet]) // Call method above when owner or enteredWallet change
const submittedHandler = (event) => {
event.preventDefault();
setOwner(enteredWallet);
// You don't need the following lines anymore
// fetchNFTHandler();
// console.log("enteredWallet:", enteredWallet);
// console.log("owner:", owner);
};
Related
https://github.com/patr4519/githubUserInfo.git
The essence of the application:
In input entering logins of github users separated by commas, the app shows the result consisting of photo, user name and data reg. After entering the user (users) into the input and pressing the Enter button, an array with users in the form of objects gets into the users state, which are then used to display cards.
The problem is that the Main component is rendered only after the Enter button is pressed and some other action is performed, for example, pressing the Clear button or additional input in the input field, despite the fact that the users state is updated after pressing the Enter button. And in theory, there should be a rerender, which does not happen when it is necessary.
What could be the problem? I attach a link to the repository.
function App() {
return (
<div className='wrapper'>
<Nav />
<Header />
<Main />
</div>
);
}
function Main() {
const [users, setUsers] = React.useState([]);
const [searchValue, setSearchValue] = React.useState('');
const onChangeSearchValue = (event) => {
setSearchValue(event.target.value);
}
const addUsers = () => {
let arrOfJson = []
for (let user of searchValue.split(', ')) {
fetch(`https://api.github.com/users/${user}`)
.then(us => us.json())
.then((json) => arrOfJson.push(json))
.catch((err) => {
console.log(err);
})
}
setUsers(arrOfJson);
}
const clearInput = () => {
setSearchValue('')
}
return (
<div className='main'>
<InputForm addUsers={addUsers} onChangeSearchValue={onChangeSearchValue} clearInput={clearInput} />
<Users users={users} />
</div>
);
}
export default App;
The fetch is asynchronous so when you have setUsers after a loop inside addUsers, it stores an empty array actually because data hasn't been populated yet:
const addUsers = () => {
let arrOfJson = []
for (let user of searchValue.split(', ')) {
fetch(`https://api.github.com/users/${user}`)
.then(us => us.json())
.then((json) => arrOfJson.push(json))
.catch((err) => {
console.log(err);
})
}
setUsers(arrOfJson); // sets an empty array
}
You can fix with something like this:
const addUsers = async () => {
for (let user of searchValue.split(", ")) {
let resultJson = await fetch(`https://api.github.com/users/${user}`);
let result = await resultJson.json();
setUsers((ps) => [...ps, result]);
}
};
I'm amazed that I can't solve this myself, but I'm having a basic issue here. Essentially I just want to destructure the user variable in the useSetWelcome hook to prevent the use of verbose chaining such as user.user.email - for instance, const { email } = user does not work and instead needs user.user.
I tried changing from const useSetWelcome = user => { to const useSetWelcome = ({ user }) => {, but that results in an infinite loop.
Where am I going wrong here? My code demo: https://stackblitz.com/edit/react-b1jroe
And the code:
import React, { useState, useEffect } from 'react';
const joe = {
name: 'Joe',
email: 'joe#bloggs.com'
};
// const useSetWelcome = ({ user }) => { // infinite loop problem
const useSetWelcome = user => {
const [name, setName] = useState(null);
const [welcomeMsg, setWelcomeMsg] = useState('No user detected');
// const { email } = user; // needs user.user
// console.log('user', user);
// console.log('{user}', { user });
// console.log('user.email', user.email); // should be joe#bloggs.com
// console.log('email', email); // should be joe#bloggs.com
console.log('user?.user?.email', user?.user?.email); // works
if (user.name) {
setName(user.name);
setWelcomeMsg('welcome ' + user.name);
}
return { name, welcomeMsg };
};
const App = () => {
// const [user, setUser] = useState(joe); // joe or {joe}? and why?
const [user, setUser] = useState(joe);
console.log('state user', user);
const toggleLogin = user => {
if (user) {
setUser(null);
} else {
setUser(joe);
}
};
const loginMsg = user ? 'Logout' : 'Login';
const Welcome = user => {
const { name, welcomeMsg } = useSetWelcome(user);
return (
<p>
{welcomeMsg} {name}
</p>
);
};
return (
<div>
<Welcome user={user} />
<button onClick={() => toggleLogin(user)}>{loginMsg}</button>
</div>
);
};
export default App;
The problem is, <Welcome /> is a component. A component receives only one parameter, props. So, when you write this: const Welcome = user => {, its actually const Welcome = props => {.
Long story short, change this line to const Welcome = ({ user }) => { (so you destruct user from props) and it will work.
P.S.: You're getting an infinite loop because inside your useSetWelcome hook, you have this condition:
if (user.name) {
setName(user.name)
}
When you use setName, the entire hook rerenders, and the condition is tested again. Again, user.name will exist, and setName will get called again, and again, and forever. To achieve what I think you intended to, you have to improve the condition to something like this:
if (user.name && user.name !== name) {
setName(user.name);
setWelcomeMsg('welcome ' + user.name);
}
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
I have an API call set up with two search buttons from one input box. Each button adds something using state to the api call which the code below should demonstrate. The calls both work fine independently and display the correct information.
If a user has clicked the 'wild cards' button the results show but then on clicking the 'standard set' button the results don't re-render the correct results until the button is pressed a second time (vice versa for both buttons).
I have removed the un-related code as to condense the question
Any help appreciated
Home.js - with api call, state and functions passed down as props to the searchBar
export const Home = () => {
const [searchTerm, setSearchTerm] = useState('')
const [wild, setWild] = useState('')
let accessToken;
const getCards = async() => {
try {
await getToken()
const response = await fetch(`https://eu.api.blizzard.com/hearthstone/cards/?collectible=1${wild}&textFilter=${searchTerm}&locale=en-US$access_token=${accessToken}`, {
headers: {
Authorization: `Bearer ${accessToken}`
}})
const data = await response.json()
const {cards} = data
if (cards){
const newCards = cards.map((card) => { ** some data i want ** }
setCards(newCards)
}
} catch (error) {
console.log(error)
}
}
return (
<SearchBar getCards = {getCards}
setWild = {setWild}
setSearchTerm = {setSearchTerm} />
</div>
</div>
)
}
SearchBar Component - again condensed for the sake of this question
export const SearchBar = (props) => {
const searchBox = useRef('')
const handleClickWild = (e) => {
e.preventDefault()
props.setWild('')
props.getCards()
}
const handleClickStandard = (e) => {
e.preventDefault()
props.setWild('&set=standard')
props.getCards()
}
const handleChange = (e) => {
props.setSearchTerm(e.target.value)
}
return (
<form>
<input className = 'input-search'
type = 'text'
placeholder = 'Search for Cards by Name...'
ref = {searchBox}
onChange = {handleChange} />
<div className = 'search-buttons'>
<input type = 'submit' className = 'button-search' onClick = {handleClickWild} value = 'Wild Cards' />
<input type = 'submit' className = 'button-search' onClick = {handleClickStandard} value = 'Standard Set' />
</div>
</form>
)
}
You have to use useEffect hook here.
You can use wild in dependency array and whenever you change the value of searchTerm use effect will automatically call your getCards function.
As you mentioned in the comment you want to show changes when user search anything then keep the wild in simple variable and add search term in useEffect and if you want you can add both in the useEffect dependency array
useEffect(()=> {
getCards()
}, [searchTerm])
Just remove explicite calls of props.getCards after setting wild from SearchBar component.
I solved this with useEffect as suggested but I added an 'if (hasSearched)' as state value of is the user had searched previously to prevent the API auto calling on page load
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