state is not updating when the component re renders? - reactjs

I'm making a Nextjs flashcard app. I'm passing a deck structure like this:
const deck = {
title: 'React 101',
flashcards: [flashcardOne, flashcardTwo],
};
as props to the Deck component. This component shows the first card in flashcards and a "next" button to increment the index and showing the next card in flashcards.
The Card component is very simple and shows the front and the back of the card depending of the state front.
This is what I got so far and it's working but if I click "next" when the card is showing the answer (flashcard.back), the next card is going to appear with the answer. And I'm not sure why, isn't the Card component re rendering when I click "next"? And if the component re renders, front is going to be set to true?
export default function Deck({ deck }) {
const [cardIndex, setCardIndex] = useState(0);
const { title, flashcards } = deck;
return (
<div className={styles.container}>
<main className={styles.main}>
<h1 className={styles.title}>{title}</h1>
{cardIndex < flashcards.length ? (
<>
<div className={styles.grid}>
<Card flashcard={flashcards[cardIndex]} />
</div>
<button onClick={() => setCardIndex((cardIndex) => cardIndex + 1)}>
Next
</button>
</>
) : (
<>
<div>End</div>
<button>
<Link href='/'>
<a>Go to Home</a>
</Link>
</button>
<button onClick={() => setCardIndex(0)}>Play again</button>
</>
)}
</main>
</div>
);
}
export function Card({ flashcard }) {
const [front, setFront] = useState(true);
return (
<>
{front ? (
<div
className={`${globalStyles.card} ${styles.card}`}
onClick={() => setFront(false)}
>
<p className={styles.front}>{flashcard.front}</p>
</div>
) : (
<div
className={`${globalStyles.card} ${styles.card}`}
onClick={() => setFront(true)}
>
<p className={styles.back}>{flashcard.back}</p>
</div>
)}
</>
);
}

When state changes, the card will re-render, but it will not re-mount. So, existing state will not be reset.
Call setFront(true) when the flashcard prop has changed:
const [front, setFront] = useState(true);
useLayoutEffect(() => {
setFront(true);
}, [flashcard]);
I'm using useLayoutEffect instead of useEffect to ensure front gets set ASAP, rather than after a paint cycle (which could cause flickering).
You can also significantly slim down the Card JSX:
export function Card({ flashcard }) {
const [front, setFront] = useState(true);
const face = front ? 'front' : 'back';
return (
<div
className={`${globalStyles.card} ${styles.card}`}
onClick={() => setFront(!front)}
>
<p className={styles[face]}>{flashcard[face]}</p>
</div>
);
}

Okay, I guess I had the same issue. Since you're using functional components, and you're re-using the same component or in better words, you're not unmounting and remounting the component really, you're just changing the props, this happens. For this, you need to do useEffect() and then setFront(true).
Here's the code I used in my App.
useEffect(() => {
setFront(true);
}, [flashcard]);
This is what I have used in my Word.js file.

Related

(Theme Toggle) How can I make className sync dynamically to active localStorage value from its key when toggled in React

