React Hooks useCallback вызывает повторный рендеринг дочерних элементов - PullRequest
1 голос
/ 07 марта 2019

Я пытаюсь перейти от компонентов класса к функциональным компонентам с помощью новых хуков. Однако кажется, что с useCallback я получу ненужные отрисовки детей в отличие от функций класса в компонентах класса.

Ниже у меня есть два относительно простых фрагмента. Первый - мой пример, написанный как классы, а второй - мой пример, переписанный как функциональные компоненты. Цель состоит в том, чтобы получить то же поведение с функциональными компонентами, что и с компонентами класса.

Тестовый набор компонентов класса

class Block extends React.PureComponent {
  render() {
    console.log("Rendering block: ", this.props.color);

    return (
        <div onClick={this.props.onBlockClick}
          style = {
            {
              width: '200px',
              height: '100px',
              marginTop: '12px',
              backgroundColor: this.props.color,
              textAlign: 'center'
            }
          }>
          {this.props.text}
         </div>
    );
  }
};

class Example extends React.Component {
  state = {
    count: 0
  }
  
  
  onClick = () => {
    console.log("I've been clicked when count was: ", this.state.count);
  }
  
  updateCount = () => {
    this.setState({ count: this.state.count + 1});
  };
  
  render() {
    console.log("Rendering Example. Count: ", this.state.count);
    
    return (
      <div style={{ display: 'flex', 'flexDirection': 'row'}}>
        <Block onBlockClick={this.onClick} text={'Click me to log the count!'} color={'orange'}/>
        <Block onBlockClick={this.updateCount} text={'Click me to add to the count'} color={'red'}/>
      </div>
    );
  }
};

ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

Пример функционального компонента

const Block = React.memo((props) => {
  console.log("Rendering block: ", props.color);
  
  return (
      <div onClick={props.onBlockClick}
        style = {
          {
            width: '200px',
            height: '100px',
            marginTop: '12px',
            backgroundColor: props.color,
            textAlign: 'center'
          }
        }>
        {props.text}
       </div>
  );
});

const Example = () => {
  const [ count, setCount ] = React.useState(0);
  console.log("Rendering Example. Count: ", count);
  
  const onClickWithout = React.useCallback(() => {
    console.log("I've been clicked when count was: ", count);
  }, []);
  
  const onClickWith = React.useCallback(() => {
    console.log("I've been clicked when count was: ", count);
  }, [ count ]);
  
  const updateCount = React.useCallback(() => {
    setCount(count + 1);
  }, [ count ]);
  
  return (
    <div style={{ display: 'flex', 'flexDirection': 'row'}}>
      <Block onBlockClick={onClickWithout} text={'Click me to log with empty array as input'} color={'orange'}/>
      <Block onBlockClick={onClickWith} text={'Click me to log with count as input'} color={'cyan'}/>
      <Block onBlockClick={updateCount} text={'Click me to add to the count'} color={'red'}/>
    </div>
  );
};

