6

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;
CB721
  • 256
  • 4
  • 15

4 Answers4

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?

CB721
  • 256
  • 4
  • 15
4

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
simmer
  • 2,639
  • 1
  • 18
  • 22
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.

Zergleb
  • 2,212
  • 15
  • 24
0

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

Roland
  • 4,619
  • 7
  • 49
  • 81
  • @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) – Roland Jan 03 '20 at 12:20
  • so hat happens is that since you passes an inline function to "component" prop that rerender the whole thing breaking focus. – Gianluca Pietrobon Apr 23 '21 at 20:44