Rerender only occurs on second state change, - reactjs

I am hoping you guys can help me,
I think I may be missing a simple concept. I have a Component within that there is another component that has an array that is prop drilled from its parent component. In the Child component, the list is then mapped and displayed.
Now the issue is when I update the state of the Array, ie add another item to the Array using SetArray([...array, newItem]),
the useEffect in the ChildComponent will console.log the new array but the actual display does not change until I add another element to the array.
When I add another element the first element I added appears but the 2nd one doesn't.
Hopefully, that makes some sense
ChildComponent:
import React, { useState, useEffect } from "react";
////EDITOR//// List
import { Grid, Button, Card } from "#material-ui/core";
import Timestamp from "./Timestamp";
const TimestampList = ({ setTime, match, setMatchEdit, render }) => {
const [timestamps, setTimestamps] = useState([]);
useEffect(() => {
const setInit = async () => {
try {
console.log(match);
const m = await match.scores.map(player => {
console.log(player);
if (player.totalScores) {
return player.totalScores;
}
});
console.log(m);
if (m[0] && m[1]) {
setTimestamps(
[...m[0], ...m[1]].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[0] && !m[1]) {
setTimestamps(
m[0].sort((a, b) => {
return a.time - b.time;
})
);
}
if (m[1] && !m[0]) {
setTimestamps(
m[1].sort((a, b) => {
return a.time - b.time;
})
);
}
} catch (error) {
console.log(error);
}
};
if (match) {
setInit();
}
console.log(match);
}, [match]);
return (
<Grid
component={Card}
style={{ width: "100%", maxHeight: "360px", overflow: "scroll" }}
>
{timestamps && timestamps.map(timestamp => {
console.log(timestamp);
const min = Math.floor(timestamp.time / 60);
const sec = timestamp.time - min * 60;
const times = `${min}m ${sec}sec`;
return (
<Timestamp
time={times}
pointsScored={timestamp.points}
/>
);
})}
<Grid container direction='row'></Grid>
</Grid>
);
};
export default TimestampList;

Related

React Typewriter effect doesn't reset

I have created a typewriting effect with React and it works perfectly fine. However, when I change the language with i18n both texts don't have the same length and it keeps writing until both texts have the same length and then it changes the language and starts the effect again.
How can I reset the input when the language has changed? How can I reset the input when the component has been destroyed?
I have recorded a video
I have the same issue when I change from one page to another, as both pages have different texts and they don't have the same length.
Here code of my component
export const ConsoleText = ({text, complete = false}) => {
const [currentText, setCurrentText] = useState("");
const translatedText = i18n.t(text);
const index = useRef(0);
useEffect(() => {
if (!complete && currentText.length !== translatedText.length) {
const timeOut = setTimeout(() => {
setCurrentText((value) => value + translatedText.charAt(index.current));
index.current++;
}, 20);
return () => {
clearTimeout(timeOut);
}
} else {
setCurrentText(translatedText);
}
}, [translatedText, currentText, complete]);
return (
<p className="console-text">
{currentText}
</p>
);
};
You are telling react to do setCurrentText(translatedText) only when it is complete or when the compared text lengths are equal, so yes it continues to write until this moment.
To reset your text when text changes, try creating another useEffect that will reset your states :
useEffect(() => {
index.current = 0;
setCurrentText('');
}, [text]);
Now, I actually did this exact same feature few days ago, here is my component if it can help you :
import React from 'react';
import DOMPurify from 'dompurify';
import './text-writer.scss';
interface ITextWriterState {
writtenText: string,
index: number;
}
const TextWriter = ({ text, speed }: { text: string, speed: number }) => {
const initialState = { writtenText: '', index: 0 };
const sanitizer = DOMPurify.sanitize;
const [state, setState] = React.useState<ITextWriterState>(initialState);
React.useEffect(() => {
if (state.index < text.length - 1) {
const animKey = setInterval(() => {
setState(state => {
if (state.index > text.length - 1) {
clearInterval(animKey);
return { ...state };
}
return {
writtenText: state.writtenText + text[state.index],
index: state.index + 1
};
});
}, speed);
return () => clearInterval(animKey);
}
}, []);
// Reset the state when the text is changed (Language change)
React.useEffect(() => {
if (text.length > 0) {
setState(initialState);
}
}, [text])
return <div className="text-writer-component"><span className="text" dangerouslySetInnerHTML={{ __html: sanitizer(state.writtenText) }} /></div>
}
export default TextWriter;
The translation is made outside of the component so you can pass any kind of text to the component.

How to create a dynamic array of React hooks for an array of components

const AnimatedText = Animated.createAnimatedComponent(Text);
function Component({ texts }) {
const [visitIndex, setVisitIndex] = React.useState(0);
// can't create an array of shared value for each text
// since useSharedValue is a hook, and that throws a warning
const textScalesShared = texts.map((_) => useSharedValue(1));
// can't create an array of animated style for each text
// since useAnimatedStyle is a hook, and that throws a warning
const animatedTextStyle = textScalesShared.map((shared) =>
useAnimatedStyle(() => ({
transform: [{ scale: shared.value }],
}))
);
useEffect(() => {
// code to reduce text scale one after another
// it will loop over the array of textScaleShared values
// passed to each component and update it
if (visitIndex === texts.length) {
return;
}
textScalesShared[visitIndex].value = withDelay(
1000,
withTiming(0.5, {
duration: 1000,
})
);
const timerId = setTimeout(() => {
setVisitIndex((idx) => idx + 1);
}, 1000);
return () => {
clearTimeout(timerId);
};
}, [visitIndex]);
return texts.map((text, index) => {
if (index <= visitIndex) {
return (
<AnimatedRevealingText
key={index}
fontSize={fontSize}
revealDuration={revealDuration}
style={animatedStylesShared[index]}
{...props}
>
{text}
</AnimatedRevealingText>
);
} else {
return null;
}
});
}
I want to apply animated styles to an array of components, but since useSharedValue and useAnimatedStyle are both hooks, I am unable to loop over the prop and create a shared value and the corresponding style for each of the component.
How can I achieve the same?
EDIT: updated to add the full code.
You can create a component to handle the useSharedValue and useAnimatedStyle hooks for every item using the visitIndex value:
AnimatedTextItem.js
const AnimatedText = Animated.createAnimatedComponent(Text);
const AnimatedTextItem = ({text, visited}) => {
const textScaleShared = useSharedValue(1);
const style = useAnimatedStyle(() => ({
transform: [{ textScaleShared.value }],
}));
useEffect(()=> {
if(visited) {
textScaleShared.value = withDelay(
1000,
withTiming(0.5, {
duration: 1000,
});
);
}
}, [visited]);
return (<AnimatedText style={style}>{text}</AnimatedText>)
}
Component.js
function Component({texts}) {
const [visitIndex, setVisitIndex] = React.useState(0);
useEffect(() => {
// code to reduce text scale one after another
// it will loop over the array of textScaleShared values
// passed to each component and update it
if (visitIndex === texts.length) {
return;
}
const timerId = setTimeout(() => {
setVisitIndex((idx) => idx + 1);
}, revealDuration);
return () => {
clearTimeout(timerId);
};
}, []);
return texts.map((text, index) => (<AnimatedTextItem text={text} visited={visitIndex === index}/>))
}
You can compose a component to handle it for you, but you need to pass the index of the text you're mapping through.
Like this
const AnimatedText = ({styleIndex}) => {
const textScaleShared = useSharedValue(styleIndex + 1);
const animatedTextStyle = useAnimatedStyle(() => ({
transform: [{ scale: textScaleShared.value }],
}));
const Animated = Animated.createAnimatedComponent(Text);
return <Animated style={animatedTextStyle}>{text}</Animated>;
};
function Component({ texts }) {
useEffect(() => {
// code to reduce text scale one after another
}, []);
return texts.map((text, index) => (
<AnimatedText key={index} styleIndex={index}>
{text}
</AnimatedText>
));
}
Interesting problem :) Let me see if i can come up a solution.
You already notice hook can't be in a dynamic array since the length of array is unknown.
Multiple components
You can have as many as components as you want, each one can have a hook, ex.
const Text = ({ text }) => {
// useSharedValue(1)
// useAnimatedStyle
}
const Components = ({ texts }) => {
return texts.map(text => <Text text={text} />)
}
Single hook
You can also see if you can find a className that can apply to all components at the same time. It's css i assume.

