Problem
I am attempting to make a dynamic content box to incrementally fill in a form in ReactJS. As part of this, I simply want a box/area that changes its content when the user clicks 'Next' or 'Prev'.
The problem I appear to be facing is that the onClick method is getting called multiple times (obviously - I've only clicked it once).
I have a MWE below to demonstrate the problem I'm facing.
import React, {useEffect, useState} from 'react';
export default function Test () {
const [section, setSection] = useState(1);
const [sectionContent, setSectionContent] = useState();
useEffect( ()=>{
console.log("ENTRY useEffect");
if (section == 1) {
setSectionContent(<div>Details</div>);
}
else if(section == 2){
setSectionContent(<div>Advanced Details</div>);
}
else if(section == 3){
setSectionContent(<div>Super Advanced Details</div>);
}
console.log("EXIT useEffect");
},[section]);
function gotoPrevSection(){
console.log("ENTRY gotoPrevSection");
if(section == 1){
setSection(1);
}
else if(section == 2){
setSection(2);
}
console.log("EXIT gotoPrevSection");
}
function gotoNextSection(){
console.log("ENTRY gotoNextSection");
if (section == 1) {
setSection(2);
}else if(section == 2){
setSection(3);
}
console.log("EXIT gotoNextSection");
}
return (
<>
{sectionContent}
<div>
<button onClick={() => gotoPrevSection()}>
Prev
</button>
<button onClick={() => gotoNextSection()}>
Next
</button>
</div>
</>
);
}
Opening the page, we have 'Details' and the 'Prev' and 'Next' buttons. When I click 'Next' I skip all the way to the 'Super Advanced Details' section - skipping the middle section !
The console log looks like this:
[Log] ENTRY gotoNextSection
[Log] EXIT gotoNextSection
[Log] ENTRY useEffect
[Log] EXIT useEffect
[Log] ENTRY gotoNextSection
[Log] EXIT gotoNextSection
[Log] ENTRY useEffect
[Log] EXIT useEffect
[Log] ENTRY gotoNextSection
[Log] EXIT gotoNextSection
You can clearly see that the 'gotoNextSection' method is called three times even though it's only clicked once. What am I doing wrong ?
What I've tried
I am aware of the need for the onClick portion to look like onClick={() => function()} rather than just onClick=function(). This isn't the problem - the method isn't getting called straight away otherwise the details page wouldn't be static and stable - it's only when clicking this occurs.
I am React 17.0.2.
The Inertia init looks like:
createInertiaApp({
title: title => `${title} - ${appName}`,
resolve: name => require(`./Pages/Test.js`),
setup({ el, App, props }) {
return render(<App {...props} />, el);
},
});
What is going on here? Is this a React bug in the newer version ?
The Ghost option in Browsersync (that is included in Laravel Mix) is causing this.
Disable it to stop button double clicking ! browsersync.io/docs/options/#option-ghostMode
I've updated your functions as below, and introduce a totalSection constant.
In addition, you should be using useMemo instead of useEffect for your type of use (you save one additional render).
Codesandbox
import React, { useEffect, useState, useMemo } from "react";
const totalSections = 3;
export default function Test() {
const [section, setSection] = useState(1);
const sectionContent = useMemo(() => {
if (section == 1) {
return(<div>Details</div>);
} else if (section == 2) {
return(<div>Advanced Details</div>);
} else if (section == 3) {
return(<div>Super Advanced Details</div>);
}
},[section])
function gotoPrevSection() {
console.log("ENTRY gotoPrevSection");
setSection((prev) => (prev === 1 ? totalSections : prev - 1));
console.log("EXIT gotoPrevSection");
}
function gotoNextSection() {
console.log("ENTRY gotoNextSection");
setSection((prev) => (prev === totalSections ? 1 : prev + 1));
console.log("EXIT gotoNextSection");
}
return (
<>
{sectionContent}
<div>
<button onClick={() => gotoPrevSection()}>Prev</button>
<button onClick={() => gotoNextSection()}>Next</button>
</div>
</>
);
}
Related
So I have a webpage that's meant to model a sort of questionnaire. Each question would take up the whole page, and the user would switch back and forth between questions using the arrow keys. That part's fine, swapping components on button pressing doesn't seem to be an issue, and I got a proof-of-concept of that working before.
The trouble is, these questionnaires won't have the same number of questions every time. That'll depend on the choice of the person making the questionnaire. So I stored the number of questions in a variable held in the Django Model, and I fetch that variable and try to generate x number of components based on that integer. At the moment I'm just trying to get it to generate the right number of components and let me navigate between them properly, I'll worry about filling them with the proper content later, because I'm already having enough trouble with just this. So here's my code:
import React, { useState, useEffect, cloneElement } from 'react';
import { useParams } from 'react-router-dom'
import QuestionTest from './questiontest';
function showNextStage(displayedTable, questionCount) {
let newStage = displayedTable + 1;
if (newStage > questionCount) {
newStage = questionCount;
}
return newStage;
}
function showPreviousStage(displayedTable) {
let newStage = displayedTable - 1;
if (newStage < 1) {
newStage = 1;
}
return newStage;
}
export default function Questionnaire(props) {
const initialState = {
questionCount: 2,
is_host: false
}
const [ roomData, setRoomData ] = useState(initialState)
const { roomCode } = useParams()
useEffect(() => {
fetch("/audio/get-room" + "?code=" + roomCode)
.then(res => res.json())
.then(data => {
setRoomData({
roomData,
questionCount: data.questionCount,
isHost: data.isHost,
})
})
},[roomCode,setRoomData])
const [ displayedTable, setDisplayedTable ] = useState(1);
let initialComponents = {};
for (let i = 1; i < roomData.questionCount + 1; i++) {
initialComponents[i] = <div><p>{i}</p> <QuestionTest /></div>
}
// "<QuestionTest />" is just a random component I made and "{i}" is
// to see if I'm on the right one as I test.
const [components] = useState(initialComponents);
useEffect(() => {
window.addEventListener('keydown', (e) => {
if (e.keyCode == '39') {
setDisplayedTable(showNextStage(displayedTable, roomData.questionCount));
} else if (e.keyCode == '37') {
setDisplayedTable(showPreviousStage(displayedTable));
}
});
return () => {
window.removeEventListener('keydown', (e) => {
if (e.keyCode == '39') {
setDisplayedTable(showNextStage(displayedTable, roomData.questionCount));
} else if (e.keyCode == '37') {
setDisplayedTable(showPreviousStage(displayedTable));
}
});
};
}, []);
return (
<div>
{components[displayedTable]}
</div>
)
}
So the trouble with this is, I need to set an initial state for the questionCount variable, or else I get errors. But this initial state is replaced almost immediately with the value set for this questionnaire, as retrieved by the fetch function, and so the state resets. The initial questionCount value of 2 however gets used in the component generation and in the adding of the eventListeners, and so when the page is generated it just has two components rather than a number matching questionCount's value for the questionnaire.
I don't really understand this tbh. If I remove the window.addEventLister from useEffect and make it standalone, then it works and the right number of components are generated, but then it adds a new EventListener every time the state refreshes, which starts to cause immense lag as you switch back and forth between pages as the function calls pile up and up.
So I don't really know how to achieve this. My entire way of going about this from the onset is probably terribly wrong (I just included it to show I made an attempt and wasn't just asking for it to be done for me), but I can't find any examples of what I'm trying to do online.
I have this function that gets data from a service using a fetch api call and waits for the response using async and await. If the response isn't null, it loads a react component and passes the fetched data to the component, it uses react state to manage data content.
Because of the wait, i had to introduce an integer counter to help me manage the react page. So the integer counter is initialized to 0 and only increments if the response from fetch call isn't null. So i keep showing a progress bar as long as the counter is 0. Once the data state changes, the integer counter is incremented and the page loads the a new react component with the fetched data.
function CheckDeliveries(){
const [deliveries, setDeliveries] = useState(null);
const urlAPpend = "delivery/findByCustomerId/";
let userid = JSON.parse(localStorage.getItem("User"))["userId"];
const httpMethod = 'GET';
let token = localStorage.getItem("Token");
console.error('RELOAD >>>>>>', reload);
if(reload < 1){
makeApiAuthCallsWithVariable(userid,urlAPpend,httpMethod,token).then(
data => {
if (data !== null) {
//console.log("Api response: Data "+JSON.stringify(data));
setDeliveries(data);
reload++
}else{
console.error('Response not ok', data);
}
}
)
}
if(reload >= 1 && deliveries !== null){
reload = 0;
console.error('RELOAD AllDeliveryDiv >>>>>>', reload);
return (<AllDeliveryDiv obj = {deliveries} />);
}else if(reload >= 1 && deliveries === null){
reload = 0;
console.error('RELOAD MakeDeliveryDiv >>>>>>', reload);
return (<MakeDeliveryDiv />);
}else if(reload === 0){
return ( <ProgressBar striped variant="primary" now={value}/>);
}
}
My Question
I have tried using a boolean state instead of integer counter but the page gets into an infinite loop whenever i update the boolean state. Is there a way i can implement this boolean state in without experiencing the infinite loop ?
After i fetch the data, and reset the counter to 0. I log the value before reset and after reset and i get 1 and 0 respectively. But when i call this function again without reloading the entire page, counter value remains 1 even after i had reset it earlier. How do i resolve this?
Any better way to implement this, please share.
It's hard to really tell what you're going for with reload, so that's why I left the whole MakeDeliveryDiv stuff out, but this would load data from your endpoint when the component is first mounted using the useEffect side effect hook:
function CheckDeliveries() {
const [deliveries, setDeliveries] = useState(null);
const [loaded, setLoaded] = useState(false);
React.useEffect(() => {
const userid = JSON.parse(localStorage.getItem("User"))["userId"];
const token = localStorage.getItem("Token");
makeApiAuthCallsWithVariable(
userid,
"delivery/findByCustomerId/",
"GET",
token,
).then((data) => {
setDeliveries(data);
setLoaded(true);
});
}, []);
if (!loaded) {
return <ProgressBar striped variant="primary" now={value} />;
}
if (deliveries === null) {
return <>Data not OK.</>;
}
return <AllDeliveryDiv obj={deliveries} />;
}
I am new to React and am trying to create my own scrollbar. I am using a local JSON API to simulate getting data which is then listing the data as 'cards'. I built a couple of other components to organize and handle the information, it looks like this:
Scrollbar (Buttons to Navigate)
|-->CardList (Handles iterating over the cards)
|-->Cards (Template for displaying an individual card)
The issue I am having is that when I trigger the Button event handleNext it will successfully update offset & limit and pass these to CardList. However, it does not reiterate over the array and output the next 5 items. Instead it removes all the Cards from CardList and I am left with an empty screen.
Scrollbar.js:
const { data isPending, error} = useFetch ('http://localhost:8000/data');
const [limit, setLimit] = useState(5);
const [offset, setOffset] = useState(0);
const handleNext = () => {
setOffset(offset + limit);
console.log("Offset: " +offset);
}
return (
<div className="scrollbar">
{error && <div>{error}</div>}
{isPending && <div>Loading...</div>}
{data && <CardList data={data} offset={offset} limit={limit}/> }
<Button onClick={handleNext}/>
</div>
);
}
export default Scrollbar;
CardList.js:
const CardList = ({data , offset, limit}) => {
return (
<div className="card-list" >
{data.slice(offset,limit).map((data) => (
<Card data={data} key={data.id}/>
))}
</div>
);
}
export default CardList;
The problem is when handleNext is triggered offset is going to be equal to 5 while limit keeps the value of 5 too, then when u do slice(offset,limit) is going to be replaced by slice(5,5) that returns an []. What u want is increasing the limit too if u increase the offset, for example:
const handleNext = () => {
setOffset(offset + limit);
setLimit(limit + limit)
}
I'm trying to build a component where an array of values are presented like a slider, where every time, the old content gets replaced by a new one, and that can be done using the buttons next and previous. The component is working, but I'm struggling a little bit in the edge cases, where I have to disable the buttons
I'll leave the link to a codesandbox where the component is being built, I'm sure it'll be easier to understand what's going on.
Link to sandbox
Try not to use state value for tracking button disabled status. Please check below.
import React, { useState } from "react";
export default function App() {
const data = ["q", "c", "s", "a"];
const [iterator, setIterator] = useState(0);
const [curr, setCurr] = useState(data[iterator]);
const fetchNext = () => {
if (iterator === data.length - 1) {
return;
}
setIterator((prev) => prev + 1);
setCurr(data[iterator + 1]);
};
const fetchPrevious = () => {
if (iterator === 0) {
return;
}
setIterator((prev) => prev - 1);
setCurr(data[iterator - 1]);
};
const nextDisabled = iterator >= data.length - 1;
const prevDisabled = iterator <= 0;
return (
<div>
<h1>{curr}</h1>
<button disabled={nextDisabled} onClick={fetchNext}>
next
</button>
<button disabled={prevDisabled} onClick={fetchPrevious}>
previous
</button>
</div>
);
}
I am using hooks in React Native. This is my code:
useEffect(() => {
if (var1.length > 0 ){
let sym = [];
var1.map((items) => {
let count = 0;
state.map((stateItems) => {
if(items === stateItems.name) {
count = count + 1;
}
})
if (count === 0) {
sym.push(items)
}
});
async function getAllStates (sym) {
let stateValues = [];
await Promise.all(sym.map(obj =>
axios.get(ServerURL + "/note?name=" + obj).then(response => {
stateValues.push(response.data[0]);
})
)).then(() =>{
setNewItem(stateValues);
});
}
getAllStates (sym);
}
}, [var1]);
useEffect(() => {
let stateValues = state;
for( let count = 0 ; count < newItem.length; count++ ){
stateValues.push(newItem[count]);
}
setState(stateValues);
}, [newItem]);
This runs successfully without any errors. However, when the state is displayed as below, I am not seeing the latest value added in the state. It shows all the previous values. When I refresh the whole application, I am able to see my value added.
return (
<View style={styles.container}>
<Text style = {{color:"white"}}>
{
state.map( (item, key) =>{
return(
<Text key = {key} style = {{color:"white"}}> {item.name} </Text>
)
})
}
</Text>
</View>
);
Can someone tell me why this is happening? I want to see the data render immediately after the axios call. I am using React Native.
when i force update using :stackoverflow.com/questions/53215285/... it works fine. However, i am looking for a better fix if anyone can provide?
This should do:
useEffect(() => {
var1.forEach(async (name) => {
if (state.some(item => item.name === name)) return;
const response = await axios.get(ServerURL + "/note?name=" + name);
setState(state => [...state, response.data[0]]);
});
}, [var1]);
I still see two issues in your approach:
this code may start the same ajax request multiple times before the result of the firstz ajax-request is added to state; this also means that the result for the same name may be added multiple times to state.
for each item of var1 times each item of state, this is an O(n*m) problem or in this case basically O(n²) as m is pretty fast catching up to n. You should find a better approach here.
And I'm almost certain that [var1] is wrong here as the dependency for useEffect. But you'd need to show where this value comes from to fix that, too.