ReactDOM.render(<Example/>, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

В первом (компоненты класса) я могу обновлять счет через красный блок без повторного рендеринга любого из блоков, и я могу свободно записывать текущий счет через оранжевый блок.

Во втором (функциональные компоненты) обновление счетчика с помощью красного блока вызовет повторную визуализацию как красного, так и голубого блока. Это связано с тем, что useCallback создаст новый экземпляр своей функции, поскольку счетчик изменился, в результате чего блоки получили новую onClick опору и, таким образом, произвели повторный рендеринг. Оранжевый блок не будет повторно отображаться, потому что useCallback, используемый для оранжевого onClick, не зависит от значения счетчика. Это было бы хорошо, но оранжевый блок не будет отображать фактическое значение счетчика при нажатии на него.

Я думал, что смысл в useCallback заключался в том, чтобы дети не получали новые экземпляры той же функции и не имели ненужных повторных визуализаций, но это происходит в любом случае, как только функция обратного вызова использует один переменная, которая случается довольно часто, если не всегда из моего опыта.

Итак, как бы я мог сделать эту функцию onClick внутри функционального компонента без повторного рендеринга детей? Это вообще возможно?

Обновление (решение): Используя ответ Райана Когсвелла, приведенный ниже, я создал специальный хук, чтобы упростить создание функций, подобных классам.

const useMemoizedCallback = (callback, inputs = []) => {
    // Instance var to hold the actual callback.
    const callbackRef = React.useRef(callback);

    // The memoized callback that won't change and calls the changed callbackRef.
    const memoizedCallback = React.useCallback((...args) => {
      return callbackRef.current(...args);
    }, []);

    // The callback that is constantly updated according to the inputs.
    const updatedCallback = React.useCallback(callback, inputs);

    // The effect updates the callbackRef depending on the inputs.
    React.useEffect(() => {
        callbackRef.current = updatedCallback;
    }, inputs);

    // Return the memoized callback.
    return memoizedCallback;
};

Затем я могу очень легко использовать это в компоненте функции и просто передать onClick дочернему элементу. Он больше не будет перерисовывать дочерний объект, но все равно будет использовать обновленные переменные.

const onClick = useMemoizedCallback(() => {
    console.log("NEW I've been clicked when count was: ", count);
}, [count]);

1 Ответ

1 голос
/ 07 марта 2019

useCallback позволит избежать ненужных повторных рендерингов из-за изменений в родительском элементе, которые являются , а не частью зависимостей для обратного вызова.Чтобы избежать повторного рендеринга потомков, когда задействованы зависимости обратного вызова, вам нужно использовать ref.Ссылки являются эквивалентом переменной экземпляра.

Ниже у меня есть onClickMemoized с использованием onClickRef, который указывает на текущий onClick (установлен через useEffect), так что он делегирует версиифункция, которая знает текущее значение состояния.

Я также изменил updateCount, чтобы использовать синтаксис функционального обновления, чтобы он не нуждался в зависимости от count.

const Block = React.memo(props => {
  console.log("Rendering block: ", props.color);

  return (
    <div
      onClick={props.onBlockClick}
      style={{
        width: "200px",
        height: "100px",
        marginTop: "12px",
        backgroundColor: props.color,
        textAlign: "center"
      }}
    >
      {props.text}
    </div>
  );
});

const Example = () => {
  const [count, setCount] = React.useState(0);
  console.log("Rendering Example. Count: ", count);

  const onClick = () => {
    console.log("I've been clicked when count was: ", count);
  };
  const onClickRef = React.useRef(onClick);
  React.useEffect(
    () => {
      onClickRef.current = onClick;
    },
    [count]
  );

  const onClickMemoized = React.useCallback(() => {
    onClickRef.current();
  }, []);

  const updateCount = React.useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);

  return (
    <div style={{ display: "flex", flexDirection: "row" }}>
      <Block
        onBlockClick={onClickMemoized}
        text={"Click me to log with empty array as input"}
        color={"orange"}
      />
      <Block
        onBlockClick={updateCount}
        text={"Click me to add to the count"}
        color={"red"}
      />
    </div>
  );
};

ReactDOM.render(<Example />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>

<div id='root' style='width: 100%; height: 100%'>
</div>

И, конечно, прелесть хуков в том, что вы можете выделить эту логику с состоянием в пользовательский хук:

import React from "react";
import ReactDOM from "react-dom";

const Block = React.memo(props => {
  console.log("Rendering block: ", props.color);

  return (
    <div
      onClick={props.onBlockClick}
      style={{
        width: "200px",
        height: "100px",
        marginTop: "12px",
        backgroundColor: props.color,
        textAlign: "center"
      }}
    >
      {props.text}
    </div>
  );
});

const useCount = () => {
  const [count, setCount] = React.useState(0);

  const logCount = () => {
    console.log("I've been clicked when count was: ", count);
  };
  const logCountRef = React.useRef(logCount);
  React.useEffect(
    () => {
      logCountRef.current = logCount;
    },
    [count]
  );

  const logCountMemoized = React.useCallback(() => {
    logCountRef.current();
  }, []);

  const updateCount = React.useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []);
  return { count, logCount: logCountMemoized, updateCount };
};
const Example = () => {
  const { count, logCount, updateCount } = useCount();
  console.log("Rendering Example. Count: ", count);

  return (
    <div style={{ display: "flex", flexDirection: "row" }}>
      <Block
        onBlockClick={logCount}
        text={"Click me to log with empty array as input"}
        color={"orange"}
      />
      <Block
        onBlockClick={updateCount}
        text={"Click me to add to the count"}
        color={"red"}
      />
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<Example />, rootElement);

Edit useCallback and useRef

...