className "page" needs to be modified to "page light-theme" or "page dark-theme" on toggle through local storage key "theme-color" with values of light-theme and dark-theme.
The active key value does change in local Storage but updates only show if the pages is refreshed. I need the changes to sync on toggle
Page to be changed
export default function Page({children}){
return(
<div className={`page ${localStorage.getItem('theme-color')}`}>
{/* <div className= "page"> */}
{children}
</div>
)
}
h3 inner text needs to change dynamically depending on the active value from key "theme-color" in local storage. I have place the variable "themeOpener" in between h3 tag. No changes take place
heres is my code
export default function Body() {
let themeOpener;
if (`${localStorage.getItem('theme-color','light-theme')}`) {
themeOpener = "🧛🏼Ahh the light it burns! Please use toggle, I prefer dark mode!";
} else {
themeOpener = "I learnt to design in React and im hooked 🤩";
}
return (
<div className="body">
{/* <h3 id="opener">I learnt to design in React and im hooked 🤩</h3> */}
<h3 id="opener">{themeOpener}</h3>
</div>
);
}
This is code for my toggle where local storage is created
const ToggleMode = () => {
// state
const [isLight, setIsLight] = useState(false);
// effect
useEffect(() => {
// check local storage
const CurrentTheme = localStorage.getItem("theme-color");
if (CurrentTheme === "light-theme") {
setIsLight(true);
} else {
setIsLight(false);
}
console.log(useEffect);
}, []);
const ToggleChecked = () => {
// logic
if (isLight) {
localStorage.setItem("theme-color", "dark-theme");
setIsLight(false);
} else {
localStorage.setItem("theme-color", "light-theme");
setIsLight(true);
}
console.log(ToggleChecked);
};
return (
<div className="toggle--container">
<input
type={"checkbox"}
id="toggle"
className="toggle--checkbox"
checked={isLight}
onChange={ToggleChecked}
/>
<label htmlFor="toggle" className="toggle--label">
<span className="toggle--label-background"></span>
</label>
<div className=""></div>
</div>
);
};
export default ToggleMode;
Yes, this is natural according to your code. Whenever toggling, ToggleMode component will only be re-rendered by changed state value isLight.
But your Body and Page component which are supposed to be ToggleMode's parent will not be re-rendered. Because their props or states never changed by ToggleChecked().
To get it done working, you need to do something to re-render parents in Page and Body component.
How? You need to create a state value in those components or create IsLight and setIsLight at the top level component. And then these two would be drilled into ToggleMode.
Something like followings.
export default function Page({children}){
// state
const [isLight, setIsLight] = useState(false);
return(
<div className={`page ${localStorage.getItem('theme-color')}`}>
{/* <div className= "page"> */}
<Body isLight={isLight} setIsLight={setIsLight} />
{children}
</div>
)
}
export default function Body({isLight, setIsLight}) {
return (
<div>
<ToggleMode isLight={isLight} setIsLight={setIsLight} />
</div>
);
}
export default function ToggleMode ({isLight, setIsLight}){
// This is not needed anymore.
// const [isLight, setIsLight] = useState(false);
return (<>Your toggle code...</>)
}
P.S. Don't you think this is quite irritating? To avoid prop drilling, we use state management utilities such as react context API or 3rd party libraries such as Redux.

Render JSX from material-ui Button onClick

I'm trying to render JSX when a material-ui button is clicked. I'm logging to the console when clicking but cannot see any of the JSX getting rendered.
interface TileProps {
address?: string;
}
const renderDisplayer = (address: string) => {
console.log('Rendering address', address!);
if (typeof(address) == 'undefined' || address == '') {
return(<div className='error'><li>No address found</li></div>)
}
return(<AddressDisplayer address={address} />)
}
const Tile = (props: TileProps) => {
return(
<div className='tile'>
<ul>
<li>{props.address}</li>
</ul>
<Button variant='contained' onClick={() => {renderDisplayer(props.address)}}>Display</Button>
</div>
)
}
export default Tile;
I can see the console.log('Rendering address', address!); running when the button is clicked, but the JSX isn't getting rendered.
Could this be because I'm using React functional components instead of class components?
Your question is somehow unclear for me. If you want to render <div className='error'><li>No address found</li></div> based on typeof(address) == 'undefined' || address == '' condition, there is no need to click on the button and it's better to use conditional rendering. For example:
{!props.address ? (
<div className='error'><li>No address found</li></div>
) : (
<AddressDisplayer address={props.address} />
)}
But if you want to render your address component by clicking on the button, you should define a state and set it true when clicking on the button. Like this:
const [shouldShowAddress, setShouldShowAddress] = useState(false);
{shouldShowAddress && (
<>
{!props.address ? (
<div className="error">
<li>No address found</li>
</div>
) : (
<AddressDisplayer address={props.address} />
)}
</>
)}
<Button
variant="contained"
onClick={() => {
setShouldShowAddress(true)
}}
>
Display
</Button>
Please read about the Life cycle This not how react work onclick function
renderDisplayer is called and return JSX to onClick event you need to use state here to render the component with ternary oprator renderDisplayer fuction do setState so DOM will update

Unexpected Behavior After State Change in React Component

