Вопрос по react-hooks, reactjs – Я забыл сохранить последнюю версию моей песочницы - ссылка обновлена.

1

аюсь перейти от компонентов класса к функциональным компонентам, используя новые хуки. Однако он чувствует, что с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

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(
    () => {
      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);

Спасибо за ответ. Я нахожу довольно утомительным то, что то, что можно сделать в компоненте класса с единственной функцией, будет в этом функциональном компоненте принимать фактическую функцию, 1 ссылка на сохранение функции в экземпляре var, 1 памятная функция для передачи дочернему элементу и 1useEffect обновить экземпляр вар. Прежде чем принять это как ответ, мне было интересно, если вы думаете, возможно ли поместить эти 4 в пользовательский хук, чтобы он оставался простым вызовом единственной функции? ApplePearPerson
Выглядит красиво, но в песочнице есть ошибка, но я определенно могу работать с этим! ApplePearPerson
Я забыл сохранить последнюю версию моей песочницы - ссылка обновлена. Ryan Cogswell
Да, именно здесь красота крючков сияет. Ответ обновлен. Ryan Cogswell

Похожие вопросы