ReactJS: как обновить компонент без задержки в 1 клик

Я изучаю React, и у меня проблема с обновлением дочернего компонента, когда состояние родителя изменяется другим дочерним элементом. Я обнаружил, что должен использовать componentWillReceiveProps(), и он работает, но с задержкой в ​​один клик. Что я должен изменить, чтобы получить немедленное обновление?

Я разместил свой код на CodePen, чтобы упростить объяснение. Если лучше опубликовать его здесь, пожалуйста, дайте мне знать, и я обновлю.

FCC: PomodoroClock - CodePen

Проблемы:

  • Когда я увеличиваю или уменьшаю длину в ClockSetter (кнопки + и -), тогда состояние родителя изменяется немедленно, но длина сеанса в Timer показывает предыдущее значение.
  • Когда я нажимаю кнопку сброса, родительское состояние изменяется немедленно, но ничего не происходит с длиной ClockSetter, а длина Timer меняется на ClockSetter. После еще одного щелчка по сбросу длина обоих дочерних элементов изменится на состояние родителя.
  • Если я пытаюсь увеличить или уменьшить после сброса (вместо второго щелчка сброса), он сходит с ума (я не могу найти правило, как оно меняется)

Можно ли это сделать только с React или стоит начать изучать Redux?

Обновлено: мой код

import React from 'react';
import './PomodoroClock.scss';

class PomodoroClock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      sessionLength: 25,
      breakLength: 5,
    };
    this.handleTime = this.handleTime.bind(this);
    this.handleReset = this.handleReset.bind(this);
  }

  handleTime(type, time) {
    this.setState({
      [type]: time
    });
  }

  handleReset() {
    this.setState({
      sessionLength: 25,
      breakLength: 5,
    });
  }

  render() {
    return (
      <div className = "container">
        <ClockSetter clockType = "break" initialLength = {this.state.breakLength} handleTime = {this.handleTime} />
        <ClockSetter clockType = "session" initialLength = {this.state.sessionLength} handleTime = {this.handleTime} />
        <Timer sessionLength = {this.state.sessionLength} reset = {this.handleReset}/>
        <span>TEST: State session - {this.state.sessionLength} State break - {this.state.breakLength}</span>
      </div>
    );
  }
}

class ClockSetter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      length: this.props.initialLength
    }
    this.handleDecrease = this.handleDecrease.bind(this);
    this.handleIncrease = this.handleIncrease.bind(this);
    this.refresh = this.refresh.bind(this);
  }

  handleDecrease() {
    if (this.state.length > 1) {
      this.setState ({
        length: this.state.length - 1
      });
    }
    this.props.handleTime(this.props.clockType+'Length', this.state.length - 1);
  }

  handleIncrease() {
    if (this.state.length < 60) {
      this.setState ({
        length: this.state.length + 1
      });
    }
    this.props.handleTime(this.props.clockType+'Length', this.state.length + 1);
  }

  refresh() {
    this.setState({
      length: this.props.initialLength
    });
  }

  componentWillReceiveProps(props) {
    if (this.state.length !== this.props.initialLength) {
      this.refresh();
    }
  }

  render() {
    let type = this.props.clockType;
    return(
      <div className = "clock-setter">
        <div id = {type + '-label'} className = "first-letter-capitalize">{type + ' Length'}</div>
        <span id = {type + '-decrement'} className = "button" onClick = {this.handleDecrease}>-</span>
        <span id = {type + '-length'}>{this.state.length}</span>
        <span id = {type + '-increment'} className = "button" onClick = {this.handleIncrease}>+</span>
      </div>
    );
  }
}

class Timer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      activeCountdown: 'Session',
      length: this.props.sessionLength
    }
    this.refresh = this.refresh.bind(this);
  }

  refresh() {
    this.setState({
      length: this.props.sessionLength
    });
  }

  componentWillReceiveProps(props) {
    if (this.state.length !== this.props.sessionLength) {
      this.refresh();
    }
  }

  render() {
    return(
      <div className = "timer">
        <span id = "timer-label">{this.state.activeCountdown}</span>
        <div id = "time-left">{this.state.length}</div>
        <span id = "start_stop" className = "button">Start/Stop</span>
        <span id = "reset" className = "button" onClick = {this.props.reset}>Reset</span>
      </div>
    );
  }
}


export default PomodoroClock;

По поводу is it better to post it here directly: да, было бы. Если вы редактировать свой вопрос, есть значок файла с </>, который вы можете использовать, чтобы встроить фрагмент в свой вопрос.

grooveplex 26.02.2019 23:33

Вы следовали руководству, чтобы получить код, который у вас есть сейчас? В вашем коде много проблем. Трудно решить только одну вашу проблему, не объясняя, почему общая архитектура плоха. Например.. componentWillReceiveProps на самом деле устарел сейчас, потому что это привело к именно тому типу проблем, с которыми вы имеете дело сейчас.

azium 26.02.2019 23:35

Кроме того, есть древняя поговорка: «Вы поймете, когда вам понадобится Flux/Redux». :)

grooveplex 26.02.2019 23:35

@grooveplex эта пословица глупа. Многие люди этого не знают, и в настоящее время существует множество конкурирующих решений для редукции в зависимости от потребностей приложения.

azium 26.02.2019 23:36

Да, это вполне возможно сделать только с помощью React. Вы можете построить что угодно и что угодно только с помощью React. Redux и другие помощники по управлению состоянием делают именно это... помогают. но вы не необходимость их. Это не означает, что вы не должны их использовать, но важно знать, как делать все с React, чтобы вы могли принять более обоснованное решение о том, когда вам нужна дополнительная помощь от большего количества библиотек/шаблонов.

azium 26.02.2019 23:38

Ответ готов! надеюсь, это поможет

azium 27.02.2019 00:52
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
3
6
1 076
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

И поскольку вы только начинаете, это идеальное время, чтобы узнать о хуки, потому что они сделают ваш код намного проще и понятнее.

Основная ловушка в вашем коде — дублирование состояния. Начнем с компонента Timer.

Вы устанавливаете его начальное состояние length равным значению его родительского состояния sessionLength. Даже если вы можете воспринимать это как некое «начальное» состояние, а затем после начала обратного отсчета length таймера не будет зависеть от sessionLength, в этом нет необходимости. На самом деле... дублирование состояния не требуется в 99% ситуаций.

Итак, какое состояние у должен у Таймера? Я бы предположил, что у таймера может быть собственное внутреннее состояние счетчика, чтобы вы отображали текущее время, например this.props.sessionLength - this.state.elapsedTime, но в вашем случае таймер на самом деле не отсчитывает время. В любом случае вы отслеживаете текущее время на родительском уровне.

Зная это... каким должно быть состояние таймера? Ничего такого! Ответ - нет государства. Таймер может быть функцией, а не классом, получать свойства и отображать их.

function Timer(props) {
  return (
    <div className = "timer">
      <span id = "timer-label">Session</span>
      <div id = "time-left">{props.sessionLength}</div>
      <span id = "start_stop" className = "button">
        Start/Stop
      </span>
      <span id = "reset" className = "button" onClick = {props.reset}>
        Reset
      </span>
    </div>
  )
}

Если это все, что вы меняете, это уже решает ваш вопрос.

Далее давайте посмотрим на компонент ClockSetter. Вы дублируете состояние здесь точно таким же образом, и не только это, у вас есть дополнительные обработчики, которые просто вызывают обработчик родителей handleTime, внося дополнительные шаги и сложность, которые добавляют ненужный шум в ваше приложение. Давайте вообще избавимся от состояния и дополнительных обработчиков, и поэтому мы снова сможем использовать функцию вместо класса:

function ClockSetter(props) {
  let type = props.clockType
  return (
    <div className = "clock-setter">
      <div id = {type + '-label'} className = "first-letter-capitalize">
        {type + ' Length'}
      </div>
      <span
        id = {type + '-decrement'}
        className = "button"
        onClick = {() => props.handleTime(type + 'Length', props.length - 1)}
      >
        -
      </span>
      <span id = {type + '-length'}>{props.length}</span>
      <span
        id = {type + '-increment'}
        className = "button"
        onClick = {() => props.handleTime(type + 'Length', props.length + 1)}
      >
        +
      </span>
    </div>
  )
}

Я встроил обработчики onClick для краткости. Вы можете написать именованные функции handleDecrease и handleIncrease над оператором return и передать им onClick, если хотите. Хотя это всего лишь вопрос предпочтений.

*Примечание: реквизит теперь length, а не initialLength. Обязательно обновите это при рендеринге компонентов ClockSetter.

