Я пытаюсь перейти от компонентов класса к функциональным компонентам с помощью новых хуков. Однако мне кажется, что с 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]);
16
ApplePearPerson 7 Мар 2019 в 16:59

1 ответ

Лучший ответ

useCallback позволит избежать ненужного повторного рендеринга дочерних элементов из-за изменений в родительском элементе, что не является частью зависимостей для обратного вызова. Чтобы избежать повторного рендеринга дочерних объектов, когда задействованы зависимости обратного вызова, вам необходимо использовать ref. 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(
    () => {
      // By leaving off the dependency array parameter, it means that
      // this effect will execute after every committed render, so
      // onClickRef.current will stay up-to-date.
      onClickRef.current = onClick;
    }
  );

  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.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/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(() => {
    // By leaving off the dependency array parameter, it means that
    // this effect will execute after every committed render, so
    // logCountRef.current will stay up-to-date.
    logCountRef.current = logCount;
  });

  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

15
Ryan Cogswell 25 Июл 2020 в 15:04