react-countdown resets when rendered in ReactJs - javascript

I am creating a game which user can tap a button as much as they can in a range of time.
I have use react-countdown to create Timer. But when I tap to a button, timer is auto reset.
Here my code.
import React, { useState } from "react"
import Countdown from 'react-countdown';
function App() {
const [name, setName] = useState("")
const [isLogin, setIsLogin] = useState(false)
const [counter, setCounter] = useState(0)
const submitName = () => {
setIsLogin(true)
}
const updateCounter = () => {
console.log(counter)
setCounter(counter + 1)
}
return (
<div>
{!isLogin ? (
<div>
<input
style={{ width: 300, height: 30 }}
placeholder="Input name here"
value={name}
onChange={e => setName(e.target.value)}
/>
<button
style={{ width: 50, height: 20, background: "red" }}
onClick={() => submitName()}
>Go</button>
</div>
) : (
<div
style={{ width: "100vw", height: "100vh", background: "yellow" }}>
<Countdown date={Date.now() + 100000} />
<h1>{name}</h1>
<h3>Click counter: {counter} </h3>
<button
style={{
width: 100,
height: 50,
background: "red",
border: "none",
borderRadius: 25,
color: "white",
cursor: "pointer"
}}
onClick={() => updateCounter()}>Click here!</button>
</div>
)
}
</div>
);
}
export default App;
The problem is when I tap the button, setCounter(counter + 1) is running and rerender the page. That made the Counter reset.
I know why it have issue but I cannot fix it.

You need to memoize the Countdown component:
const CountdownWrapper = () => <Countdown date={Date.now() + 100000} />;
const MemoCountdown = React.memo(CountdownWrapper);
// usage
<MemoCountdown/>
Beforehand, on every render Date.now() gets a new value which resets the timer.

import React from 'react';
const time = React.useMemo(() => {
return Date.now() + 120 * 1000;
}, []);

Related

How to rid error of null properties in react hook?

I'm trying to custom hook to get few data. I want check where is one element in comparison to second element. I want to center it among themselves. It works but I have repeated errors in console.
Error:
Uncaught TypeError: Cannot read properties of null (reading
'offsetLeft') at getPosition (usePosition.js:18:1)
import { useEffect, useRef, useState } from "react";
const usePosition = () => {
const elRef = useRef();
const elSecondRef = useRef();
const [fromLeftEl, setFromLeftEl] = useState();
const [fromTopEl, setFromTopEl] = useState();
const [widthEl, setWidthEl] = useState();
const [heightEl, setHeightEl] = useState();
const [widthElSecond, setWidthElSecond] = useState();
const [finalX, setFinalX] = useState();
const [finalY, setFinalY] = useState();
// This function calculates the position underneath the element and centering it with respect to the other element
const getPosition = () => {
const fromLeftEl = elRef.current.offsetLeft;
setFromLeftEl(fromLeftEl);
const fromTopEl = elRef.current.offsetTop;
setFromTopEl(fromTopEl);
const widthEl = elRef.current.offsetWidth;
setWidthEl(widthEl);
const heightEl = elRef.current.offsetHeight;
setHeightEl(heightEl);
const widthElSecond = elSecondRef.current.offsetWidth;
setWidthElSecond(widthElSecond);
const middleEl = widthEl / 2;
const middleElSecond = widthElSecond / 2;
const finalX = fromLeftEl + middleEl - middleElSecond;
setFinalX(finalX);
const finalY = fromTopEl + heightEl;
setFinalY(finalY);
};
// Get the position of the first element
useEffect(() => {
getPosition();
}, []);
useEffect(() => {
window.addEventListener("resize", getPosition);
window.addEventListener("click", getPosition);
window.addEventListener("scroll", getPosition);
return () => window.removeEventListener("scroll", getPosition);
}, []);
return {
elRef,
elSecondRef,
fromLeftEl,
fromTopEl,
widthEl,
heightEl,
finalY,
finalX,
widthElSecond,
};
};
export default usePosition;
component that uses usePosition:
import classNames from "classnames";
import { useState } from "react";
import useSticky from "./useSticky";
import usePosition from "./usePosition";
import "../style/sass/Nav.sass";
import { NavLink } from "react-router-dom";
const Nav = (props) => {
const [isActive, setActive] = useState(false);
const { sticky, stickyRef } = useSticky();
const { elRef, elSecondRef, finalY, finalX } = usePosition();
const handleToggle = () => {
setActive(!isActive);
};
return (
<div
ref={stickyRef}
className={classNames("nav", { sticky })}
style={{
display: props.display,
height: sticky ? `${stickyRef.current?.clientHeight}px` : "10vh",
width: props.width,
gridTemplateColumns: props.navGridCol,
gridTemplateRows: props.navGridRow,
height: props.height,
position: props.position,
left: props.left,
animation: props.animationMenu,
}}
>
<div
className="nav__logo"
style={{
gridArea: props.gArea1,
height: props.heightHomeBtn,
width: props.btnWidth,
animation: props.animationHome,
display: props.homeDisplay,
}}
>
<NavLink className="link__logo" to="/">
<i
class="icon-home"
style={{
width: props.btnWidth,
fontSize: props.fweight,
}}
></i>
</NavLink>
</div>
<NavLink
className="link__about"
to="/about"
exact="true"
style={{ gridArea: props.gArea2 }}
>
<button
className="nav__aboutBtn"
style={{
height: props.btnHeight,
width: props.btnWidth,
animation: props.animationAbout,
display: props.aboutDisplay,
}}
>
<p>o firmie</p>
</button>
</NavLink>
<button
onClick={handleToggle}
className={!isActive ? "offerDD__btn" : "offerDD__btn_active"}
style={{
gridArea: props.gArea3,
height: props.btnHeight,
width: props.btnWidth,
animation: props.animationOffer,
display: props.offerDisplay,
}}
ref={elRef}
>
<i class="icon-down-open"></i>
<p>oferta</p>
</button>
<div
className={
!isActive ? "offerDD__dropdown_disabled" : "offerDD__dropdown"
}
ref={elSecondRef}
style={{ top: `${finalY}px`, left: `${finalX}px` }}
>
<NavLink className="link__air" to="/air-conditioning">
<div
className="air__p"
style={{ height: props.btnHeight, width: props.btnWidth }}
>
<p>klimatyzacje</p>
</div>
</NavLink>
<NavLink className="link__vent" to="/ventilation">
<div
className="vent__p"
style={{ height: props.btnHeight, width: props.btnWidth }}
>
<p>wentylacje</p>
</div>
</NavLink>
<NavLink className="link__heat" to="/heat-pump">
<div
className="heat__p"
style={{ height: props.btnHeight, width: props.btnWidth }}
>
<p>pompy ciepła</p>
</div>
</NavLink>
<NavLink className="link__recu" to="/recuperation">
<div
className="recu__p"
style={{ height: props.btnHeight, width: props.btnWidth }}
>
<p>rekuperacja</p>
</div>
</NavLink>
<NavLink className="link__fire-protection" to="/fire-protection">
<div
className="fire__p"
style={{ height: props.btnHeight, width: props.btnWidth }}
>
<p>ppoż</p>
</div>
</NavLink>
<NavLink className="link__shop" to="/shop">
<div
className="shop__p"
style={{ height: props.btnHeight, width: props.btnWidth }}
>
<p>sklep</p>
</div>
</NavLink>
</div>
<NavLink
className="link__blog"
to="/blog"
style={{ gridArea: props.gArea4 }}
>
<button
className="nav__blogBtn"
style={{
height: props.btnHeight,
width: props.btnWidth,
animation: props.animationBlog,
display: props.blogDisplay,
}}
>
<p>blog</p>
</button>
</NavLink>
<NavLink
className="link__contact"
to="/contact"
style={{ gridArea: props.gArea5 }}
>
<button
className="nav__contactBtn"
style={{
height: props.btnHeight,
width: props.btnWidth,
animation: props.animationContact,
display: props.contactDisplay,
}}
>
<p>kontakt</p>
</button>
</NavLink>
<NavLink
className="link__login"
to="/login"
style={{ gridArea: props.gArea6 }}
>
<button
className="nav__loginBtn"
style={{
height: props.btnHeight,
width: props.btnWidth,
animation: props.animationLogin,
display: props.loginDisplay,
}}
>
<p>zaloguj</p>
</button>
</NavLink>
</div>
);
};
export default Nav;
I want know how to code this issue correctly without error in console and working hook.
I know that this error is due to the fact that it is impossible to read something that is not there and therefore it is null but how to write this code to make it correct?
I don't see any overt issue with the way the elRef is used in the consuming component. elRef is attached to a DOM element, e.g. button, and there is no conditional rendering so button is rendered to the DOM during the initial render.
const Nav = (props) => {
...
const { elRef, elSecondRef, finalY, finalX } = usePosition();
...
return (
<div
...
>
...
<button
onClick={handleToggle}
className={!isActive ? "offerDD__btn" : "offerDD__btn_active"}
style={{
gridArea: props.gArea3,
height: props.btnHeight,
width: props.btnWidth,
animation: props.animationOffer,
display: props.offerDisplay,
}}
ref={elRef}
>
<i class="icon-down-open"></i>
<p>oferta</p>
</button>
...
</div>
);
};
I do however see potential issue in the usePosition hook. The hook doesn't clean up all the effects it creates. In the case of accessing into potentially null or undefined references it is advised to use a null-check/guard-clause or the Optional Chaining operator prior to accessing properties. In this case an if block to check if the refs have a truthy current value is sufficient.
const usePosition = () => {
const elRef = useRef();
const elSecondRef = useRef();
const [fromLeftEl, setFromLeftEl] = useState();
const [fromTopEl, setFromTopEl] = useState();
const [widthEl, setWidthEl] = useState();
const [heightEl, setHeightEl] = useState();
const [widthElSecond, setWidthElSecond] = useState();
const [finalX, setFinalX] = useState();
const [finalY, setFinalY] = useState();
// This function calculates the position underneath the element and centering it with respect to the other element
const getPosition = () => {
// Only access ref properties if there is a current value
if (elRef.current && elSecondRef.current) {
const fromLeftEl = elRef.current.offsetLeft;
setFromLeftEl(fromLeftEl);
const fromTopEl = elRef.current.offsetTop;
setFromTopEl(fromTopEl);
const widthEl = elRef.current.offsetWidth;
setWidthEl(widthEl);
const heightEl = elRef.current.offsetHeight;
setHeightEl(heightEl);
const widthElSecond = elSecondRef.current.offsetWidth;
setWidthElSecond(widthElSecond);
const middleEl = widthEl / 2;
const middleElSecond = widthElSecond / 2;
const finalX = fromLeftEl + middleEl - middleElSecond;
setFinalX(finalX);
const finalY = fromTopEl + heightEl;
setFinalY(finalY);
}
};
useEffect(() => {
window.addEventListener("resize", getPosition, { passive: true });
window.addEventListener("click", getPosition);
window.addEventListener("scroll", getPosition, { passive: true });
// Call once on initial component render
getPosition();
// Return cleanup function to remove all listener callbacks!
return () => {
window.removeEventListener("resize", getPosition, { passive: true });
window.removeEventListener("click", getPosition);
window.removeEventListener("scroll", getPosition, { passive: true });
};
}, []);
return {
elRef,
elSecondRef,
fromLeftEl,
fromTopEl,
widthEl,
heightEl,
finalY,
finalX,
widthElSecond,
};
};
export default usePosition;

Add a success notification for an action

The site has a button to remove the device from the shopping cart.
The principle of the button is as follows:
the user clicks the delete button;
a window opens (something like a modal window made using Dialog mui) with a warning about the danger of deletion and two buttons: cancel and confirm;
2a) when you click the cancel button, the window closes;
2b), when the confirmation button is pressed, the deletion process begins, which is accompanied by a scroll wheel. After deletion, the window closes and the user can continue working on the site.
I would like, after closing the window, to display a notification for a few seconds that the item was successfully deleted. The whole difficulty lies in the fact that there is no fixed deletion time (the deletion time is different depending on the amount of information about the device) and it is not clear to me when the notification window should be called.
Help to understand please.
Here is my short working code
export function Delete() {
const [alertShown, setAlertShown] = useState(false);
const [alertLoading, setAlertLoading] = useState(false);
const onNo = () => {
setAlertShown(false);
};
const onYes = async () => {
setAlertLoading(true);
await deleteItem();
setAlertShown(false);
setAlertLoading(false);
};
return <ButtonGroup >
<div onClick={() => setAlertShown(true)}>
<DeleteForeverIcon/>
</div>
{alertShown && (
<Dialog open={onYes}>
{alertLoading
? <div ><Spinner/></div>
: <DialogActions >
<Button onClick={onNo}>Cancel</Button >
<Button onClick={onYes}>Confirm </Button >
</DialogActions>}
</Dialog>
)}
</ButtonGroup>
}
Here, for a better understanding, I demonstrate a demo version of what I have going on at the moment. The code in the codeSandbox is slightly different from the code I showed above. But the principle is the same. I will be grateful for any help
I just added some basic things on what you can do here:
import { useState } from "react";
import "./styles.css";
const sleep = (ms = 1000) => new Promise((resolve) => setTimeout(resolve, ms));
function randomIntFromInterval(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
async function deleteItem(item, firestore, urlParams) {
await deleteRecord(firestore, item.id);
}
async function deleteRecord(firestore, recordId) {
await firestore.collection("records").doc(recordId).delete();
}
const firestoreDummy = {
collection: () => ({
doc: () => ({
delete: async () => {
// Simulating delete action on firestore
await sleep(randomIntFromInterval(3000, 10000));
}
})
})
};
const Dialog = ({ loading, onYes, onNo }) => {
return (
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.6)"
}}
>
<div style={{ backgroundColor: "white", padding: 16 }}>
<h4>Are you sure</h4>
<p>Do you really want to delete this item?</p>
{loading && (
<div style={{ marginBottom: "8px" }}>
<span style={{ fontSize: "12px", color: "gray" }}>
Item is being deleted. Please wait...
</span>
</div>
)}
<button disabled={loading} onClick={onYes}>
Yes
</button>
<button disabled={loading} onClick={onNo}>
No
</button>
</div>
</div>
);
};
const SuccessDialog = ({ setSuccessAlertShown }) => {
return (
<div
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "rgba(0, 0, 0, 0.6)"
}}
>
<div style={{ backgroundColor: "white", padding: 16 }}>
<h4>Sucessfully deleted!</h4>
<button onClick={() => setSuccessAlertShown(false)}>Close</button>
</div>
</div>
);
};
export default function App() {
const [alertShown, setAlertShown] = useState(false);
const [alertLoading, setAlertLoading] = useState(false);
const [alertSucess, setSuccessAlertShown] = useState(false);
const onNo = () => {
setAlertShown(false);
};
const onYes = async () => {
setAlertLoading(true);
try {
await deleteItem({}, firestoreDummy);
setAlertShown(false);
} finally {
setAlertLoading(false);
// A simple notification
alert("The record deleted!");
// A more classic notification
setSuccessAlertShown(true);
}
};
return (
<div className="App">
<button onClick={() => setAlertShown(true)}>Delete item</button>
{alertShown && (
<Dialog onNo={onNo} onYes={onYes} loading={alertLoading} />
)}
{alertSucess && (
<SuccessDialog setSuccessAlertShown={setSuccessAlertShown} />
)}
</div>
);
}
This is the sandbox: https://codesandbox.io/s/nostalgic-hill-r8sh1x

