I want to change State with child elements in React. However, when I click once, it is not immediately updated. Click twice, it shows the correct answer.
How to update async?
export default function Example() {
const onClick = async () => {
console.log('a', test)
// should be 'b', but console log 'a'
}
const [test, setTest] = useState('a')
return (
<ClickExample setTest={setTest} onClick={onClick} />
)
}
export default function ClickExample() {
const next = useCallback(
(alphabet: string) => {
setTest(alphabet)
onClick()
},
[onClick, setTest],
)
return <SelectButton onClick={() => next('b')} />
}
You can receive the value to be updated as an argument from the onClick callback. It'll be something like this:
export default function Example() {
const [test, setTest] = useState('a')
const handleClick = (newValue) => {
setTest(newValue);
}
return (
<ClickExample onClick={handleClick} />
)
}
export default function ClickExample({ onClick }) {
return <SelectButton onClick={() => onClick('b')} />
}
NOTE: You should avoid using useCallback() when it is not necessary. Read more over the web but this article from Kent C. Dodds is a good start. As a rule of thumb: Never use useCallback()/useMemo() unless you REALLY want to improve performance after needing that improvement.
In the first render, the value of test is equal to'a'. So when the console.log is executed, it has already captured 'a' as the value of test state. (See closures and stale closures).
One way to fix this would be to create a handleClick function in the parent component which receives the new value of test as its input and set the state and log the new value(which will be updated in the next render) using its argument.
// ClickExample
const handleClick = (alphabet) => {
setTest(alphabet);
console.log('a', alphabet);
};
codesandbox
Related
I made a context to share the value of the variable "clicked" throughout my nextjs pages, it seems to give no errors but as you can see the variable's value remains FALSE even after the click event. It does not change to TRUE. This is my first time working with context, what am I doing wrong?
I'm using typescript
PS: After the onClick event the log's number shoots up by 3 or 4, is it being executed more than once, but how?
controlsContext.tsx
import { createContext, FC, useState } from "react";
export interface MyContext {
clicked: boolean;
changeClicked?: () => void;
}
const defaultState = {
clicked: false,
}
const ControlContext = createContext<MyContext>(defaultState);
export const ControlProvider: FC = ({ children }) => {
const [clicked, setClicked] = useState(defaultState.clicked);
const changeClicked = () => setClicked(!clicked);
return (
<ControlContext.Provider
value={{
clicked,
changeClicked,
}}
>
{children}
</ControlContext.Provider>
);
};
export default ControlContext;
Model.tsx
import ControlContext from "../contexts/controlsContext";
export default function Model (props:any) {
const group = useRef<THREE.Mesh>(null!)
const {clicked, changeClicked } = useContext(ControlContext);
const handleClick = (e: MouseEvent) => {
//e.preventDefault();
changeClicked();
console.log(clicked);
}
useEffect(() => {
console.log(clicked);
}, [clicked]);
useFrame((state, delta) => (group.current.rotation.y += 0.01));
const model = useGLTF("/scene.gltf ");
return (
<>
<TransformControls enabled={clicked}>
<mesh
ref={group}
{...props}
scale={clicked ? 0.5 : 0.2}
onClick={handleClick}
>
<primitive object={model.scene}/>
</mesh>
</TransformControls>
</>
)
}
_app.tsx
import {ControlProvider} from '../contexts/controlsContext';
function MyApp({ Component, pageProps }: AppProps) {
return (
<ControlProvider>
<Component {...pageProps}
/>
</ControlProvider>
)
}
export default MyApp
Issues
You are not actually invoking the changeClicked callback.
React state updates are asynchronously processed, so you can't log the state being updated in the same callback scope as the enqueued update, it will only ever log the state value from the current render cycle, not what it will be in a subsequent render cycle.
You've listed the changeClicked callback as optional, so Typescript will warn you if you don't use a null-check before calling changeClicked.
Solution
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked && changeClicked();
}}
>
...
</mesh>
...
Or declare the changeClicked as required in call normally. You are already providing changeClicked as part of the default context value, and you don't conditionally include in in the provider, so there's no need for it to be optional.
export interface MyContext {
clicked: boolean,
changeClicked: () => void
}
...
const { clicked, changeClicked } = useContext(ControlContext);
...
<mesh
...
onClick={(event) => {
changeClicked();
}}
>
...
</mesh>
...
Use an useEffect hook in to log any state updates.
const { clicked, changeClicked } = useContext(ControlContext);
useEffect(() => {
console.log(clicked);
}, [clicked]);
Update
After working with you and your sandbox it needed a few tweaks.
Wrapping the index.tsx JSX code with the ControlProvider provider component so there was a valid context value being provided to the app. The UI here had to be refactored into a React component so it could itself also consume the context value.
It seems there was some issue with the HTML canvas element, or the mesh element that was preventing the Modal component from maintaining a "solid" connection with the React context. It wasn't overtly clear what the issue was here, but passing the context values directly to the Modal component as props resolved the issue with the changeClicked callback becoming undefined.
A few things -
setClicked((prev) => !prev);
instead of
setClicked(!clicked);
As it ensures it's not using stale state. Then you are also doing -
changeClicked
But it should be -
changeClicked();
Lastly, you cannot console.log(clicked) straight after calling the set state function, it will be updated in the next render
This one has turned out to be a head scratcher for a while now...
I have a react component that updates state on a click event. The state is a simple boolean so I'm using a ternary operator to toggle state.
This works however as soon as I add a second function to the click event state no longer updates. Any ideas why this is happening and what I'm doing wrong?
Working code...
export default function Activity(props) {
const [selected, setSelected] = useState(false);
const selectActivity = () => {
selected ? setSelected(false) : setSelected(true);
return null;
};
const clickHandler = (e) => {
e.preventDefault();
selectActivity();
};
return (
<div
onClick={(e) => clickHandler(e)}
className={`visit card unassigned ${selected ? 'selected' : null}`}
>
//... some content here
</div>
);
}
State not updating...
export default function Activity(props) {
const [selected, setSelected] = useState(false);
const selectActivity = () => {
selected ? setSelected(false) : setSelected(true);
return null;
};
const clickHandler = (e) => {
e.preventDefault();
selectActivity();
props.collectVisitsForShift(
props.day,
props.startTime,
props.endTime,
props.customer
);
};
return (
<div
onClick={(e) => clickHandler(e)}
className={`visit card unassigned ${selected ? 'selected' : null}`}
>
//... some content here
</div>
);
}
I went for a walk and figured this one out. I'm changing state in the parent component from the same onClick event, which means the child component re-renders and gets its default state of 'false'.
I removed the state change from the parent and it works.
Thanks to Andrei for pointing me towards useCallback!
I loaded your code in a CodeSandbox environment and experienced no problems with the state getting updated. But I don't have access to your collectVisitsForShift function, so I couldn't fully reproduce your code.
However, the way you're toggling the state variable doesn't respect the official guidelines, specifically:
If the next state depends on the current state, we recommend using the updater function form
Here's what I ended up with in the function body (before returning JSX):
const [selected, setSelected] = useState(false);
// - we make use of useCallback so toggleSelected
// doesn't get re-defined on every re-render.
// - setSelected receives a function that negates the previous value
const toggleSelected = useCallback(() => setSelected(prev => !prev), []);
const clickHandler = (e) => {
e.preventDefault();
toggleSelected();
props.collectVisitsForShift(
props.day,
props.startTime,
props.endTime,
props.customer
);
};
The documentation for useCallback.
My goal is very simple. I am just looking to set my react context from within a reusable function-only (stateless?) react component.
When this reusable function gets called it will set the context (state inside) to values i provide. The problem is of course you can't import react inside a function-only component and hence I cannot set the context throughout my app.
There's nothing really to show its a simple problem.
But just in case:
<button onCLick={() => PlaySong()}></button>
export function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //cannot call useContext in this component
}
If i use a regular react component, i cannot call this function onClick:
export default function PlaySong() {
const {currentSong, setCurrentSong} = useContext(StoreContext) //fine
}
But:
<button onCLick={() => <PlaySong />}></button> //not an executable function
One solution: I know i can easily solve this problem by simply creating a Playbtn component and place that in every song so it plays the song. The problem with this approach is that i am using a react-player library so i cannot place a Playbtn component in there...
You're so close! You just need to define the callback inside the function component.
export const PlaySongButton = ({...props}) => {
const {setCurrentSong} = useContext(StoreContext);
const playSong = () => {
setCurrentSong("some song");
}
return (
<button
{...props}
onClick={() => playSong()}
/>
)
}
If you want greater re-usability, you can create custom hooks to consume your context. Of course where you use these still has to follow the rules of hooks.
export const useSetCurrentSong = (song) => {
const {setCurrentSong} = useContext(StoreContext);
setCurrentSong(song);
}
It is possible to trigger a hook function by rendering a component, but you cannot call a component like you are trying to do.
const PlaySong = () => {
const {setCurrentSong} = useContext(StoreContext);
useEffect( () => {
setCurrentSong("some song");
}, []
}
return null;
}
const MyComponent = () => {
const [shouldPlay, setShouldPlay] = useState(false);
return (
<>
<button onClick={() => setShouldPlay(true)}>Play</button>
{shouldPlay && <PlaySong />}
</>
)
}
I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I'm trying to understand if passing the setter from useState is an issue or not.
In this example, my child component receives both the state and the setter to change it.
export const Search = () => {
const [keywords, setKeywords] = useState('');
return (
<Fragment>
<KeywordFilter
keywords={keywords}
setKeywords={setKeywords}
/>
</Fragment>
);
};
then on the child I have something like:
export const KeywordFilter: ({ keywords, setKeywords }) => {
const handleSearch = (newKeywords) => {
setKeywords(newKeywords)
};
return (
<div>
<span>{keywords}</span>
<input value={keywords} onChange={handleSearch} />
</div>
);
};
My question is, should I have a callback function on the parent to setKeywords or is it ok to pass setKeywords and call it from the child?
There's no need to create an addition function just to forward values to setKeywords, unless you want to do something with those values before hand. For example, maybe you're paranoid that the child components might send you bad data, you could do:
const [keywords, setKeywords] = useState('');
const gatedSetKeywords = useCallback((value) => {
if (typeof value !== 'string') {
console.error('Alex, you wrote another bug!');
return;
}
setKeywords(value);
}, []);
// ...
<KeywordFilter
keywords={keywords}
setKeywords={gatedSetKeywords}
/>
But most of the time you won't need to do anything like that, so passing setKeywords itself is fine.
why not?
A setter of state is just a function value from prop's view. And the call time can be anytime as long as the relative component is live.