10

I'm trying to debounce textarea value with react/redux and show the debounced value in div#preview but i'm getting synthetic event warning after first function call.

I have reducer for handling textarea value state which is working as intended, but for simplicity i've wrote local state in this snippet.

If there is a better method besides debounce to avoid react rerendering after each keypress I would love to know. Thanks in advance.

class TextArea extends React.Component {
  constructor(props){
    super(props);
    this.state = {foo: ''}
  }
  
  handleInputChange = _.debounce((e) => {
    e.persist();
    let value = e.target.value;
    this.setState({foo: value});
  }, 300);

  render() {
    return (
      <div>
       <textarea onChange={(e)=>this.handleInputChange(e)} value={this.state.foo}></textarea>
       <p id="preview">{this.state.foo}</p>
      </div>
    );
  }
}


ReactDOM.render(
  <TextArea />,
  document.getElementById("react")
);
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
nehel
  • 845
  • 3
  • 16
  • 29
  • Your value is based on state, but that only updates in a debounced way... Does that work for you? Why do you need to debounce? – Omri Aharon Apr 30 '17 at 18:09
  • As stated above, i have a `preview div` and if there is a long keypress or like 100+ characters in `textarea` react is starting to be laggy as hell due to constant rerendering. And yes, the debounce is working though only once, after given debounce time warning shows up. React warning for nullified values : `This synthetic event is reused for performance reasons. If you're seeing this, you're accessing the property target on a released/nullified synthetic event. This is set to null. If you must keep the original synthetic event around, use event.persist().` – nehel Apr 30 '17 at 18:17
  • I have encountered lags when dealing with redux cycles. On local state of the component I haven't seen this happening.. can you reproduce in a fiddle? – Omri Aharon Apr 30 '17 at 18:23
  • Hmm indeed, got the same. Expected a similar situation with local state. Aight, then apparently it's something with redux cycles. First guess it's due to constant mapping states to props and constant dispatching to props, so maybe i can debounce a mapping state to props :o – nehel Apr 30 '17 at 18:28
  • I'd suggest rethinking architecture, perhaps you don't need the value in the redux store? – Omri Aharon Apr 30 '17 at 18:30
  • I wish I wouldn't need, but i'm sending the `textarea` value with `author` and `title` of the post to the db. Thanks for help though, at least i know where to find the fix :D – nehel Apr 30 '17 at 18:31

4 Answers4

4

You get this error because you try to .persist() the event inside the debounce's timeout. When the timeout invokes the callback, the synthetic event was already released. So you'll have to persist the event outside of the debounce.

However, your idea has another problem. Since the textbox is a controlled component, debouncing the updated value, would cause the textbox to render (part of) the text only after the used stopped typing.

To prevent that you need to update the state for the controlled element immediately, and debounce the update for the display state (or the redux action dispatch).

For example:

class TextArea extends React.Component {
  constructor(props){
    super(props);
    this.state = { foo: '', controlled: '' }
  }
  
  updateFoo = _.debounce((value) => { // this can also dispatch a redux action
    this.setState({foo: value});
  }, 300);
  
  handleInputChange = (e) => {
    const value = e.target.value;
    
    this.setState({
      controlled: value
    });
    
    this.updateFoo(value);
  }

  render() {
    return (
      <div>
       <textarea onChange={ this.handleInputChange }
       value={this.state.controlled} />
       <p id="preview">{this.state.foo}</p>
      </div>
    );
  }
}


ReactDOM.render(
  <TextArea />,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="react"></div>
Ori Drori
  • 183,571
  • 29
  • 224
  • 209
  • Ohh i see where the magic comes from now. Though, since there is second state `controlled` does that mean that I have to add second state to the reducer that is for updating textarea value or separate reducer? Unless i have to set `controlled` state locally. – nehel May 01 '17 at 14:14
  • You can set the `controlled` state locally, since it's needed just to update the `textarea`, in the `debounce` you'll dispatch the action that will update the store. – Ori Drori May 01 '17 at 15:42
1

The other answer already covered the problem with persisting the event. For the input debouncing aspect, you may want to read my blog post Practical Redux, Part 7: Form Change Handling. In that post, I show a reusable component that can handle debouncing text input updates for the rest of the application, while allowing them to re-render immediately with the current value.

markerikson
  • 63,178
  • 10
  • 141
  • 157
  • Please provide an actual explanation instead of providing an external link. Not only will it avoid your answer becoming obsolete if the article ever disappear, it'll also help narrow the actual interesting part of your lengthy article. :) – Vadorequest Feb 14 '21 at 19:09
0

I struggled a lot with this and wasn't able to achieve a satisfying result by myself. Eventually, I used https://github.com/nkbt/react-debounce-input which works perfectly and is much simpler than my previous failed attempts.

  /**
   * Updates the current node "text" value.
   *
   * @param event
   */
  const onTextInputValueChange = (event: any) => {
    const newValue = event.target.value;
    patchCurrentNode({
      data: {
        text: newValue,
      },
    } as InformationNodeData);
  };
    

  <DebounceInput
          element={'textarea'}
          debounceTimeout={500}
          placeholder={'Say something here'}
          onChange={onTextInputValueChange}
          value={node?.data?.text}
        />

patchCurrentNode writes to my Recoil store.

The DebounceInput component handles an internal state to display the latest value, while only updating the value in the store only once in a while.

Implementation isn't specific to Recoil, and would likely work like a charm using Redux.

Vadorequest
  • 16,593
  • 24
  • 118
  • 215
0

Functional component:

import React, { useState, useMemo } from "react";
import debounce from "lodash.debounce";

export default function TextArea() {
  const [foo, setFoo] = useState("");
  const [controlled, setController] = useState("");

  const updateFoo = useMemo(
    () =>
      debounce((value) => {
        // this can also dispatch a redux action
        setFoo(value);
      }, 1000),
    []
  );

  const handleInputChange = (e) => {
    const value = e.target.value;

    setController(value);

    updateFoo(value);
  };

  return (
    <div>
      <textarea onChange={handleInputChange} value={controlled} />
      <p id="preview">{foo}</p>
    </div>
  );
}

Reinier68
  • 2,450
  • 1
  • 23
  • 47