RenderImages = (): React.ReactElement => {
let selected = this.state.results.filter(x=>this.state.selectedGroups.includes(x.domain))
console.log(selected)
return(
<div className="results_wrapper">
{selected.map((r,i)=>{
let openState = (this.state.selectedImage==i)?true:false;
return(
<RenderPanel panelType={PanelType.large} openState={openState} title={r.domain+'.TheCommonVein.net'} preview={(openIt)=>(
<div className="result" onClick={openIt} style={{ boxShadow: theme.effects.elevation8}}>
<img src={r.url} />
</div>
)} content={(closeIt)=>(
<div className="panel_wrapper">
<div className="panel_content">{r.content}</div>
{this.RenderPostLink(r.domain,r.parent)}
<div onClick={()=>{
closeIt();
this.setState({selectedImage:2})
console.log('wtfff'+this.state.selectedImage)
}
}>Next</div>
<img src={r.url} />
</div>
)}/>
)
})}
</div>
)
}
When I change the state of 'selectedImage', I expect the variable 'openState' to render differently within my map() function. But it does not do anything.
Console.log shows that the state did successfully change.
And what is even stranger, is if I run "this.setState({selectedImage:2})" within componentsDidMount(), then everything renders exactly as expected.
Why is this not responding to my state change?
Update
I have tried setting openState in my component state variable, but this does not help either:
RenderImages = (): React.ReactElement => {
let selected = this.state.results.filter(x=>this.state.selectedGroups.includes(x.domain))
console.log(selected)
let html = selected.map((r,i)=>{
return(
<RenderPanel key={i} panelType={PanelType.large} openState={this.state.openState[i]} title={r.domain+'.TheCommonVein.net'} preview={(openIt)=>(
<div className="result" onClick={openIt} style={{ boxShadow: theme.effects.elevation8}}>
<img src={r.url} />
</div>
)} content={(closeIt)=>(
<div className="panel_wrapper">
<div className="panel_content">{r.content}</div>
{this.RenderPostLink(r.domain,r.parent)}
<div onClick={()=>{
closeIt();
let openState = this.state.openState.map(()=>false)
let index = i+1
openState[index] = true;
this.setState({openState:openState},()=>console.log(this.state.openState[i+1]))
}
}>Next</div>
<img src={r.url} />
</div>
)}/>
)
})
return(
<div className="results_wrapper">
{html}
</div>
)
}
https://codesandbox.io/s/ecstatic-bas-1v3p9?file=/src/Search.tsx
To test, just hit enter at the search box. Then click on 1 of 3 of the results. When you click 'Next', it should close the pane, and open the next one. That is what I'm trying to accomplish here.
#Spitz was on the right path with his answer, though didn't follow through to the full solution.
The issue you are having is that the panel's useBoolean doesn't update it's state based on the openState value passed down.
If you add the following code to panel.tsx, then everything will work as you described:
React.useEffect(()=>{
if(openState){
openPanel()
}else{
dismissPanel();
}
},[openState, openPanel,dismissPanel])
What this is doing is setting up an effect to synchronize the isOpen state in the RenderPanel with the openState that's passed as a prop to the RenderPanel. That way while the panel controls itself for the most part, if the parent changes the openState, it'll update.
Working sandbox
I believe it's because you set openState in your map function, after it has already run. I understand you think the function should rerender and then the loop will run once more, but I think you'll need to set openState in a function outside of render.
The problem is that even though you can access this.state from the component, which is a member of a class component, there's nothing that would make the component re-render. Making components inside other components is an anti-pattern and produces unexpected effects - as you've seen.
The solution here is to either move RenderImages into a separate component altogether and pass required data via props or context, or turn it into a normal function and call it as a function in the parent component's render().
The latter would mean instead of <RenderImages/>, you'd do this.RenderImages(). And also since it's not a component anymore but just a function that returns JSX, I'd probably rename it to renderImages.
I tire to look at it again and again, but couldn't wrap my head around why it wasn't working with any clean approach.
That being said, I was able to make it work with a "hack", that is to explicitly call openIt method for selectedImage after rendering is completed.
RenderImages = (): React.ReactElement => {
let selected = this.state.results.filter((x) =>
this.state.selectedGroups.includes(x.domain)
);
return (
<div className="results_wrapper">
{selected.map((r, i) => {
let openState = this.state.selectedImage === i ? true : false;
return (
<RenderPanel
key={i}
panelType={PanelType.medium}
openState={openState}
title={r.domain + ".TheCommonVein.net"}
preview={(openIt) => {
/* This is where I am making explicit call */
if (openState) {
setTimeout(() => openIt());
}
/* changes end */
return (
<div
className="result"
onClick={openIt}
style={{ boxShadow: theme.effects.elevation8 }}
>
<img src={r.url} />
</div>
);
}}
content={(closeIt) => (
<div className="panel_wrapper">
<div className="panel_content">{r.content}</div>
{this.RenderPostLink(r.domain, r.parent)}
<div
onClick={() => {
closeIt();
this.setState({
selectedImage: i + 1
});
}}
>
[Next>>]
</div>
<img src={r.url} />
</div>
)}
/>
);
})}
</div>
);
};
take a look at this codesandbox.

