5

I'm new to reactjs and I'm trying read data from input. Problem is when I type a sign, my input loose focus. But only when all logic is inside function. When Input with button and logic is in different file - it's working. I don't really know why...

I have created separate file with same code and import it to sollution - it's ok. I have tried with onChange={handleChange} - lost focus as well.

export default function MyInput(){
const [citySearch, updateCitySearch] = useState();

function searchCityClick(){
   alert(citySearch);
}
const SearchComponent = ()=> (
    <div> 
        <input 
        value={citySearch}
        onChange={(e) => updateCitySearch(e.target.value)}/>
        <Button variant="contained" color="primary" onClick={searchCityClick}>
            Search
        </Button>
    </div>

);
return(
    <div>
        <div>
            <SearchComponent />
        </div>
    </div>
)}
skyboyer
  • 22,209
  • 7
  • 57
  • 64
michal
  • 71
  • 1
  • 8
  • Whenever state is getting changed, react is trying to re-render the component and creating `SearchComponent` function again which is different than previous and causing focus lose. – Ashish Jun 25 '19 at 17:51

3 Answers3

11

The SearchComponent is a functional component, and shouldn't be defined inside another component. Defining SearchComponent inside MyInput will cause SearchComponent to be recreated (not rerendered), and in essence it's DOM would be removed, and then added back on every click.

The solution is pretty straightforward, extract SearchComponent from MyInput, and pass the functions, and the data via the props object:

const { useState, useCallback } = React;

const SearchComponent = ({ citySearch, updateCitySearch, searchCityClick }) => (
  <div> 
    <input
      value={citySearch}
      onChange={e => updateCitySearch(e.target.value)} />
    
    <button onClick={searchCityClick}>Search</button>
  </div>
);

const MyInput = () => {
  const [citySearch, updateCitySearch] = useState('');

  const searchCityClick = () => alert(citySearch);

  return(
    <div>
      <SearchComponent 
        citySearch={citySearch} 
        updateCitySearch={updateCitySearch}
        searchCityClick={searchCityClick} />
    </div>
  );
};

ReactDOM.render(
  <MyInput />,
  root
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • That is it! I thought that there is something wrong with rendering element, but didn't know how to deal with it. Thank you very much for your help! – michal Jun 26 '19 at 16:20
1

I am also new to React, so take my explanation with a pinch of salt (hopefully someone else can elaborate).. I believe it has something to do with nesting components and how React is re-rendering..

If you use SearchComponent as a variable, instead of an anonymous function, this works as expected.

I am also curious as to why using nested functions like that (when using JSX) causes this behavior... possibly an anti-pattern?

function MyInput() {
  const [citySearch, updateCitySearch] = React.useState();

  function searchCityClick() {
    alert(citySearch);
  }
  
  const SearchComponent = (
      <div> 
          <input 
          value={citySearch}
          onChange={(e) => updateCitySearch(e.target.value)}/>
          <button variant="contained" color="primary" onClick={searchCityClick}>
              Search
          </button>
      </div>
  );
  
  return (
    <div>
      <div>
        {SearchComponent}
      </div>
    </div>
  );
}


let div = document.createElement("div");
div.setAttribute("id", "app");
document.body.append(div);
ReactDOM.render(<MyInput />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>

Even if you change the nested function to an "actual component", focus is lost after each key press (aka onChange)..

Does not work:

function MyInput() {
  const [citySearch, updateCitySearch] = React.useState();

  function searchCityClick() {
    alert(citySearch);
  }

  const SearchComponent = () => {
      return (
        <div> 
            <input 
            value={citySearch}
            onChange={(e) => updateCitySearch(e.target.value)}/>
            <button variant="contained" color="primary" onClick={searchCityClick}>
                Search
            </button>
        </div>
    );
  }

  return (
    <div>
      <div>
        <SearchComponent />
      </div>
    </div>
  );
}
Matt Oestreich
  • 8,219
  • 3
  • 16
  • 41
1

This happens because the useState hook is not "hooked" to your SearchComponent, but your MyInput component. Whenever, you call updateCitySearch() you change the state of MyInput, thus forcing the entire component to re-render.

SearchComponent, is explicitly defined inside MyInput. When citySearch-state is updated, SearchComponent loses focus because the initial virual DOM surrounding it is no longer intact, instead you have a completely new piece of DOM. Essentially, you are creating a brand new SearchComponent each time the MyInput is updated by state.

Consider the following example:

function App() {
  const [citySearch, updateCitySearch] = useState("");
  console.log("rendered App again"); //always prints
  const SearchComponent = () => {
    console.log("rendered Search"); //always prints
    const searchCityClick = () => {
      alert(citySearch);
    };
    return (
      <div>
        <input
          value={citySearch}
          onChange={e => {
            updateCitySearch(e.target.value);
          }}
        />
        <button onClick={searchCityClick}>Search</button>
      </div>
    );
  };
  return (
    <div>
      <div>
        <SearchComponent />
      </div>
    </div>
  );
}

Every time you update state, you would trigger both console.log(), the App component re-renders and SearchComponent gets re-created. A new iteration of myInput is rendered each time and a new SearchComponent gets created.

But if you were to define useState inside SearchComponent, then only SearchComponent will re-render whens state changes, thus leaving the original myInput component unchanged, and the current SearchComponent intact.

function App() {
  console.log("rendered App again"); //would never print a 2nd time.
  const SearchComponent = () => {
    const [citySearch, updateCitySearch] = useState("");
    console.log("rendered Search"); //would print with new state change
    const searchCityClick = () => {
      alert(citySearch);
    };
    return (
      <div>
        <input
          value={citySearch}
          onChange={e => {
            updateCitySearch(e.target.value);
          }}
        />
        <button onClick={searchCityClick}>Search</button>
      </div>
    );
  };
  return (
    <div>
      <div>
        <SearchComponent />
      </div>
    </div>
  );
}
Chris Ngo
  • 15,460
  • 3
  • 23
  • 46
  • 1
    React component renders are never supposed to cause side-effects, and creating an entirely new component inside of a function component is definitely a side-effect. It also makes it very difficult to read. `SearchComponent` should be moved outside of `App` and accept any `props` it needs. – joshwilsonvu Jun 25 '19 at 19:59
  • 1
    @jwilson I'm aware of that and it looks like so does OP. He tested that it works when he extracts the component outside the function. My answer is not to point out the obvious best-practice (OP already knows what that is) but rather trying to help understand why the feature does not work as expected with current construction. – Chris Ngo Jun 25 '19 at 20:02