useEffect causing error message to pop up multiple times [duplicate]

I am working of a Guessing Game for 'React Native' where the user enters a number and the phone tries to guess it. Each time the phone generates a guess the user can click Greater/Lower. When the user entered number and the computer made guess equal each other we are taken to the game over screen.
The game over screen is not rendering. The logic to render the game over screen is placed inside of a useEffect()
Problem
useEffect is only fired once during the mounting phase and never again?
const { userSelectedNumber, onGameOver } = props;
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver]);*emphasized text*
(./screens/GameScreen.js)
We should exit the GameScreen when currentGuess === userSelectedNumber but this code is only run once.
Full code for GameScreen below:
import React, { useState, useRef, useEffect } from "react";
import { View, StyleSheet, Button, Text, Alert } from "react-native";
import NumberContainer from "../components/NumberContainer";
import Card from "../components/Card";
const randNumberGeneratorBetween = (min, max, exclude) => {
min = Math.ceil(min);
max = Math.floor(max);
const randNum = Math.floor(Math.random() * (max - min)) + min;
if (randNum === exclude) {
return randNumberGeneratorBetween(1, 100, exclude);
} else {
return randNum;
}
};
const GameScreen = props => {
const [currentGuess, setCurrentGuess] = useState(
randNumberGeneratorBetween(1, 100, props.userSelectedNumber)
);
const [rounds, setRounds] = useState(0);
const currentLow = useRef(1);
const currentHigh = useRef(100);
const { userSelectedNumber, onGameOver } = props;
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver]);
const nextGuessHandler = direction => {
if (
(direction === "lower" && currentGuess < props.userSelectedNumber) ||
(direction === "greater" && currentGuess > props.userSelectedNumber)
) {
Alert.alert("Don't Lie", "You know this is wrong", [
{ text: "Sorry", style: "cancel" }
]);
}
if (direction === "lower") {
currentHigh.current = currentGuess;
} else {
currentLow.current = currentGuess;
}
const nextNumber = randNumberGeneratorBetween(
currentLow.current,
currentHigh.current,
currentGuess
);
console.log('nextNumber',nextNumber);
setCurrentGuess(nextNumber);
setRounds(currRounds => currRounds + 1);
console.log('currRound',rounds);
};
return (
<View style={styles.screen}>
<Text>Opponents Guess</Text>
<NumberContainer>{currentGuess}</NumberContainer>
<Card style={styles.buttonContainer}>
<Button
title="Lower"
onPress={nextGuessHandler.bind(this, "lower")}
></Button>
<Button
title="Greater"
onPress={nextGuessHandler.bind(this, "greater")}
></Button>
</Card>
</View>
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
padding: 10,
alignItems: "center"
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 20,
width: 300,
maxWidth: "80%"
}
});
export default GameScreen;
Project can be found here:
https://codesandbox.io/s/github/SMasood1/guessingGame?file=/screens/GameScreen.js:852-1039
You need to add rounds and currentGuess to the dependencies array in the useEffect hook
useEffect(() => {
console.log(currentGuess, userSelectedNumber);
if (currentGuess === userSelectedNumber) {
onGameOver(rounds);
}
}, [userSelectedNumber, onGameOver,currentGuess,rounds]);
Also it is considered a anti-pattern to use props to initialize a state, so I would recommend to add an other useEffect hook:
useEffect(()=>{
setCurrentGuess(randNumberGeneratorBetween(1, 100, props.userSelectedNumber))
},[props.userSelectedNumber]);
The useEffect hook causes the component to update whenever any of the values of the dependency array changes. Make sure the values you use to trigger that hook are in fact changing.
You can elegantly trigger useEffect by supplying a timestamp on you navigation.navigate call
e.g.
// someComponent.tsx
navigation.navigate('Home', {
showSubscriptionModal: true
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false
useEffect(() => {
if(showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal])
will only fire once, while
// someComponent.tsx
navigation.navigate('Home', {
showSubscriptionModal: true,
updateTs: new Date()
})
// HomeScreen.tsx
const showSubscriptionModal = props.route.params?.showSubscriptionModal ?? false
useEffect(() => {
if(props.route.params?.showSubscriptionModal) setIsShowingModal(true)
},[showSubscriptionModal, props.route.params?.updateTs])
will fire every time you re-navigate to your screen via navigation.navigate()

React update to state/context works sporadically

I'm trying to build a React context object that keeps track of the window size so I can use specific components based on window.innerWidth without having to determine the value in multiple places. Ive set the following "DeviceContext" component up:
import React, { createContext, useState, useLayoutEffect } from "react"
import { size } from "../utilities/breakpoints"
export const DeviceContext = createContext()
const getSize = () => {
let width = window.innerWidth > 0 ? window.innerWidth : window.screen.width
if (width > size.huge) {
return "huge"
} else if (width >= size.large) {
return "large"
} else if (width >= size.med) {
return "med"
} else {
return "small"
}
}
export function DeviceProvider({ children }) {
let [size, setSize] = useState(getSize())
useLayoutEffect(() => {
function resize() {
setSize(getSize())
}
window.addEventListener("resize", resize)
return () => {
window.removeEventListener("resize", resize)
}
})
return (
<DeviceContext.Provider value={size}>{children}</DeviceContext.Provider>
)
}
I then wrap my Layout component in <DeviceProvider> and consume it in a menu component like this:
export const Menu = ({ children }) => {
return (
<DeviceContext.Consumer>
{screenSize => {
if (screenSize === "huge" || screenSize === "large") {
return <div>Not done yet...</div>
} else {
return <ResponsiveNav />
}
}}
</DeviceContext.Consumer>
)
}
Menu.propTypes = {
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
}
export default Menu
What I expect to see is "Not done yet..." when window.innerwidth is larger than a specific value and the <ResponsivNav/> component when it is not. I expect my context to update when the window is resized (i.e. through dev tools) but it only does so sporadically. If I remove the references to "previous" in my resize method, it doesn't update at all.
Thanks in advance for any advice you might have!
By not providing a dependency array in your useLayoutEffect the event listener is being readded on every state update. Setting an empty array there will only apply the listener on mount.
useLayoutEffect(() => {
function resize() {
setSize(getSize())
}
window.addEventListener("resize", resize)
return () => {
window.removeEventListener("resize", resize)
}
}, []) // <--- empty dependency array

React rewriting class with hooks causes re-render loops

I am working on an image transition react component, where it waits to have img1 loaded and then on a user click loads the img2, but fades img1 smoothly.
Tried re-writing the application with hooks to set the states - but when this is applied it creates an re-render loop error.
Is it because we are always setting img1 as currentImgSrc initially?
const [imgSrcOne, setImgSrcOne] = useState(currentImgSrc)
errors in the if/else block? or is setting the useState in the return causing the bug
tried removing the if/else block to make the application functional
https://jsfiddle.net/b531L6ho/
import {
ImageTransition
} from './imageTransition/imageTranition'
const {
useState
} = React
interface ImageContainerProps {
layer: string
currentImgSrc: string
notifyOnError: (url: string) => void
updateLayerLoading: (hasLoaded: boolean) = void
}
export const ImageTransitionContainer: React.SFC < ImageContainerProps > = ({
currentImgSrc,
layer,
notifyOnError,
updateLayerLoading
}) => {
const [imgSrcOne, setImgSrcOne] = useState(currentImgSrc)
const [displayImgOne, setdisplayImgOne] = useState(true)
const [imgOneLoaded, setImgOneLoaded] = useState(false)
const [imgSrcTwo, setImgSrcTwo] = useState(currentImgSrc)
const [displayImgTwo, setdisplayImgTwo] = useState(true)
const [imgTwoLoaded, setImgTwoLoaded] = useState(false)
if (imgSrcOne && currentImgSrc !== imgSrcOne) {
console.log("in the if statement")
setImgSrcTwo(currentImgSrc)
setDisplayImgTwo(two)
}
if (currentImgSrc !== imgSrcOne) {
setImgSrcne(currentImgSrc)
}
if (!imgSrcOne && !imgSrcTwo) {
setImgSrcOne(currentImgSrc)
setDisplayImgOne(true)
} else if (imgSrcOne && currentImgSrc !== imgSrcOne) {
setImgSrcTwo(currentImgSrc)
setDisplayImgTwo(true)
} else if (imgSrcTwo && currentImgSrc !== imgSrcTwo) {
setImgSrcOne(currentImgSrc)
setDisplayImgOne(true)
}
console.log("state --", imgSrcOne, displayImgOne, imgOneLoaded, imgSrcTwo, displayImgTwo, imgTwoLoaded)
return (
<>
<ImageTransition
displayImg={displayImgOne}
imgLoaded={imgOneLoaded}
imgSrc={imgSrcOne}
onExit={() => {
setImgSrcOne(null)
setImgOneLoaded(false)
}}
onLoad={() => {
setImgOneLoaded(true)
setDisplayImgTwo(false)
}}
/>
<ImageTransition
displayImg={displayImgTwo}
imgLoaded={imgTwoLoaded}
imgSrc={imgSrcTwo}
onExit={() => {
setImgSrcTwo(null)
setImgTwoLoaded(false)
}}
onLoad={() => {
setImgTwoLoaded(true)
setDisplayImgOne(false)
}}
/>
</>
)
}

Resources