简体   繁体   中英

Parent component unnecessarily re-rendering child on parent state change

I am creating a simple Magic The Gathering search engine. The vision is to have a list of search results, and when a search result is clicked the main display renders extended information about the card selected.

You can see it here

The top level App component contains the state of what card is to be displayed and the ScrollView component maintains the state of the card selected for only the highlighting of the selected card in the list. I propagate down the setDisplayCard handler so that when a card is clicked in the list, I can set the display card as a callback.

function App(props) {

  const [displayCard, setDisplayCard] = useState(null)

  return (
    <div className="App">
      <SearchDisplay handleCardSelect={setDisplayCard}/>
      <CardDisplay card={displayCard} />
    </div>
  );
}
function SearchDisplay({handleCardSelect}) {

    const [cards, setCards] = useState([]);

    useEffect(() => {
        (async () => {
            const cards = await testCardSearch();
            setCards(cards);
        })();
    }, []);

    async function handleSearch(searchTerm) {
        const searchCards = await cardSearch({name: searchTerm});
        setCards(searchCards)
    };

    return (
        <StyledDiv>
            <SearchBar 
                handleSubmit={handleSearch}
            />
            <ScrollView 
                handleCardSelect={handleCardSelect} 
                cards={cards}
            />
        </StyledDiv>
    );
}
function ScrollView({cards, handleCardSelect}) {

    const [selected, setSelected] = useState(null);

    return (
        <ViewContainer>
            {cards.map((card, idx) =>
                <li 
                    key={idx} 
                    style={selected === idx ? {backgroundColor: "red"} : {backgroundColor: "blue"}}
                    onClick={() => {
                        setSelected(idx);
                        handleCardSelect(card);
                    }}
                >
                    <Card card={card} />
                </li>
            )}
        </ViewContainer>
    );
}

The issue I am having is that calling setDisplayCard re-renders my ScrollView and eliminates its local state of the card that was selected so I am unable to highlight the active card in the list. Based on my understanding of react, I don't see why ScrollView re-renders as it does not depend on the state of displayCard . And I am not sure what approach to take to fix it. When I click on a card in the list, I expect it to highlight red.

By default (stateless) components re-render under 3 conditions

  1. It's props have changed
  2. It's state has changed
  3. It's parent re-renders

This behavior can be changed using either shouldComponentUpdate for components or memo for stateless-components.

// If this function returns true, the component won't rerender
areEqual((prevProps, nextProps) => prevProps.cards === nextProps.card)

export default React.memo(ScrollView, areEqual);

However I don't think this is your problem. You are using an array Index idx as your element key which can often lead to unexpected behavior.

Try to remove key={idx} and check if this fixes your issue.

A child component's render method will always be called, once its parent's render method is invoked. The same goes for if its props or state change.

Since you're using functional components, you could use the React.memo HOC to prevent unnecessary component re-renders.

React.memo acts similar to a PureComponent and will shallowly compare ScrollView 's old props to the new props and only trigger a re-render if they're unequal:

export default React.memo(ScrollView);

React.memo also has a second argument, which gives you control over the comparison:

function areEqual(prevProps, nextProps) {
  // only update if a card was added or removed
  return prevProps.cards.length === nextProps.cards.length;
}

export default React.memo(ScrollView, areEqual);

If you were to use class-based components, you could use the shouldComponentUpdate life cycle method as well.

So your App component is supposed to hold the state of the card the user clicked? Right now your App component is stateless. It's a functional component. Try converting it to a class component with an initial, and maintained, state.

What is the logic of your setDisplayCard()?

I've heard that in React 16? there is something like 'useState()' and 'hooks', but I'm not familiar with it.

This person seemed to be having a similar problem,

React functional component using state

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM