Функциональный компонент React hooks предотвращает повторную визуализацию при обновлении состояния

#reactjs #react-hooks

#reactjs #реагирующие хуки

Вопрос:

Я изучаю React Hooks, что означает, что мне придется перейти от классов к функциональным компонентам. Ранее в классах у меня могли быть переменные класса, независимые от состояния, которые я мог обновлять без повторного рендеринга компонента. Теперь, когда я пытаюсь воссоздать компонент как функциональный компонент с помощью хуков, я столкнулся с проблемой, заключающейся в том, что я не могу (насколько я знаю) создавать переменные для этой функции, поэтому единственный способ сохранить данные — через useState хук. Однако это означает, что мой компонент будет повторно визуализироваться всякий раз, когда это состояние обновляется.

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

 class ClassExample extends React.Component {
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => {
    if (ref) {
      this._blockRef = ref;
    }
  }
  
  // Animate the block.
  onClick = () => {
    if (this._isAnimating) {
      return;
    }

    this._isAnimating = true;
    Velocity(this._blockRef, {
      translateX: 500,
      complete: () => {
        Velocity(this._blockRef, {
          translateX: 0,
          complete: () => {
            this._isAnimating = false;
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  };
  
  render() {
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
      </div>
    );
  }
}

const FunctionExample = (props) => {
  console.log("Rendering FunctionExample");
  
  const [ isAnimating, setIsAnimating ] = React.useState(false);
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => {
    if (isAnimating) {
      return;
    }

    setIsAnimating(true);
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            setIsAnimating(false);
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  });
  
  return(
    <div>
      <div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
    </div>
  );
};

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<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>  

Если вы нажмете на панель ClassExample (розовая), вы увидите, что она не выполняет повторный рендеринг во время анимации, однако, если вы нажмете на панель FunctionExample (красная), она будет повторно отображаться дважды во время анимации. Это потому, что я использую setIsAnimating , который вызывает повторный рендеринг. Я знаю, что это, вероятно, не очень влияет на производительность, но я хотел бы предотвратить это, если это вообще возможно с функциональным компонентом. Есть предложения / я делаю что-то не так?

Обновление (попытка исправления, решения пока нет): Ниже пользователь lecstor предложил, возможно, изменить результат useState на let вместо const, а затем установить его напрямую let [isAnimating] = React.useState(false); . К сожалению, это тоже не работает, как вы можете видеть во фрагменте ниже. Щелчок по красной полоске запустит ее анимацию, щелчок по оранжевому квадрату вызовет повторный рендеринг компонента, и если вы снова нажмете на красную полоску, он напечатает, что isAnimating значение false сброшено, даже если панель все еще анимируется.

 const FunctionExample = () => {
  console.log("Rendering FunctionExample");

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  let [isAnimating] = React.useState(false);
  
  // Var to force a re-render.
  const [ forceCount, forceUpdate ] = React.useState(0);

  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => {
    console.log("Is animating: ", isAnimating);
    if (isAnimating) {
      return;
    }
    
    isAnimating = true;
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            isAnimating = false;
          }
        }, {
          duration: 5000
        });
      }
    }, {
      duration: 5000
    });
  });

  return (
  <div>
    <div
      id = 'block'
      onClick = {onClick}
      ref = {blockRef}
      style = {
        {
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        }
      }
      >
      {}
    </div>
    <div onClick={() => forceUpdate(forceCount   1)} 
      style = {
        {
          width: '100px',
          height: '100px',
          marginTop: '12px',
          backgroundColor: 'orange'
        }
      }/>
  </div>
  );
};

ReactDOM.render( < div > < FunctionExample / > < /div>, document.getElementById('root'));  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<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>  

Обновление 2 (решение): Если вы хотите иметь переменную в функциональном компоненте, но не хотите, чтобы она повторно отображала компонент при его обновлении, вы можете использовать useRef вместо useState . useRef может использоваться не только для элементов dom и фактически предлагается использовать для переменных экземпляра. Смотрите: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

Комментарии:

1. Пример, использованный для демонстрации этого вопроса, был излишне сложным и подробным. Простого ввода числа было бы достаточно. Кроме того, пожалуйста, не предоставляйте ответы в вопросах, поскольку они, скорее всего, будут пропущены. Вместо этого вам следует добавить ответ ниже.

2. Не стесняйтесь создавать новый вопрос с более простым примером, но это был мой друг из usecase

Ответ №1:

Используйте ref для сохранения значений между вызовами функции без запуска рендеринга

 class ClassExample extends React.Component {
  _isAnimating = false;
  _blockRef = null;
  
  onBlockRef = (ref) => {
    if (ref) {
      this._blockRef = ref;
    }
  }
  
  // Animate the block.
  onClick = () => {
    if (this._isAnimating) {
      return;
    }

    this._isAnimating = true;
    Velocity(this._blockRef, {
      translateX: 500,
      complete: () => {
        Velocity(this._blockRef, {
          translateX: 0,
          complete: () => {
            this._isAnimating = false;
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  };
  
  render() {
    console.log("Rendering ClassExample");
    return(
      <div>
        <div id='block' onClick={this.onClick} ref={this.onBlockRef} style={{ width: '100px', height: '10px', backgroundColor: 'pink'}}>{}</div>
      </div>
    );
  }
}

const FunctionExample = (props) => {
  console.log("Rendering FunctionExample");
  
  const isAnimating = React.useRef(false)
  const blockRef = React.useRef(null);
  
  // Animate the block.
  const onClick = React.useCallback(() => {
    if (isAnimating.current) {
      return;
    }

    isAnimating.current = true
    
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
          complete: () => {
            isAnimating.current = false
          }
        },
        {
          duration: 1000
        });
      }
    },
    {
      duration: 1000
    });
  });
  
  return(
    <div>
      <div id='block' onClick={onClick} ref={blockRef} style={{ width: '100px', height: '10px', backgroundColor: 'red'}}>{}</div>
    </div>
  );
};

ReactDOM.render(<div><ClassExample/><FunctionExample/></div>, document.getElementById('root'));  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<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>  

Комментарии:

1. ну вот и все, спасибо, чувак. Я еще не начал играть с useRef.. это как раз то, что доктор прописал.

2. Спасибо, я даже не думал смотреть на useRef это, но, похоже, они предлагают это для чего-то вроде этого: reactjs.org/docs /…

Ответ №2:

Почему вы думаете, что не можете использовать внутренние переменные так же, как вы делаете с классами?

хорошо, кажется немного грязным, но как насчет изменения состояния useState? 8)

нет, это работает не так, как задумано состояние сбрасывается при повторной визуализации

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

 const FunctionExample = ({ count }) => {
  console.log("Rendering FunctionExample", count);

  // let isAnimating = false; // no good if component rerenders during animation
  
  // abuse useState var instead?
  // let [isAnimating] = React.useState(false);
  
  const blockRef = React.useRef(null);

  // Animate the block.
  const onClick = React.useCallback(() => {
    // use feature of the anim itself
    if (/velocity-animating/.test(blockRef.current.className)) {
      return;
    }
    console.log("animation triggered");
    
    Velocity(blockRef.current, {
      translateX: 500,
      complete: () => {
        Velocity(blockRef.current, {
          translateX: 0,
        }, {
          duration: 1000
        });
      }
    }, {
      duration: 5000
    });
  });

  return (
  <div>
    <div
      id = 'block'
      onClick = {onClick}
      ref = {blockRef}
      style = {
        {
          width: '100px',
          height: '10px',
          backgroundColor: 'red'
        }
      }
      >
      {}
    </div>
  </div>
  );
};

const Counter = () => {
  const [count, setCount] = React.useState(0);
  return <div>
    <FunctionExample count={count} />
    <button onClick={() => setCount(c => c   1)}>Count</button>
  </div>;
}

ReactDOM.render( < div > < Counter / > < /div>, document.getElementById('root'));  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.2/velocity.min.js"></script>

<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.11/lodash.min.js"></script>
<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>  

Комментарии:

1. Они будут переназначаться каждый раз, когда компонент выполняет рендеринг (поскольку функциональный компонент буквально является функцией рендеринга), и, таким образом, он перезапишет свое предыдущее значение. Я признаю, что в этом примере это не имеет значения, но для компонентов, у которых есть дополнительные причины, по которым они могут повторно визуализировать, это будет проблемой. Допустим, что-то вызывает повторный рендеринг блока во время его середины анимации, если пользователь нажмет на это, то он поставит в очередь другую анимацию, потому что для isAnimating снова будет установлено значение false, даже если это все еще середина анимации.

2. хорошо, я понимаю, да. Вы могли бы переместить объявление isAnimating за пределы функции компонента, но тогда, я думаю, это будет применяться ко всем отображаемым экземплярам компонента.

3. Модифицировал его, чтобы изменить состояние useState.. это не рекомендуется использовать, но только потому, что компонент не будет повторно визуализироваться (afaik), но это то, что вы хотите в данном случае.

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

5. Это была хорошая попытка. В идеале компонент представлял бы собой рендеринг своих реквизитов и состояния, вот почему этот isAnimated var не был ни state, ни props в примере класса, поскольку он не влияет на сам рендеринг. Функциональные компоненты, похоже, не имеют этой функции, из-за чего я не могу хранить переменные, отличные от состояния, что вызывает неудачную повторную визуализацию.