react hooks, cant updating state via props - reactjs

trying to update state variable('visible') via internal function(setVisible) in component. I checked the tutorıal and did same but its not updating after initialization of state.
Sandobx link here.
props.visible is true when user click ShowModal button. but value of visible in function component is still false. (I have checked the content on debugger)
code:
import Modal from '../Helpers/AppModal'
class Streams extends Component {
constructor(props) {
super(props)
this.state = { showModal: false }
}
componentDidMount() {
this.props.getStreams()
}
showDeleteModal = (isShow) =>
{
this.setState({ showModal: isShow });
}
onClickBackdrop = () => {this.setState({ showModal: false });}
render() {
return (
<div>
<button onClick={()=> this.showDeleteModal(true)} className="btn btn-danger">Delete</button>
<Modal visible={this.state.showModal} onClickBackdrop={this.onClickBackdrop} />
</div>
)
}
}
AppModal.js:
const AppModal = (props) => {
const [visible, setVisible] = useState(props.visible)
useEffect(() =>{
setVisible(props.visible)
},[props.visible])
debugger
return (
<Modal visible={visible} fade={true} onClickBackdrop={props.onClickBackdrop}>
<div className="modal-header">
<h5 className="modal-title">{props.title}</h5>
</div>
<div className="modal-body">
{props.body}
</div>
<div className="modal-footer">
<React.Fragment>
<button type="button" className="btn btn-default" onClick={()=>setVisible(false)}>
Close
</button>
</React.Fragment>
</div>
</Modal>
)
}