React limits the number of renders to prevent an infinite loop error, while using empty array dependencies in useEffect()

I am trying to render some fetched data to the DOM from an API which is called in useEffect() hook. I set a boolean "isFetching" to start rendering the data after the fetch process ends, but I get an error for infinite loop instead.
The only way that my fetched data appears on render is while I use setTimeout() and wait for the data to be fetched, but this is only for testing...
import "bootstrap/dist/css/bootstrap.css";
import { useEffect } from "react";
import axios from "axios";
export default function App() {
//the array of loadedQuestions that is fetched from the API
let [loadedQuestions, setLoadedQuestions] = React.useState([]);
let [answers, setAnswers] = React.useState([]);
let [isFetching, setIsFetching] = React.useState(true);
async function fetchData() {
const response = await axios.get(
"https://opentdb.com/api.php?amount=10&category=18&difficulty=easy&type=multiple"
);
console.log(response);
const data = await response.data;
loadedQuestions = data.results;
setLoadedQuestions(loadedQuestions);
//adding the correct answer to incorrect_answers array
for (let obj of loadedQuestions)
obj.incorrect_answers.splice(
Math.floor(Math.random() * 4),
0,
obj.correct_answer
);
setIsFetching(false);
isFetching = false;
console.log(isFetching);
}
useEffect(() => {
fetchData();
}, []);
const [isClicked, setIsClicked] = React.useState(false);
const [currentQuestion, setCurrentQuestion] = React.useState(0);
const [score, setScore] = React.useState(0);
const [finished, setFinished] = React.useState(false);
const [beforeFinish, setBeforeFinish] = React.useState("");
//setTimeout only for testing....
// setTimeout(() => {
if (!isFetching) {
setAnswers(
loadedQuestions[currentQuestion].incorrect_answers.map((element) => (
<li>
<span
key={currentQuestion.toString()}
onClick={() => handleClick(element)}
style={{ cursor: "pointer" }}
className={isClicked ? textColor(element) : "bg"}
>
{element}
</span>
</li>
))
);
setBeforeFinish(
<div>
<h5 className="m-3" style={{ textDecoration: "underline" }}>
{loadedQuestions[currentQuestion].question}
</h5>
<ol style={{ listStyleType: "lower-latin" }}>{answers}</ol>
<button
onClick={() => nextQuestionFunction()}
style={{
backgroundColor: "#0c88fb",
color: "white",
marginLeft: 10,
}}
>
Next Question
</button>
<h5 style={{ marginTop: 15, marginLeft: 10 }}>
Your score is{" "}
<span style={{ color: "#0c88fb", fontWeight: "bold" }}>{score}</span>!
</h5>
</div>
);
}
// }, 2000);
const afterFinish = (
<div>
<h1>Finished!</h1>
<h5>
Your Score is{" "}
<span style={{ color: "#0c88fb", fontWeight: "bold" }}>{score}</span>
</h5>
<button
onClick={() => tryAgain()}
style={{ backgroundColor: "#0c88fb", color: "white", marginLeft: 2 }}
>
Try Again
</button>
</div>
);
function handleClick(element) {
setIsClicked(true);
textColor(element);
if (element === loadedQuestions[currentQuestion].correct_answer) {
setScore(score + 100 / loadedQuestions.length);
}
}
function textColor(element) {
let classN = "bg ";
element === loadedQuestions[currentQuestion].correct_answer
? (classN += "bg-info")
: (classN += "bg-secondary");
return classN;
}
function nextQuestionFunction() {
if (currentQuestion + 1 === loadedQuestions.length) {
setFinished(true);
} else {
setCurrentQuestion(currentQuestion + 1);
setIsClicked(false);
}
}
function textDisplay() {
if (finished) {
return afterFinish;
} else {
return beforeFinish;
}
}
function tryAgain() {
setCurrentQuestion(0);
setScore(0);
setIsClicked(false);
setFinished(false);
}
return textDisplay();
}
if (!isFetching) {
setAnswers(
loadedQuestions[currentQuestion].incorrect_answers.map((element) => (
// etc
))
);
setBeforeFinish(
<div>
// etc
</div>
);
}
You are calling setAnswers and setBeforeFinish in the middle of rendering. Setting state causes the component to rerender and when it renders you set state again, which renders again, which sets state again, etc.
These don't look like they should even be states at all. loadedQuestions and isFetching are the true state, and then answers and beforeFinish are just values calculated from that state. I recommend you delete the answers and beforeFinish states, and where you used to have the if (!isFetching) code you do:
let answers = null;
let beforeFinish = null;
if (!isFetching) {
answers = loadedQuestions[currentQuestion].incorrect_answers.map(
(element) => (
<li>
<span
key={currentQuestion.toString()}
onClick={() => handleClick(element)}
style={{ cursor: "pointer" }}
className={isClicked ? textColor(element) : "bg"}
>
{element}
</span>
</li>
)
);
beforeFinish = (
<div>
<h5 className="m-3" style={{ textDecoration: "underline" }}>
{loadedQuestions[currentQuestion].question}
</h5>
<ol style={{ listStyleType: "lower-latin" }}>{answers}</ol>
<button
onClick={() => nextQuestionFunction()}
style={{
backgroundColor: "#0c88fb",
color: "white",
marginLeft: 10,
}}
>
Next Question
</button>
<h5 style={{ marginTop: 15, marginLeft: 10 }}>
Your score is{" "}
<span style={{ color: "#0c88fb", fontWeight: "bold" }}>{score}</span>!
</h5>
</div>
);
}

