I have a React video player project and I have a way of skipping the video by 1 second when a keyDown event occurs. This is effective because if you hold the key down multiple events are fired so the video continues to skip at e.g. 5-second intervals up until the user releases the key.
I now want to implement it differently so when a user taps the key down once and then, even if they lift their finger the video carries on skipping until the user presses the key down once again (so they don't need to hold the key down)
How do I do this? This is a section of my code so far. It's actually for a Smart TV APP so any reference you read regards focus is just the navigation pattern for selecting divs with your TV remote. The main bit of code is inside the keyDown function that then calls handleSkipForward or handleSkipBack. Also the videoElement ref is shared across functional components using store.VideoElement.
I was thinking of something like having a boolean state hook for skipping but when that state is true how do I repeatedly request a skip of 5 seconds or is there something within the video element that you can just set to progress through a video at a set rate?
focusKey,
elementRef: containerRef,
onKeyDown: (event) => {
// if (!playerControlsVisible) {
// event.stopPropagation()
// event.preventDefault()
// return
// }
if (event.key === "Enter" || event.key === " ") {
skipDirection === "Forward"
? handleSkipForwardAction()
: handleSkipBackwardAction()
}
},
onFocus: (event) => {},
})
function handleSkipForwardAction() {
if (store.videoElement.currentTime + skipTime < duration) {
store.videoElement.currentTime += skipTime
} else if (store.videoElement.currentTime < duration) {
store.videoElement.currentTime = duration
} else return
}
function handleSkipBackwardAction() {
if (store.videoElement.currentTime - skipTime > 0) {
store.videoElement.currentTime -= skipTime
} else if (store.videoElement.currentTime > 0) {
store.videoElement.currentTime = 0
} else return
}```
It should be simple. What you need to do is implement the setInterval function.
You can just add an interval (infinite loop) and store the Interval ID on a state so that you can stop that infinite loop using the clearInterval function.
onKeyDown: (event) => {
if (event.key === "Enter" || event.key === " ") {
if (moveForward){
clearInterval(moveForward);
// Assuming you use Functional Component and `useState` hooks.
setMoveForward(null);
return;
}
const action = skipDirection === "Forward"
? handleSkipForwardAction
: handleSkipBackwardAction;
setMoveForward(setInterval(action,100));
// 100 is in milisecond. so 1000 = 1 second.
// 100 = 10 function call in 1 second. Meaning 6 second skip/second.
// You can adjust the calculation on your own.
}
},
This is not tested, so do let me know if it works or not. But the idea is to use a looping function when the user clicks the specific button and stop the looping function when the user clicks the specific button again.
References:
setInterval
clearInterval
Related
I am developing a simple Battleships board game and need to increment a state variable when a player clicks on a cell with a valid move.
CodeSandbox: https://codesandbox.io/s/trusting-leakey-6ek9o
All of the code pertaining to this question is found within the following file: >src>GameboardSetup>GameboardSetup.js
The state variable in question is called placedShips. It is designed to keep track of how many ships have been placed onto the board. The variable is initialised to 0 and is supposed to increment up until it reaches a set integer value:
const [placedShips, setPlacedShips] = useState(0);
When the user clicks on the grid, an onClick handler fires. If the cell was a valid cell, then a ship should be placed into an array and the value of placedShips should increment. When the value increments, a new ship is then selected to be placed on a subsequent click. Each new ship has a different length.
Currently, when the user clicks on a valid grid cell, a ship is correctly placed into the array. The issue arises on subsequent valid clicks, whereby the same ship is then placed into the array. This issue is being driven by the onClick handler apparently not receiving an updated state value for placedShips.
While I can see from a { useEffect } hook that the state is in fact increment, the placedShips variable within the event handler I believe is constantly set to 0. Below is how I believe I have validated this issue.
Here is the onClick event handler, containing a console log for the state variable:
const onClickHandler = (e) => {
console.log(placedShips)
let direction = ships[placedShips].direction;
let start = parseInt(e.target.id);
let end = start + ships[placedShips].length - 1;
console.log(playerGameboard.checkIfShipPresent());
if ((playerGameboard.checkValidCoordinates(direction, start, end)) && (!playerGameboard.checkIfShipPresent(direction, start, end))) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips(oldValue => oldValue + 1);
}
}
and here is the { useEffect } hook with another console log for the state variable:
useEffect(() => {
console.log(placedShips)
}, [placedShips])
By selecting multiple valid cells, this is the console log output:
GameboardSetup.js:70 0 <--- Event handler console log
GameboardSetup.js:74 false
GameboardSetup.js:143 1 <--- useEffect console log
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 2
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 3
You can see that the event handler console log always reports placedShips as 0. Whereas the { useEffect } hook shows it incrementing with each successive click.
Apologies for the longwinded question, I have been stuck on this problem for 2 weeks now and haven't made any significant progress with it. Any advice would be hugely appreciated.
Thank you!
Your onClickHandle is not updated when the UI Render. Move it into your return. and it works fine. No need to use humanSetUpGrid. Just get the result from createUiGrid. So cells will be updated when app render.
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return (
<div
className="cell"
id={counter}
onClick={onClickHandler}
onMouseOut={onMouseOutHandler}
onMouseOver={onMouseOverHandler}
/>
);
});
return result;
};
<div className="setup-grid">
<Table grid={createUiGrid()} />
</div>
I built a carousel that displays 3 slides. While the center one is 100% width displayed, the left and right ones are only visible 10% of the width.
After accessing the website, the carousel starts moving automatically using this:
componentDidMount() {
this.carouselTimer = setInterval(() => {
this.handleButtonNext();
}, 5000);
}
And if I manually change the slide, I reset the interval that changes the slides automatically using clearInterval and setInterval again.
In order to be able to add a sliding animation, I want to change the state properties (leftSlide & rightSlide) from false to true and after the animation back to false.
I tried to change the properties from false to true inside handleButtonNext() method making changes here:
<Slide
className={` ${this.state.leftSlide ? ' left-slide' : ''} ${this.state.rightSlide ? 'right-slide' : ''}`}
...the rest of slides.../>
The dilemma I have and the problem I encountered so far is that I cannot remove the added class in such a manner that it won't break the autoplay feature.
I tried using a reset method and restarting the autoplay, but no solution seems to be working.
Without the removal of the added class, the autoplay (and reset in case of a manual change of the slides) works just fine, but that's not enough.
This is the method that handles next button:
handleButtonNext() {
this.setState({
rightSlide: true
});
// this.wait1ms = setInterval(() => {
// }, 1100); (useless attempt)
this.setState({
activeSlide: this.nextSlide()
})
}
nextSlide() {
let nextIndex = this.state.activeSlide+1;
return (nextIndex>this.state.slides.length-1) ? 0 : nextIndex ;
}
*The method is used here:*
<a className="button-container right">
<div className="carousel-button next" onClick={this.handleButtonNext}></div>
</a>
#same for the left button
I need to mention that I do not master React and I am fairly new to it. Thank you for the time you will take to help me! I wish you a great day.
L.E: I forgot to mention that I would like to do this using class component, not the hooks that function provides.
The problem is that I cannot remove the added class in such a manner that it won't break the autoplay feature.
setNextSlideStates(){
this.setState({
// remove rightSlide state
rightSlide: false,
// update the active slide state
activeSlide: this.nextSlide()
});
}
// for usert button click handler, set withAnimation to false.
handleButtonNext(withAnimation) {
if(this.carouselTimer) clearTimeout(this.carouselTimer)
// if there are any animations, terminate them and call setNextSlideStates manually
if(this.animationTimer) {clearTimeout(this.animationTimer); this.setNextSlideStates(); }
// start the animation
this.setState({
rightSlide: true
});
// wait 1.1 sec for animation to end
this.animationTimer = setTimeout(() => {
this.setNextSlideStates()
this.animationTimer = null;
// autoplay the next slide
this.carouselTimer = setTimeout(() => {
this.handleButtonNext(true);
}, 3900);
}, withAnimation ? 1100 : 0)
}
Also change your componentDidMount to:
componentDidMount() {
this.carouselTimer = setTimeout(() => {
this.handleButtonNext(true);
}, 3900);
}
Button Handler:
{/* no animation */}
<button onClick={() => {this.handleButtonNext(false)}}>next</button>
I am trying to make something where I have 8 images, with 4 shown at a time. I need them to have an animation when they enter and an animation when they exit. The first 4 exit when one of them is clicked. But I only see the enter animation. I assume because the enter and exit animation happen at the same time.
Is there a way to add a delay or something similar using hooks to make one happen before the other that is compatible with internet explorer (super important)? Code shown below:
const [question, setQuestion] = React.useState(props.Questions[0]);
const [animate, setAnimate] = React.useState("enter");
React.useEffect(() => {
if (Array.isArray(props.Questions) && props.Questions.length) {
// switch to the next iteration by passing the next data to the <Question component
setAnimate("enter");
setQuestion(props.Questions[0]);
} else {
// if there are no more iterations (this looks dumb but needs to be here and works)
$("#mrForm").submit();
}
});
const onStubClick = (e, QuestionCode, StubName) => {
e.preventDefault();
// store selected stub to be submitted later
var newResponse = response.concat({ QuestionCode, StubName });
setResponse(newResponse);
setAnimate("exit");
// remove the first iteration of stubs that were already shown
props.Questions.splice(0, 1);
if (props.QuestionText.QuestionTextLabel.length > 1) {
// remove first iteration of questiontext if applicable
props.QuestionText.QuestionTextLabel.splice(0, 1);
props.QuestionText.QuestionTextImage.splice(0, 1);
// switch the question text
setQuestionLabel({ Label: props.QuestionText.QuestionTextLabel[0], Image: props.QuestionText.QuestionTextImage[0] });
}
};
My code is causing an unexpected amount of re-renders.
function App() {
const [isOn, setIsOn] = useState(false)
const [timer, setTimer] = useState(0)
console.log('re-rendered', timer)
useEffect(() => {
let interval
if (isOn) {
interval = setInterval(() => setTimer(timer + 1), 1000)
}
return () => clearInterval(interval)
}, [isOn])
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
Note the console.log on line 4. What I expected is the following to be logged out:
re-rendered 0
re-rendered 0
re-rendered 1
The first log is for the initial render. The second log is for the re-render when the "isOn" state changes via the button click. The third log is when setInterval calls setTimer so it's re-rendered again. Here is what I actually get:
re-rendered 0
re-rendered 0
re-rendered 1
re-rendered 1
I can't figure out why there is a fourth log. Here's a link to a REPL of it:
https://codesandbox.io/s/kx393n58r7
***Just to clarify, I know the solution is to use setTimer(timer => timer + 1), but I would like to know why the code above causes a fourth render.
The function with the bulk of what happens when you call the setter returned by useState is dispatchAction within ReactFiberHooks.js (currently starting at line 1009).
The block of code that checks to see if the state has changed (and potentially skips the re-render if it has not changed) is currently surrounded by the following condition:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
My assumption on seeing this was that this condition evaluated to false after the second setTimer call. To verify this, I copied the development CDN React files and added some console logs to the dispatchAction function:
function dispatchAction(fiber, queue, action) {
!(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;
{
!(arguments.length <= 3) ? warning$1(false, "State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;
}
console.log("dispatchAction1");
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
var update = {
expirationTime: renderExpirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
var lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
flushPassiveEffects();
console.log("dispatchAction2");
var currentTime = requestCurrentTime();
var _expirationTime = computeExpirationForFiber(currentTime, fiber);
var _update2 = {
expirationTime: _expirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// Append the update to the end of the list.
var _last = queue.last;
if (_last === null) {
// This is the first update. Create a circular list.
_update2.next = _update2;
} else {
var first = _last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
_last.next = _update2;
}
queue.last = _update2;
console.log("expiration: " + fiber.expirationTime);
if (alternate) {
console.log("alternate expiration: " + alternate.expirationTime);
}
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
console.log("dispatchAction3");
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var _eagerReducer = queue.eagerReducer;
if (_eagerReducer !== null) {
var prevDispatcher = void 0;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
_update2.eagerReducer = _eagerReducer;
_update2.eagerState = _eagerState;
if (is(_eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
{
if (shouldWarnForUnbatchedSetState === true) {
warnIfNotCurrentlyBatchingInDev(fiber);
}
}
scheduleWork(fiber, _expirationTime);
}
}
and here's the console output with some additional comments for clarity:
re-rendered 0 // initial render
dispatchAction1 // setIsOn
dispatchAction2
expiration: 0
dispatchAction3
re-rendered 0
dispatchAction1 // first call to setTimer
dispatchAction2
expiration: 1073741823
alternate expiration: 0
re-rendered 1
dispatchAction1 // second call to setTimer
dispatchAction2
expiration: 0
alternate expiration: 1073741823
re-rendered 1
dispatchAction1 // third and subsequent calls to setTimer all look like this
dispatchAction2
expiration: 0
alternate expiration: 0
dispatchAction3
NoWork has a value of zero. You can see that the first log of fiber.expirationTime after setTimer has a non-zero value. In the logs from the second setTimer call, that fiber.expirationTime has been moved to alternate.expirationTime still preventing the state comparison so re-render will be unconditional. After that, both the fiber and alternate expiration times are 0 (NoWork) and then it does the state comparison and avoids a re-render.
This description of the React Fiber Architecture is a good starting point for trying to understand the purpose of expirationTime.
The most relevant portions of the source code for understanding it are:
ReactFiberExpirationTime.js
ReactFiberScheduler.js
I believe the expiration times are mainly relevant for concurrent mode which is not yet enabled by default. The expiration time indicates the point in time after which React will force a commit of the work at the earliest opportunity. Prior to that point in time, React may choose to batch updates. Some updates (such as from user interactions) have a very short (high priority) expiration, and other updates (such as from async code after a fetch completes) have a longer (low priority) expiration. The updates triggered by setTimer from within the setInterval callback would fall in the low priority category and could potentially be batched (if concurrent mode were enabled). Since there is the possibility of that work having been batched or potentially discarded, React queues a re-render unconditionally (even when the state is unchanged since the previous update) if the previous update had an expirationTime.
You can see my answer here to learn a bit more about finding your way through the React code to get to this dispatchAction function.
For others who want to do some digging of their own, here's a CodeSandbox with my modified version of React:
The react files are modified copies of these files:
https://unpkg.com/react#16/umd/react.development.js
https://unpkg.com/react-dom#16/umd/react-dom.development.js
I have a form that contains several questions. Some of the questions contains a group of subquestions.
The logic to render sub questions is written inside componentDidUpdate method.
componentDidUpdate = (prevProps, prevState, snapshot) => {
if (prevProps !== this.props) {
let questions = this.props.moduleDetails.questions,
sgq = {};
Object.keys(questions).map((qId) => {
sgq[qId] = (this.state.groupedQuestions[qId]) ? this.state.groupedQuestions[qId] : [];
let answerId = this.props.formValues[qId],
questionObj = questions[qId],
groupedQuestions = [];
if(questionObj.has_grouped_questions == 1 && answerId != null && (this.state.groupedQuestions != null)) {
groupedQuestions = questions[qId].question_group[answerId];
let loopCount = this.getLoopCount(groupedQuestions);
for(let i=0; i<loopCount; i++) {
sgq[qId].push(groupedQuestions);
}
}
});
this.setState({groupedQuestions: sgq});
}
}
The problem is that on every key stroke of text field, handleChange method is invoked which will ultimately invoke componentDidUpdate method. So the same question groups gets rendered on every key stroke.
I need a way to detect if the method componentDidUpdate was invoked due to the key press(handleChange) event so that i can write logic as follows.
if(!handleChangeEvent) {
Logic to render question group
}
Any idea on how to integrate this will be appreciated.
I assume your textfield is a controlled component, meaning that its value exists in the state. If this is the case, you could compare the previous value of your textfield to the new one. If the value is different, you know the user entered something. If they are equal however, the user did something else at which point you want your snippet to actually execute.
Basically:
componentDidUpdate = (prevProps) => {
// if value of textfield didn't change:
if (prevProps.textfieldValue === this.props.textfieldValue) {
// your code here
}
}
Another approach is to use componentDidReceiveProps(). There you can compare the props to the previous ones, similarly to the above, and execute your code accordingly. Which method is most suitable depends on how your app works.