Skip to content

Instantly share code, notes, and snippets.

@sastraxi
Last active June 22, 2020 07:28
Show Gist options
  • Select an option

  • Save sastraxi/22a00125c14fcd5536adb7f83ef1f781 to your computer and use it in GitHub Desktop.

Select an option

Save sastraxi/22a00125c14fcd5536adb7f83ef1f781 to your computer and use it in GitHub Desktop.
Store quickly-changing state directly in Redux (e.g. the value of a textarea) without a bajillion actions being dispatched
import React from 'react';
import _ from 'lodash';
/**
* Protect a heavyweight setter by caching values locally.
* Allows functional controlled components with e.g. a redux backing store,
* without dispatching actions on every keypress.
*
* @param {*} valueProp the value to cache (e.g. a key from mapStateToProps)
* @param {*} setterProp the heavyweight setter to protect (e.g. a key from mapDispatchToProps)
* @param {*} debounceTime maximum time to delay calls to props.setterProp, default 500ms
*/
export const debouncedProp = (valueProp, setterProp, debounceTime = 500) =>
function (WrappedComponent) {
return class DP extends React.Component {
// N.B. we keep our own "version" of the value in state to give instant feedback
// to the component without using the relatively-heavy setter (e.g. redux action)
state = {
[valueProp]: undefined,
};
constructor(props) {
super(props);
this.callSetter = _.debounce(this.callSetter, debounceTime);
this.state = {
[valueProp]: props[valueProp],
};
}
// we want to respond to external events that will modify our value,
// but by dispatching our debounced change to e.g. redux, we'll asynchronously
// receive the props we just set later on. this can cause the input value
// to revert to a previously-typed value. So we'll keep track of the values
// that we've dispatched and ignore them when they come back in
// via. componentWillReceiveProps.
pendingValueChanges = [];
componentWillReceiveProps = (nextProps) => {
if (nextProps[valueProp] !== this.state[valueProp]) {
const index = this.pendingValueChanges.indexOf(nextProps[valueProp]);
if (index === -1) {
this.setState({
[valueProp]: nextProps[valueProp],
});
} else {
// ignore this query change (once!) as we were the source
this.pendingValueChanges = [
...this.pendingValueChanges.slice(0, index),
...this.pendingValueChanges.slice(index + 1),
];
}
}
};
valueChanged = (value) => {
this.setState({ [valueProp]: value });
this.callSetter(value);
};
callSetter = (value) => {
this.pendingValueChanges = [...this.pendingValueChanges, value];
this.props[setterProp](value);
};
render = () =>
<WrappedComponent
{...this.props}
{...{
[valueProp]: this.state[valueProp],
[setterProp]: this.valueChanged,
}}
/>;
};
};
@semoal
Copy link

semoal commented Sep 9, 2019

Refactored to match 16.9 methods deprecated:

/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/jsx-props-no-spreading */
import React from 'react';
import debounce from 'lodash.debounce';

/**
 * Protect a heavyweight setter by caching values locally.
 * Allows functional controlled components with e.g. a redux backing store,
 * without dispatching actions on every keypress.
 *
 * @param {*} valueProp the value to cache (e.g. a key from mapStateToProps)
 * @param {*} setterProp the heavyweight setter to protect (e.g. a key from mapDispatchToProps)
 * @param {*} debounceTime maximum time to delay calls to props.setterProp, default 500ms
 */
const debouncedProp = (valueProp, setterProp, debounceTime = 500) => WrappedComponent => {
  return class DP extends React.Component {
    // N.B. we keep our own "version" of the value in state to give instant feedback
    // to the component without using the relatively-heavy setter (e.g. redux action)
    // eslint-disable-next-line react/state-in-constructor
    state = {
      [valueProp]: undefined
    };

    // we want to respond to external events that will modify our value,
    // but by dispatching our debounced change to e.g. redux, we'll asynchronously
    // receive the props we just set later on. this can cause the input value
    // to revert to a previously-typed value. So we'll keep track of the values
    // that we've dispatched and ignore them when they come back in
    // via. componentWillReceiveProps.
    pendingValueChanges = [];

    constructor(props) {
      super(props);
      this.callSetter = debounce(this.callSetter, debounceTime);
      this.state = {
        [valueProp]: props[valueProp]
      };
    }

    componentDidUpdate = prevProps => {
      if (this.props[valueProp] !== prevProps[valueProp]) {
        const index = this.pendingValueChanges.includes(this.props[valueProp]);
        if (index) {
          // eslint-disable-next-line react/no-did-update-set-state
          this.setState({
            [valueProp]: this.props[valueProp]
          });
        } else {
          // ignore this query change (once!) as we were the source
          this.pendingValueChanges = [
            ...this.pendingValueChanges.slice(0, index),
            ...this.pendingValueChanges.slice(index + 1)
          ];
        }
      }
    };

    valueChanged = value => {
      this.setState({ [valueProp]: value });
      this.callSetter(value);
    };

    callSetter = value => {
      this.pendingValueChanges = [...this.pendingValueChanges, value];
      this.props[setterProp](value);
    };

    render() {
      return (
        <WrappedComponent
          {...this.props}
          {...{
            [valueProp]: this.state[valueProp],
            [setterProp]: this.valueChanged
          }}
        />
      );
    }
  };
};

export default debouncedProp;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment