The component dot't render when it must be rendered - reactjs

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

Related

Updating displayed data on mouse enter

I would like to update text which is displayed inside a <div> element. I would love to do it when the cursor enters the <div> element.
Basically I'm fetching some data from the API and I display only one parameter (name). If a user enters the <div> with the name displayed I would like to show some details, i.e. description and price.
This is my code which I tried to complete my task.
import {useEffect, useState} from "react";
import requestOptionsGet from "../utilities/requestOptions";
import validateResponse from "../utilities/validators";
const Warehouse = () => {
const [items, setItems] = useState([]);
const [texts, setTexts] = useState([]);
const getItems = async () => {
const url = "http://localhost:8000/api/items/"
return await fetch(url, requestOptionsGet)
.then((response) => validateResponse(response, url))
.then((response) => response.json())
.then((data) => setItems(data))
};
useEffect(() => {
getItems();
}, []);
useEffect(() => {
setTexts(items.map((item) => (
{
id: item.id,
name: item.name,
description: item.description,
price: item.price,
currentDisplay: <h2>{item.name}</h2>,
})
))
}, [items]);
const displayName = (data) => {
console.log(
"displayName"
);
};
const displayDetails = (data) => {
const itemID = parseInt(data.currentTarget.getAttribute("data-item"));
const displayInfo = texts.find(text => text.id === itemID);
displayInfo.currentDisplay = <p>{displayInfo.description}</p>
setTexts(texts);
console.log(texts);
console.log(
"displayDetails"
);
return(
displayInfo.currentDisplay
);
};
return(
<div className="container">
<h1>Your warehouse.</h1>
<h2>All your items are listed here.</h2>
<hr />
{texts.map((text) => (
<button className="container-for-single-item" id={text.id} key={text.id}
onMouseEnter={displayDetails} onMouseLeave={displayName} data-item={text.id}>
{text.currentDisplay}
</button>
))}
</div>
);
}
export default Warehouse;
The functions work (everything is displayed in the console as it should be) and even the texts change. However the paragraph does not appear. How can I fix my code? Thanks!
Never modify state directly
const newTexts = texts.map(text => text.id === itemID ? { ...text, currentDisplay: <p>{text.description}</p> } : text);
setTexts(newTexts);

First time input doesn't get submitted in React

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);
};

Very simple react destructuring issue with a custom hook

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);
}

Searchbar cause input unchecked

I have a ul that displays users with a checkbox input. When searching for a user by surname/first name, the previously selected input checkboxes are removed. How to prevent it?
function App() {
let copyList = useRef();
const [contacts, setContacts] = useState([]);
useEffect(() => {
fetch(api)
.then((res) => res.json())
.then((data) => {
copyList.current = data;
setContacts(copyList.current);
})
.catch((err) => console.log(err));
}, []);
contacts.sort((a, b) => (a.last_name > b.last_name ? 1 : -1));
const searchHandler = (value) => {
const contactsList = [...copyList.current].filter((x) => {
if (value.toLowerCase().includes(x.last_name.toLowerCase())) {
return x.last_name.toLowerCase().includes(value.toLowerCase());
} else if (value.toLowerCase().includes(x.first_name.toLowerCase())) {
return x.first_name.toLowerCase().includes(value.toLowerCase());
} else if (value === "") {
return x;
}
});
setContacts(contactsList);
};
return (
<div className="App">
<Header />
<SearchBar onSearch={(value) => searchHandler(value)} />
<ContactsList contacts={contacts} />
</div>
);
}
Input component is in ContactsList component.
function Input(props) {
const [isChecked, setIsChecked] = useState(false);
const [id] = useState(props.id);
const handleChange = () => {
setIsChecked(!isChecked);
console.log(id);
};
return (
<input
type="checkbox"
className={style.contact_input}
checked={isChecked}
onChange={handleChange}
value={id}
/>
);
}
When you filter the contacts and update the contacts state, the list in ContactList will be re-rendered as it is a new array which means you will have a new set of Input components with the default state. In order to persist the previously selected items you will also have to store the array of selected IDs in state. The Input component should receive the isChecked and onChange values from props so you can pass in a condition where you check if the current ID is in the list of selected IDs which means it is checked and when the user clicks on it, you can just simply update that array by adding the ID if it's not currently in the array or removing from it if it is already present (copying the array first since it's a state variable).

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

Resources