setState second argument callback function alternative in state hooks - reactjs

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();
});
}}

Related

React Hooks: State is one step behind

I have a modal that opens on click event. It gets the event data (date and time string) and displays it inside the modal. The problem is that my state is always one step behind. The content being displayed by the modal is always in the previous state. When I close the modal and reopen it, It does render the content but It is the previous event. How to solve this issue? am I using the react hooks wrong? I cannot post the whole code as It's quite large so I am just posting the parts where I am using hooks, please let me know if you need more information I will provide it.
const [isModalVisible, setIsModalVisible] = useState(false)
const [modelContent, setModalContent] = useState('')
const [modalTitle, setModalTitle] = useState('')
const handleEventAdd = (e) => {
form.resetFields()
setModalTitle('Add an Event')
const event = _.cloneDeep(e)
form.setFieldsValue({
datePicker: dateString,
timeRangePicker: [dateString, eventEnd.date],
})
}
const content = () => (
<div>
<Form
form={form}
initialValues={{
datePicker: moment(e.start),
timeRangePicker: [moment(e.start), moment(e.end)],
}}
>
<Form.Item name="datePicker" label="Current Event Date">
<DatePicker
className="w-100"
format={preferredDateFormat}
onChange={setValueDate}
/>
</Form.Item>
<Form.Item>
<Button
onClick={() => {
form
.validateFields()
.then((values) => {
form.resetFields()
})
}}
>
Add Event
</Button>
</Form.Item>
</Form>
</div>
)
setModalContent(content)
setIsModalVisible(true)
}
const handleModalClose = () => {
setIsModalVisible(false)
form.resetFields()
setModalContent('')
}
UPDATE::
my return function consists,
<Modal visible={isModalVisible} footer={null} onCancel={handleModalClose} title={modalTitle}>
{modelContent}
</Modal>
This problem is due to the asynchronous behavior of setState. This method generates a "pending state transition" (see this for more information). In order to solve this issue, you have two options:
use ref instead of state. In contrast to state, ref has synchronous behavior. I have developed an example for myself that you can check out here. you can see that there is an asynchronous behavior in the state (dialog box vs. in webpage).
You can explicitly pass the new state value as part of the event being raised.

react hooks, cant updating state via props

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);

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

Render a component on a button click

I'm building a trivia game app with React-Redux and need to be able to render a TriviaCard component which starts a new game if a user decides to play again. Here is my button...
So, I have two buttons and need to be able to do something like this in my handleSubmit function...
<button onClick={this.handleSubmit} name="yes">Yes</button>
<button onClick={this.handleSubmit} name="no">No</button>
handleSubmit = (event) => {
const answer = event.target.name
if (answer === "yes") {
return <TriviaCard />
} else {
//Do nothing...
}
}
You can save the answer in the component's state and then use conditional render for TriviaCard component. Here is an example:
class Game extends React.Component {
state = { answer: '' }
handleSubmit = (event) => {
this.setState({ answer: event.target.name });
}
render() {
<div>
{this.state.answer === "yes" && <TriviaCard />}
<button onClick={this.handleSubmit} name="yes">Yes</button>
<button onClick={this.handleSubmit} name="no">No</button>
</div>
}
}
When you click on one of the buttons, the state of the component will change triggering a rerender of the component. Then in render the state is used to determine whether to render TriviaCard or not based on the value of answer.

how react programmatically focus input

