I am having a slight issue with my react code. I want the data to be completely fetched before rendering. However, I have tried various ways such as 'setGroupLoaded to true' after the async call, but it is still not working. When the component first mounts, 'groupLoaded == false and myGroup == [],', then it goes to the next conditional statement where 'groupLoaded == true' but the myGroup [] is still empty. I was expecting the myGroup [] to be filled with data since groupLoaded is true. Please I need help with it.
function CreateGroup({ currentUser }) {
const [name, setName] = useState("");
const [myGroup, setMyGroup] = useState([]);
const [groupAdded, setGroupAdded] = useState(false);
const [groupLoaded, setGroupLoaded] = useState(false);
const handleChange = (e) => {
const { value, name } = e.target;
setName({
[name]: value,
});
};
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
useEffect(() => {
let groupId = myGroup.length ? myGroup[0].id : "";
let groupName = myGroup.length ? myGroup[0].groupName : "";
if (myGroup.length) {
JoinGroup(currentUser, groupId, groupName);
setTimeout(() => fetchGroupMembers(), 3000);
}
}, [myGroup]);
let itemsToRender;
if (groupLoaded && myGroup.length) {
itemsToRender = myGroup.map((item) => {
return <Group key={item.id} item={item} deleteGroup={deleteGroup} />;
});
} else if (groupLoaded && myGroup.length === 0) {
itemsToRender = (
<div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="exampleInputTitle">Group Name</label>
<input
type="text"
className="form-control"
name="name"
id="name"
aria-describedby="TitleHelp"
onChange={handleChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Add group{" "}
</button>
</form>
</div>
);
}
return (
<div>
<h3>{currentUser ? currentUser.displayName : ""}</h3>
{itemsToRender}
</div>
);
}
The problem is here:
useEffect(() => {
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
fetchGroupCreated();
setGroupLoaded(true);
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
When you call setGroupLoaded(true), the first call to setMyGroup(groupArr) hasn't happened yet because fetchMyGroup(currentUser) is asynchronous. If you've never done this before, I highly recommend putting in some logging statements, to see the order in which is executes:
useEffect(() => {
const fetchGroupCreated = async () => {
console.log("Got data")
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
};
console.log("Before starting to load")
fetchGroupCreated();
setGroupLoaded(true);
console.log("After starting to load")
return () => {
setMyGroup([]);
};
}, [currentUser.id, groupAdded, deleteGroupStatus]);
The output will be:
Before starting to load
After starting to load
Got data
This is probably not the order you expected, but explains exactly why you get groupLoaded == true, but the myGroup is still empty.
The solution (as Nick commented) is to move the setGroupLoaded(true) into the callback, so that it runs after the data is retrieved:
const fetchGroupCreated = async () => {
let groupArr = await fetchMyGroup(currentUser);
setMyGroup(groupArr);
setGroupLoaded(true);
};
fetchGroupCreated();
An alternative approach may be to await the call to fetchGroupCreated(). I'm not sure if it'll work, but if it does it'll be simpler:
await fetchGroupCreated();
Related
I have a situation that I can not solve for some reason.
Let me explain what I'm trying to do:
I am trying to get number of crypto tokens received by entering a contract and an address (which helps me to reach out the API results).
For the moment, it works like this: https://thecryptoguetter.netlify.app/pages/HowMuchGlobal
So as you can see, I ask the user to chose a network (because API endpoints are different but they are all structured the same way)
My goal: Allowing users to simply enter a contract and an address, and I'll manage to get the right answer on my side.
What do I need:
I need to make multiple requests and get the answer of the one which works. For that, this is quite easy on the paper, the message in the API is "OK" if it's ok, and "NOTOK" if not. Simple ahah
But for some reason, I'm drowning with the logic, and tried multiple things but my brain is bugging hard on this one.
Here is what I did:
import axios from "axios";
const HowMuchTest = () => {
const [tokenContract, setTokenContract] = useState("");
const [address, setAddress] = useState("");
const [answerAPI, setAnswerAPI] = useState([]);
const [total, setTotal] = useState([]);
const [decimals, setDecimals] = useState(18);
const [txDatasArray, setTxDatasArray] = useState();
const handleTokenContract = (event) => {
const searchTokenContract = event.target.value;
setTokenContract(searchTokenContract);
};
const handleAddress = (event) => {
const searchAddress = event.target.value;
setAddress(searchAddress);
};
const clearToken = () => {
setTokenContract("");
setAddress("");
setTotal([]);
};
useEffect(() => {
axios
.all([
axios.get(
`https://api.bscscan.com/api?module=account&action=tokentx&contractaddress=${tokenContract}&address=${address}&page=1&offset=100&startblock=0&endblock=27025780&sort=asc&apikey=[KEY]`
),
axios.get(
`https://api.etherscan.io/api?module=account&action=tokentx&contractaddress=${tokenContract}&address=${address}&page=1&offset=100&startblock=0&endblock=27025780&sort=asc&apikey=[KEY]`
),
])
.then(
axios.spread((...res) => {
setAnswerAPI(res);
})
);
}, [address, tokenContract]);
console.log(answerAPI[0].data);
useEffect(
(answerAPI) => {
if (answerAPI[0].data.message === "OK") {
setTxDatasArray(answerAPI[0]);
} else if (answerAPI[1].data.message === "OK") {
setTxDatasArray(answerAPI[1]);
}
},
[answerAPI]
);
console.log(txDatasArray);
useEffect(() => {
let dataArray = [];
for (let i = 0; i < answerAPI.length; i++) {
let toLower = answerAPI[i]?.to?.toLowerCase();
let addressLower = address?.toLowerCase();
if (toLower === addressLower) {
dataArray.push({
ticker: answerAPI[0].tokenSymbol,
value: Number(answerAPI[i].value),
});
}
}
setDecimals(Number(answerAPI[0]?.tokenDecimal));
setTotal(dataArray);
}, [address, answerAPI, decimals]);
const totaltotal = total.reduce((a, v) => (a = a + v.value), 0);
const FinalTotal = () => {
if (decimals === 6) {
return (
<div>
{(totaltotal / 1000000).toFixed(2).toLocaleString()} {total[0].ticker}
</div>
);
} else if (decimals === 18) {
return (
<div>
{(totaltotal / 1000000000000000000).toFixed(2).toLocaleString()}{" "}
{total[0].ticker}
</div>
);
}
};
return (
<div className="how-much">
<div className="received-intro">
<h1>Find out how much did you receive on a particular token</h1>
</div>
<div className="token-contract">
<input
type="text"
placeholder="Token Contract Address"
value={tokenContract}
onChange={handleTokenContract}
/>
<input
type="text"
placeholder="ERC20 Address"
value={address}
onChange={handleAddress}
/>
</div>
{totaltotal !== 0 && (
<div className="show-total-received">
<div className="total-received">
<FinalTotal />
</div>
<div>{total.ticker}</div>
</div>
)}
<button className="btn btn-info" onClick={clearToken}>
Clear
</button>
</div>
);
};
export default HowMuchTest;
The part to look is the two first useEffect.
I get the following answer from the console: Cannot read properties of undefined (reading 'data')
Which is weird because I can reach answerAPI[0].data in the first console.log
So yes, I'm totally lost and can't figure it out. I tried a for loop in a useEffect too but it ended in an infinite loop...
Thank you in advance for the ones who will read until there!
Have a good day
The problem is in your useEffect implementation. The function passed to useEffect does not take any arguments, so answerAPI as written will always be undefined, because inside this function it is the non-existing argument, and not your state as you might have expected.
You can change it to:
useEffect(() => {
if (answerAPI[0]?.data?.message === "OK") {
setTxDatasArray(answerAPI[0]);
} else if (answerAPI[1]?.data?.message === "OK") {
setTxDatasArray(answerAPI[1]);
}
}, [answerAPI]);
I changed option but useeffect not update input. Please guide me where i make mistake. First i use useEffect to setCurrency after that i use mapping for getCurrency to add it on Select option. onCitySelect i added it to setSelectedId when i change Select option. Lastly, i tried to get address with api but the problem is i need to change api/address?currency=${selectId.id}` i added selectedId.id but everytime i change option select it is not affect and update with useEffect. I tried different solution couldn't do it. How can i update useEffect eveytime option select change (selectId.id) ?
export default function Golum() {
const router = useRouter();
const dispatch = useDispatch();
const [getCurrency, setCurrency] = useState("");
const [getAddress, setAddress] = useState("");
const [selectCity, setSelectCity] = useState("");
const [selectId, setSelectId] = useState({
id: null,
name: null,
min_deposit_amount: null,
});
const [cityOptions, setCityOptions] = useState([]);
useEffect(() => {
setSelectCity({ label: "Select City", value: null });
setCityOptions({ selectableTokens });
}, []);
const onCitySelect = (e) => {
if (e == null) {
setSelectId({
...selectId,
id: null,
name: null,
min_deposit_amount: null,
});
} else {
setSelectId({
...selectId,
id: e.value.id,
name: e.value.name,
min_deposit_amount: e.value.min_deposit_amount,
});
}
setSelectCity(e);
};
const selectableTokens =
getCurrency &&
getCurrency.map((value, key) => {
return {
value: value,
label: (
<div>
<img
src={`https://central-1.amazonaws.com/assets/icons/icon-${value.id}.png`}
height={20}
className="mr-3"
alt={key}
/>
<span className="mr-3 text-uppercase">{value.id}</span>
<span className="currency-name text-uppercase">
<span>{value.name}</span>
</span>
</div>
),
};
});
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/currencies`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setCurrency(data);
});
}
return () => (mounted = false);
}, []);
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/address?currency=${selectId.id}`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setAddress(data.address);
})
.catch((error) => {});
}
return () => (mounted = false);
}, []);
return (
<div className="row mt-4">
<Select
isClearable
isSearchable
onChange={onCitySelect}
value={selectCity}
options={selectableTokens}
placeholder="Select Coin"
className="col-md-4 selectCurrencyDeposit"
/>
</div>
<div className="row mt-4">
<div className="col-md-4">
<Form>
<Form.Group className="mb-3" controlId="Form.ControlTextarea">
<Form.Control className="addressInput" readOnly defaultValue={getAddress || "No Address"} />
</Form.Group>
</Form>
</div>
</div>
);
}
The second parameter of the useEffect hook is the dependency array. Here you need to specify all the values that can change over time. In case one of the values change, the useEffect hook re-runs.
Since you specified an empty dependency array, the hook only runs on the initial render of the component.
If you want the useEffect hook to re-run in case the selectId.id changes, specify it in the dependency array like this:
useEffect(() => { /* API call */ }, [selectId.id]);
I think you are accessing the e object wrong. e represents the click event and you should access the value with this line
e.target.value.id
e.target.value.value
I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Löschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.
Hello i have a prefilled slider which is for rating, i prefill it with data from the API and if a user slides i like to update the API Database. Everything works fine except the slider always jumps back to the prefilled value by useEffect. How can i change the state so that the value from the user takes precedence over the prefilled value ?
This is my code:
const [ user ] = useContext(Context);
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
const movieId = splitLocation[2];
const [ MovieResults, setMovieResults ] = useState([]);
const [ value, setValue ] = useState(5);
useEffect(() => {
let isMounted = true;
fetch(`${API_URL}account/xxxxxxxx/rated/movies?api_key=${API_KEY}&session_id=${user.sessionId}&sort_by=created_at.desc`)
.then(res => res.json())
.then(data => isMounted ? setMovieResults(data.results) : null)
.then(isMounted ? MovieResults.forEach((arrayItem) => {
if(arrayItem.id.toString() === movieId.toString()) {
setValue(arrayItem.rating);
}
}) : null)
return () => { isMounted = false };
},[user.sessionId, MovieResults, movieId]);
const changeSlider = (e) => {
// This value should override the default value from the api
setValue(e.currentTarget.value);
};
return (
<div>
<input
type="range"
min="1"
max="10"
className="rate-slider"
value={value}
onChange={e => changeSlider(e)}
/>
{value}
<p>
<button className="rate-button" onClick={() => callback(value)}>Rate</button>
</p>
</div>
)
Thanks help is appreciated.
I finally managed to solve it. I needed to give one of the two setValue precedence over the other based on the condition that the user slided the slider. Up to now i didnt know useRef, with this hook i could manage it. Here is the code:
const [ user ] = useContext(Context);
const location = useLocation();
const { pathname } = location;
const splitLocation = pathname.split("/");
const movieId = splitLocation[2];
const [ MovieResults, setMovieResults ] = useState([]);
const [ value, setValue ] = useState(5);
const ref = useRef(0);
useEffect(() => {
let isMounted = true;
fetch(`${API_URL}account/jensgeffken/rated/movies?api_key=${API_KEY}&session_id=${user.sessionId}&sort_by=created_at.desc`)
.then(res => res.json())
.then(data => isMounted ? setMovieResults(data.results) : null)
.then(isMounted ? MovieResults.forEach((arrayItem) => {
if(arrayItem.id.toString() === movieId.toString()) {
//console.log(ref.current)
if(ref.current === 0) {
setValue(arrayItem.rating);
}
}
}) : null)
return () => { isMounted = false };
},[user.sessionId, MovieResults, movieId]);
const handleSlide = (e) => {
ref.current = e.currentTarget.value
setValue(ref.current);
}
return (
<div>
<input
type="range"
min="1"
max="10"
className="rate-slider"
value={value}
onChange={e => handleSlide(e)}
/>
{value}
<p>
<button className="rate-button" onClick={() => callback(value)}>Rate</button>
</p>
</div>
)
Following your general approach, this seems to work fine for me:
const fetch = () => new Promise(resolve => setTimeout(() =>
resolve({json: () => ({rating: 4})})
, 1000));
const App = () => {
const [sliderVal, setSliderVal] = React.useState(0);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
fetch("foo")
.then(res => res.json())
.then(data => {
setLoading(false);
setSliderVal(data.rating);
});
}, []);
const handleSliderChange = evt => {
setSliderVal(evt.target.value);
};
return (
<div>
{loading
? <p>loading...</p>
: <div>
<input
type="range"
min="1"
max="10"
value={sliderVal}
onChange={handleSliderChange}
/>
{sliderVal}
</div>
}
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
What the below code does is to get data from API, and then render it on the page. searchChange function takes a value from the input tag, and setValue for query state. My api endpoint takes argument to filter the API such as http://127.0.0.1:8000/api/deals/?q=${query}.
I'm very confused how I can update the DealList component with the API updated with query state whenever typing something in the input tag. I'm thinking of that I need to something in searchChange function, but not sure what to do there.
index.js
const useFetch = (url, query, defaultResponse) => {
const [result, setResult] = useState(defaultResponse);
const getDataFromAPI = async url => {
try {
const data = await axios.get(url);
setResult({
isLoading: false,
data
});
} catch (e) {}
};
useEffect(() => {
if (query.length > 0) {
getDataFromAPI(`${url}?q=${query}`);
} else {
getDataFromAPI(url);
}
}, []);
return result;
};
const Index = ({ data }) => {
const query = useInput("");
const apiEndpoint = "http://127.0.0.1:8000/api/deals/";
const dealFetchResponse = useFetch(apiEndpoint, query, {
isLoading: true,
data: null
});
const searchChange = e => {
query.onChange(e);
query.setValue(e.target.value);
};
return (
<Layout>
<Head title="Home" />
<Navigation />
<Container>
<Headline>
<h1>The best lease deal finder</h1>
<h4>See all the lease deals here</h4>
</Headline>
<InputContainer>
<input value={query.value} onChange={searchChange} />
</InputContainer>
{!dealFetchResponse.data || dealFetchResponse.isLoading ? (
<Spinner />
) : (
<DealList dealList={dealFetchResponse.data.data.results} />
)}
</Container>
</Layout>
);
};
export default Index;
The biggest challenge in something like this is detecting when a user has stopped typing.. If someone is searching for 'Milk' - when do you actually fire off the API request? How do you know they aren't searching for 'Milk Duds'? (This is hypothetical, and to demonstrate the 'hard' part in search bars/APIs due to their async nature)..
This is typically solved by debouncing, which has been proven to work, but is not very solid.
In this example, you can search Github repos...but even in this example, there are unnecessary requests being sent - this is simply to be used as a demonstration. This example will need some fine tuning..
const GithubSearcher = () => {
const [repos, setRepos] = React.useState();
const getGithubRepo = q => {
fetch("https://api.github.com/search/repositories?q=" + q)
.then(res => {
return res.json();
})
.then(json => {
let formattedJson = json.items.map(itm => {
return itm.name;
})
setRepos(formattedJson);
});
}
const handleOnChange = event => {
let qry = event.target.value;
if(qry) {
setTimeout(() => {
getGithubRepo(qry);
}, 500);
} else {
setRepos("");
}
};
return (
<div>
<p>Search Github</p>
<input onChange={event => handleOnChange(event)} type="text" />
<pre>
{repos ? "Repo Names:" + JSON.stringify(repos, null, 2) : ""}
</pre>
</div>
);
};
ReactDOM.render(<GithubSearcher />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>