'React input losing focus when typing
I have seen some related questions on StackOverflow, but none of them seem to solve the problem for me. (And many don't use React Classes)
I use React with classes. I have a modal that renders 'filter-rows'. Each filter-row is a dropdown selection + a string query.
When one is made (both value are selected/filled in) an extra row can be added. See the screenshot as example.
The problem I have is that each typed character removes the focus of the input. I have tried many things, like creating an extra local variable to keep track of the strings, but that makes other functionality difficult (e.g. making sure the filter is valid, putting existing values in the rows, etc.)
I also don't wanna go the direction of automatically force-focus on an input-field when changing a character.
This is the code of the component.
import React from "react"
import ReactDOM from "react-dom"
import { hot } from 'react-hot-loader/root'
import PropTypes from "prop-types"
import axios from 'axios';
import qs from 'qs';
import { useTranslation, withTranslation, Trans } from 'react-i18next';
class OtherFilters extends React.Component {
// ---> Lifecycle functions <---
constructor(props)
{
super(props);
this.clearFilters = this.clearFilters.bind(this);
this.cancelChanges = this.cancelChanges.bind(this);
this.saveFilters = this.saveFilters.bind(this);
this.changeFilter = this.changeFilter.bind(this);
this.changeQuery = this.changeQuery.bind(this);
this.saveFilter = this.saveFilter.bind(this);
this.removeFilter = this.removeFilter.bind(this);
this.isFilterValid = this.isFilterValid.bind(this);
this.indexOfFilter = this.indexOfFilter.bind(this);
this.state = {
filters: this.props.otherFilters,
creatingFilter: {
backgroundFilter: null,
query: "",
},
};
}
componentDidMount()
{
$(ReactDOM.findDOMNode(this)).modal('show');
$(ReactDOM.findDOMNode(this)).on('hidden.bs.modal', this.props.callbackHideModal);
}
componentDidUpdate(previousProps, previousState)
{
// Enable all tooltips.
TooltipHelper.enableTooltips([].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')));
}
// ---> UI interaction functions <---
/**
* Clear all filters.
*/
clearFilters()
{
this.setState({
filters: [],
creatingFilter: {
backgroundFilter: null,
query: "",
},
});
}
/**
* Close the modal without saving changes.
*/
cancelChanges()
{
}
/**
* Close the modal and save the changes.
*/
saveFilters()
{
// Combine all saved filters + the in-progress filter.
// For each, check if it's valid.
let filtersToSave = [ ...this.state.filters, this.state.creatingFilter ]
.filter(f => this.isFilterValid(f));
this.props.callbackOtherFiltersChanged(filtersToSave);
}
/**
* Change the target for a filter.
*/
changeFilter(filter, selectedFilter)
{
if (filter == null)
{
this.setState(previousState => {
previousState.creatingFilter.backgroundFilter = selectedFilter;
return previousState;
});
}
else
{
let index = this.state.filters.indexOf(filter);
this.setState(previousState => {
previousState.filters[index].backgroundFilter = selectedFilter;
return previousState;
})
}
}
/**
* Change the query for a filter.
*/
changeQuery(filter, event)
{
if (filter == null)
{
this.setState(previousState => {
previousState.creatingFilter.query = event.target.value;
return previousState;
});
}
else
{
let index = this.state.filters.indexOf(filter);
this.setState(previousState => {
previousState.filters[index].query = event.target.value;
return previousState;
});
}
}
/**
* Save a filter (and introduce a new empty filter).
*/
saveFilter()
{
this.setState(previousState => {
previousState.filters.push({
backgroundFilter: previousState.creatingFilter.backgroundFilter,
query: previousState.creatingFilter.query
});
previousState.creatingFilter = {
backgroundFilter: null,
query: ""
};
return previousState;
});
}
/**
* Remove a previously saved filter.
*/
removeFilter(filter)
{
let index = this.state.filters.indexOf(filter);
this.setState(previousState => {
previousState.filters.splice(index, 1);
return previousState;
});
}
// --> Helper functions <--
indexOfFilter(filter)
{
if (filter == null)
{
return -1;
}
else
{
return this.state.filters.indexOf(filter);
}
}
/**
* Check whether a filter is valid.
*/
isFilterValid(filter)
{
return filter.backgroundFilter != null && filter.query.trim() != '';
}
render () {
const {
filters,
creatingFilter,
} = this.state;
const { t } = this.props;
const _this = this;
function FilterRow(props)
{
var filter = props.filter;
return (
<div className="input-group mb-3">
<FilterRowDropdown filter={filter} />
<FilterRowTextInput filter={filter} />
{
filter == null
? <button className={`btn btn-outline-light ${_this.isFilterValid(creatingFilter) ? '' : 'disabled'}`}
type="button"
onClick={e => _this.saveFilter()}>
{t('Filtering.Other.button_add_filter')}
</button>
: <button className="btn btn-outline-light"
type="button"
onClick={e => _this.removeFilter(filter)}>
{t('Filtering.Other.button_remove_filter')}
</button>
}
</div>
);
}
/**
* A dropdown with possible filters.
*/
function FilterRowDropdown(props)
{
var filter = props.filter;
return (
<>
<button className="btn btn-outline-light dropdown-toggle"
type="button"
id="question-filter-dropdown-button"
data-bs-toggle="dropdown"
aria-expanded="false">
{
filter == null
? (creatingFilter.backgroundFilter != null
? t(creatingFilter.backgroundFilter.title)
: t('Filtering.Other.dropdown_title_select_filter'))
: t(filter.backgroundFilter.title)
}
</button>
<ul className="dropdown-menu" aria-labelledby="question-filter-dropdown-button">
{
Object.keys(BackgroundFilters).length > 0
? <FilterRowDropdownHeader text={t('Filtering.Other.dropdown_header_background')} />
: null
}
{
Object.keys(BackgroundFilters).map(key => {
var backgroundFilter = BackgroundFilters[key];
return (
<FilterRowDropdownItem key={`background-${backgroundFilter.id}`} backgroundFilter={backgroundFilter} filter={filter} />
);
})
}
</ul>
</>
);
}
/**
* A dropdown (section) header.
*/
function FilterRowDropdownHeader(props)
{
return (
<li>
<h6 className="dropdown-header">
{props.text}
</h6>
</li>
);
}
/**
* A single dropdown-item for filtering.
*/
function FilterRowDropdownItem(props)
{
var backgroundFilter = props.backgroundFilter;
var filter = props.filter;
var currentId = filter == null
? creatingFilter.backgroundFilter?.id
: filter.backgroundFilter.id;
return (
<li>
<button className="dropdown-item"
type="button"
onClick={e => _this.changeFilter(filter, backgroundFilter)}>
{t(backgroundFilter.title)}
{
currentId === backgroundFilter.id
? <i className="bi bi-check float-end" />
: ''
}
</button>
</li>
);
}
function FilterRowTextInput(props)
{
var filter = props.filter;
var value = filter == null
? creatingFilter.query
: filter.query;
return (
<input type="text"
key={`input_${_this.indexOfFilter(filter)}`}
className="form-control"
aria-label="Filter text"
placeholder={t('Filtering.Other.input_placeholder_create_filter')}
value={value}
onChange={e => _this.changeQuery(filter, e)} />
);
}
return (
<div id="other-filters-modal" className="modal fade show" data-bs-backdrop="static" tabIndex="-1">
<div className="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div className="modal-content">
<div className="modal-header align-items-top">
<div className="d-flex w-100 me-2 justify-content-between">
<span className="modal-title">
<h5 className="mb-0">{t('Filtering.Other.modal_title')}</h5>
</span>
</div>
<button type="button"
className="btn-close"
onClick={e => _this.cancelChanges()}
data-bs-dismiss="modal"
aria-label="Close">
</button>
</div>
<div className="modal-body">
<div className="row mb-2">
{filters.map((filter, index) => (
<FilterRow key={index} filter={filter} />
))}
<FilterRow key="creatingFilter" />
</div>
</div>
<div className="modal-footer">
<button type="button"
className="btn btn-danger me-auto"
onClick={e => _this.clearFilters()}>
{t('Filtering.Other.modal_clear_filters')}
</button>
<button type="button"
className="btn btn-light"
onClick={e => _this.cancelChanges()}
data-bs-dismiss="modal">
{t('Filtering.Other.modal_cancel')}
</button>
<button type="button"
className="btn btn-dark"
onClick={e => _this.saveFilters()}
data-bs-dismiss="modal">
{t('Filtering.Other.modal_save')}
</button>
</div>
</div>
</div>
</div>
);
}
}
export default hot(withTranslation()(OtherFilters));
How would I go about making the input-fields retain focus? I have already tried giving all parent-components (from the input until the highest component) keys based on an index (so, keys that don't change in-between renders), but that doesn't seem to make any difference.
Thanks in advance.
EDIT: In many questions it seems to be about keys that are not set correctly, or are changing on each re-render. I tried to set keys up to the highest component.
I'm using I18Next, which seems to add a layer to each component, see the screenshot. Could this be causing problems?
(Using inspector I see that when each time I type, it's OtherFilters and everything inside there that's re-rendered, nothing else)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|


