functional component still rerender with react.memo - javascript

I want to make a quiz app with a countdown timer.
The timer use setinterval and the quiz has a functional component for Question.
I have already set react.memo for the question function component. But it keeps rerender each second. It works fine on desktop. But it flickers on mobile since the input for answer keep refocus
import { useEffect, useState, memo } from 'react';
function Game() {
let interval;
const [timer, setTimer] = useState(180000);
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [timer]);
const QuestionComponent = memo(props => {
return(<div></div>);
});
return (
<>
<div>{timer}</div>
<QuestionComponent/>
</>
);
}
The above code is the Game component, which will be called from App component
The question component keeps rerendering even there is nothing in it.
May anyone please advise? Thank you very much

Ideally you should define your components outside your other components. As mentioned in comment, this line of code will run on every render.
Even if you use React.memo(), this line itself will run on every render, in contrast to writing it outside where it will run normally.
If you are writing it inside, you have to persist the value between renders. It can be done using useCallback because a react component is technically a function:
const Component = useCallback(
memo(() => {
console.log("rerender");
return <div></div>;
}),
[]
);
Or another way is using a ref.
import { useEffect, useState, memo, useMemo, useCallback, useRef } from "react";
function App() {
let interval;
const [timer, setTimer] = useState(180000);
const componentRef = useRef();
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [timer]);
useEffect(() => {
const Component = memo(() => {
console.log("rerender");
return <div></div>;
});
componentRef.current = Component;
}, []);
return (
<>
<div>{timer}</div>
<>{componentRef.current && <componentRef.current />}</>
</>
);
}
export default App;
Link
But both above are hacks. And you will definitely run into issues when you use hooks.
Much better to just write it outside.
const QuestionComponent = memo(props => {
return(<div></div>);
});
import { useEffect, useState, memo } from 'react';
function Game() {
let interval;
const [timer, setTimer] = useState(180000);
useEffect(() => {
interval = setInterval(() => {
setTimer(timer - 1000);
}, 1000);
return () => {
if (interval) {
clearInterval(interval)
}
}
}, [timer]);
return (
<>
<div>{timer}</div>
<QuestionComponent/>
</>
);
}

Related

clearInterval not clearing the set interval onMouseUp

Trying to make a button event onMouseDown, a function should run at the end of the set amount of time. The function runs onMouseDown and clears the interval onMouseUp, but the interval still runs after releasing the button.
This is the code currently. I have the interval global and set it in the planting function. It should unset in the notPlanting function, but it does not.
import React from "react";
function PlantDefuser() {
var interval
function planting() {
interval = setInterval(() => {
console.log("Defuser Planted")
}, 1000)
}
function notPlanting() {
console.log(interval)
clearInterval(interval)
}
return (
<button onMouseDown={planting} onMouseUp={notPlanting}>Press and Hold</button>
)
}
export default PlantDefuser
This could help you:
useRef allows us to store and update data in the component without triggering a re-render. Now the only re-render happens when the props are updated.
We can store interval in a ref like so
import { useRef } from "react";
const PlantDefuser = () => {
const interval = useRef();
function planting() {
interval.current = setInterval(() => {
console.log("Defuser Planted");
}, 1000);
}
function notPlanting() {
clearInterval(interval.current);
}
return (
<button onMouseDown={planting} onMouseUp={notPlanting}>
Press and Hold
</button>
);
}
export default PlantDefuser
When you declare variables like so in the function component, it is being created on each render. You should be saving the interval id in a state like so:
import React, { useState } from "react";
const PlantDefuser = () => {
const [plantingInterval, setPlantingInterval] = useState(null);
const planting = () => {
const plantingIntervalId = setInterval(() => {
console.log("Defuser Planted");
}, 1000);
setPlantingInterval(plantingIntervalId);
};
const notPlanting = () => {
clearInterval(plantingInterval);
setPlantingInterval(null);
};
return (
<button onMouseDown={planting} onMouseUp={notPlanting}>
Press and Hold
</button>
);
};
export default PlantDefuser;
You might also want to make sure the interval is being cleared when the component unmounts.
You can use useEffect hook with cleanup function to manage the clearInterval method .
like this :
function PlantDefuser() {
const [run, setRun] = useState(false);
useEffect(() => {
if (run) {
const countTimer = setInterval(() => {
console.log("Defuser Planted");
}, 1000);
return () => {
console.log(countTimer);
clearInterval(countTimer);
};
}
}, [run]);
return (
<button onMouseDown={() => setRun(!run)} onMouseUp={() => setRun(!run)}>
Press and Hold
</button>
);
}
export default PlantDefuser;

React render component only for few seconds

In my existing react component, I need to render another react component for a specific time period.
As soon as the parent component mounts/or data loads, the new-component (or child component) should be visible after 1-2 seconds and then after another few seconds, the new-component should be hidden. This needs to be done only if there is no data available.
This is what currently I've tried to achieve:
import React, { useState, useEffect } from "react";
function App() {
const [showComponent, setShowComponent] = useState(false);
const sampleData = [];
useEffect(() => {
if (sampleData.length === 0) {
setTimeout(() => {
setShowComponent(true);
}, 1000);
}
}, [sampleData]);
useEffect(() => {
setTimeout(() => {
setShowComponent(false);
}, 4000);
}, []);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
The current implementation is not working as expected. The new-component renders in a blink fashion.
Here is the working snippet attached:
Any help to resolve this is appreciated!
Every time App renders, you create a brand new sampleData array. It may be an empty array each time, but it's a different empty array. Since it's different, the useEffect needs to rerun every time, which means that after every render, you set a timeout to go off in 1 second and show the component.
If this is just a mock array that will never change, then move it outside of App so it's only created once:
const sampleData = [];
function App() {
// ...
}
Or, you can turn it into a state value:
function App() {
const [showComponent, setShowComponent] = useState(false);
const [sampleData, setSampleData] = useState([]);
// ...
}
I have modified the code to work, hope this how you are expecting it to work.
import React, { useState, useEffect } from "react";
const sampleData = [];
// this has to be out side or passed as a prop
/*
reason: when the component render (caued when calling setShowComponent)
a new reference is created for "sampleData", this cause the useEffect run every time the component re-renders,
resulting "<h2>found error</h2>" to flicker.
*/
function App() {
const [showComponent, setShowComponent] = useState(false);
useEffect(() => {
if (sampleData.length === 0) {
const toRef = setTimeout(() => {
setShowComponent(true);
clearTimeout(toRef);
// it is good practice to clear the timeout (but I am not sure why)
}, 1000);
}
}, [sampleData]);
useEffect(() => {
if (showComponent) {
const toRef = setTimeout(() => {
setShowComponent(false);
clearTimeout(toRef);
}, 4000);
}
}, [showComponent]);
const componentTwo = () => {
return <h2>found error</h2>;
};
return <>First component mounted{showComponent && componentTwo()}</>;
}
export default App;
You can try this for conditional rendering.
import { useEffect, useState } from "react";
import "./styles.css";
const LoadingComponent = () => <div>Loading...</div>;
export default function App() {
const [isLoading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const onLoadEffect = () => {
setTimeout(() => {
setLoading(false);
}, 2000);
setTimeout(() => {
setIsError(true);
}, 10000);
};
useEffect(onLoadEffect, []);
if (isLoading) {
return <LoadingComponent />;
}
return (
<div className="App">
{isError ? (
<div style={{ color: "red" }}>Something went wrong</div>
) : (
<div>Data that you want to display</div>
)}
</div>
);
}
I needed to do imperatively control rendering an animation component and make it disappear a few seconds later. I ended up writing a very simple custom hook for this. Here's a link to a sandbox.
NOTE: this is not a full solution for the OP's exact use case. It simply abstracts a few key parts of the general problem:
Imperatively control a conditional render
Make the conditional "expire" after duration number of milliseconds.

why I can't stop interval in react functional component

In my code I have some problems with intervals. I have situation like this
import { useState } from 'react';
function App() {
const [count, setCount] = useState(0);
function handleClick() {
const timer = setInterval(() => {
if (count >= 10) {
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
export default App;
what am trying to do is to start an interval when user click a button and stop it when counter reaches 10, but it never stops and debugger says that inside setInterval count is always 0. Does anybody know what is the problem? I also found that if I rewrite component to class component like this
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
handleClick() {
let myInterval = setInterval(() => {
if (this.state.count >= 10) {
clearInterval(myInterval);
} else {
this.setState((prevState) => ({
count: prevState.count + 1,
}));
}
}, 1000);
}
render() {
return (
<>
<button onClick={this.handleClick.bind(this)}>start</button>
<p>{this.state.count}</p>
</>
);
}
}
it works perfectly.
I don't know what's happening and I spend almost day trying to figure out.
So does anybody know why class component works and why functional component doesn't?
Thanks in advance
You're seeing a stale count in the setInterval callback function since it captures the value at the time you're starting the timer.
You can use the "ref boxing" pattern to have a readable reference to the latest value of the state atom:
import {useState, useRef} from 'react';
function App() {
const [count, setCount] = useState(0);
const countRef = useRef(null);
countRef.current = count; // Update the boxed value on render
function handleClick() {
const timer = setInterval(() => {
if (countRef.current >= 10) { // Read the boxed value
clearInterval(timer);
} else {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
}
return (
<>
<button onClick={handleClick}>start</button>
<p>{count}</p>
</>
);
}
Note that you're currently not correctly cleaning up the timer on unmount, leading to React warning you about that when you'd unmount your app. The same holds for your class component.

Why eventListener re-render React Component

I am creating a stopwatch in React.js and i am wondering why window.addEventListener('keydown', callback) re-render my component?
import { useEffect, useState } from 'react';
import './App.scss';
import Timer from './Timer';
import Button from './Button';
import Time from './Time';
const App = () => {
const [isRunning, setIsRunning] = useState(false);
const [start, setStart] = useState(new Time(0));
const [stop, setStop] = useState(new Time(0));
const handleStart = () => {
const now = new Date();
setIsRunning(true);
setStart(new Time(now));
setStop(new Time(now));
};
const handleStop = () => {
setIsRunning(false);
setStop(new Time(new Date()));
};
const getTime = () => {
if (isRunning) {
return new Time(new Date().getTime() - start.origin);
} else {
return new Time(stop.origin - start.origin);
}
};
const handleKeyDown = (key) => {
console.log(key.code === 'Space');
};
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
});
return (
<div className="stopwatch">
<Timer getTime={getTime} />
<div className="buttons">
<Button role={'start'} callback={handleStart} />
<Button role={'stop'} callback={handleStop} />
</div>
</div>
);
};
export default App;
When i click start and then stop after let's say 3s. <Timer /> show correctly time that has passed, but then when i press Space on keyboard <Timer /> is re-rendering, showing new time. Then, when i switch my web-browser to VSCode and again to web-browser, <Timer /> isn't re-rendering
Here is my Timer component
import { memo, useEffect, useRef } from 'react';
const Timer = ({ getTime }) => {
const timer = useRef();
console.log('timer rendered');
useEffect(() => {
function run() {
const time = getTime().formatted();
timer.current.textContent = `${time.m}:${time.s}.${time.ms}`;
requestAnimationFrame(run);
}
run();
return () => {
cancelAnimationFrame(run);
};
});
return <div ref={timer} className="timer"></div>;
};
export default memo(Timer);
no matter if I use [] in both or none of useEffect nothing changes.
As #davood-falahati says, adding an empty array as a second argument to useEffect would probably be desirable. From the docs:
... If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run. This isn’t handled as a special case — it follows directly from how the dependencies array always works. ...
In your use case:
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, []);

Why does my timer component behave strangely when passing a dependency to useEffect?

I have the following timer using useEffect and pass a function dependency to it:
const Timer = () => {
const [count, setCount] = useState(0);
const setId = () => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}
useEffect(() => {
setId();
}, [setId])
}
However the timer behaves strangely: the first several seconds is normal, then it starts showing the count randomly. What caused the problem? What's the correct way of doing it?
Check this snippet, A good doc by Dan
import React, { useCallback, useState, useEffect, useRef } from "react";
import "./styles.css";
export default function App() {
let [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <div className="App">{count}</div>;
}
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
Working codesandbox
Update
As keith suggested don't pass the function to array deps as shown follow.
import React, { useState, useEffect } from "react";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <div>{count}</div>;
}
clear the interval on unmount
useEffect(() => {
let id;
const setId = () => {
id = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
setId();
return () => clearInterval(id);
}, []);
#DILEEP THOMAS update answer for something like this is how I would do it.
But #thinkvantagedu comment
still not too sure why it is okay to pass []
Maybe needs more explaining, unfortunately too much for comments so I'm posting this.
To answer this, we need to take a step back, and first ask, what is the point of the dependency array?. The simple answer is anything inside the useEffect that depends on something that if changed from say either useState or Props that would require different handling, it would want putting in the array.
For example, if say you had a component that simply displayed a userProfile, but getting this info was async, so requires a useEffect. It might look something like this ->
<UserProfile userId={userId}/>
Now the problem here is if we passed [] as the dependency, the useEffect would not re-fire for the the new userId, so the props might say userId = 2, but the data we currently have stored in state is for userId = 1,.. So for something like this [props.userId] would make total sense.
So back to the OP's component <App/> what is there here that would change?, well it's not the props, as none are passed. So what about count you might ask, well again ask yourself the question does the state of count warrant a new instance of a setInterval been destroyed / created?. And of course the answer here is no, and as such passing [] makes total sense here.
Hope that make sense,.

Categories

Resources