I want to run some logic when a button is clicked and I want to change the state in that logic. So far react is giving me a to many rerenders error. Here is my code so far
const [current, setCurrent] = useState(0)
// const [theRest, theRest] = useState(false);
const next = (order, i) => {
let nextSlide = order + 1
setCurrent(nextSlide)
if (nextSlide > i) {
nextSlide = 0
}
}
{data.allSanitySlideDeck.edges.map(({ node: slide }, i) => (
<React.Fragment key={i}>
{/* {current === slide.order && ( */}
<>
<Card>
<h1>{slide.postTitle}</h1>
<h2></h2>
<p></p>
</Card>
<button>back</button>
<button onClick={next(slide.order, i)}>next</button>
</>
// )}
</React.Fragment>
))}
onClick expects a function inside the {} not the result of calling a function
Try changing to :
onClick={() => next(slide.order, i)}
What is basically happening is that function call is being made every time render() occurs and causes a state update which causes a new render call and therefore a race condition
Related
I am using react-transition-group to fade out various components. I'm converting simple conditional renders such as:
{valueToDisplay && <MyComponent {...valueToDisplay} />}
To transitions such as:
<CSSTransition
in={!!valueToDisplay}
unmountOnExit
classNames="fade"
addEndListener={(node, done) => node.addEventListener("transitionend", done, false)}
>
<MyComponent {...valueToDisplay} />
</CSSTransition>
The issue I'm running into is when the "in" property of the transition becomes false, and the exit transition is running, the child component will now have null prop values. This can cause exceptions or cause the child content to flash and change during the exit. What I would like to see instead is that during the exit transition, the content will remain unchanged.
The first solution I came up with was to make child components to cache previous values of their props, and then use those previous values when their props become null. However I don't like this solution because it forces all components which will be transitioned to introduce new and confusing internal logic.
The second attempt I made was to create a wrapper component which cached the previous value of props.children, and whenever "in" becomes false, renders the cached children instead. This essentially "freezes" the children as they were the last time in was true, and they don't change during the exit transition. (If this solution is the general practice, is there a better way of doing this, perhaps with the useMemo hook?)
For such a common use case of fading content out, this solution doesn't seem very intuitive. I can't help but feeling I'm going about this the wrong way. I can't really find any examples of having to cache/memoize content to keep it displaying during fade outs. It seems like something somewhere has to remember the values to display when performing the exit transition. What am I missing?
Here is a minimal example and working example:
import { useRef, useEffect, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { CSSTransition } from 'react-transition-group';
const Pet = ({ type, age }) => {
return (
<div>
Your pet {type || 'null'} is age {age || 'null'}
</div>
);
};
const Fade = ({ show, children }) => {
const nodeRef = useRef(null);
return (
<CSSTransition
nodeRef={nodeRef}
in={show}
unmountOnExit
classNames="fade"
addEndListener={(done) => nodeRef.current.addEventListener("transitionend", done, false)}
>
<span ref={nodeRef}>
{children}
</span>
</CSSTransition>
);
};
const FadeWithMemo = ({ show, children }) => {
const previousChildren = useRef();
useEffect(() => {
previousChildren.current = show ? children : null;
}, [show, children]);
return (
<Fade show={show}>
{show ? children : previousChildren.current}
</Fade>
);
};
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => setCurrentPet(getPet())}>Set</button>
<button onClick={() => setCurrentPet(null)}>Clear</button>
<div>
The Problem:
<Fade show={!!currentPet}>
<Pet {...currentPet} />
</Fade>
</div>
<div>
Potential Fix:
<FadeWithMemo show={!!currentPet}>
<Pet {...currentPet} />
</FadeWithMemo>
</div>
</>
);
};
const root = createRoot(document.getElementById('root'));
root.render(<Example />);
You can detach the visible condition from the pet state so that you have more granular control over whether something is visible and what is actually being displayed.
const Example = () => {
const [currentPet, setCurrentPet] = useState(null);
const [showPet, setShowPet] = useState(false);
const getPet = () => {
return {
type: (Math.random() > .5) ? 'Cat' : 'Dog',
age: Math.floor(Math.random() * 15) + 1
};
};
return (
<>
<button onClick={() => {
setCurrentPet(getPet());
setShowPet(true);
}}>Set</button>
<button onClick={() => setShowPet(false)}>Clear</button>
<div>
<Fade show={showPet}>
<Pet {...currentPet} />
</Fade>
</div>
</>
);
};
or you can have the visible be part of the pet state and only set that part to false.
There is a task! It is necessary to create a new clicker by clicking on the button, which will be created with a value equal to the sum of all previous clickers (if there are clickers with a value of 3 and 5, the next one should be created with a value of 8). At the moment, I'm stuck on the problem that when creating a new clicker, the value of the previous one is stored in the volume. Tell me how to solve the problem? (Yes, and according to the condition, every fourth component should not be a clicker, but a timer)
function App() {
const [click, setClick] = useState([]);
function addClicker() {
setClick([...click, <Counter />]);
}
function del() {
setClick([...click.slice(0, click.length - 1)]);
}
return (
<div className='app'>
<AddDelButton />
<button className='btn' onClick={addClicker}>addClicker</button>
<button className='btn' onClick={del}>delClicker</button>
{click.map((item, i) =>
!((i + 1) % 4) ? <NoClicker key={i} /> : <Counter props={click} key={i} />
)}
</div>
);
}
////
function Counter(props) {
const State = useSelector((state) => state.ourState.count);
const dispatch = useDispatch();
console.log('====>>>', State);
return (
<div className='counter'>
<button className='btnClick'
onClick={() => dispatch(incrementValue())}
>
Increment
</button>
<h2>{State}</h2>
<button className='btnClick'
onClick={() => dispatch(decrementValue())}
>
Decrement
</button>
<button>Delete</button>
</div>
);
}
I'm trying out useFormContext in react to update a form by adding another section. Here's my code so far: https://codesandbox.io/s/funny-swanson-fiomq
I have the following:
season_form.js - the main form
season_data_form.js - the sub-form; originally, this was the only one being called by season_form.
challenge_form.js - this is the new section, also called by season_form.
My idea was to take out the form hooks from season_data_form.js and challenge_form.js, move it to season_form.js, and use useFormContext to transfer stuff in between, such as the registers, and submit actions.
However, I keep getting the following error, when run locally:
Cannot update a component (SeasonForm) while rendering a different
component (SeasonDataForm).
And then somewhere down the line, it says this:
Maximum update depth exceeded
I'm not sure which one to take first.
Near as I can tell, it happens in season_form.js, whenever the user tries to load the data in currentSeasonData by changing the value of seasonIndex:
const SeasonForm = () => {
const [seasonsData, setSeasonsData] = useState();
const [seasonIndex, setSeasonIndex] = useState(0);
const [currentSeasonData, setCurrentSeasonData] = useState(null);
const formMethods = useForm({ mode: "onBlur" });
const { register, errors, control, handleSubmit, reset, formState, setError } = formMethods;
const onSubmit = async (data) => { console.log(data); };
useEffect((_) => {
async function fetchData() {
try {
let data_seasons = await dbGetSeasons();
setSeasonsData(data_seasons);
if (data_seasons[0]) { setSeasonIndex(data_seasons[0].id); }
// ^^ Comment that out and the page will load, but will crash the moment you select anything in the dropdown.
} catch (error) {
console.error(error);
}
}
fetchData();
}, []);
useEffect((_) => {
seasonsData &&
seasonsData.forEach((data) => {
if (data.id === seasonIndex) { setCurrentSeasonData(data); }
});
},
[seasonIndex]
);
return (
<Box>
<Dropdown>
<Dropdown.Toggle variant="success" id="dropdown-basic">
{currentSeasonData ? (
<> {currentSeasonData.shortname} ({currentSeasonData.name}) </>
) : (
<>Choose a season...</>
)}
</Dropdown.Toggle>
{seasonsData && (
<Dropdown.Menu>
{seasonsData.map((season) => {
const seasonKey = "season-" + season.id;
return (
<Dropdown.Item key={seasonKey} onClick={() => { setSeasonIndex(season.id); }}>
{season.shortname} ({season.name})
</Dropdown.Item> );
})}
</Dropdown.Menu>
)}
</Dropdown>
<Accordion defaultActiveKey="toggle_season">
<FormProvider {...formMethods}>
<form onSubmit={handleSubmit(onSubmit)}>
<Card>
<Accordion.Toggle as={Card.Header} eventKey="toggle_season">
<Button variant="link">Season Data and Battle Pass</Button>
</Accordion.Toggle>
<Accordion.Collapse eventKey="toggle_season">
<Card.Body>
{currentSeasonData ? (<SeasonDataForm seasonData={currentSeasonData} />) : null}
</Card.Body>
</Accordion.Collapse>
</Card>
<Card>
<Accordion.Toggle as={Card.Header} eventKey="toggle_challenges">
<Button variant="link">Challenges</Button>
</Accordion.Toggle>
<Accordion.Collapse eventKey="toggle_challenges">
<Card.Body>
{currentSeasonData && currentSeasonData.pass_data && currentSeasonData.pass_data.challenges ? (
<ChallengeForm challenges={currentSeasonData.pass_data.challenges || {}} />
) : (
<Alert variant="danger">Missing `challenges` data</Alert>
)}
</Card.Body>
</Accordion.Collapse>
</Card>
</form>
</FormProvider>
</Accordion>
</Box> );
};
export default SeasonForm;
I really can't pinpoint what's happening. My limited experience with react tells me that some value somewhere, when changed, causes an infinite loop.
Of note is that commenting out <SeasonDataForm> allows the page to work, albeit with half the subforms gone.
Any ideas where I can fix this?
Can you try the following
const methods = useFormContext();
const register = methods.register();
const errors = methods.errors;
const control = methods.control;
const handleSubmit = methods.handleSubmit();
const reset = methods.reset;
const formState = methods.formState;
const setError = methods.setError();
const clearErrors = methods.clearErrors();
to
const methods = useFormContext();
const register = methods.register;
const errors = methods.errors;
const control = methods.control;
const handleSubmit = methods.handleSubmit;
const reset = methods.reset;
const formState = methods.formState;
const setError = methods.setError;
const clearErrors = methods.clearErrors;
It doesn't seem like the infinite loop is from your SeasonData, but because your are reassigning the functions to those variables, its not an issue to do so,
but the problem is that you are not just reassigning the functions to new variables, you are executing them by using "()", thats different you are getting the result of the function call not the function itself that way, so i removed the "()" from all except from "useFormContext()" where its mostly intentionally done ,
I hope this fix your issue
So I have this Display() function which takes events from the Google Calendar and the function returns a list of elements (each element is associated with a slider) to be rendered on the screen (see return statement of Display() function) and renders them as seen here. So each element comes with a Remove button so that I can remove an unwanted element from the page using the hideMe() function inside the Display() function. The hideMe() function does seem to do its work, however, it removes all the elements on the page as seen here. So I am struggling to figure out what I should fix so that when I click on the Remove button, it only removes the element and the slider associated to that remove button. I am new to React and JavaScript so please help. Any help is appreciated and thank you in advance.
function Display() {
const isMounted = useRef(true);
const [items, saveItems] = useState([]);
const [visible, setVisible] = useState(true);
const [fading, setFading] = useState(false);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
(async () => {
const items = await fetchItems();
//Do not update state if component is unmounted
if (isMounted.current) {
saveItems(items);
}
})();
}, []);
function hideMe() {
setFading(true);
setTimeout(() => setVisible(false), 650);
}
return (
<Tab.Pane attached={false}>
<h5>Rate stress level for each event</h5>
<br/>
{items.map(item => (
<div key={item.id} isvisible={!fading}
style={visible ? null : { display: "none" }}>
<Typography id="discrete-slider-restrict" gutterBottom>
{item}
<button onClick={hideMe}>Remove</button>
</Typography>
<PrettoSlider aria-label="pretto slider" defaultValue={98} step={null}marks={stresslevel}/>
</div>
))}
</Tab.Pane>
)
}
It seems to me that this issue is happening because all elements are available in same state or i would say that they all share same state. So, this executes for all. If it is possible for you to extract it to a new component and use the hideMe function there. This will i am sure work for each individual elements.
It is my suggestion please go through below. May be you have to tweak a little bit.
You can extract the elements in a separate component like:
const Item = props => {
const [visible, setVisible] = useState(true);
const [fading, setFading] = useState(false);
function hideMe() {
setFading(true);
setTimeout(() => setVisible(false), 650);
}
return (
<div isvisible={!fading} style={visible ? null : { display: "none" }}>
<Typography id="discrete-slider-restrict" gutterBottom>
{item}
<button onClick={hideMe}>Remove</button>
</Typography>
<PrettoSlider aria-label="pretto slider" defaultValue={98}
step={null} marks={stresslevel}/>
</div>
);
};
export default Item;
Then you can use it like:
// import Item
{items.map(item => (
<Item key={item.id} itemObj={item} />
// in case if you need item obj then props.itemObj will get you the object.
))}
In this way you can manage the hideMe function with the separate specific Item component.
I have a component that is checking if some state is true or false. I show a <p> tag if true and hide a <h3>. I am pulling the data from a gaphQL query so there is a map method and there are three <Card>'s now if I click the card and run my showFull function it shows the p tags on all the elements, instead I just want to isolate the specific one it is clicking on.
Here is my component
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<Card onClick={showFull} background={testimonial.testimonialImage.url}>
{testimonialFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
))}
</Testimonials>
Here is my state and my function
const [testimonialFull, setTestimonialFull] = useState(false)
const showFull = () => {
setTestimonialFull(true)
}
Attempting Alexander's answer. The issue I am having now is Cannot read property 'testimonialImage' of undefined
Here is the component
const IndexPage = ({ data }) => {
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}
return (
...
Here is where I invoke it in the map function
...
return (
... (bunch of other jsx/html)
<Testimonials className="testimonaials">
{data.allDatoCmsTestimonial.edges.map(({ node: testimonial }) => (
<TestimonialCard/>
))}
</Testimonials>
...
Wrap the cards in a custom component
const TestimonialCard = ({testimonial})=>{
const [showFull, setShowFull] = useState(false)
const handleClick = useCallback(()=>{
setShowFull(true)
//setShowFull(s=>!s)//If you want toggle behaviour
},[])
return <Card onClick={handleClick} background={testimonial.testimonialImage.url}>
{showFull ?
<p>{testimonial.fullLengthQuote}</p>
:
<h3>{testimonial.shortQuote}</h3>
}
</Card>
}