React input loses focus after each keystroke
Asked Answered
C

4

7

I have a search bar on my nav component. After each keystroke, the input loses focus and you have to re-click on it to type the next key.

Here is the input:

<input
    type="text"
    name="search"
    placeholder="Search"
    value={search}
    onChange={handleInputChange}
/>

Here is the handleInputChange function:

function handleInputChange(event) {
   event.preventDefault();
   let value = event.target.value;
   setSearch(value);
}

Here is the hook for setting the search:

const [search, setSearch] = useState("");

I've tried adding a key to the input, but that doesn't work. When I move the search input to a new component, that also doesn't work.

Here is the complete code:

import React, { useEffect, useState, useCallback } from "react";
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import { Row, Col } from '../Grid';
import IconButton from '@material-ui/core/IconButton';
import SearchIcon from '@material-ui/icons/Search';
import ShoppingCartOutlinedIcon from '@material-ui/icons/ShoppingCartOutlined';
import MenuIcon from '@material-ui/icons/Menu';
import Badge from '@material-ui/core/Badge';
import useScrollTrigger from '@material-ui/core/useScrollTrigger';
import Slide from '@material-ui/core/Slide';
import SideMenu from '../SideMenu';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import { toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { css } from 'glamor';
import "./style.css";


function Navbar(props) {
    const cart = useSelector(state => state.cart);
    const [cartTotal, setCartTotal] = useState(0);
    const [loggedIn, setLoggedIn] = useState(false);
    const [search, setSearch] = useState("");
    const [isOpen, setIsOpen] = useState(false);
    const [renderMiddleCol, setMiddleCol] = useState(true);
    const [menuClass, setMenuClass] = useState("no-menu");

    useEffect(() => {
        if (cart[0]) {
            setCartTotal(cart[0].line_items.length)
        }
    }, [cart[0]]);
    useEffect(() => {
        if (window.sessionStorage.id) {
            setLoggedIn(true);
        } else {
            setLoggedIn(false);
        }
    }, [loggedIn]);
    useEffect(() => {
        if (window.innerWidth < 450) {
            setMiddleCol(false);
        } else {
            setMiddleCol(true);
        }
    }, [window.innerWidth]);
    function HideOnScroll(props) {
        const { children, window } = props;
        const trigger = useScrollTrigger({ target: window ? window() : undefined });
        return (
            <Slide appear={false} direction="down" in={!trigger}>
                {children}
            </Slide>
        );
    }
    HideOnScroll.propTypes = {
        children: PropTypes.element.isRequired,
        window: PropTypes.func,
    };
    function CheckCart() {
        if (window.sessionStorage.id) {
            window.location.href = "/cart";
        } else {
            toast("Please login to view your cart", {
                className: css({
                    background: '#3E0768',
                    boxShadow: '2px 2px 20px 2px rgba(0,0,0,0.3)',
                    borderRadius: '17px'
                }),
                bodyClassName: css({
                    fontSize: '20px',
                    color: 'white'
                }),
                progressClassName: css({
                    background: "linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(62,7,104,1) 80%)"
                })
            });
        }
    }
    function Search() {
        if (search) {
            sessionStorage.setItem("search", search);
            window.location.href = "/search";
        } else {
            toast("Search field cannot be empty", {
                className: css({
                    background: '#3E0768',
                    boxShadow: '2px 2px 20px 2px rgba(0,0,0,0.3)',
                    borderRadius: '17px'
                }),
                bodyClassName: css({
                    fontSize: '20px',
                    color: 'white'
                }),
                progressClassName: css({
                    background: "linear-gradient(90deg, rgba(0,0,0,1) 0%, rgba(62,7,104,1) 80%)"
                })
            });
        }
    }
    function logOut(event) {
        event.preventDefault();
        setIsOpen(false);
        sessionStorage.clear();
        window.location.href = "/login";
    }
    function handleInputChange(event) {
        event.preventDefault();
        let value = event.target.value;
        setSearch(value);
    }
    function toggleMenu(event) {
        event.preventDefault();
        setIsOpen(!isOpen);
        if (menuClass === "no-menu") {
            setMenuClass("menu-background");
        } else {
            setMenuClass("no-menu");
        }
    }
    const theme = createMuiTheme({
        palette: {
            primary: {
                main: '#000000',
                contrastText: '#ffffff',
            },
            secondary: {
                light: '#3E0768',
                main: '#3E0768',
                contrastText: '#ffffff',
            },
            tertiary: {
                main: '#ffffff',
            }
        },
    });

    return (
        <MuiThemeProvider theme={theme}>
            <HideOnScroll {...props}>
                <AppBar position="fixed" color="primary">
                    <Toolbar>
                        <Col size="md-1">
                            <IconButton
                                onClick={toggleMenu}
                                aria-label="Menu"
                            >
                                <MenuIcon
                                    fontSize="large"
                                    className="white"
                                />
                            </IconButton>
                        </Col>
                        <Col size="md-2">
                            <a href="/" className="white "><h6>Demo Company</h6></a>
                        </Col>
                        {renderMiddleCol ? (
                            <Col size="lg-6 md-5 sm-3" />
                        ) : (<div />)}
                        <Col size="md-2 4">
                            <Row no-gutters>
                                <div className="search-box">
                                    <Col size="md-2 1">
                                        <IconButton onClick={Search} aria-label="search" >
                                            <SearchIcon className="white" />
                                        </IconButton>
                                    </Col>
                                    <Col size="md-8 9">
                                        {/* <SearchForm
                                            value={search}
                                            onChange={handleInputChange}
                                        /> */}
                                        <input
                                            className="search-field white"
                                            type="text"
                                            name="search"
                                            placeholder="Search"
                                            value={search}
                                            onChange={handleInputChange}
                                        />
                                    </Col>
                                </div>
                            </Row>
                        </Col>
                        <Col size="md-1">
                            <IconButton
                                onClick={CheckCart}
                                aria-label="Go to cart"
                            >
                                <MuiThemeProvider theme={theme}>
                                    <Badge
                                        badgeContent={cartTotal}
                                        color="secondary"
                                    >
                                        <ShoppingCartOutlinedIcon className="white" />
                                    </Badge>
                                </MuiThemeProvider>
                            </IconButton>
                        </Col>
                    </Toolbar>
                </AppBar>
            </HideOnScroll>
            <SideMenu
                isOpen={isOpen}
                menuClass={menuClass}
                toggleMenu={toggleMenu}
                loggedIn={loggedIn}
                logOut={logOut}
            />
        </MuiThemeProvider>
    );
}

export default Navbar;
Catalectic answered 5/12, 2019 at 16:49 Comment(4)
is your component re rendering in each keystroke? As it is setting statePrivation
post more code pleaseBrainard
This codesanbox with the exact code you posted seems to not have the issue. More context might be needed. codesandbox.io/s/hopeful-cerf-iyt16Toad
I edited the question with the complete codeCatalectic
C
5

I ended up using a ref on the input and setting it focus on each re-render. Here is the code that fixed it.

const [search, setSearch] = useState("");
const searchInput = React.useRef(null);
useEffect(() => {
  searchInput.current.focus();
}, [search]);

And here is the input:

<input
  ref={searchInput}
  className="search-field white"
  type="text"
  name="search"
  placeholder="Search"
  value={search}
  onChange={handleInputChange}
/>

Credit for the solution: React: set focus on componentDidMount, how to do it with hooks?

Catalectic answered 6/12, 2019 at 21:26 Comment(0)
E
5

Here's a detailed explanation that I've found helpful: https://reactkungfu.com/2015/09/react-js-loses-input-focus-on-typing/

To summarize: without an unchanging key, React is throwing away the previous instance of your controlled input upon state change and is creating a new one in its place. The input that had focus is removed immediately after its value changes.

Make sure:

  • Your controlled input has a key attribute
  • The value of key isn't derived from the input's value in any way, because you don't want the key to change when the value changes
Empennage answered 5/12, 2019 at 19:3 Comment(4)
Since I'm not mapping out multiple elements, I shouldn't need a key. And when I do add a key, it still loses focus.Catalectic
Doesn't seem to work for me using useState hook. I tried with mapping and without mapping my elementNutshell
Life saver! My elements' key was changing everytime and therefore was losing focus on rerender. Wouldn't have realized for a while...Honig
Just adding a key fixed that error, thanks!Inundate
C
5

I ended up using a ref on the input and setting it focus on each re-render. Here is the code that fixed it.

const [search, setSearch] = useState("");
const searchInput = React.useRef(null);
useEffect(() => {
  searchInput.current.focus();
}, [search]);

And here is the input:

<input
  ref={searchInput}
  className="search-field white"
  type="text"
  name="search"
  placeholder="Search"
  value={search}
  onChange={handleInputChange}
/>

Credit for the solution: React: set focus on componentDidMount, how to do it with hooks?

Catalectic answered 6/12, 2019 at 21:26 Comment(0)
S
2

What fixed this issue for me was to not use inner components. Such as

const NewComponent = (props: {text: string}) => <div>{text}</div>;
return <div><NewComponent text="Text" /><div>;

When I declared input components this way it caused a re-render on every keystroke. The solution was to extract the components to another file or just put the whole component in the JSX without using the above method to clean up the code.

It has something to do with React not being able to know when to rerender.

Slaver answered 4/1, 2023 at 12:16 Comment(0)
A
1

I don't know exactly why, but my focus problem was solved by changing this code:

import { Route } from 'react-router-dom'
<Route path='xxx' component={() => <TheComponent... />}

where TheComponent contains the input element that loses focus while typing, to this code:

<Route path='xxx'>
  <TheComponent... />
</Route>

See my SO question, hopefully someone will soon shed some light on how this worked

Alienable answered 3/1, 2020 at 11:48 Comment(2)
@AsafAviv so if I need props, I cannot use input in the original code. I tried also to add id or key. But here, the main issue is if the original poster, I did not have a chance to study the entire code, can fix his/her problem with my hint. (Ooops, AsafAviv removed his comment)Alienable
so hat happens is that since you passes an inline function to "component" prop that rerender the whole thing breaking focus.Coleencolella

© 2022 - 2024 — McMap. All rights reserved.