I have a specific component that renders twice. Basically, I am generating two random numbers and displaying them on the screen and for a split second, it shows undefined for both then shows the actual numbers. To test it further I did a simple console.log and it indeed logs it twice. So my question splits into two - 1. Why does the typography shows undefined for a split second before rendering? 2 - Why does it render twice?
Here's the related code:
Beginnging.js - this a counter that counts down from 3 and fires an RTK action, setting gameStart true.
function Beginning() {
const [count, setCount] = useState(3);
const [message, setMessage] = useState("");
const dispatch = useDispatch();
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const handleCount = () => {
if (countRef.current === 1) {
return setMessage("GO");
}
return setCount((count) => count - 1);
};
useEffect(() => {
const interval = setInterval(() => {
handleCount();
}, 1000);
if (message==='GO') {
setTimeout(() => {
dispatch(start());
}, 1000);
}
return () => clearInterval(interval);
}, [count, message]);
return (
<>
<Typography variant="h1" fontStyle={'Poppins'} fontSize={36}>GET READY...</Typography>
<Typography fontSize={48} >{count}</Typography>
<Typography fontSize={60} >{message}</Typography>
</>
);
}
export default Beginning;
AdditionMain.js - based on gameStart, this is where I render Beginning.js, once it counts down, MainInput is rendered.
const AdditionMain = () => {
const gameStart = useSelector((state) => state.game.isStarted);
const operation = "+";
const calculation = generateNumbersAndResults().addition;
if (!gameStart){
return <Beginning/>
}
return (
<>
<MainInput operation={operation} calculation={calculation} />
</>
);
};
export default AdditionMain;
MainInput.js - the component in question.
const MainInput = ({ operation, calculation }) => {
const [enteredValue, setEnteredValue] = useState("");
const [correctValue, setCorrectValue] = useState(false);
const [calculatedNums, setCalculatedNums] = useState({});
const [isIncorrect, setIsIncorrect] = useState(false);
const [generateNewNumbers, setGenerateNewNumbers] = useState(false);
const [haveToEnterAnswer, setHaveToEnterAnswer] = useState(false);
const dispatch = useDispatch();
const seconds = useSelector((state) => state.game.seconds);
const points = useSelector((state) => state.game.points);
const lives = useSelector((state) => state.game.lives);
const timerValid = seconds > 0;
const newChallenge = () => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
dispatch(restart());
};
const handleCount = () => {
dispatch(loseTime());
};
useEffect(() => {
let interval;
if (timerValid) {
interval = setInterval(() => {
handleCount();
}, 1000);
}
return () => {
console.log("first");
clearInterval(interval);
};
}, [timerValid]);
useEffect(() => {
setCalculatedNums(calculation());
setGenerateNewNumbers(false);
setCorrectValue(false);
setEnteredValue("");
}, [generateNewNumbers]);
const submitHandler = () => {
if (correctValue) {
setGenerateNewNumbers(true);
dispatch(gainPoints());
dispatch(gainTime());
}
if (+enteredValue === calculatedNums.result) {
setCorrectValue(true);
} else if (enteredValue.length === 0) {
setHaveToEnterAnswer(true);
} else {
setIsIncorrect(true);
dispatch(loseLife());
}
};
const inputValueHandler = (value) => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
setEnteredValue(value);
};
const submitOrTryNewOne = () => {
return correctValue ? "Try new one" : "Submit";
};
return (
<>
<Navbar />
{console.log("hello")}
{seconds && lives > 0 ? (
<>
<GameInfo></GameInfo>
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography>
Fill in the box to make the equation true.
</Typography>
<Typography fontSize={28}>
{operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
<TextField
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
type="number"
name="sum"
id="outlined-basic"
label=""
variant="outlined"
onChange={(event) => {
inputValueHandler(event.target.value);
}}
disabled={correctValue}
value={enteredValue}
></TextField>
{haveToEnterAnswer && enterAnswer}
{correctValue && correctAnswer}
{isIncorrect && wrongAnswer}
<Button
type="button"
sx={{ marginTop: 1 }}
onClick={() => submitHandler()}
variant="outlined"
>
{isIncorrect ? "Try again!" : submitOrTryNewOne()}
</Button>
</Box>
</Container>
</>
) : (
<>
<Typography>GAME OVER</Typography>
<Typography>Final Score: {points}</Typography>
<Button onClick={newChallenge}>New Challenge</Button>
</>
)}
</>
);
};
export default MainInput;
PS: I'm trying to figure out how to get this running on Codesandbox
The problem is that you render the page before the calculaed nums are established.
Try this instead. It will prevent the element from displaying until nums are generated.
const [calculatedNums, setCalculatedNums] = useState(null);
// This is down in your render area. Only render once calculatedNums are non-null.
{ calculatedNums &&
<Typography fontSize={28}>
operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
}
In addition, you are probably generating your numbers twice, because you will generate new numbers on initial load, but then when you hit setGenerateNewNumbers(true); it will trigger a new calculation, which will then set the generatedNewNumbers to false, which will call the calc again, since it triggers whenever that state changes. It stops after that because it tries to set itself to false again and doesn't change.
You are changing a dependency value within the hook itself, causing it to run again. Without looking a lot more into your program flow, a really hacky way of fixing that would just be to wrap your useEffect operation inside an if check:
useEffect(()=>{
if (generateNewNumbers === true) {
//All your stuff here.
}
}, [generateNewNumbers]
That way, it won't run again when you set it to false.
Related
I have a function that I am passing to my re-used component as a prop. This function generates and returns two random numbers and their addition result. Everything works on the first go, however, I can't figure out how to get it to generate new numbers and result on submit.
PS: I get a warning on useEffect dependency saying that
React Hook useEffect has a missing dependency: 'calculation'. Either include it or remove the dependency array. If 'setCalculatedNums' needs the current value of 'calculation', you can also switch to useReducer instead of useState and read 'calculation' in the reducer.
However, using calculation as the dependency doesn't work either.
PS: I had the code working perfectly initially before I decided it's a better practice to re-use pieces of code that don't need repetition. Just can't figure out how to get it done this way.
Here's the code:
the re-used component MainInput.js:
const correctAnswer = <Typography>Correct!</Typography>;
const wrongAnswer = <Typography>Wrong!</Typography>;
const enterAnswer = <Typography>Enter your answer!</Typography>;
const MainInput = ({operation, calculation}) => {
const [enteredValue, setEnteredValue] = useState("");
const [correctValue, setCorrectValue] = useState(false);
const [calculatedNums, setCalculatedNums] = useState({});
const [isIncorrect, setIsIncorrect] = useState(false);
const [generateNewNumbers, setGenerateNewNumbers] = useState(false);
const [haveToEnterAnswer, setHaveToEnterAnswer] = useState(false);
useEffect(() => {
setCalculatedNums(calculation);
setGenerateNewNumbers(false);
setCorrectValue(false);
setEnteredValue("");
}, [generateNewNumbers]);
const submitHandler = () => {
console.log(calculation.additionResult)
if (correctValue) {
setGenerateNewNumbers(true);
}
if (+enteredValue === calculation.additionResult) {
setCorrectValue(true);
} else if (enteredValue.length === 0) {
setHaveToEnterAnswer(true);
} else {
setIsIncorrect(true);
}
};
const inputValueHandler = (value) => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
setEnteredValue(value);
};
const submitOrTryNewOne = () => {
return correctValue ? "Try new one" : "Submit";
};
return (
<>
<Navbar />
<Card
sx={{
marginTop: 2,
height: 50,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography></Typography>
</Card>
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography>Fill in the box to make the equation true.</Typography>
<Typography fontSize={28}>
{calculatedNums.number1} {operation} {calculatedNums.number2}{" "}
=
</Typography>
<TextField
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
type="number"
name="sum"
id="outlined-basic"
label=""
variant="outlined"
onChange={(event) => {
inputValueHandler(event.target.value);
}}
disabled={correctValue}
value={enteredValue}
></TextField>
{haveToEnterAnswer && enterAnswer}
{correctValue && correctAnswer}
{isIncorrect && wrongAnswer}
<Button
type="button"
sx={{ marginTop: 1 }}
onClick={() => submitHandler()}
variant="outlined"
>
{isIncorrect ? "Try again!" : submitOrTryNewOne()}
</Button>
</Box>
</Container>
</>
);
};
export default MainInput;
MainArithmetics.js
export const generateNumbersAndResults = () => {
const randomNum1 = () => {
return Math.floor(Math.random() * 50);
};
const randomNum2 = () => {
return Math.floor(Math.random() * 50);
};
const number1 = randomNum1() || 0;
const number2 = randomNum2() || 0;
const additionResult = number1 + number2 || 0;
return {
additionResult,
number1,
number2
};
};
AdditionMain.js
import React from "react";
import MainInput from "../components/MainInput";
import { generateNumbersAndResults } from "../MainArithmetics";
const AdditionMain = () => {
const operation = '+'
const calculation = generateNumbersAndResults()
return (
<>
{console.log(calculation)}
<MainInput
operation={operation}
calculation={calculation}
/>
</>
);
};
export default AdditionMain;
There is a lot of unneeded code, but the core issue is that calculation doesn't change. Once called, it's going to have a fixed set of numbers, so when you call:
setCalculatedNums(calculation);
It's just going to set the 'same' numbers again. If you want new numbers, you'll need to do something like:
setCalculatedNums(generateNumbersAndResults());
edit after comment
The core issue is that you are not passing a function, you are passing it's result. If you want to pass the function, instead of:
const calculation = generateNumbersAndResults();
you'll want:
const calculation = generateNumbersAndResults;
Then later on you make sure you call this function when you want to generate a new 'calculation'
setCalculatedNums(calculation());
I tried to count down the second and reload the page, the second showed in the component is correct but the printed on the console was always 5, so I couldn't reload.
I want to count down and reload the page. Where do I put window.location.reload()? and why count always showed 5, but setCount is normal and decrease every second?
This is the code:
function Register() {
const dispatch = useDispatch();
const [count, setCount] = useState(5);
const timeountToReload = () => {
const interval = setInterval(() => {
console.log(count); // always 5 here
if (count === 0) {
window.history.pushState({}, '', '/');
window.location.reload();
}
setCount((count) => count - 1);
}, 1000);
return () => clearInterval(interval);
};
const handleRegister = async (e, value) => {
e.preventDefault();
setSuccessful(false);
setFormControlDisable(true);
await dispatch(register({ username, email, password }))
.then(unwrapResult)
.then((result) => {
setSuccessful(true);
timeountToReload();
})
.catch((error) => {});
setFormControlDisable(false);
};
return (
<Container style={{ marginTop: '15%' }}>
<Row className='justify-content-md-center'>
<Col md={{ span: 4, offset: 0 }} style={{ border: 'green solid 5px' }}>
{count}
<RegisterForm
handleUsernameChange={handleUsernameChange}
handlePasswordChange={handlePasswordChange}
handleComfirmedPasswordChange={handleComfirmedPasswordChange}
handleEmailChange={handleEmailChange}
handleRegister={handleRegister}
username={username}
password={password}
comfirmedPassword={comfirmedPassword}
email={email}
errors={errors}
message={message}
successful={successful}
count={count} // component shows normal and refresh every second
formControlDisable={formControlDisable}
/>
</Col>
</Row>
</Container>
);
}
export default Register;
I think that inside the interval you passed a function instead of a value to the setCount. try to replace
setCount(count => count - 1)
with
setCount(count - 1)
Try to keep track of count changes using the useEffect hook:
const timeountToReload = () => {
const interval = setInterval(() => {
setCount((count) => count - 1);
}, 1000);
return () => clearInterval(interval);
};
useEffect(() => {
console.log(count);
if (count === 0) {
window.location.reload();
}
}, [count]);
Link to playground.
you should use useEffect with the count as dependency to watch its chages, because inside setInterval it won't update
useEffect(() => {
if (count === 0) {
window.history.pushState({}, '', '/');
window.location.reload();
};
}, [count]);
I'm working on a project and wanted to try and implement an infinitely scrollable page to list users. Filtering and such works fine and all but every time the scrolling component reaches the ref element the component makes an API call to append to the list and then the parent component re-renders self completely.
const UsersList = () => {
const [searchString, setSearchString] = useState('')
const [next, setNext] = useState('')
const { userList, error, nextPage, loading, hasMore } = useFetch(next)
const [usersList, setUsersList] = useState([])
const observer = useRef()
const lastElemRef = useCallback(
(node) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setNext((prev) => (prev = nextPage))
setUsersList((prev) => new Set([...prev, ...userList]))
}
})
if (node) {
observer.current.observe(node)
}
},
[loading, nextPage, hasMore],
)
useEffect(() => {
setUsersList((prev) => new Set([...prev, ...userList]))
console.log(error)
}, [])
return (
<>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<CContainer
className="w-100 justify-content-center"
style={{ maxWidth: 'inherit', overflowY: 'auto', height: 600 }}
>
<CFormInput
className="mt-2 "
id="userSearchInput"
value={searchString}
onChange={(e) => {
setSearchString(e.target.value)
}}
/>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<>
{Array.from(usersList)
.filter((f) => f.username.includes(searchString) || searchString === '')
.map((user) => {
if (Array.from(usersList)[usersList.size - 1] === user) {
return (
<UserCard
key={user.id}
user={user}
parentRef={searchString ? null : lastElemRef}
/>
)
} else {
return <UserCard key={user.id} user={user} />
}
})}
</>
)}
</CContainer>
)}
</>
)
}
export default UsersList
This is the component entirely.
Here's my useFetch hook;
export const useFetch = (next) => {
const [userList, setUserList] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [nextPage, setNextPage] = useState(next)
const [hasMore, setHasMore] = useState(true)
useEffect(() => {
setLoading(true)
setError('')
axios
.get(next !== '' ? `${next}` : 'http://localhost:8000/api/getUsers/', {
headers: {
Authorization: 'Bearer ' + localStorage.getItem('access_token'),
},
})
.then((res) => {
setUserList((userList) => new Set([...userList, ...res.data.results]))
setNextPage((prev) => (prev = res.data.next))
if (res.data.next === null) setHasMore(false)
setLoading(false)
})
.catch((err) => {
setError(err)
})
}, [next])
return { userList, error, nextPage, loading, hasMore }
}
export default useFetch
I'm using Limit Offset Pagination provided by Django Rest Framework, next object just points to the next set of objects to fetch parameters include ?limit and ?offset added at the end of base API url. What is it that I'm doing wrong here ? I've tried many different solutions and nothing seems to work.
Solved
Apparently it was just my back-end not cooperating with my front-end so I've changed up the pagination type and now it seems to behave it self.
I'm having an issue where useEffect isn't triggering a re-render based on useState changing or useState isn't changing which isn't triggering useEffect. I noticed this issue once I selected an asset that should update useState as the selected component and then I select another its no problem but once I select an asset that has already been selected.. nothing happens? Any suggestions or anything is greatly appreciated!! Thanks!
export default function SingleAsset({svg, name, size = '60', group}) {
const [assetType, setAssetType] = React.useState(null);
const [officeType, setOfficeType] = React.useState(null);
const [industrialType, setIndustrialType] = React.useState(null);
const [financingType, setFinancingType] = React.useState(null);
const [investmentType, setInvestmentType] = React.useState(null)
const acquistionStatus = useSelector(state => state.Acquisition)
const dispatch = useDispatch()
const classes = useStyles()
React.useEffect(() => {
if(financingType === 'Acquisition') {
const data = {financingType}
dispatch(updateFormData(data))
dispatch(toggleAcquisitionOn())
}
if(financingType) {
if(financingType !== 'Acquisition') dispatch(toggleAcquisitionOff())
const data = {financingType}
dispatch(updateFormData(data))
}
if(industrialType) {
const data = {industrialType}
dispatch(updateFormData(data))
}
if(officeType) {
const data = {officeType}
dispatch(updateFormData(data))
}
if(investmentType) {
const data = {investmentType}
dispatch(updateFormData(data))
console.log(data)
}
if(assetType) dispatch(updateAssetData(assetType))
console.log(financingType)
console.log(officeType)
console.log(industrialType)
console.log(investmentType)
},[investmentType,assetType,officeType,industrialType,financingType])
const handleSelect = (group, name) => {
switch(group) {
case 'main':
setAssetType(name)
break
case 'office':
setOfficeType(name)
break
case 'industrial':
setIndustrialType(name)
break
case 'financing':
setFinancingType(name)
break
case 'investment':
setInvestmentType(name)
break
default:
throw new Error('group not found')
}
}
return (
<Grid
className={classes.container}
item
>
<Grid
container
direction="column"
alignItems="center"
>
<IconButton onClick={() => handleSelect(group, name)}>
<img src={svg} color="white" height={size} />
</IconButton>
<Typography
variant="body1"
color="white"
align="center"
>
{name}
</Typography>
</Grid>
</Grid>
)
}
That's actually an expected behavior.
React uses "shallow comparison" (check this other great question for more on that), which essentially means it'll compare the previous and new value with ===. This is the reason one should not mutate state objects. Because of this, when your code tries to update the state to the same value it already has it won't actually do it... it's the same, so no re-render will be triggered.
To solve this, we can force React to update with some clever coding that will make React detect a state change:
// Setup a new state
const [, updateState] = useState();
// Create a function that will update state with a new object
// This works because {} === {} is always false, making React trigger a re-render
const forceUpdate = useCallback(() => updateState({}), []);
Your sample code would be something like this:
export default function SingleAsset({svg, name, size = '60', group}) {
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
const [assetType, setAssetType] = React.useState(null);
const [officeType, setOfficeType] = React.useState(null);
const [industrialType, setIndustrialType] = React.useState(null);
const [financingType, setFinancingType] = React.useState(null);
const [investmentType, setInvestmentType] = React.useState(null)
const acquistionStatus = useSelector(state => state.Acquisition)
const dispatch = useDispatch()
const classes = useStyles()
React.useEffect(() => {
if(financingType === 'Acquisition') {
const data = {financingType}
dispatch(updateFormData(data))
dispatch(toggleAcquisitionOn())
}
if(financingType) {
if(financingType !== 'Acquisition') dispatch(toggleAcquisitionOff())
const data = {financingType}
dispatch(updateFormData(data))
}
if(industrialType) {
const data = {industrialType}
dispatch(updateFormData(data))
}
if(officeType) {
const data = {officeType}
dispatch(updateFormData(data))
}
if(investmentType) {
const data = {investmentType}
dispatch(updateFormData(data))
console.log(data)
}
if(assetType) dispatch(updateAssetData(assetType))
console.log(financingType)
console.log(officeType)
console.log(industrialType)
console.log(investmentType)
},[investmentType,assetType,officeType,industrialType,financingType])
const handleSelect = (group, name) => {
switch(group) {
case 'main':
setAssetType(name)
break
case 'office':
setOfficeType(name)
break
case 'industrial':
setIndustrialType(name)
break
case 'financing':
setFinancingType(name)
break
case 'investment':
setInvestmentType(name)
break
default:
throw new Error('group not found')
}
}
return (
<Grid
className={classes.container}
item
>
<Grid
container
direction="column"
alignItems="center"
>
<IconButton onClick={() => {
handleSelect(group, name);
forceUpdate(); // <-- this is were the magic happens
}}>
<img src={svg} color="white" height={size} />
</IconButton>
<Typography
variant="body1"
color="white"
align="center"
>
{name}
</Typography>
</Grid>
</Grid>
)
}
Please note forcing a re-render should be a last resort, re-rendering can be a rather expensive operation and should be handled with care.
I'm a bit new to React. I'm trying to make simple To do list with react hooks and struggling to make "delete all button". I thought it could be work to using setState [] or return []
but it didn't work...
and also it's showing an error.
TypeError: tasks.map is not a function
Does anyone know how it figure out?
Here is my code
import React, {useState} from 'react'
let INITIAL_TASK = {
title: 'React',
doing: false,
}
const App = () => {
const [tasks, setTasks] = useState([INITIAL_TASK])
const [task_title, setTask_title] = useState('')
const handleTextFieldChanges = e => {
setTask_title(e.target.value)
}
const resetTextField = () => {
setTask_title('')
}
const isTaskInclude = () => {
return tasks.some(task => task.title === task_title)
}
const addTask = () => {
setTasks([...tasks, {
title: task_title,
doing: false,
}])
resetTextField()
}
const deleteTask = (task) => {
setTasks(tasks.filter(x => x !== task))
}
const deleteAllTask = () => {
-------------
}
const handleCheckboxChanges = task => {
setTasks(tasks.filter(x => {
if (x === task) x.doing = !x.doing
return x
}))
}
return (
<React.Fragment>
<Container component='main' maxWidth='xs'>
<CssBaseline/>
<Box
mt={5}
display='flex'
justifyContent='space-around'
>
<TextField
label='title'
value={task_title}
onChange={handleTextFieldChanges}
/>
<Button
disabled={task_title === '' || isTaskInclude()}
variant='contained'
color='primary'
onClick={addTask}
href=''
>
add
</Button>
<Button
// disabled={task_title === '' || isTaskInclude()}
variant='contained'
color='secondary'
onClick={deleteAllTask}
href=''
>
all delete
</Button>
</Box>
<List
style={{marginTop: '48px'}}
component='ul'>
{tasks.map(task => (
<ListItem key={task.title} component='li'>
<Checkbox
checked={task.doing}
value='primary'
onChange={() => handleCheckboxChanges(task)}
/>
<ListItemText>{task.title}</ListItemText>
<Button
href=''
onClick={() => deleteTask(task)}
>
delete
</Button>
</ListItem>
))}
</List>
</Container>
</React.Fragment>
)
}
export default App
You can try doing below
const deleteAllTask = () => {
setTasks([]);
};
or if you want it to set to initial value, you can do below
const deleteAllTask = () => {
setTasks([INITIAL_TASK]);
};