Switch between divs when clicking prev/next buttons using React Hooks

First of all, I am really new to React, so there is a lot of stuff I have no idea about. But trying to learn I've come across Hooks, which seems really nice for a noob like me. However, my next "project" I am not quite sure about.
Basically I have 4 divs of the same size, but with different content. What I would like to do is to start at the first div (rest not visible), and then in that container I have a "Next" button, if I click that it changes/switches to the second div, which then has a "Prev" and "Next" button, and so on.
I have no idea if that is even possible with Hooks using useState or something like that.
EDIT with example:
So hopefully this illustrates my idea. And this was my initial idea. I think I may be stuck at the return where I am not sure how to actually insert that into the HTML, or what you call it in React-lingo.
const App = ( ) => {
const initialCount = 0
const [count, setCount] = useState(initialCount)
if (count = 0) {
<div className="box">
<p>DIV 1</p>
<div className="btn" onClick={ () => setCount(count + 1) } >Next</div>
</div>
} else if (count = 1) {
<div className="box">
<p>DIV 2</p>
<div className="btn" onClick={ () => setCount(count - 1) } >Prev</div>
<div className="btn" onClick={ () => setCount(count + 1) } >Next</div>
</div>
}
return (
<div>
{one of the if-statement elements}
</div>
);
};
export default memo(App);
In order to understand this you need to understand how react works, in general to draw/insert HTML elements you must either return it from your function component, or use render class method in your Class component, putting HTML somewhere else will not render them to the page
As for how to achieve this, there are many ways, the simplest for me would be define a set of components you want to render in an array then render them according to the current count number
const App = () => {
const [count, setCount] = React.useState(0);
const components = [
<div>1</div>,
<div>2</div>,
<div>3</div>,
<div>4</div>
]
return <div>
{
// render component from our components array
components[count]
}
{/* show previous button if we are not on first element */}
{count > 0 && <button onClick={() => setCount(count - 1)}>prev</button>}
{/* hide next button if we are at the last element */}
{count < components.length - 1 && <button onClick={() => setCount(count + 1)}>next</button>}
</div>
}

React Countup trigger start when state is true

I would like to start the Counter when the related state value is true.
According to the document, the counter can be triggered with a button for example if the button is inside the Component. I would like to find a way how to start is from outside of the Component. I can't figure it out how to trigger the start.
<CountUp start={0} end={100}>
{({ countUpRef, start }) => (
<div>
<span ref={countUpRef} />
<button onClick={start}>Start</button>
</div>
)}
</CountUp>
Reference: https://www.npmjs.com/package/react-countup
I think this is said in the documentation
<CountUp start={0} end={100} delay={0}>
{({ countUpRef }) => (
<div>
<span ref={countUpRef} />
</div>
)}
</CountUp>
https://www.npmjs.com/package/react-countup#autostart-with-render-prop
EDIT: You can use the useCountUp hook
const { countUp, start, pauseResume, reset, update } = useCountUp(...)
Then you can get the start function and trigger it from outside. Below a simplified example from the doc.
import { useCountUp } from 'react-countup';
const ExternalComponent = () => {
const { countUp, start } = useCountUp({
start: 0,
end: 1234567,
});
useEffect(()=> start(), [])
return (
<div>
<div>{countUp}</div>
</div>
);
};
Here I trigger after the first render but you can easily put other condition on it.
if you pass redraw={true} as a prop to CountUp component ,then it will rerendered after any state change (or after any render)

Resources