Для этого последнего рефакторинга я обновил ваш cdn React, чтобы он указывал на последнюю стабильную версию 16.8.3, так как она включает хуки.

Вместо использования класса давайте напишем обычную функцию и воспользуемся хуком React.useState. Теперь код выглядит так:

function PomodoroClock() {
  let [sessionLength, setSessionLength] = React.useState(25)
  let [breakLength, setBreakLength] = React.useState(5)

  function handleReset() {
    setSessionLength(25)
    setBreakLength(5)
  }

  return (
    <div className = "container">
      <ClockSetter
        clockType = "break"
        length = {breakLength}
        handleTime = {setBreakLength}
      />
      <ClockSetter
        clockType = "session"
        length = {sessionLength}
        handleTime = {setSessionLength}
      />
      <Timer
        sessionLength = {sessionLength}
        reset = {handleReset}
      />
      <span>
        Parent's state TEST: session - {sessionLength} break -
        {breakLength}
      </span>
    </div>
  )
}

и вместо того, чтобы иметь один объект состояния с ключами, которые ссылаются на каждый таймер, поскольку раньше это был единственный способ для компонентов с состоянием, мы дважды вызываем useState каждый с их соответствующим состоянием и обработчиками. Теперь мы можем удалить аргумент type + Length в нашем компоненте ClockSetter:

onClick = {() => props.handleTime(props.length + 1)}

Теперь вся программа:

function PomodoroClock() {
  let [sessionLength, setSessionLength] = React.useState(25)
  let [breakLength, setBreakLength] = React.useState(5)

  function handleReset() {
    setSessionLength(25)
    setBreakLength(5)
  }

  return (
    <div className = "container">
      <ClockSetter
        clockType = "break"
        length = {breakLength}
        handleTime = {setBreakLength}
      />
      <ClockSetter
        clockType = "session"
        length = {sessionLength}
        handleTime = {setSessionLength}
      />
      <Timer
        sessionLength = {sessionLength}
        reset = {handleReset}
      />
      <span>
        Parent's state TEST: session - {sessionLength} break -
        {breakLength}
      </span>
    </div>
  )
}

function ClockSetter(props) {
  let type = props.clockType
  return (
    <div className = "clock-setter">
      <div id = {type + '-label'} className = "first-letter-capitalize">
        {type + ' Length'}
      </div>
      <span
        id = {type + '-decrement'}
        className = "button"
        onClick = {() => props.handleTime(props.length - 1)}
      >
        -
      </span>
      <span id = {type + '-length'}>{props.length}</span>
      <span
        id = {type + '-increment'}
        className = "button"
        onClick = {() => props.handleTime(props.length + 1)}
      >
        +
      </span>
    </div>
  )
}

function Timer(props) {
  return (
    <div className = "timer">
      <span id = "timer-label">Session</span>
      <div id = "time-left">{props.sessionLength}</div>
      <span id = "start_stop" className = "button">
        Start/Stop
      </span>
      <span id = "reset" className = "button" onClick = {props.reset}>
        Reset
      </span>
    </div>
  )
}

ReactDOM.render(
  <PomodoroClock />,
  document.getElementById('root')
);

Ссылка на кодеп

Мы сократили более 50 строк кода, его стало намного легче читать, и нет потенциальной проблемы с дублированием состояния.

Надеюсь, что это поможет и удачного кодирования! Пожалуйста, задавайте любые вопросы, если вам нужно.

Вау! Ты восхитителен. Большое тебе спасибо. Я даже не знал, что есть новая реакция (я использовал create-react-app, когда она была 16.7, и все еще копировал и изменял старые проекты вместо того, чтобы снова использовать create-react-app), поэтому мне нужно обновить свою реакцию, узнать о ловушках а потом попробую воспользоваться вашими советами. Большое холодное пиво для вас, и я обновлю, когда справлюсь с этим :)

Milo K 27.02.2019 20:56

@MiloK Вы слышали о npx? Это похоже на npm, за исключением того, что каждый раз будет загружаться последняя версия приложения create-react-app (и других пакетов). Если у вас есть недавняя версия npm (6.x или около того), она должна быть у вас. :)

grooveplex 01.03.2019 19:35

вы можете обновить существующий проект create-react-app, установив npm последнюю версию react-scripts. не нужно снова вызывать create-реагировать-приложение или что-либо переносить

azium 02.03.2019 00:59

Другие вопросы по теме