0

For learning purpose,

I am trying prevent re-render on <InputWithLable /> component whenever i Dismiss a search result (see deploy in Full code)

I have use React.memo but it still re-render. So I think maybe its props is the culprit. I use React.useCallback to handleSearch prop, but it doesn't work.

Full code

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
import React from 'react';

const API_ENDPOINT = 'https://hn.algolia.com/api/v1/search?query=';

const useSemiPersistentState = (key, initialState) => {
  const [value, setValue] = React.useState(
    localStorage.getItem(key) || initialState
  );

  React.useEffect(() => {
    localStorage.setItem(key, value);
  }, [value, key]);

  return [value, setValue];
};

function storiesReducer(prevState, action) {
  switch (action.type) {
    case "SET":
      return { ...prevState, data: action.data, isLoading: false, isError: false };
    case "REMOVE":
      return {
        ...prevState,
        data: prevState.data.filter(
          story => action.data.objectID !== story.objectID
        )
      }
    case "ERROR":
      return { ...prevState, isLoading: false, isError: true };
    default:
      throw new Error();
  }
}

const App = () => {
  const [searchTerm, setSearchTerm] = useSemiPersistentState(
    'search',
    'Google'
  );

  const [stories, dispatchStories] = React.useReducer(storiesReducer, { data: [], isLoading: true, isError: false });
  const [url, setUrl] = React.useState("");
  const handleFetchStories = React.useCallback(() => {
    fetch(url)
      .then((response) => response.json())
      .then((result) => {
        console.log(result);
        dispatchStories({ type: "SET", data: result.hits })
      })
      .catch(err => dispatchStories({ type: "ERROR", data: err }))
  }, [url])

  React.useEffect(() => {
    handleFetchStories();
  }, [handleFetchStories])


  const handleRemoveStory = React.useCallback(
    (item) => {
      dispatchStories({ type: "REMOVE", data: item });
    },
    [], // chi render 1 lan vi props khong thay doi
  )

  const handleSearch = React.useCallback(
    (e) => {
      setSearchTerm(e.target.value);
    },
    [],
  )

  // Chuc nang filter la cua server (vd: database)
  // const searchedStories = stories.data ? stories.data.filter(story =>
  //   story.title.toLowerCase().includes(searchTerm.toLowerCase())
  // ) : null; // nghich cai nay!

  console.log('App render');

  return (
    <div>
      <h1>My Hacker Stories</h1>

      <InputWithLabel
        id="search"
        value={searchTerm}
        isFocused
        onInputChange={handleSearch}
      >
        <strong>Search:</strong>
      </InputWithLabel>

      <button onClick={() => setUrl(API_ENDPOINT + searchTerm)}>Search!</button>

      <hr />

      {stories.isError && <h4>ERROR!</h4>}

      {stories.isLoading ? <i>Loading...</i>
        : <List list={stories.data} onRemoveItem={handleRemoveStory} />}
    </div>
  );
};

const InputWithLabel = React.memo(
  ({
    id,
    value,
    type = 'text',
    onInputChange,
    isFocused,
    children,
  }) => {
    const inputRef = React.useRef();

    React.useEffect(() => {
      if (isFocused) {
        inputRef.current.focus();
      }
    }, [isFocused]);

    console.log('Search render')

    return (
      <>
        <label htmlFor={id}>{children}</label>
        &nbsp;
        <input
          ref={inputRef}
          id={id}
          type={type}
          value={value}
          onChange={onInputChange}
        />
      </>
    );
  }
);

// Prevent default React render mechanism: Parent rerender -> Child rerender
const List = React.memo(
  ({ list, onRemoveItem }) =>
    console.log('List render') || list.map(item => (
      <Item
        key={item.objectID}
        item={item}
        onRemoveItem={onRemoveItem}
      />
    ))
);

const Item = ({ item, onRemoveItem }) => (
  <div>
    <span>
      <a href={item.url}>{item.title}</a>
    </span>
    <span>{item.author}</span>
    <span>{item.num_comments}</span>
    <span>{item.points}</span>
    <span>
      <button type="button" onClick={() => onRemoveItem(item)}>
        Dismiss
      </button>
    </span>
  </div>
);

export default App;

8
  • 1
    Is there a particular reason to prevent a rerender there? Of course it needs to rerender when the search term changes. Also, just logging renders and seeing multiple of them in the console isn't necessarily indicative of a performance issue. Commented Oct 22, 2021 at 8:16
  • Start by figuring out what prop is causing the rerender Commented Oct 22, 2021 at 8:18
  • Hi! Please update your question with a minimal reproducible example demonstrating the problem, ideally a runnable one using Stack Snippets (the [<>] toolbar button). Stack Snippets support React, including JSX; here's how to do one. What is searchTerm? It appears out of nowhere in the code. Presumably it's state in App? Commented Oct 22, 2021 at 8:18
  • I don't see anything else in the parent component (App) but things that would, quite correctly, need to re-render InputWithLabel. What change are you making that you think shouldn't re-render it? Commented Oct 22, 2021 at 8:19
  • 1
    Not a direct answer to your question, but this will save you a lot of effort in the future. I would recommend you to use [why-did-you-render][1] package to automatically detect unnecessary rendering. It also provided some directions on why might this happen and how to solve it. [1]: github.com/welldone-software/why-did-you-render Commented Oct 22, 2021 at 8:33

1 Answer 1

1

You should not be looking at how many times a component's render function gets called; React is free to call it as many times as it likes (and indeed, in strict mode, it calls them twice to help you not make mistakes).

But to answer your question (with the actual code that uses children):

<InputWithLabel>
   <strong>Search:</strong>
</InputWithLabel>

compiles down to

React.createElement(InputWithLabel, null,
    React.createElement("strong", null, "Search:"))

the identity of the children prop (the <strong /> element) changes for each render of the parent component since React.createElement() returns new objects for each invocation. Since that identity changes, React.memo does nothing.

If you wanted to (but please don't), you could do

const child = React.useMemo(() => <strong>Search:</strong>);
// ...
<InputWithLabel>{child}</InputWithLabel>

but doing that for all of your markup leads to nigh-unreadable code.

Sign up to request clarification or add additional context in comments.

1 Comment

For learning purpose only. Thanks. Though, I just remove children and instead put <strong>Search:</strong> inside <label></label> in <InputWithLabel \>. But it still re-render, why?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.