The argument passed to useStateis just the initial state. Pass a prop to it doesn't mean that the state will be synchronized with props. You can setup an effect to mirror those changes into your local state.
Currently your Modal only see visible from the local state, changing the props value won't cause Modal to change
//Inside child
useEffect(() =>{
setVisible(props.visible)
},[props])
Why should I use props instead of props.visible there?
The dependencies array exists to keep synchronicity, you're telling react:
"Hey, everytime one of those values changes re run this effect."
The problem is that React performs a shallow comparison (Object.is) between old and new props, uppon each render a new props object is generated which is what is triggering your effect in the first place.
React doesn't know how to "react" to nested changes. What is really changing here is props, react doesn't know (and doesn't care) about props.visible, passing it as a dependency is the same as passing []
Actually passing props as dependency is useless, since props changes every render you can omit the dependencies array, which will trigger the effect on each render
useEffect(() => {
setVisible(props.visible)
})

visible is a boolean.
Try changing the way you call setVisible like so:
setVisible(false)
instead of
setVisible({visible:false})

If this is a toggle switch then you should do this:
onClick={() => setVisible(!visible)}
Then it'll toggle on/off correctly.
You might want to set the initial value more explicitly though:
const [visible, setVisible] = useState(false);

Related

React: Setting and updating based on props

Currently I am facing the problem that I want to change a state of a child component in React as soon as a prop is initialized or changed with a certain value. If I solve this with a simple if-query, then of course I get an infinite loop, since the components are then rendered over and over again.
Component (parent):
function App() {
const [activeSlide, setActiveSlide] = useState(0);
function changeSlide(index) {
setActiveSlide(index);
}
return (
<div className="app">
<div className="app__nav">
<Button icon="FiSun" handler={changeSlide} active={activeSlide} index="0" />
<Button icon="FiSettings" handler={changeSlide} active={activeSlide} index="1" />
</div>
</div>
);
}
Component (child):
function Button(props) {
const Icon = Icons[props.icon];
const [activeClass, setActiveClass] = useState("");
// This attempts an endless loop
if(props.active == props.index) {
setActiveClass("active");
}
function toggleView(e) {
e.preventDefault();
props.handler(props.index);
}
return(
<button className={activeClass} data-index={props.index} onClick={toggleView}>
<Icon />
</button>
)
}
Is there a sensible and simple approach here? My idea would be to write the if-query into the return() and thus generate two different outputs, even though I would actually like to avoid this
The React docs have a nice checklist here used to determine if something does or does not belong in state. Here is the list:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
The active class does not meet that criteria and should instead be computed when needed instead of put in state.
return(
<button className={props.active == props.index ? 'active' : ''} data-index={props.index} onClick={toggleView}>
<Icon />
</button>
)
This is a great use of useEffect.
instead of the if statement you can replace that with;
const {active, index} = props
useEffect(_ => {
if(active == index) {
setActiveClass("active");
}
}, [active])
The last item in the function is a dependency, so useEffect will only run if the active prop has changed.
React automatically re-renders a component when there is a change in the state or props. If you're just using activeClass to manage the className, you can move the condition in the className as like this and get rid of the state.
<button className={props.active === props.index ? 'active' : ''} data-index={props.index} onClick={toggleView}>
<Icon />
</button>
however, if you still want to use state in the child component, you can use the useEffect hook to to update the state in the child component.
Try to use the hook useEffect to prevent the infinite loop. (https://fr.reactjs.org/docs/hooks-effect.html)
Or useCallback hook. (https://fr.reactjs.org/docs/hooks-reference.html#usecallback)
Try this and tell me if it's right for you :
function App() {
const [activeSlide, setActiveSlide] = useState(0);
const changeSlide = useCallback(() => {
setActiveSlide(index);
}, [index]);
return (
<div className="app">
<div className="app__nav">
<Button icon="FiSun" handler={changeSlide} active={activeSlide} index="0" />
<Button icon="FiSettings" handler={changeSlide} active={activeSlide} index="1" />
</div>
</div>
);
}

How to pass state from parent to child in react?

How do I pass a state attribute from parent to child? In the following implementation, the Dropdown component has a state "isActive" and I want to access it in the Button component to attach propper styling to it. The Dropdown has to generic as it is supposed to take different sorts of buttons.
<Dropdown items="...">
<Button active ="false" />
</Dropdown>
Dropdwon.js
...
constructor(props){
super(props)
this.state = {
isActive: true,
}
}
render (){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children} /* want to set active prop for the child button here */
</div>
);
}
...
You have two possibilities:
Lift your Dropdown state and keep it in its parent component;
Use useContext hook;
The first approach would be better, but it may not be good for your application (I cannot know that). Let me make an example for both cases.
This is an example where I've lifted the isActive state to the parent component.
const ParentComponent = () => {
const [isActive, setIsActive] = useState(false);
handleIsActiveChange = (newValue) => {
setIsActive(newValue);
}
<Dropdown isActive={isActive} setIsActive={handleIsActiveChange}>
<Button isActive={isActive} />
</Dropdown>
}
const Dropdown = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
// You can use `props.handleIsActiveChange` to update the `isActive` state.
}
const Button = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
}
Instead, this exploits the useContext API:
const dropdownContext = React.createContext(null);
const Dropdown = props => {
const [isActive, setIsActive] = useState(false);
return (
<dropdownContext.Provider value={{ isActive }}>
{props.children}
</dropdownContext.Provider>
);
}
const Button = props => {
const dropdownCtx = React.useContext(dropdownContext);
// You can use `dropdownCtx.isActive` to know whether the dropdown is active or not.
}
Aside from the answer I linked, there might be another way of achieving this that I didn't see mentioned.
You can send a function as a children element of your dropdown which will take isActive as a variable :
<Dropdown items="...">
{isActive => <Button active={isActive} />}
</Dropdown>
Then, is the render function, simply call the function and send your state value as a parameter :
render(){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children(this.state.isActive)}
</div>
);
}
<Dropdown >
<Button isActive={this.state.isActive} />
</Dropdown>
In your button get it with this.props.isActive

setState second argument callback function alternative in state hooks

