How to create a stopwatch in React using useReducer hook and setInterval? - javascript

I am trying to figure out how to implement useReducer in my stopwatch app that I made a while ago that uses setState. I am having lots of trouble implementing it, and I feel like my confusion centers around how the reducer function actually works.
The major problem that I am having is that I am not sure how the reducer function actually works, as I am having trouble implementing the logic for the timer buttons. And I am assuming that I should be putting the logic in the switch statement.
Here is my actual code:
import React, { useReducer } from "react"
function init(initialState) {
return { time: initialState }
}
let initialState = {
timerOn: true,
timerStart: 0,
timerTime: 0
};
function reducer(state, action) {
switch (action.type) {
case 'start timer':
return startTimer()
case 'reset timer':
return zerotimer(initialState)
case 'resume timer':
return
default:
return state
}
}
function zerotimer(initialState) {
return { initialState };
}
function startTimer(initialState) {
this.timer = setInterval(() => {
this.setState({
timerTime: Date.now() - this.state.timerStart
});
}, 10);
}
function Timer({ initialState }) {
const [state, dispatch] = useReducer(reducer, initialState)
let centiseconds = (Math.floor(initialState / 10) % 100);
let seconds = (Math.floor(initialState / 1000) % 60);
let minutes = (Math.floor(initialState / 60000) % 60);
let hours = (Math.floor(initialState / 3600000));
return (
<div>
<div>
<div>
<h2>Time to run!</h2>
</div>
<div>Stopwatch</div>
<div>
{hours} : {minutes} : {seconds} : {centiseconds}
</div>
{state.timerOn === false && state.timerTime === 0 && (
<button onClick={() => dispatch({ type: 'start timer' })}>Start</button>
)}
{state.timerOn === true && (
<button onClick={this.stopTimer}>Stop</button>
)}
{state.timerOn === false && state.timerTime > 0 && (
<button onClick={this.startTimer}>Resume</button>
)}
{state.timerOn === false && state.timerTime > 0 && (
<button onClick={() => dispatch({ type: 'reset timer' })}>Reset</button>
)}
</div>
</div>
)
}
export default Timer;

Here's a minimal implementation using useState first, then below I'll cover the useReducer case.
I dropped the hours, and removed a few other things to make it simpler.
Using useState :
// import React from "react";
import React, { useState, useRef, useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
function Timer(props) {
const [centiseconds, setCentiseconds] = useState(0);
const [seconds, setSeconds] = useState(0);
const [minutes, setMinutes] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useInterval(() => {
if (!timerOn) return;
setCentiseconds((centiseconds) =>
centiseconds === 99 ? 0 : centiseconds + 1
);
}, 10);
useInterval(() => {
if (!timerOn) return;
setSeconds((seconds) => (seconds === 59 ? 0 : seconds + 1));
}, 1000);
useInterval(() => {
if (!timerOn) return;
setMinutes((minutes) => (minutes === 59 ? 0 : minutes + 1));
}, 60000);
return (
<>
<div>
{String(minutes).padStart(2, "0")} : {String(seconds).padStart(2, "0")}{" "}
: {String(centiseconds).padStart(2, "0")}
</div>
{timerOn === false && (
<button onClick={() => setTimerOn(true)}>Start</button>
)}
{timerOn === true && (
<button onClick={() => setTimerOn(false)}>Stop</button>
)}
{
<button
onClick={() => {
setTimerOn(false);
setCentiseconds(0);
setSeconds(0);
setMinutes(0);
}}
>
Reset
</button>
}
</>
);
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
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]);
}
ReactDOM.render(
<React.StrictMode>
<Timer />
</React.StrictMode>,
document.getElementById("root")
);
From here, it's pretty straightforward to convert it into useReducer.
What you need to do is replace the setState calls with actions, and handle the actions in the reducer.
It's honestly an overkill for this case, and it's too much boilerplate code, but here it is anyway.
Using useReducer:
import React, { useRef, useEffect, useReducer } from "react";
import ReactDOM from "react-dom";
const initialState = {
centiseconds: 0,
seconds: 0,
minutes: 0,
timerOn: false,
};
const reducer = (state, action) => {
switch (action.type) {
case "START":
return {
...state,
timerOn: true,
};
case "STOP":
return {
...state,
timerOn: false,
};
case "RESET":
return initialState;
case "SET_CENTISECONDS":
return {
...state,
centiseconds: action.payload.centiseconds,
};
case "SET_SECONDS":
return {
...state,
seconds: action.payload.seconds,
};
case "SET_MINUTES":
return {
...state,
minutes: action.payload.minutes,
};
default:
return initialState;
}
};
function Timer(props) {
const [state, dispatch] = useReducer(reducer, initialState);
const { centiseconds, seconds, minutes, timerOn } = state;
useInterval(() => {
if (!timerOn) return;
dispatch({
type: "SET_CENTISECONDS",
payload: {
centiseconds: centiseconds === 99 ? 0 : centiseconds + 1,
},
});
}, 10);
useInterval(() => {
if (!timerOn) return;
dispatch({
type: "SET_SECONDS",
payload: {
seconds: seconds === 59 ? 0 : seconds + 1,
},
});
}, 1000);
useInterval(() => {
if (!timerOn) return;
dispatch({
type: "SET_MINUTES",
payload: {
minutes: minutes === 59 ? 0 : minutes + 1,
},
});
}, 60000);
return (
<>
<div>
{String(minutes).padStart(2, "0")} : {String(seconds).padStart(2, "0")}{" "}
: {String(centiseconds).padStart(2, "0")}
</div>
{timerOn === false && (
<button onClick={() => dispatch({ type: "START" })}>Start</button>
)}
{timerOn === true && (
<button onClick={() => dispatch({ type: "STOP" })}>Stop</button>
)}
{<button onClick={() => dispatch({ type: "RESET" })}>Reset</button>}
</>
);
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
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]);
}
ReactDOM.render(
<React.StrictMode>
<Timer />
</React.StrictMode>,
document.getElementById("root")
);
This is not the only way to accomplish this. You can use useReducer however you want. You can pass the new state you want to be merged instead of passing an action, similar to setState in react classes. (check this article).