I'm trying to implement a very simple use case, a UI feature, where:
There is a label with some content in it
If clicked, a text input replaces it with the content of label available
User can edit the content
When enter is pressed, the input hides and label is back with updated content
I could get finally all correct (in fact with a MongoBD backend, redux, etc.), and the only thing I couldn't ever do (paying a complete day in googling and reading S.O.F similar posts) was this:
When my text input appears, I can't transfer focus to it! First I tired this way:
<div className={((this.state.toggleWordEdit) ? '' : 'hidden')}>
<input id={this.props.word._id} className="form-control"
ref="updateTheWord"
defaultValue={this.state.word}
onChange={this.handleChange}
onKeyPress={this.handleSubmit}
autoFocus={this.state.toggleWordEdit}/></div>
<div className={((this.state.toggleWordEdit) ? 'hidden' : '')}>
<h3 onClick={this.updateWord}>
{this.state.word}</h3>
</div>
but autoFocus sure didn't work (I "guess" because the form is rendered, but in hidden state, making autoFocus useless).
Next I tried in my this.updateWor, many of suggestions I found on google and S.O.F.:
this.refs.updateTheWord.focus();
which together with similar suggestions all didn't work. Also I tried to fool React just to see if at all I can do something! I used real DOM:
const x = document.getElementById(this.props.word._id);
x.focus();
and it didn't work either. One thing I even could not understand to put into word is a suggestion like this:
having ref as a method (I "guess")
I didn't even try it because I have multiples of these components and I need ref to further get value of, per component, and I couldn't imagine if my ref is not named, how I could get the value of!
So could you please give an idea, helping me to understand that in case I'm not using a Form (because I need a single input box replacing a label) how I could set its focus when it's CSS (Bootstrap) class is losing 'hidden' please?
The way you have used refs is not the most preferred way or else its not the best practice anymore . try some thing like this
class MyClass extends React.Component {
constructor(props) {
super(props);
this.focus = this.focus.bind(this);
}
focus() {
this.textInput.current.focus();
}
render() {
return (
<div>
<input
type="text"
ref={(input) => { this.textInput = input; }} />
<input
type="button"
value="Set Focus"
onClick={this.focus}
/>
</div>
);
}
}
Update
From React 16.3 upwards you can use the React.createRef() API
class MyClass extends React.Component {
constructor(props) {
super(props);
// create a ref to store the textInput DOM element
this.textInput = React.createRef();
this.focus = this.focus.bind(this);
}
focus() {
// Explicitly focus the text input using the raw DOM API
// Note: we're accessing "current" to get the DOM node
this.textInput.current.focus();
}
render() {
// tell React that we want to associate the <input> ref
// with the `textInput` that we created in the constructor
return (
<div>
<input
type="text"
ref={this.textInput} />
<input
type="button"
value="Set Focus"
onClick={this.focus}
/>
</div>
);
}
}
From React 18.xx upwards you can use the useRef Hook
import React, { useRef } from "react";
export const Form = () => {
const inputRef = useRef(null);
const focus = () => {
inputRef.current.focus();
};
return (
<div>
<input type="text" ref={inputRef} />
<input type="button" value="Set Focus" onClick={focus} />
</div>
);
};
Just add autofocus attribute to the input. (of course in JSX it is autoFocus)
<input autoFocus ...
useFocus hook
// General Focus Hook
const useFocus = (initialFocus = false, id = "") => {
const [focus, setFocus] = useState(initialFocus)
const setFocusWithTrueDefault = (param) => setFocus(isBoolean(param)? param : true)
return ([
setFocusWithTrueDefault, {
autoFocus: focus,
key: `${id}${focus}`,
onFocus: () => setFocus(true),
onBlur: () => setFocus(false),
},
])
}
const FocusDemo = () => {
const [labelStr, setLabelStr] = useState("Your initial Value")
const [setFocus, focusProps] = useFocus(true)
return (
<> {/* React.Fragment */}
<input
onChange={(e)=> setLabelStr(e.target.value)}
value={labelStr}
{...focusProps}
/>
<h3 onClick={setFocus}>{labelStr}</h3>
</>
)
}
For a more complete demo click here.
In addition to the previous answers, I've added setTimeout to make it work
handleClick() {
if (this.searchInput) {
setTimeout(() => {
this.searchInput.focus();
}, 100);
}
}
where searchInput is the jsx ref of the input
<input
type="text"
name="searchText"
ref={(input) => { this.searchInput = input; }}
placeholder="Search" />
and the handleClick() is an onClick handler to any element
#BenCarp's answer in typescript
Pass the inputRef to an input and just call setFocus to set the focus to it.
export const useInputFocus = (): [MutableRefObject<HTMLInputElement | undefined>, () => void] => {
const inputRef = useRef<HTMLInputElement>();
const setFocus = (): void => {
const currentEl = inputRef.current;
if (currentEl) {
currentEl.focus();
}
};
return [inputRef, setFocus];
};
Use componentDidUpdate method to every time update the component
componentDidUpdate(prevProps, prevState) {
this.input.focus();
}
You can use "useRef" hook and make a reference to your input control, then use your reference.current.focus()

Resources