I made a code sandbox example for my problem: https://codesandbox.io/s/react-form-submit-problem-qn0de. Please try to click the "+"/"-" button on both Function Example and Class Example and you'll see the difference. On the Function Example, we always get the previous value while submitting.
I'll explain details about this example below.
We have a react component like this
function Counter(props) {
return (
<>
<button type="button" onClick={() => props.onChange(props.value - 1)}>
-
</button>
{props.value}
<button type="button" onClick={() => props.onChange(props.value + 1)}>
+
</button>
<input type="hidden" name={props.name} value={props.value} />
</>
);
}
It contains two buttons and a numeric value. User can press the '+' and '-' button to change the number. It also renders an input element so we can use it in a <form>.
This is how we use it
class ClassExample extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 1,
lastSubmittedQueryString: ""
};
this.formEl = React.createRef();
}
handleSumit = () => {
if (this.formEl.current) {
const formData = new FormData(this.formEl.current);
const search = new URLSearchParams(formData);
const queryString = search.toString();
this.setState({
lastSubmittedQueryString: queryString
});
}
};
render() {
return (
<div className="App">
<h1>Class Example</h1>
<form
onSubmit={event => {
event.preventDefault();
this.handleSumit();
}}
ref={ref => {
this.formEl.current = ref;
}}
>
<Counter
name="test"
value={this.state.value}
onChange={newValue => {
this.setState({ value: newValue }, () => {
this.handleSumit();
});
}}
/>
<button type="submit">submit</button>
<br />
lastSubmittedQueryString: {this.state.lastSubmittedQueryString}
</form>
</div>
);
}
}
We render our <Counter> component in a <form>, and want to submit this form right after we change the value of <Counter>. However, on the onChange event, if we just do
onChange={newValue => {
this.setState({ value: newValue });
this.handleSubmit();
}}
then we won't get the updated value, probably because React doesn't run setState synchronously. So instead we put this.handleSubmit() in the second argument callback of setState to make sure it is executed after the state has been updated.
But in the Function Example, as far as I know in state hooks there's nothing like the second argument callback function of setState. So we cannot achieve the same goal. We found out two workarounds but we are not satisfied with either of them.
Workaround 1
We tried to use the effect hook to listen when the value has been changed, we submit our form.
React.useEffect(() => {
handleSubmit();
}, [value])
But sometimes we need to just change the value without submitting the form, we want to invoke the submit event only when we change the value by clicking the button, so we think it should be put in the button's onChange event.
Workaround 2
onChange={newValue => {
setValue(newValue);
setTimeout(() => {
handleSubmit();
})
}}
This works fine. We can always get the updated value. But the problem is we don't understand how and why it works, and we never see people write code in this way. We are afraid if the code would be broken with the future React updates.
Sorry for the looooooooong post and thanks for reading my story. Here are my questions:
How about Workaround 1 and 2? Is there any 'best solution' for the Function Example?
Is there anything we are doing wrong? For example maybe we shouldn't use the hidden input for form submitting at all?
Any idea will be appreciated :)
Can you call this.handleSubmit() in componentDidUpdate()?
Since your counter is binded to the value state, it should re-render if there's a state change.
componentDidUpdate(prevProps, prevState) {
if (this.state.value !== prevState.value) {
this.handleSubmit();
}
}
This ensure the submit is triggered only when the value state change (after setState is done)
It's been a while. After reading React 18's update detail, I realize the difference is caused by React automatically batching state updates in function components, and the "official" way to get rid of it is to use ReactDOM.flushSync().
import { flushSync } from "react-dom";
onChange={newValue => {
flushSync(() => {
setValue(newValue)
});
flushSync(() => {
handleSubmit();
});
}}

How to pass a callback to a child without triggering it

I have a React app with modal, that pop-ups with rules of the game when one clicks a button. What I want to do is make it so when I click anywhere outside this pop up window it will close. i have three files. app.js, dialog.js, and outsidealerter.js . In my main app.js when I click a button it sets a state to visible, so my element takes it and renders based upon it. my outsideralerer.js basicly detects if there is a click outside anything wrapped with specific tags. Now the problem comes that i have a method that changes the state of visibility in app.js, so in order for outsderalerter.js to use it, I pass it to it so it can have access to my main state and change it so that when a click is outside the zone the pop up window disappears. Kind of works except it closes it down even if i click within a pop up window, because when i pass the value to outsidealerter it considers the whole body as a no click zone. My question is how can I prevent it from triggering and just pass it a value, or is it possible to change the state value of app.js from outsidealerter.js
App.js
updateState() {
this.setState({ isOpen: false });
}
<div id='rule-button'>
<button onClick={(e)=>this.setState({isOpen : true})} id="modalBtn" class="button">Open Rules</button>
</div>
<OutsideAlerter updateParent={ this.updateState.bind(this)}/>
<Dialog isOpen={this.state.isOpen} onClose={(e)=>this.setState({isOpen : false})}>
</Dialog>
outsidealerter.js
handleClickOutside(event) {
if (this.wrapperRef && !this.wrapperRef.contains(event.target)) {
//alert('You clicked outside of me!');
{this.props.updateParent()};
}
}
I think it will be simpler to have the modal take the full space of the window height and width and just make it invisible except for the content of what you want to show.
We can wrap the modal with onClick={hideModal} and wrap the inner content with onClick={e => e.stopPropagation()} which will prevent our wrapper for triggering the hideModal handler.
class ModalWrapper extends React.Component {
state = { isModalOpen: true };
toggleModal = () => {
this.setState(({ isModalOpen }) => ({
isModalOpen: !isModalOpen
}));
};
render() {
const { isModalOpen } = this.state;
return (
<div className="App">
<button onClick={this.toggleModal}>Open Modal</button>
{isModalOpen && <Modal hideModal={this.toggleModal} />}
</div>
);
}
}
function Modal({ hideModal }) {
return (
<div onClick={hideModal} className="modal">
<div onClick={e => e.stopPropagation()} className="modal__content">
Modal content
</div>
</div>
);
}
Working example

why <button onClick={this.props.onClick}> is different from <button onClick={(e) => {this.props.onClick(e)}}> here?

I found the following code in this post.(sandbox) I am not sure why these buttons behave differently.
One possible explanation is: Because Button's render method is not invoked for updating, the first button's click handler remains the same. However, this.props of the Button instance has changed to include the new onClick prop. In other words,if the props of a component element is changed, nextProps will finally become this.props even when shouldComponentUpdate returns false.
const submit = val => alert(val);
class App extends React.Component {
state = { val: "one" }
componentDidMount() {
this.setState({ val: "two" })
}
render() {
return <Form value={this.state.val} />
}
}
const Form = props => (
<Button
onClick={() => {
submit(props.value)
}}
/>
)
class Button extends React.Component {
shouldComponentUpdate() {
// lets pretend like we compared everything but functions
return false
}
handleClick = () => this.props.onClick()
render() {
return (
<div>
<button onClick={this.props.onClick}>This one is stale</button>
<button onClick={() => this.props.onClick()}>This one works</button>
<button onClick={this.handleClick}>This one works too</button>
</div>
)
}
}
Your explanation is correct on that when ShouldComponentUpdate() returns false, the Button component does not re-render on props change.
In the first <button> element, the onClick event is this.props.OnClick which is actually the function () => { submit("one"); } at the moment of the initial rendering. "one" is here because at this particular time point in the React lifecycle, Form's props.value evaluates to "one". Note that the function gets executed has nothing to do with Button's props.
In the second <button> element, however, the onClick event is () => this.props.onClick(). Regardless whether the Button component gets re-rendered, it is always the this.props.onClick() that get executed, where this.props changes when the parent components get state/props updates.
In the case of ShouldComponentUpdate() returning true, Button component will re-render on props updates. The first button's onClick event becomes a new function () => { submit("two"); } on the re-render triggered by the App component's state change. The two button elements' onClick event functions are never the same function, although they appear to produce the same result.
<button onClick={this.props.onClick}> Will call your onClick function and send an event object as its first parameter. This is the equivalent of doing this :
<button onClick={event => { this.props.onClick(event) }}>
<button onClick={() => this.props.onClick()}> Will set what your onClick function returns as the onClick function... It is the short syntax of the following code :
<button onClick={() => { return this.props.onClick() }}>
What you may be looking for is this : <button onClick={() => { this.props.onClick() }}>
In this case, clicking will simply call your function without sending any arguments
I suggest reading this documentation to know more about arrow/anonymous functions and their syntax

Resources