Reputation: 757
I have a parent component which passes down an array as props to its child component. The child component, which displays an input field connected to redux-form and a list with the array items, saves this array in its internal state and uses a handleKeyUp() function to filter the list and return a new list every time a user types a letter in the input field. The function also updates the component state by saving the new filtered list. Here is the code for the handleKeyUp() function:
handleKeyUp () {
console.log(this.state.options) <= Here is always the initial state array
const val = (this.props.input.value).toLowerCase()
if(this.props.input.value) {
var optionsFiltered = _.filter(this.state.options, function(item){
var itemName = item.name.toLowerCase()
var itemCode = item.code.toLowerCase()
return itemName.indexOf(val)>-1 || itemCode.indexOf(val)>-1;
});
var otherOptionsFiltered = _.filter(this.state.otherOptions, function(item){
var itemName = item.name.toLowerCase()
var itemCode = item.code.toLowerCase()
return itemName.indexOf(val)>-1 || itemCode.indexOf(val)>-1;
});
this.setState({
options: optionsFiltered,
otherOptions: otherOptionsFiltered
})
}
}
Everything works just fine, the issue seems to be that the setState function inside the handleKeyUp() function has not yet finished when the user types the second letter, resulting in a visual flickering in the list (that is, I type the first letter, list gets filtered correctly, I type the second letter and I see the entire list for 1 ms and then the filtered list), so there is a re-render of the component after the setState inside of my function. I am using Redux too and I was wondering if at this point I should use Redux to handle this, or if there is something I can change to make it work with internal state. Posting the entire code if needed:
class MyDropdown extends Component {
constructor(props) {
super(props);
this.state = {
dropdownIsVisible: false,
dropdownCssClass: 'dropdown',
options: [],
otherOptions: []
}
this.handleFocus = this.handleFocus.bind(this);
this.handleBlur = _.debounce(this.handleBlur.bind(this), 150);
this.handleClick = this.handleClick.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
}
componentWillReceiveProps(nextProps) {
this.setState({
options: nextProps.options,
otherOptions: nextProps.otherOptions
})
}
handleFocus () {
this.setState({
dropdownIsVisible: true,
dropdownCssClass: ['dropdown', pageStyles.dropdown].join(' ')
})
}
handleBlur () {
this.setState({
dropdownIsVisible: false,
dropdownCssClass: 'dropdown'
})
}
handleClick (id, label, fieldName, otherField) {
this.props.input.onChange(label)
this.props.updateField("evaluationInfo", fieldName, id)
this.props.updateField("evaluationInfo", fieldName + "_display", label)
if(otherField) {
this.props.updateField("evaluationInfo", otherField, '')
this.props.updateField("evaluationInfo", otherField + "_display", '')
}
this.handleBlur()
}
handleKeyUp () {
console.log(this.state.options) <= Here is always the initial state array
const val = (this.props.input.value).toLowerCase()
if(this.props.input.value) {
var optionsFiltered = _.filter(this.state.options, function(item){
var itemName = item.name.toLowerCase()
var itemCode = item.code.toLowerCase()
return itemName.indexOf(val)>-1 || itemCode.indexOf(val)>-1;
});
var otherOptionsFiltered = _.filter(this.state.otherOptions, function(item){
var itemName = item.name.toLowerCase()
var itemCode = item.code.toLowerCase()
return itemName.indexOf(val)>-1 || itemCode.indexOf(val)>-1;
});
this.setState({
options: optionsFiltered,
otherOptions: otherOptionsFiltered
})
}
}
render() {
const {
input,
label,
options,
optionsLabel,
optionsField,
otherOptionsLabel,
otherOptions,
otherOptionsField,
showCode,
cssClass,
meta: {
touched,
error
}
} = this.props
if(options) {
var listItems = this.state.options.map((item) =>
<li key={ item.id } onClick={() => this.handleClick(item.id, item.name, optionsField, otherOptionsField) }>{ showCode ? item.code + ' - ' + item.name : item.name }</li>
)
}
if(otherOptions) {
var listOtherItems = this.state.otherOptions.map((item) =>
<li key={ item.id } onClick={() => this.handleClick(item.id, item.name, otherOptionsField, optionsField) }>{ showCode ? item.code + ' - ' + item.name : item.name }</li>
)
}
return (
<div className={ cssClass }>
<label>{ label }</label>
<input {...input } type="text" onFocus={ this.handleFocus } onBlur={ this.handleBlur } onKeyUp={ this.handleKeyUp } autoComplete="off" autoCorrect="off" spellCheck="false" />
<div className="relative">
<ul className={ this.state.dropdownCssClass }>
{ optionsLabel ? <li className={ pageStyles.optionsLabel }>{ optionsLabel }</li> : null }
{ listItems }
{ otherOptionsLabel ? <li className={ pageStyles.optionsLabel }>{ otherOptionsLabel }</li> : null }
{ otherOptions ? listOtherItems : null }
</ul>
</div>
{ touched && error && <span className="error">{ error }</span> }
</div>
)
}
}
Upvotes: 1
Views: 300
Reputation: 2732
You should save the filtered list into state only when the retrieval is fully done. You can achieve this using generator function and maintain a flag.
When the KeyUp event is fired, set the flag as 'pending' and fetch the result in generator function. Once the result fetch is done, set the flag as 'done' inside generator function.
Render your result based only when flag is 'done'.
Edit:
Some sample code
function* getResult() {
yield this.setState({status: 'pending'})
const result = yield callApi()
yield this.setState({listOfItems: result})
yield this.setState({status: 'done'})
}
Upvotes: 1