I have a input field that is filtering the elements on of an array.
The search results are always one keystroke behind, I assume because setState doesn't instantly update the view? What's the best way to work around that?
class App extends Component {
constructor() {
super();
this.state = {
images:[],
searchfield: '',
filteredImages:[],
suggestedKeywords:[],
inputValue: ''
}
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value});
this.setState({inputValue: event.target.value});
let filteredImages = this.state.images.filter(image => {
return image.labels.includes(this.state.searchfield.toLowerCase());
});
console.log(event.target.value);
this.setState({filteredImages});
}
}
const SearchBox = ({searchfield, searchChange, inputValue}) => {
return (
<div>
<input
type="search"
value={inputValue}
onChange={searchChange}
placeholder="Search images..."
/>
</div>
);
}
The search results are always one keystroke behind, I assume because setState doesn't instantly update the view? What's the best way to work around that?
That isn't the problem.
Your problem is that you are assuming updates to setState occur instantly.
this.setState({searchfield: event.target.value}); //You update searchfield here
return image.labels.includes(this.state.searchfield.toLowerCase());
//but this.state.searchfield doesn't reflect the update yet!
So instead, simply work off of the updated value rather than the one from the store.
return image.labels.includes(event.target.value.toLowerCase());
setState is asynchronous, so you will be filtering by the old searchfield when you do this.state.searchfield.toLowerCase(). With this in mind you could do something like this instead:
onSearchChange = (event) => {
const { value } = event.target;
const newState = {
searchField: value,
inputValue: value
};
newState.filteredImages = this.state.images.filter(image => {
return image.labels.includes(value.toLowerCase());
});
this.setState(newState);
}
Related
I have in an input field that should use some delay option on typing and making a search request. Also I need Re-run the this search component whenever the search text change in props. But I got an issue with and Search field is hanging after pasting value that could not be found and trying to remove it.
export class TableToolbar extends Component {
state = {
search: this.props.search,
}
static getDerivedStateFromProps(props, state) {
// Re-run the table whenever the search text change.
// we need to store prevSearch to detect changes.
if (props.search !== state.prevSearch) {
return {
search: props.search,
prevSearch: state.search,
}
}
return null
}
captureInput = e => {
if (this.timer) {
clearTimeout(this.timer)
delete this.timer
}
this.input = e.target.value
this.setState({search: this.input})
this.timer = setTimeout(() => {
this.props.handleSearch(this.input)
delete this.input
}, capturedInputTimeout)
}
render() {
<input onChange={this.captureInput} value={this.state.search} />
}
}
Also I found another solution to debounce this handleSearch request with use-debounce https://github.com/xnimorz/use-debounce
But still not quite understand how to rerender component correctly.
I need pass search props from parent in some case when I want to keep search value when move between pages.
Second variant with use-debounce, but still not quite understand how to rerender component when search props updates
const TableToolbar = ({search, handleSearch}) => {
const [searchValue, setSearchValue] = useState(search)
const [debouncedText] = useDebounce(searchValue, 500)
useEffect(() => {
handleSearch(debouncedText)
},
[debouncedText]
)
render() {
<input onChange={e => setSearchValue(e.target.value)} />
}
}
For the issue with hanging I think your captureInput function runs on every re-render. You should can call it like this to avoid that onChange={() => this.captureInput
For rerendering on change you could look into shouldComponentUpdate where you've got acccess to nextProps which you can compare with the current props and return true if different.
I'm running into a recurring issue in my code where I want to grab multiple pieces of data from a component to set as states, and push those into an array which is having its own state updated. The way I am doing it currently isn't working and I think it's because I do not understand the order of the way things happen in js and react.
Here's an example of something I'm doing that doesn't work: jsfiddle here or code below.
import React, {Component} from 'react';
class App extends Component {
constructor(props) {
super(props);
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
}
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
setCategoryStates = (categoryTitle, categorySubtitle) => {
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
render() {
return (
<CategoryComponent
setCategoryStates={this.setCategoryStates}
categoryTitle={'Category Title Text'}
categorySubtitle={'Category Subtitle Text'}
/>
);
}
}
class CategoryComponent extends Component {
render() {
var categoryTitle = this.props.categoryTitle;
var categorySubtitle = this.props.categorySubtitle;
return (
<div onClick={() => (this.props.setCategoryStates(
categoryTitle,
categorySubtitle,
))}
>
<h1>{categoryTitle}</h1>
<h2>{categorySubtitle}</h2>
</div>
);
}
}
I can see in the console that I am grabbing the categoryTitle and categorySubtitle that I want, but they get pushed as null into this.state.categoryArray. Is this a scenario where I need to be using promises? Taking another approach?
This occurs because setState is asynchronous (https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly).
Here's the problem
//State has categoryTitle as null and categorySubtitle as null.
this.state = {
categoryTitle: null,
categorySubtitle: null,
categoryArray: [],
}
//This gets the correct values in the parameters
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
})
this.pushToCategoryArray();
}
//This method is using the state, which as can be seen from the constructor is null and hence you're pushing null into your array.
pushToCategoryArray = () => {
this.state.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
Solution to your problem: pass callback to setState
setCategoryStates = (categoryTitle, categorySubtitle) => {
//This is correct, you're setting state BUT this is not sync
this.setState({
categoryTitle: categoryTitle,
categorySubtitle: categorySubtitle,
}, () => {
/*
Add state to the array
This callback will be called once the async state update has succeeded
So accessing state in this variable will be correct.
*/
this.pushToCategoryArray()
})
}
and change
pushToCategoryArray = () => {
//You don't need state, you can simply make these regular JavaScript variables
this.categoryArray.push({
'categoryTitle': this.state.categoryTitle,
'categorySubtitle': this.state.categorySubtitle,
})
}
I think React doesn't re-render because of the pushToCategoryArray that directly change state. Need to assign new array in this.setState function.
// this.state.categoryArray.push({...})
const prevCategoryArray = this.state.categoryArray
this.setState({
categoryArray: [ newObject, ...prevCategoryArray],
)}
I'm having an issue where I want to save the data from a particular fieldset with the default values on componentDidMount().
The data saving happens in the parent component, after it is sent up from the child component. However, as React's setState() is asynchronous, it is only saving data from one of the fields. I have outlined a skeleton version of my problem below. Any ideas how I can fix this?
// Parent Component
class Form extends Component {
super(props);
this.manageData = this.manageData.bind(this);
this.state = {
formData: {}
}
}
manageData(data) {
var newObj = {
[data.name]: data.value
}
var currentState = this.state.formData;
var newState = Object.assign({}, currentState, newObj);
this.setState({
formData: newState, // This only sets ONE of the fields from ChildComponent because React delays the setting of state.
)};
render() {
return (
<ChildComponent formValidate={this.manageData} />
)
}
// Child Component
class ChildComponent extends Component {
componentDidMount() {
const fieldA = {
name: 'Phone Number',
value: '123456678'
},
fieldB = {
name: 'Email Address',
value: 'john#example.com'
}
this.props.formValidate(fieldA);
this.props.formValidate(fieldB)
}
render() {
/// Things happen here.
}
}
You're already answering you're own question. React handles state asynchronously and as such you need to make sure you use the current component's state when setState is invoked. Thankfully the team behind React is well-aware of this and have provided an overload for the setState method. I would modify your manageData call to the following:
manageData(data) {
this.setState(prevState => {
const nextState = Object.assign({}, prevState);
nextState.formData[data.name] = data.value;
return nextState;
});
}
This overload for the setState takes a function whose first parameter is the component's current state at the time that the setState method is invoked. Here is the link where they begin discussing this form of the setState method.
https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
Change manageData to this
manageData(data) {
const newObj = {
[data.name]: data.value
};
this.setState(prevState => ({
formData: {
...prevState.formData,
...newObj
}
}));
}
Suppose that I have a component class which is responsible to change any number entered into textbox to text:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
// HERE I NEED TO NOTIFY ABOUT CHANGES
}
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
Now the usage would look something like this:
<NumbersToText>
<input onChange={this.saveValue}
</NumbersToText>
Let's say that all works, and the value gets changed to text.
Now the problem is that after I change numbers to text and assign that value to input onChange handlers are not executed again, thus saveValue is not called with updated value.
How should this problem be approached in order to trigger onChange handlers with new value?
I don't know exactly what you mean by numbers to text so I'll just assume you want to modify the value before calling the onChange function in the input, and also reflect that value in the input.
First of all, what you're doing will never work on React, React reflects internal virtual objects into the DOM, meaning you shloud not modify the DOM directly and instead you should modify this internal representantion (via setState, props) to reflect this change into the DOM.
There's also two types of inputs on React, controlled and uncontrolled. I will assume you want to use this on uncontrolled inputs.
The only possible solution I can see, is to transform the input using the React.cloneElement function adding a aditional step before calling the input's onChange callback.
Here's a possible implementation that will make the input uppercase.
class UpperCase extends React.Component {
constructor(props) {
super(props);
}
onChange(e, input, next) {
let value = input.value || '';
value = value.toUpperCase();
input.value = value;
next(value);
}
render() {
let childs = React.Children.map(this.props.children, child => {
let input = null; //Will take advantage of javascript's closures
let onChangeChild = child.props.onChange.bind(child);
return React.cloneElement(child, {
ref: ref => input = ref,
onChange: e => {
this.onChange(e, input, onChangeChild)
}
});
});
return (
<span>
{childs}
</span>
);
}
}
And you can use it like this:
<UpperCase>
<input onChange={(val) => console.log(val)}></input>
<textarea onChange={(val) => console.log(val)}></textarea>
</UpperCase>
Thanks to #tiagohngl I came up with a similar, but maybe a little less cluttered (without cloning elements) way:
class NumbersToText extends Component {
onChange(event) {
const { target } = event;
const { value } = target;
if (hasNumbers(value)) {
target.value = numbersToText(value);
this.childrenOnChange(event);
}
}
childrenOnChange(event) {
const { children } = this.props;
React.Children.forEach(children, child => {
if (child.props.onChange) {
child.props.onChange(event);
}
});
}
render() {
return (
<span onChange={this.onChange}>
{this.props.children}
</span>
);
}
}
export default class NumbersToText extends React.Component {
constructor(props) {
super(props)
this.onChange = this.onChange.bind(this);
}
componentWillMount() {
this.setState({ anyData: [] });
}
onChange(event) {
this.setState({anyData: event.target.value},
()=>{console.log("AnyData: "+this.state.anyData)});
// callback to console.log after setState is done
}
render() {
return (
<input type="text"
value={this.state.anyData}
onChange={this.onChange} />
);
}
}
As you mention that,
onChange is not called after changed value.
There are multiple possibilities.
onChange is not binded.
There are no state change in render method, so it will not re-render
make use of console.log() to trace the problem
I slightly ammend the code for illustration.
Hope it helps.
How react handle State Change (answer I posted before)
I encountered this problem building a webapp and I replicated it in this jsfiddle. Essentially, I would like an input to call this.setState({message: input_val}) every time I type something into it, then pass it into the parent App class which then re-renders the message onto the Message class. However the output seems to always be one step behind what I actually type. The jsfiddle demo should be self explanatory. I am wondering if I did anything wrong to prompt this.
html
<script src="http://facebook.github.io/react/js/jsfiddle-integration.js"></script>
<div id='app'></div>
js
var App = React.createClass({
getInitialState: function() {
return {message: ''}
},
appHandleSubmit: function(state) {
this.setState({message: state.message});
console.log(this.state.message);
},
render: function() {
return (
<div className='myApp'>
<MyForm onChange={this.appHandleSubmit}/>
<Message message={this.state.message}/>
</div>
);
}
});
var MyForm = React.createClass({
handleSubmit: function() {
this.props.onChange(this.state);
},
handleChange: function(e) {
console.log(e.target.value);
this.setState({message: e.target.value});
this.handleSubmit();
},
render: function() {
return (
<form className="reactForm" onChange={this.handleChange}>
<input type='text' />
</form>
);
}
});
var Message = React.createClass({
render: function() {
return (
<div className="message">
<p>{this.props.message}</p>
</div>
)
}
});
React.render(
<App />,
document.getElementById('app')
);
A call to setState isn't synchronous. It creates a "pending state transition." (See here for more details). You should explicitly pass the new input value as part of the event being raised (like to handleSubmit in your example).
See this example.
handleSubmit: function(txt) {
this.props.onChange(txt);
},
handleChange: function(e) {
var value = e.target.value;
this.setState({message: value});
this.handleSubmit(value);
},
There is a much simpler way to do this, setState(updater, callback) is an async function and it takes the callback as second argument,
Simply pass the handleSubmit as a callback to setState method, this way after setState is complete only handleSubmit will get executed.
For eg.
handleChange: function(e) {
console.log(e.target.value);
this.setState({message: e.target.value}, this.handleSubmit);
}
Try to change the handleChange() method like above and it will work.
for syntax of using setState check this link
with setState hook
useEffect(() => {
your code...
}, [yourState]);
I was pulling my hair out for like an hour because of this so I decided to share... If your callback is still one step behind and seemingly not working, ensure you don't CALL the function with parenthesis... Just pass it in. Rookie mistake.
RIGHT:
handleChange: function(e) {
console.log(e.target.value);
this.setState({message: e.target.value}, this.handleSubmit);
}
VS
WRONG:
handleChange: function(e) {
console.log(e.target.value);
this.setState({message: e.target.value}, this.handleSubmit());
}
There's no reason for MyForm to be using state here. Also putting the onChange on the form instead of the input you're interested in is odd. Controlled components should be preferred because their behavior is more obvious, and any time App's message state changes (even if you e.g. allow Message to change it later), it'll be correct everywhere.
This also makes your code a bit shorter, and considerably simpler.
var App = React.createClass({
getInitialState: function() {
return {message: ''}
},
appHandleSubmit: function(message) {
this.setState({message: message});
},
render: function() {
return (
<div className='myApp'>
<MyForm onChange={this.appHandleSubmit}
message={this.state.message} />
<Message message={this.state.message}/>
</div>
);
}
});
var MyForm = React.createClass({
handleInputChange: function(e){
this.props.onChange(e.target.value);
},
// now always in sync with the parent's state
render: function() {
return (
<form className="reactForm">
<input type='text' onChange={this.handleInputChange}
value={this.props.message} />
</form>
);
}
});
jsbin
Knowing the problem is with not having asyncronous behaviour of setState I solved my issue with async await
onChangeEmail=async x =>{
await this.setState({email:x})
}
You could refactor your class-based component to a functional component as someone else mentioned. The drawbacks are that this can be quite time-consuming depending on how many code lines you have to refactor and is likely prone to error.
I will use your example of a changeHandler to display how it could be done in a functional component.
const INITIAL_DATA = {
message: ""
}
const [form, setForm] = useState({...INITIAL_DATA})
const changeHandler = (e) = setForm({
...form,
[e.target.name]: e.target.value
})
<InputField name={message} value={form.message} onChange={changeHandler}>
^ The above code will produce that behavior as you explained of onChange being one step behind the setState. As others have said, this is due to the asynchronous nature of the setState hook.
The way to solve this is to use the useEffect hook, which allows you to introduce state dependencies. So when setState is finished going through its update cycle the useEffect will fire. So add the below to the above code and BAM.. it should show the state changes without a step delay.
useEffect(() => {
doSomeValidationHere()
orSendOffTheForm()
}, [form])
*Notice how we add a dependency array after the useEffect(() => {}) hook.
Extra info:
If the dependency array is empty it will only run on component mount and first render
If there is no array, it will run every time the page renders
If there is a state name in the array it will run every time that state is finished setting
I found it very cumbersome for me to define 3 handler functions just to get some value to a component's state, so I decided not to use state at all. I just defined an additional property to the component that stored desired value.
So I ended up with a code that looked something like this:
//...
},
text: '',
handleChange: function(event) {
this.text = event.target.value;
this.forceUpdate();
},
render: function() {
return <div>
<InputComponent onChange={this.handleChange}/>
<DisplayComponent textToDisplay={this.text}/>
</div>
}
//...
or as in my case - just use onKeyUp, instead of down...
There are some solutions mentions above, some of them have some problems, but
ajsaule is the correct ansower. Here is show my Code in TypeScript Example:
// solve the problem: setstate always one step behind
useEffect(() => { isFormValid(fields, setFieldsError) }, [fields])
const handleChange = (field: string ) => (evt: React.ChangeEvent<HTMLInputElement>) => {
setFields({ ...fields, [field]: evt.target.value })
}
const isFormValid = (fields: FieldsState, setFieldsError: React.Dispatch<React.SetStateAction<TempObj>>) => {
const tempObj: TempObj = {}
const { email, password } = fields
if( !isEmail(email) ) tempObj.email = 'Invalid Email Address'
if( password.length < 8 ) tempObj.password = 'password must by atleast 8 character long'
Object.keys(fields).forEach(field => {
if(!fields[field as keyof FieldsState]) tempObj[field] = `'${field}' is emapty`
})
setFieldsError(tempObj)
return Object.values(tempObj).every( item => item == '' )
}