Related

my countdown timer is not working fine everytime in react

This is a game that I am building and in this game I have created some levels. But when the game ends for the first time the second time my countdown timer is not getting decremented.
this is my App.js component:
import "./App.css";
import React, { useState, useEffect, useCallback } from "react";
import SingleCard from "./components/singleCard/SingleCard";
import Timer from "./components/timer/Timer";
import Modal from "./components/modal/Modal";
import soundOn from "./images/soundon.png";
import soundOff from "./images/soundoff.png";
import { Helmet } from "react-helmet";
const cardImages = [
{ src: "/img/img-1.png", matched: false },
{ src: "/img/img-2.png", matched: false },
{ src: "/img/img-3.png", matched: false },
{ src: "/img/img-4.png", matched: false },
{ src: "/img/img-5.png", matched: false },
{ src: "/img/img-6.png", matched: false },
];
function App({ background, wrongAns, correctAns, deadSound, winSound }) {
const [cards, setCards] = useState([]);
const [turns, setTurns] = useState(0);
const [choiceOne, setChoiceOne] = useState(null);
const [choiceTwo, setChoiceTwo] = useState(null);
const [disabled, setDisabled] = useState(false);
const [isgameEnd, setGameEnd] = useState(false);
const [timerStart, setTimerStart] = useState(false);
const [playSound, setPlaySound] = useState(false);
const [count, setCount] = useState(0);
const [IsPlaying, setIsPlaying] = useState(true);
const [isModalOpen, setModalIsOpen] = useState(false);
const [restartTimer, setRestartTimer] = useState(false);
const [isMute, setMute] = useState(false);
const [loading, setLoading] = useState(true);
function handleMute(state = false) {
background.muted = state;
wrongAns.muted = state;
correctAns.muted = state;
deadSound.muted = state;
winSound.muted = state;
setMute(state);
}
let timer;
// function that will decide the condition for opening the modal
const toggleModal = () => {
setModalIsOpen(true);
};
// function that will execute when we click a button in the modal
const handlePlaySound = () => {
setPlaySound(false);
};
// function that will execute when game is set to background in android
function AudioBgOnPause() {
if (playSound === true) {
background.pause();
setIsPlaying(false);
}
}
// functiona that will execute when game is again resumed
function AudioBgOnResume() {
if (IsPlaying === false) {
setIsPlaying(true);
}
}
// creating there global reference so that we can call these functions in the index file
window.AudioBgOnPause = AudioBgOnPause;
window.AudioBgOnResume = AudioBgOnResume;
// check if playSound is off or on
if (playSound === false) {
background.pause();
} else if (playSound === true && IsPlaying === true) {
background.play();
}
// Play Again
const playAgain = () => {
// setCards([]);
shuffleCards();
setTurns(0);
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
setGameEnd(false);
setTimerStart(false);
setPlaySound(false);
setCount(0);
setIsPlaying(true);
setModalIsOpen(false);
setRestartTimer(true);
setMute(false);
};
const restartGame = () => {
playAgain();
};
// check if isGameEnd is true i.e. the game is ended
// losing condition
useEffect(() => {
if (turns < 6 && isgameEnd === true) {
setDisabled(true);
setTimerStart(false);
clearInterval(timer);
if (playSound === true) {
deadSound.play();
}
setPlaySound(false);
setTimeout(() => {
toggleModal();
}, 2000);
}
}, [turns, isgameEnd]);
// winning situation
useEffect(() => {
if (
(turns === 6 && isgameEnd === false) ||
(turns === 6 && isgameEnd === true)
) {
// clearInterval(timer);
// setDisabled(true);
setRestartTimer(true);
setTimerStart(false);
if (playSound === true) {
winSound.play();
}
setPlaySound(playSound);
shuffleCards();
// setTimeout(() => {
// toggleModal();
// }, 2000);
}
}, [turns, isgameEnd]);
// shuffle Cards
const shuffleCards = () => {
const shuffleCards = [...cardImages, ...cardImages]
.sort(() => Math.random() - 0.5)
.map((card) => ({ ...card, id: Math.random() }));
setCards(shuffleCards);
setTurns(0);
};
// console.log("cards array", cards);
// handle a choice
const handleChoice = (card) => {
setTimerStart(true);
// background.play();
background.loop = true;
// checking if the counter is one only then set sound to true when the card is flipped for first time
count === 1 ? setPlaySound(true) : setPlaySound(playSound);
// after that increment the counter so that the upper condition should not hold anymore
setCount(count + 1);
choiceOne ? setChoiceTwo(card) : setChoiceOne(card);
};
// compare 2 selected cards
useEffect(() => {
if (choiceOne && choiceTwo) {
setDisabled(true);
if (choiceOne.src === choiceTwo.src) {
setCards((prevCards) => {
return prevCards.map((card) => {
if (card.src === choiceOne.src) {
return { ...card, matched: true };
} else {
return card;
}
});
});
if (playSound === true) {
correctAns.play();
}
setTurns((prevTurns) => prevTurns + 1);
resetTurn();
} else {
if (playSound === true) {
wrongAns.play();
}
setTimeout(() => resetTurn(), 500);
}
}
}, [choiceOne, choiceTwo]);
// start a new game automatically
// set counter to one when the component first mounts so that sound starts to play on first click only
useEffect(() => {
shuffleCards();
setCount(count + 1);
}, []);
// reset choices
const resetTurn = () => {
setChoiceOne(null);
setChoiceTwo(null);
setDisabled(false);
};
// console.log("restart App", restartTimer);
// timer callback
const onGameEnd = useCallback(() => {
setGameEnd(!isgameEnd);
}, [isgameEnd]);
useEffect(() => {
setTimeout(() => {
setLoading(false);
}, 4000);
}, []);
return (
<>
<Helmet>
<meta charSet="utf-8" />
<title>Match Maker</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
</Helmet>
<div className="App">
{loading && (
<div className="loader-container">
<div className="spinner"></div>
</div>
)}
<>
{/* <img
className="logo"
src="https://cheetay.pk/static/images/newLandingPage/logo.svg"
alt="card back"
/> */}
<div
style={
loading ? { visibility: "hidden" } : { visibility: "inherit" }
}
>
<div className="soundBtn">
{!isMute === true ? (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOn} alt="soundOff" />
</div>
) : (
<div
className="soundIcon"
style={{
cursor: "pointer",
khtmlUserSelect: "none",
MozUserSelect: "none",
OUserSelect: "none",
userSelect: "none",
}}
onClick={() => handleMute(!isMute)}
>
<img src={soundOff} alt="soundOff" />
</div>
)}
</div>
<div className="card-grid">
{cards.map((card) => (
<SingleCard
key={card.id}
card={card}
handleChoice={handleChoice}
flipped={
card === choiceOne || card === choiceTwo || card.matched
}
disabled={disabled}
isModalOpen={isModalOpen}
/>
))}
</div>
<div className="TimerAndTurnsInfo">
<Timer
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
<p>matched {turns}</p>
</div>
</div>
</>
</div>
{isModalOpen && (
<Modal handlePlaySound={handlePlaySound} restartGame={restartGame} />
)}
</>
);
}
export default App;
and this is my timer component:
import React, { useEffect, useState } from "react";
import "./Timer.css";
const Child = ({ timerStart, timer, onGameEnd, restartTimer }) => {
const [seconds, setSeconds] = useState(40);
// let time = 40;
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
// console.log("restart Timer", restartTimer);
useEffect(() => {
if (timerStart === true) {
timer = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
if (seconds === 0) {
onGameEnd(true);
clearInterval(timer);
}
}, 1000);
}
return () => clearInterval(timer);
});
return (
<p className="time">
Time{" "}
<span className="span1">
<span>{seconds}s</span>
</span>
</p>
);
};
const Timer = React.memo(({ timerStart, timer, onGameEnd, restartTimer }) => (
<Child
timerStart={timerStart}
timer={timer}
onGameEnd={onGameEnd}
restartTimer={restartTimer}
/>
));
export default Timer;
my timer gets re initialized when restartTimer state is set to true.
I think your problem is here
useEffect(() => {
if (restartTimer === true) {
setSeconds(40);
}
}, [seconds, restartTimer]);
whenever the seconds reduce and restartTimer === true (can't find where you set it to false after useState) you reset it to 40.
try removing seconds from the dependency

(React)Child component pause its useEffect when the parent re-renders

Edit: This problem has been solved by adding a second parameter in the useEffect Hooks.
I am new to react and am building a toy react game. I borrowed this timer I found here. If user performs an action and the parent component re-renders, the timer stop counting down for a short period of time. According to (https://reactjs.org/docs/hooks-effect.html), react will remember the useEffect function, and call it later after performing the DOM updates. If that's the reason, are there any solutions or alternatives to this? If not, can anyone point me in the right direction? Any help will be appreciated!
Here is my code snippet for reference:
class Chalkboard extends Component {
constructor(props) {
super(props);
this.state = {
minutes: 1,
seconds: 0,
addSeconds: 0,
};
}
updateTimer() {
this.setState({ addSeconds: 0 });
}
handleTimer() {
window.removeEventListener('keydown', this.keyHandling);
this.setState({
gameOver: true,
});
}
render() {
return(
<Timer
initialMinutes={this.state.minutes}
initialSeconds={this.state.seconds}
onTimer={this.handleTimer}
addSeconds={this.state.addSeconds}
updateTimer={this.updateTimer}
/>
)
}
import React, { useState, useEffect } from 'react';
const Timer = (props) => {
const { initialMinutes = 0, initialSeconds = 0 } = props;
const [minutes, setMinutes] = useState(initialMinutes);
const [seconds, setSeconds] = useState(initialSeconds);
useEffect(() => {
let myInterval = setInterval(() => {
if (seconds > 0) {
setSeconds(seconds - 1);
}
else if (seconds === 0) {
if (minutes === 0) {
clearInterval(myInterval);
} else {
setSeconds(59);
setMinutes(minutes - 1);
}
}
}, 1000);
return () => {
clearInterval(myInterval);
};
});
useEffect(() =>{
if(props.addSeconds>0){
setSeconds(seconds + props.addSeconds);
props.updateTimer()
}
if(minutes === 0 && seconds === 0){
props.onTimer()
}
});
return (
<div className = "timer-container" >
{minutes === 0 && seconds === 0 ? null: (
<div>
{' '}
{minutes}:{seconds < 10 ? `0${seconds}` : seconds}
</div>
)}
</div>
);
};
export default Timer;
Can you provide an example on https://codesandbox.io/?
Try to pass [props, minutes, seconds] as a second parameter in useEffect hook.
https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

Convert class based to Function based compnent

Here I'm working on AutoComplete and Auto fill of react.
I'm trying to convert it to react hooks as I have written all of my code is in hooks only.
I've to some level converted it to hooks based as per my understanding. But I'm not able to completely convert it.
Original code
import React, { Component } from "react";
class App extends Component {
constructor(props) {
super(props);
this.state = {
item: {
code: "",
name: "",
unit: "",
rate: ""
},
cursor: 0,
searchItems: []
};
this.autocomplete = this.autocomplete.bind(this);
this.handleKeyup = this.handleKeyup.bind(this);
this.handleKeydown = this.handleKeydown.bind(this);
this.handleListKeydown = this.handleListKeydown.bind(this);
this.selectItem = this.selectItem.bind(this);
this.handleChange = this.handleChange.bind(this);
}
autocomplete(evt) {
let text = evt.target.value;
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
this.setState({ searchItems: data });
});
}
handleKeyup(evt) {
if (evt.keyCode === 27) {
this.setState({ searchItems: [] });
return false;
}
}
handleKeydown(evt) {
const { cursor, searchItems } = this.state;
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
this.setState((prevState) => ({
cursor: prevState.cursor - 1
}));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
this.setState((prevState) => ({
cursor: prevState.cursor + 1
}));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const { name, code, rate, unit } = currentItem;
this.setState({ item: { name, code, rate, unit }, searchItems: [] });
}
}
if (evt.keyCode === 8) {
this.setState({ item: { name: "", code: "", rate: "", unit: "" } });
}
}
selectItem(id) {
const { searchItems } = this.state;
let selectedItem = searchItems.find((item) => item.code === id);
const { code, name, unit, rate } = selectedItem;
this.setState({ item: { code, name, unit, rate } });
this.setState({ searchItems: [] });
}
handleListKeydown(evt) {
console.log(evt.keyCode);
}
handleChange(evt) {
this.setState({ item: { [evt.target.name]: evt.target.value } });
}
render() {
const { searchItems, cursor, item, handleChange } = this.state;
const { code, name, unit, rate } = item;
return (
<div className="container mt-3">
<h1 className="h2 text-center">Autocomplete Example</h1>
<div className="form-group">
<label htmlFor="autocomplete">Item Name </label>
<input
type="text"
id="autocomplete"
onChange={this.autocomplete}
onKeyUp={this.handleKeyup}
onKeyDown={this.handleKeydown}
value={name}
className="custom-input form-control"
/>
{searchItems.length > 0 && (
<ul className="list-group">
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? "active list-group-item"
: "list-group-item"
}
key={idx}
onClick={() => this.selectItem(item.code)}
onKeyDown={(evt) => this.handleListKeydown(evt, item.code)}
>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
}
export default App;
Link to original code: https://codepen.io/regexp/details/RwPNaLe
Using hooks
Here is the code that I tried to convert to hooks.
import React, { useState } from "react";
export default function FunctionName(props) {
const [item, setItem] = useState({
vendorNameData: invoiceDetail[0].invoiceData.vendor,
vendorAccountData: invoiceDetail[0].invoiceData.vendaAccount,
vendorAddressData: invoiceDetail[0].invoiceData.vendorAddress
});
const [cursor, setCursor] = useState(0);
const [searchItems, SetSearchItems] = useState([]);
function AutoComplete(evt) {
let text = evt.target.value;
console.log(text);
fetch(`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`)
.then((res) => res.json())
.then((data) => {
SetSearchItems(data);
});
}
function HandleKeyUp(evt) {
if (evt.keyCode === 27) {
SetSearchItems([]);
return false;
}
}
function HandleKeyDown(evt) {
// const [cursor, setCursor] = useState();
// const [searchItems, SetSearchItems] = useState()
if (evt.keyCode === 38 && cursor > 0) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((cursor) => ({ cursor: cursor + 1 }));
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = currentItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({
vendorNameData: "",
vendorAccountData: "",
vendorAddressData: ""
});
}
}
function SelectItem(id) {
const [searchItems, SetSearchItems] = useState();
let selectedItem = searchItems.find((item) => item.code === id);
const {
vendorNameData,
vendorAccountData,
vendorAddressData
} = selectedItem;
setItem({ vendorNameData, vendorAccountData, vendorAddressData });
SetSearchItems([]);
}
function HandleListKeyDown(evt) {
console.log(evt.keyCode);
}
function HandleChange(evt) {
setItem({ item: { [evt.target.name]: evt.target.value } });
}
}
It would be really helpful to point me out where I'm lagging. I've tried my best but this is what I could come up with.
Any help would really be appreciated.
I've 'traduced' and cleaned up your original component, resulting as follows: (please see notes below)
import React, {useState, useCallback} from 'react';
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
export {Input};
useState replaces Class Components State management. It is good practice to split your state into smaller pieces as possible, because new values will completely replace old ones (there is no merging like Class Components this.setState does). Using cursor as an example, do const [cursor, setCursor] = useState(0); to initialize your state (in this case initial state is 0). Then use cursor to use the value and call setCursor(newValue) to update it. Each time you update your state you will most likely trigger a re-rendering.
useCallback allows you to only re-declare a function when its dependencies have changed, thus improving performance. Function dependencies are specified in the second argument array. On each render, React will compare the new values with the old ones and will always return the same function when dependencies have not changed.
The return statement replaces your previous render method.
Follows a working snippet. Please note that OP original code behavior has not been changed.
const {useState, useCallback, StrictMode} = React;
function Input() {
const [item, setItem] = useState({
code: '',
name: '',
unit: '',
rate: ''
});
const [cursor, setCursor] = useState(0);
const [searchItems, setSearchItems] = useState([]);
const autocomplete = useCallback((evt) => {
const text = evt.target.value;
fetch(
`https://invoiceitems.herokuapp.com/items?name_like=${text}&_limit=6`
)
.then((res) => res.json())
.then((data) => {
setSearchItems(data);
});
}, []);
const handleKeyup = useCallback((evt) => {
if (evt.keyCode === 27) {
setSearchItems([]);
return false;
}
return true;
}, []);
const handleKeydown = useCallback(
(evt) => {
// arrow up/down button should select next/previous list element
if (evt.keyCode === 38 && cursor > 0) {
setCursor((prevCursor) => prevCursor - 1);
} else if (evt.keyCode === 40 && cursor < searchItems.length - 1) {
setCursor((prevCursor) => prevCursor + 1);
}
if (evt.keyCode === 13) {
let currentItem = searchItems[cursor];
if (currentItem !== undefined) {
const {code, name, unit, rate} = currentItem;
setItem({code, name, unit, rate});
setSearchItems([]);
}
}
if (evt.keyCode === 8) {
setItem({code: '', name: '', unit: '', rate: ''});
}
},
[cursor, searchItems]
);
const selectItem = useCallback(
(id) => {
let selectedItem = searchItems.find((item) => item.code === id);
const {code, name, unit, rate} = selectedItem;
setItem({code, name, unit, rate});
setSearchItems([]);
},
[searchItems]
);
const handleListKeydown = useCallback((evt) => {
console.log(evt.keyCode);
}, []);
return (
<div className={'container mt-3'}>
<h1 className={'h2 text-center'}>{'Autocomplete Example'}</h1>
<div className={'form-group'}>
<label htmlFor={'autocomplete'}>{'Item Name'}</label>
<input
type={'text'}
id={'autocomplete'}
onChange={autocomplete}
onKeyUp={handleKeyup}
onKeyDown={handleKeydown}
value={item.name}
className={'custom-input form-control'}
/>
{searchItems.length > 0 && (
<ul className={'list-group'}>
{searchItems.map((item, idx) => (
<li
className={
cursor === idx
? 'active list-group-item'
: 'list-group-item'
}
key={idx}
onClick={() => selectItem(item.code)}
onKeyDown={handleListKeydown}>
{item.name}
</li>
))}
</ul>
)}
</div>
</div>
);
}
ReactDOM.render(
<StrictMode>
<Input />
</StrictMode>,
document.getElementById('root')
);
.active {
color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.0/umd/react-dom.production.min.js"></script>
<div id='root' />

Maximum update depth exceeded in React Infinite Scroll

I am trying to implement a lazy loading in a MERN stack app like in producthunt. I want to have the posts created on the current date shown by default. If the user scroll down, it will fetch more data on the previous date. I am using react infinite scroll. However, it seems like the app requests to api like an infinite loop without listening on scrolling. I got the following error.
Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
The function is async/await so I don't understand why it keeps calling new requests even though the old request is not resolved yet.
In a Post components
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import PostItem from './PostItem';
import UserItem from '../users/UserItem';
import TopDiscussion from '../TopDiscussion';
import SmallAbout from '../SmallAbout';
import { getPostsByDate } from '../../actions/post';
import Moment from 'react-moment';
import InfiniteScroll from 'react-infinite-scroller';
const Posts = ({ getPostsByDate, post: { posts, loading } }) => {
const now = new Date();
const startOfToday = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate()
);
// startOfToday = startOfToday -1
useEffect(() => {
getPostsByDate(startOfToday);
}, [getPostsByDate]);
const [date, setDate] = useState(startOfToday);
const [shown, setShown] = useState();
const getPosts = () => {
getPostsByDate(date);
let count = new Date(date);
count.setDate(count.getDate() - 1);
setDate(count);
};
return loading ? (
<Spinner />
) : (
<div className='main-grid'>
<div className='posts-grid'>
<h1 className='large text-primary'>Ideas</h1>
<div className='posts'>
<div className='post-dummy'>
<InfiniteScroll
dataLength={posts.length}
pageStart={0}
loadMore={getPosts}
hasMore={posts && posts.length < 10}
loader={
<div className='loader' key={0}>
Loading ...
</div>
}
>
{posts
.sort((a, b) =>
a.likes.length > b.likes.length
? -1
: b.likes.length > a.likes.length
? 1
: 0
)
.map(post => (
<PostItem key={post._id} post={post} />
))}
</InfiniteScroll>
</div>
</div>
</div>
<div className='right-panel-grid'>
<SmallAbout />
<UserItem />
<TopDiscussion posts={posts} />
<div
className='fb-group'
data-href='https://www.facebook.com/groups/ideatoshare/'
data-width='350'
data-show-social-context='true'
data-show-metadata='false'
></div>
<iframe
title='producthunt'
style={{ border: 'none' }}
src='https://cards.producthunt.com/cards/posts/168618?v=1'
width='350'
height='405'
frameBorder='0'
scrolling='no'
allowFullScreen
></iframe>
</div>
</div>
);
};
Posts.propTypes = {
getPostsByDate: PropTypes.func.isRequired,
post: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(
mapStateToProps,
{ getPostsByDate }
)(Posts);
Post reducer
import {
GET_POSTS,
POST_ERROR,
UPDATE_LIKES,
UPDATE_LIKE,
UPDATE_COMMENT_LIKES,
DELETE_POST,
ADD_POST,
GET_POST,
ADD_COMMENT,
REMOVE_COMMENT,
ADD_SUB_COMMENT,
REMOVE_SUB_COMMENT,
UPDATE_STATUS
} from '../actions/types';
const initialState = {
posts: [],
post: null,
loading: true,
error: {}
};
export default function(state = initialState, action) {
const { type, payload } = action;
switch (type) {
case GET_POSTS:
return {
...state,
posts: [...state.posts, ...payload],
// posts: payload,
loading: false
};
case GET_POST:
return {
...state,
post: payload,
loading: false
};
case ADD_POST:
return {
...state,
post: payload,
// posts: [payload, ...state.posts],
loading: false
};
case POST_ERROR:
return {
...state,
error: payload,
loading: false
};
case UPDATE_COMMENT_LIKES:
return {
...state,
post: {
...state.post,
comments: payload
},
loading: false
};
case UPDATE_LIKES:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.id ? { ...post, likes: payload.likes } : post
),
loading: false
};
case UPDATE_LIKE:
return {
...state,
post: { ...state.post, likes: payload },
loading: false
};
case UPDATE_STATUS:
return {
...state,
posts: state.posts.map(post =>
post._id === payload.id ? { ...post, status: payload.status } : post
),
loading: false
};
case DELETE_POST:
return {
...state,
posts: state.posts.filter(post => post._id !== payload),
loading: false
};
case ADD_COMMENT:
return {
...state,
// payload is all the comments
post: { ...state.post, comments: payload },
loading: false
};
case ADD_SUB_COMMENT:
return {
...state,
// payload is all the comments of a post
post: { ...state.post, comments: payload },
loading: false
};
case REMOVE_COMMENT:
return {
...state,
post: {
...state.post,
comments: state.post.comments.filter(
comment => comment._id !== payload
),
loading: false
}
};
case REMOVE_SUB_COMMENT:
return {
...state,
post: {
...state.post,
comments: payload
// comments: state.post.comments.map(comment =>
// {
// if (comment._id === payload.commentId) {
// comment.subComments.filter(
// subcomment => subcomment._id === payload.subcommentId
// );
// }
// }
// )
},
loading: false
};
default:
return state;
}
}
Post action
//GetTodayPost
export const getPostsByDate = date => async dispatch => {
try {
const res = await axios.get(`/api/posts/${date}`);
dispatch({
type: GET_POSTS,
payload: res.data
});
} catch (err) {
dispatch({
type: POST_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
post API
router.get('/:date', async (req, res) => {
try {
const startOfToday = new Date(req.params.date);
const endOfToday = new Date(req.params.date);
endOfToday.setDate(endOfToday.getDate() + 1);
const posts = await Post.find({
date: { $gte: startOfToday, $lte: endOfToday }
}).sort({
date: -1
});
res.json(posts);
} catch (err) {
console.error(err.message);
res.send(500).send('Server Error');
}
});
Edit: I have updated your repo with a working example.. Your issue is that your API is not 'unlimited', as you claimed, and you do in fact need to check if all posts have been loaded or not.. Using the example I supplied along with the updates I made to your repo, you should be able to figure out things from here.
Ok.. so after some testing with InfiniteScroll, this appears to be happening because your hasMore property always equals true... You have to specify some type of condition so that InfiniteScroll knows when to, and when not to, load more data.
I got the same error as you before adding a check, which tells InfiniteScroll that there is no more data to load.
I have built the following example to show how to use InfiniteScroll
You can view a live demo here
PostsContainer.js
import React, { useState, useEffect } from "react";
import Posts from "./Posts";
import InfiniteScroll from "react-infinite-scroller";
const loadingStyle = {
textAlign: "center",
fontSize: "48px",
color: "red"
};
function PostsContainer({ url, itemsToDisplay = 5 }) {
const [data, setData] = useState();
const [shownData, setShownData] = useState();
useEffect(() => {
(async () => {
let items = await fetchPosts(url);
let itemsToShow = selectNItems(items, itemsToDisplay);
setShownData(itemsToShow);
setData(items);
})();
}, [url]);
async function fetchPosts(url) {
let res = await fetch(url);
return await res.json();
}
const selectNItems = (obj, n) => {
return obj.slice(0, n);
}
const loadMorePosts = () => {
let items =
data &&
shownData &&
selectNItems(data, shownData.length + itemsToDisplay)
setShownData(items);
};
return (
<InfiniteScroll
pageStart={0}
loadMore={loadMorePosts}
hasMore={data && shownData && data.length > shownData.length}
loader={<div style={loadingStyle}>Loading ...</div>}
useWindow={true}
>
<Posts posts={shownData} />
</InfiniteScroll>
);
}
export default PostsContainer;
Posts.js
import React from 'react';
import Post from './Post';
const headingStyle = {
textAlign: 'center',
}
function Posts({ posts }) {
return(
<div>
<h1 style={headingStyle}>Posts</h1>
{posts && posts.length > 0 && posts.map((p, i) => <Post key={i} data={p} index={i} />)}
</div>
);
}
export default Posts;
Post.js
import React from "react";
const containerStyle = {
border: "1px solid black",
margin: "10px auto",
maxWidth: "50vw",
padding: '0px 10px 0px 0px'
};
const postHeaderStyle = {
textAlign: "center",
padding: "0px"
};
function Post({ data, index }) {
return (
<div style={containerStyle}>
{index !== "" && <h3 style={postHeaderStyle}>Post #{index}</h3>}
<ul>
<li>
<b>userId:</b> {data.userId}
</li>
<li>
<b>id:</b> {data.id}
</li>
<li>
<b>title:</b> {data.title}
</li>
<li>
<b>body:</b> {data.body}
</li>
</ul>
</div>
);
}
export default Post;
index.js
import React from "react";
import { render } from "react-dom";
import PostsContainer from "./Components/PostsContainer";
function App() {
return (
<PostsContainer
itemsToDisplay={5}
url="https://jsonplaceholder.typicode.com/posts"
/>
);
}
render(<App />, document.getElementById("root"));

Props in functional component are missing in props validation

Eslint throwing eslint(react/prop-types) error despite already declared propTypes. I'm using eslint-plugin-react
I've looked at a couple of other similar problems and as well as the lint rule for the proptype but they don't address my issue.
import React from 'react';
import { View, Text, TouchableHighlight, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
const PASTEL_PINK = '#dea5a4';
const PASTEL_BLUE = '#779ecb';
const Buttons = ({ onPressStart, onPressPause, onPressReset, onGoing }) => (
<View >
<TouchableHighlight
onPress={onPressStart}
disabled={onGoing}
>
<Text >{START_TIMER}</Text>
</TouchableHighlight>
<TouchableHighlight
onPress={onPressPause}
disabled={!onGoing}
>
<Text >{PAUSE_TIMER}</Text>
</TouchableHighlight>
<TouchableHighlight onPress={onPressReset}>
<Text >{RESET_TIMER}</Text>
</TouchableHighlight>
</View>
);
Buttons.protoTypes = {
onPressStart: PropTypes.func.isRequired,
onPressPause: PropTypes.func.isRequired,
onPressReset: PropTypes.func.isRequired,
onGoing: PropTypes.bool.isRequired,
};
export default Buttons;
Parent component supplying the props
import React from 'react';
import Buttons from './components/Buttons'
import Container from './components/Container';
import Timer from './components/Timer';
import Inputs from './components/Inputs';
import Logo from './components/Logo';
import Buttons from './components/Buttons'
import Header from './components/Header'
export default class Home extends React.Component {
constructor(props){
super(props)
this.state = {
initialMinute: '00',
initialSecond: '00',
minute: '00',
second: '00',
completed: false,
onGoing: false,
}
componentWillMount() {
this.setState({
minute: this.state.initialMinute,
second: this.state.initialSecond,
}
);
}
componentWillUnmount() {
clearInterval(this.interval);
}
startTimer = () => {
console.log("Timer Started")
this.setState(
(prevState) => (
{
completed: false,
onGoing: true,
}
)
)
// start the timer
this.interval = setInterval(
this.decrementTime,
1000
)
}
decrementTime = () => {
if (this.state.second > 0) {
console.log(`second: ${this.state.second}`)
this.setState(
(prevState) => (
{second: prevState.second - 1}
)
)
if (this.props.second < 10) {
this.setState({
second: '0'+this.state.second
});
}
}
else {
if (this.state.minute > 0) {
this.setState(
(prevState) => (
{
minute: prevState.minute - 1,
second: prevState.second + 59,
}
)
)
if (this.props.minute < 10) {
this.setState({
state: '0'+this.state.minute
});
}
}
else {
this.resetTimer();
this.timesUp(true);
}
}
}
pauseTimer = () => {
console.log("Timer stopped")
clearInterval(this.interval);
this.setState({
onGoing: false,
}
)
}
resetTimer = () => {
console.log("Timer is reset")
this.pauseTimer();
this.setState({
minute: this.state.initialMinute,
second: this.state.initialSecond,
}
);
}
timesUp = (bool) => {
this.setState(
(prevState) => (
{
completed: bool,
}
)
)
}
optionPressed = () => {
console.log("Header is pressed")
}
handleMinuteInput = (text) => {
// clamp minute between 0 and 60
// const number = helper.clamp(parseInt(text), 0, 60)
this.setState(
{
initialMinute: text,
}
)
}
handleSecondInput = (text) => {
// const number = helper.clamp(parseInt(text+''), 0, 60)
this.setState(
{
initialSecond: text,
}
)
}
render() {
return (
<Container>
<Header onPress={this.optionPressed}/>
<Logo
slogan={'Get studying, the Pomodoro way!'}
imageSource={'../../assets/pomo-timer-logo-small.png'}
/>
<Timer
minute={this.state.minute}
second={this.state.second}
completed={this.state.completed}
onGoing={this.state.onGoing}
/>
<Buttons
onPressStart={this.startTimer}
onPressPause={this.pauseTimer}
onPressReset={this.resetTimer}
onGoing={this.state.onGoing} // true when not onGoing
/>
<Inputs
inputType={'Work'}
labelColor={PASTEL_BLUE}
handleMinuteInput={this.handleMinuteInput}
handleSecondInput={this.handleSecondInput}
onGoing={this.state.onGoing}
/>
<Inputs
inputType={'Rest'}
labelColor={PASTEL_PINK}
// setTimer={this.setTimer}
handleMinuteInput={this.handleMinuteInput}
handleSecondInput={this.handleSecondInput}
onGoing={this.state.onGoing}
/>
</Container>
)
}
}
I don't expect these error to show up but it does.
'onPressStart' is missing in props validation
'onPressPause' is missing in props validation
'onPressReset' is missing in props validation
'onGoing' is missing in props validation
Replace
Buttons.protoTypes
with
Buttons.propTypes
I have done this mistake too many times
It's propTypes, not protoTypes :)

Categories

Resources