Loop through array with setInterval play button and pause it

I want to loop through a array with a pause and a start button. Right now it automatically plays and I am not sure how to connect the setInterval to the play function and not sure how to stop the interval as everything I try just crashes it. Maybe there is a good a library for doing this as I can imagine this is quite a normal thing to do? If not please help me fix my code, maybe there is better ways to do this if so please tell :)
Codesandbox attempt: https://codesandbox.io/s/modern-dream-4kldkg?file=/src/App.js
import "./styles.css";
import { useState } from "react";
export default function App() {
const array = [1, 2, 3, 5, 6, 7];
const [current, setCurrent] = useState(0);
const play = () => {
setCurrent(current + 1);
};
const pause = () => {};
const reset = () => {
setCurrent(0);
};
var intervalID = setInterval(function () {
if (current >= array.length) {
setCurrent(0);
}
setCurrent(current + 1);
}, 5000);
return (
<div className="App">
<button onClick={() => play()}>play</button>
<br></br>
<button onClick={() => pause()} style={{ marginTop: "5px" }}>
Pause
</button>
<br></br>
<button onClick={() => reset()} style={{ marginTop: "5px" }}>
Reset
</button>
<br></br>
<h1>{array[current]}</h1>
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
You should remove the interval on each render, please see the demo:
import "./styles.css";
import { useState, useEffect } from "react";
export default function App() {
const array = [1, 2, 3, 4, 5, 6, 7];
const [current, setCurrent] = useState(0);
const [paused, setPaused] = useState(true);
const play = () => {
setPaused(false);
};
const resume = () => {
setPaused(false);
};
const pause = () => {
setPaused(true);
};
const reset = () => {
setCurrent(0);
setPaused(true);
};
useEffect(
function () {
var timeout;
if (!paused) {
timeout = setTimeout(function () {
const next = current < array.length - 1 ? current + 1 : 0;
setCurrent(next);
}, 1000);
}
return function () {
clearTimeout(timeout);
};
},
[paused, current, array.length]
);
return (
<div className="App">
<button onClick={() => play()}>play</button>
<br></br>
<button onClick={() => pause()} style={{ marginTop: "5px" }}>
Pause
</button>
<button onClick={() => resume()} style={{ marginTop: "5px" }}>
Resume
</button>
<br></br>
<button onClick={() => reset()} style={{ marginTop: "5px" }}>
Reset
</button>
<br></br>
<h1>{array[current]}</h1>
</div>
);
}
https://codesandbox.io/s/throbbing-cdn-4c77nr

React component not re-rendering when useEffect dependency changes

PROBLEM:
I am currently creating a react app that allows you to checkout a book to a professor. In this app it has a couple of things that need to obviously update when a user checks out a book.
So first off there is the number of totalBooks that is checked out, or just when the entire book object changes then the component should re-render.
I have a useEffect function that is making an api call to a mongodb database and is accessing a document that will yield a book object in the response to the react app. Here is that useEffect function:
useEffect(() => {
const getBook = async () => {
// console.log(book)
await api.getBookById(props.id).then(async book => {
setBook({...book.data.data})
setCopies([...book.data.data.copies])
var num = 0;
await book.data.data.copies.map(async (copy, index) => {
if(copy.checkedOut){
num++;
}
})
setNumCheckedOut(num)
}).catch(e => {console.log(e)})
}
getBook();
}, [book])
I have even subbed out the book object dependency for something like book.checkedOutCopies. Which should return a number and if that number is different from the last then it should re-render the component. This is however, not the case. No matter what I try I am unable to re-render the component when this document changes. I even created a number called reRender and updated it when the api call to checkout a book finished its call. This would be undesired even if it worked because it would not change for someone who was already on the page, but not on the same computer as the person that clicked the checkout button.
I simply just want this component to re-render when the book object in the mongo db database has changed. Which from my understanding the right way to do it is above. The problem is that even after I successfully checkout a book the state never updates. The number of checked out books on the screen stays static:
Here is a screen shot of what the page looks like:
The green books should turn to red when the update button is clicked and a success status is responded. The Total Checked Out should also change. Neither of these happen.
Here is the book object:
const Book = new Schema(
{
bookName: { type: String, required: true },
bookDesc: { type: String, required: false},
numCheckedOut: { type: Number, required: false },
copiesAvail: {type: Number},
whosChecked: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'prof'
}],
copies: [Copy],
ISBN: { type: Number, required: true },
nextDue: {type: String},
nextProf: {type: Schema.Types.ObjectId, ref: 'prof'}
},
{ timestamps: true },
)
I don't understand why it isn't updating would appreciate any help here is the file in its entirety:
import React, { useState, useEffect, useRef } from 'react'
import api from '../api'
import { InputGroup, Form, FormControl, Container, Button, Col, Row, Toast } from 'react-bootstrap'
import { FaBook } from 'react-icons/fa'
import BookIconFunc from '../components/helperComponents/bookIconFunc'
import Select from 'react-dropdown-select';
import './bookslist.css'
import Axios from "axios";
import BookIcoContext from '../context/BookIconContext';
import DatePicker from "react-datepicker";
import ColoredLine from '../components/helperComponents/ColoredLine'
import CheckoutBookHistroy from '../components/CheckoutBookHistory/CheckoutBookHistory'
import { useHistory } from 'react-router-dom';
const handleCheckout = async (e) => {
e.preventDefault();
}
const topLeftBook = {
marginTop: "1.0rem",
display: "flex",
width:"fit-content",
height: "fit-content",
justifyContent: "left",
flexDirection: "column",
alignItems: "center",
borderRadius: "0px",
border: "2px solid #73AD21",
padding: "0.5rem 2.5rem",
}
const booksRows = {
// marginTop: "4.0rem",
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
padding: "1.0rem",
justifyContent: "left",
// border: "2px solid black",
width: "50%",
marginLeft: "1.0rem"
}
const bottomForm = {
flexGrow: "1"
}
const indBook = {
margin: "0.5rem",
color: "green"
}
const updateButtonStyle = {
display: "flex",
width: "100%",
justifyContent:"center"
}
const topOfPage = {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
verticalAlign: "middle"
}
const bookIcon = {
width: "10.0rem",
}
const bottomOfPage = {
display: "flex",
flexDirection: "row",
marginBottom: "1.0rem",
verticalAlign: "middle",
flexGrow: "1",
marginLeft: "1.0rem"
}
const topForm = {
width: "100%",
marginLeft: "2.0rem",
verticalAlign: "middle",
alignItems: "center",
justifyContent: "center"
}
export default function BooksUpdate(props) {
/* TOP FORM VARIABLES */
const [book, setBook] = useState(null)
const [bookName, setBookName] = useState()
const [desc, setDesc] = useState()
const [ISBN, setISBN] = useState()
const [copies, setCopies] = useState()
const history = useHistory();
/* BOOK ICON CONTEXT STATE */
const [iconState, setIconState] = useState(false)
/* re render state when */
const [reRender, setReRender] = useState(0);
/* BOTTOM FORM VARIABLES */
const [newCopies, setNewCopies] = useState()
const [checkoutCopiesNum, setCheckoutCopiesNum] = useState(0)
const [numCheckedOut, setNumCheckedOut] = useState(0)
const [allProfs, setAllProfs] = useState()
const [profChosen, setProfChosen] = useState()
const [bookicoref, setIcoRefs] = useState([]);
const [dueDate, setDueDate] = useState(new Date());
var anotherRender = 0;
const submitCheckout = async (e) => {
e.preventDefault();
try {
const checkoutBookData = {book, checkoutCopiesNum, profChosen, dueDate};
const checkoutBookRes = await Axios.post("http://localhost:8174/api/book/checkout/" + props.id, checkoutBookData)
if(checkoutBookRes.statusText === 'OK'){
setShowCheckoutToast(!showCheckoutToast)
}
/* Display toast that confirms the book was checked out */
setReRender(reRender+1)
anotherRender = anotherRender + 1
// history.push("/Books/Update/" + props.id)
}
catch (err) {
alert(err)
}
}
const handleSetBook = (bookData) => {
setBook({...bookData})
}
useEffect(() => {
const getBook = async () => {
// console.log(book)
await api.getBookById(props.id).then(async book => {
await handleSetBook(book.data.data)
// setBook(book.data.data)
setCopies([...book.data.data.copies])
var num = 0;
await book.data.data.copies.map(async (copy, index) => {
if(copy.checkedOut){
num++;
}
})
setNumCheckedOut(num)
}).catch(e => {console.log(e)})
}
getBook();
}, [book])
useEffect( () => {
const getProfs = async () => {
await Axios.get('http://localhost:8174/user/professors').then((ps) => {
var array = []
ps.data.data.map((prof, index) => {
array.push({label: prof.name, value: prof, key: prof._id})
})
setAllProfs(array)
})
}
getProfs()
}, [])
/* EFFECT TO CREATE REFS FOR EACH BOOK ICON */
useEffect(() => {
// add or remove refs
copies &&
setIcoRefs(bookicorefs => (
Array(copies.length).fill().map((_, i) => bookicorefs[i] || React.createRef())
))
}, [copies]);
const handleUpdate = () => {
console.log("handling update")
}
const findCheckedOut = (array) => {
var numChecked = 0;
array.filter(arr => {
if(arr.checkedOut){
numChecked = numChecked + 1
}
})
return numChecked
}
const [showCheckoutToast, setShowCheckoutToast] = useState(false)
const toggleCheckToast = () => {
setShowCheckoutToast(!showCheckoutToast)
}
/* EFFECT TO VALIDATE THE INFORMATION INSIDE THE CHECKOUT BOOKS FIELD */
useEffect(() => {
if(!copies){
return
}
if(checkoutCopiesNum > copies.length){
alert("There isn't that much of this book available")
return;
}
// console.log(numCheckedOut)
if(checkoutCopiesNum > (copies.length - numCheckedOut)){
setCheckoutCopiesNum(0)
alert('You cannot checkout that many copies as there is already to many checked out')
return;
}
// for(var i = 0; i < checkoutCopiesNum; i++){
// }
},[checkoutCopiesNum, numCheckedOut])
return (
book ?
copies ?
<div>
<Container>
{/* Show the book icon with the title of the book underneath */}
<div style={topOfPage}>
<div style={topLeftBook}>
<FaBook style={bookIcon} size={200}/>
<h4 className="">{book.bookName}</h4>
</div>
<Form style={topForm}>
<Row>
<Col className="pl-0">
<InputGroup className="mb-3 mt-3">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Book Name</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setBookName(e.target.value)}
defaultValue={book.bookName}
aria-label="Name"
aria-describedby="basic-addon1"
/>
</InputGroup>
<Form.Group controlId="exampleForm.ControlTextarea1">
<Form.Control as="textarea" rows={5} defaultValue={book.bookDesc}/>
</Form.Group>
</Col>
<Col className="m-0 pr-0">
<InputGroup className="mb-4 mt-3">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">ISBN</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setISBN(e.target.value)}
defaultValue={book.ISBN}
aria-label="Name"
aria-describedby="basic-addon1"
/>
</InputGroup>
<InputGroup className="mb-4 mt-4">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Total Copies</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setNewCopies(e.target.value)}
aria-label="Name"
aria-describedby="basic-addon1"
defaultValue={copies.length}
/>
</InputGroup>
<InputGroup className="mb-4">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon1">Total Checked Out</InputGroup.Text>
</InputGroup.Prepend>
<FormControl onChange={(e) => setNewCopies(e.target.value)}
aria-label="Name"
aria-describedby="basic-addon1"
defaultValue={findCheckedOut(book.copies)}
/>
</InputGroup>
</Col>
<Button style={updateButtonStyle} onClick={handleUpdate}>Update</Button>
</Row>
</Form>
</div>
<Row style={{justifyContent: "space-between", verticalAlign: "middle"}}>
<Toast
show={showCheckoutToast}
onClose={toggleCheckToast}
style={{
position: 'absolute',
top: 0,
right: 0,
}}
>
<Toast.Header>
<img
src="holder.js/20x20?text=%20"
className="rounded mr-2"
alt=""
/>
<strong className="mr-auto">Success!</strong>
</Toast.Header>
<Toast.Body>Successfully Checked out a Book</Toast.Body>
</Toast>
<div style={bottomOfPage}>
<Form style={bottomForm} onSubmit={submitCheckout}>
<InputGroup className="mt-4">
<InputGroup.Prepend>
<InputGroup.Text id="basic-addon4">Checkout Out Copies:</InputGroup.Text>
</InputGroup.Prepend>
<FormControl
onChange={(e) => setCheckoutCopiesNum(e.target.value)}
placeholder={checkoutCopiesNum}
aria-label="Name"
aria-describedby="basic-addon1"
/>
</InputGroup>
<Select
className="mt-4"
style={{width: "100%"}}
name="Select"
required
// loading
searchable
placeholder="To:"
options={allProfs}
onChange={(values) => {setProfChosen(values[0].value)}}
/>
<DatePicker className="mt-4" selected={dueDate} onChange={date => setDueDate(date)} />
<Button type="submit" className="mt-3 w-100">Checkout</Button>
</Form>
</div>
<BookIcoContext.Provider value={{iconState, setIconState}}>
<div style={booksRows} onClick={() => setIconState(true)} onMouseUp={() => setIconState(false)}>
{
copies ? copies.map((copy, index) => {
return <div
key={index}
>
<BookIconFunc
checkedOut={copy.checkedOut}
ref={bookicoref[index]}
>
</BookIconFunc>
</div>
})
:
<div>none</div>
}
</div>
</BookIcoContext.Provider>
</Row>
</Container>
<ColoredLine color="grey" m={20} height={1}/>
<Container>
{/* {book.whosChecked.map(prof => {
// console.log(prof)
// <Col>{prof}</Col>
})} */}
<CheckoutBookHistroy book_id={props.id} book={book} reRender={reRender}></CheckoutBookHistroy>
</Container>
</div>
:
<div>no data</div>
:
<div>no data</div>
)
}

Categories

Resources