pzavolinsky / react-value

React Value helpers

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

React value

When we think about how a user interacts with our components, we can classify those components in one of two categories: read-only components and value components.

Value components display a value and provide a way to update the value. Read-only components only display a value.

The scenic route

Intuitively which of the following would you say is a read-only component and which one is a value component?

const WithBorder1 = ({ value }) =>
  <div className="with-border">
    {value}
  </div>;

const WithBorder2 = ({ value, onChange }) =>
  <div className="with-border">
    <input value={value} onChange={onChange} />
  </div>;

The dead giveaway is the onChange prop. We supply a value in the value prop and, when the user produces a new value, the component calls the onChange function with that new value.

How about this one?

const MoodSwing = ({ happy, newMood }) =>
  <button onClick={() => newMood(!happy)}>
    {happy
      ? ':)'
      : ':('
    }
  </button>;

Even though the names are all wrong, this component toggles the happy prop. This means that happy is the component's value and newMood is a degenerate onChange.

Lets rewrite MoodSwing to use value and onChange:

const MoodSwing = ({ value, onChange }) =>
  <button onClick={() => onChange({ target:{ value: !value } })}>
    {value
      ? ':)'
      : ':('
    }
  </button>;

Now what's all that nonsense in the onChange call?

If you recall from React's built-in components, the argument to onChange is an event. More specifically, a synthetic event whose target prop represents the component that changed with an updated value prop.

So in short, if we want to be (a bit) consistent with the built-in components we need to wrap our new value in something that has a target, thus:

onChange({ target: { value: newValue } })

Technically, if we wanted to be even more consistent, at the expense of efficiency, we should merge the previous props into target as well:

onChange({ target: Object.assign({}, this.props, { value: newValue }) })

Personally, I think this is too much so I just stick with target: { value: newValue }.

But wait, why should we even bother to be consistent?

Consider this guy:

const Upper = ({ children, value, onChange }) =>
  React.cloneElement(children, {
  		value,
  		onChange: e => onChange({
  			target: {
    			value: (e.target.value || '').toUpperCase()
    		}
  		})
  	});

Now let's apply Upper to input (a built-in component):

const UpperInput = props => <Upper {...props}><input/></Upper>;

Cool, how about the old WithBorder2 from a previous example:

const UpperBorder = props => <Upper {...props}><WithBorder2/></Upper>;

So the obvious argument for consistency is that you can use your components in place of the built-in ones.

Regardless of whether you wrap your onChange value or not, and whether you merge the props into target or not, there something pretty powerful going on here:

The value/onChange props provide a predictable interface for components that hold a value.

In other words, if we stick to the value/onChange props, just by looking at the component's props you know what can change (i.e. the value, and only the value) and what is just arguments required to display or control the component's behavior (e.g. className, readOnly, etc.).

Making our life easier

Ok, so by now we are cool with sticking everything that can change in a single value prop and to inform value changes though a single onChange callback. How can we simplify all the target boilerplate?

Lets do this step-by-step:

class MoodSwing extends React.Component {
  render() {
    const { value, onChange } = this.props;
    return <button onClick={() => onChange({ target: { value: !value } })}>
      {value
        ? ':)'
        : ':('
      }
    </button>;
  }
}

Notice the call to onChange, that whole target and value business? We are going to be doing that thing quite a bit, so we could write a helper for that:

class ValueComponent extends React.Component {
  setValue(value) {
    const { onChange } = this.props;
    onChange({ target: { value } });
  }
}

class MoodSwing extends ValueComponent {
  render() {
    const { value } = this.props;
    return <button onClick={() => this.setValue(!value)}>
      {value
        ? ':)'
        : ':('
      }
    </button>;
  }
}

Better, but how about the case when we just want to put stuff around an existing component? For example:

class MoodSwingWithBorder extends ValueComponent {
  render() {
    const { value } = this.props;
    return <div className="with-border">
      <MoodSwing value={value} onChange={e => this.setValue(e.target.value)} />
    </div>;
  }
}

Is there a way to simplify that onChange?

You can think of setValue as a way of wrapping a value with an event and e.target.value as a way of unwrapping an event to extract the value. So calling setValue(e.target.value) yields an event similar to e (at least regarding to its value):

class MoodSwingWithBorder extends ValueComponent {
  render() {
    const { value, onChange } = this.props;
    return <div className="with-border">
      <MoodSwing value={value} onChange={onChange} />
    </div>;
  }
}

How about when value is a complex object? For example:

class Person extends ValueComponent {
  render() {
    const { value } = this.props;
    return <div>
      <div>
      First
      <input
        value={value.first}
        onChange={e => this.setValue(Object.assign({}, value, { first: e.target.value }))}
      />
      </div>
      <div>
      Last
      <input
        value={value.last}
        onChange={e => this.setValue(Object.assign({}, value, { last: e.target.value }))}
      />
      </div>
    </div>;
  }
}

Clearly we need a way to merge a known key into the value when the input changes so lets add that helper into ValueComponent:

class ValueComponent extends React.Component {
  setValue(value) {
    const { onChange } = this.props;
    onChange({ target: { value } });
  }
  mergeValue(propName, propValue) {
    const { value } = this.props;
    this.setValue(Object.assign({}, value, { [propName]: propValue }));
  }
}

class Person extends ValueComponent {
  render() {
    const { value } = this.props;
    return <div>
      <div>
      First
      <input
        value={value.first}
        onChange={e => this.mergeValue('first', e.target.value)}
      />
      </div>
      <div>
      Last
      <input
        value={value.last}
        onChange={e => this.mergeValue('last', e.target.value)}
      />
      </div>
    </div>;
  }
}

Better, but we can go further. See how the built-in input is a value component, and we want to merge its value into first and last? We could write a helper to simplify this case even further:

class ValueComponent extends React.Component {
  setValue(value) {
    const { onChange } = this.props;
    onChange({ target: { value } });
  }
  mergeValue(propName, propValue) {
    const { value } = this.props;
    this.setValue(Object.assign({}, value, { [propName]: propValue }));
  }
  onChangeMergeValue(propName) {
    return e => this.mergeValue(propName, e.target.value);
  }
}

class Person extends ValueComponent {
  render() {
    const { value } = this.props;
    return <div>
      <div>
      First
      <input
        value={value.first}
        onChange={this.onChangeMergeValue('first')}
      />
      </div>
      <div>
      Last
      <input
        value={value.last}
        onChange={this.onChangeMergeValue('last')}
      />
      </div>
    </div>;
  }
}

Playing nice with React

So far in our examples we've been passing lambdas to our child components. For example:

class Person extends ValueComponent {
  render() {
    const { value } = this.props;
    return <div>
      <div>
      First
      <input
        value={value.first}
        onChange={e => this.mergeValue('first', e.target.value)}
      />
      </div>
      <div>
      Last
      <input
        value={value.last}
        onChange={e => this.mergeValue('last', e.target.value)}
      />
      </div>
    </div>;
  }
}

See that e => this.mergeValue('first', e.target.value)? That's a lambda that we are passing to input and this is bad.

The reason why this is bad is that every time that we render() we will be creating a new function with essentially the same implementation. In other words, the props for the inputs will change on every render, even if we don't change the value. This in turn makes it very difficult to apply performance optimizations to those inputs.

If you look back at our onChangeMergeValue implementation you'll see that we are returning a lambda so even though is not apparent, we have the same problem. That is:

this.onChangeMergeValue('last') // is the same as
e => this.mergeValue('last', e.target.value)

Luckily there is an easy workaround: we can cache those lambdas by the propName.

class ValueComponent extends React.Component {
  constructor(props, ctx) {
    super(props, ctx);
    this.mergeValueCache = {};
  }
  setValue(value) {
    const { onChange } = this.props;
    onChange({ target: { value } });
  }
  mergeValue(propName, propValue) {
    const { value } = this.props;
    this.setValue(Object.assign({}, value, { [propName]: propValue }));
  }
  onChangeMergeValue(propName) {
    return this.mergeValueCache[propName]
      || (this.mergeValueCache[propName] =
        e => this.mergeValue(propName, e.target.value)
      );
  }
}

Note that this makes sense because the propNames that we use are always string literals (e.g. first, last, etc.) and we know for a fact that there's going to be only a few of those.

Stateless value components

So far our ValueComponent base class is looking good but there is one catch, we can only use it for class components. It would be nice if we could write a high-order function that could turn any stateless component into a ValueComponent.

Lets give it a try:

const valueComponent = compFn =>
  class FnValueComponent extends ValueComponent {
    constructor(props, ctx) {
      super(props, ctx);
      this.setValue = this.setValue.bind(this);
      this.mergeValue = this.mergeValue.bind(this);
      this.onChangeMergeValue = this.onChangeMergeValue.bind(this);
    }
    render() {
      return compFn(
        Object.assign({}, this.props, {
          setValue: this.setValue,
          mergeValue: this.mergeValue,
          onChangeMergeValue: this.onChangeMergeValue
        })
      );
    }
  };  

And now we can rewrite our Person to be stateless:

const StatelessPerson = valueComponent(
  ({ value, onChangeMergeValue }) =>
    <div>
      <div>
        First
        <input
          value={value.first}
          onChange={onChangeMergeValue('first')}
        />
      </div>
      <div>
        Last
        <input
          value={value.last}
          onChange={onChangeMergeValue('last')}
        />
      </div>
    </div>
);

Summary

export class ValueComponent extends React.Component {
  constructor(props, ctx) {
    super(props, ctx);
    this.mergeValueCache = {};
  }
  setValue(value) {
    const { onChange } = this.props;
    onChange({ target: { value } });
  }
  mergeValue(propName, propValue) {
    const { value } = this.props;
    this.setValue(Object.assign({}, value, { [propName]: propValue }));
  }
  onChangeMergeValue(propName) {
    return this.mergeValueCache[propName]
      || (this.mergeValueCache[propName] =
        e => this.mergeValue(propName, e.target.value)
      );
  }
}

export const valueComponent = compFn =>
  class FnValueComponent extends ValueComponent {
    constructor(props, ctx) {
      super(props, ctx);
      this.setValue = this.setValue.bind(this);
      this.mergeValue = this.mergeValue.bind(this);
      this.onChangeMergeValue = this.onChangeMergeValue.bind(this);
    }
    render() {
      return compFn(
        Object.assign({}, this.props, {
          setValue: this.setValue,
          mergeValue: this.mergeValue,
          onChangeMergeValue: this.onChangeMergeValue
        })
      );
    }
  };  

About

React Value helpers

License:MIT License


Languages

Language:TypeScript 100.0%