Scott Schafer
Scott Schafer

Reputation: 497

How to make reusable React/MobX components?

The examples I've seen of integrating React components with MobX stores seem to be tightly coupled. I would like to do this in a more reusable way, and would appreciate help understanding the "right" way to do this.

I wrote the following code (React + MobX + Typescript) to illustrate what I want to do and the issue I'm running into.

The store has multiple observable timestamps.

/***
 * Initialize simple store
 */
class MyStore {
  @observable value: number;
  @action setValue(val: number) { this.value = val; }

  @observable startTimestamp: number;
  @action setStartTimestamp(val: number) { this.startTimestamp = val; }

  @observable endTimestamp: number;
  @action setEndTimestamp(val: number) { this.endTimestamp = val; }
}

Let's say I want to make a reusable date input component allowing the user to enter a date for either startTimestamp, endTimestamp, or some other store property. More generally, I want to create a component that I can use to modify any arbitrary property of any store.

My best understanding of React/MobX integration is that components receive a MobX store, read observable properties of the store and may execute actions to modify those properties. However, this seems to assume that components are wired to the names of store properties, which makes them not fully reusable.

I've experimented with the following "proxy store" approach to expose the property I want to the component as "value":

class MyStoreTimestampProxy {
  constructor(private store: MyStore, private propertyName: 'startTimestamp' | 'endTimestamp') {
  }

  @observable get value() {
    return this.store[this.propertyName];
  }

  @action setValue(val: number) {
    this.store[this.propertyName] = val;
  }
};
const myStoreStartTimestamp = new MyStoreTimestampProxy(myStore, 'startTimestamp');
const myStoreEndTimestamp = new MyStoreTimestampProxy(myStore, 'endTimestamp');

However, I feel like I'm not doing things the React/MobX way somehow, and want to understand the best practice here. Thank you!

Full code follows:

import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { observable, action } from 'mobx';
import { observer } from 'mobx-react';

/***
 * Initialize simple store
 */
class MyStore {
  @observable value: number;
  @action setValue(val: number) { this.value = val; }

  @observable startTimestamp: number;
  @action setStartTimestamp(val: number) { this.startTimestamp = val; }

  @observable endTimestamp: number;
  @action setEndTimestamp(val: number) { this.endTimestamp = val; }
}

const myStore = new MyStore();
myStore.setValue(new Date().getTime());

/**
 * My time input component. Takes in a label for display, and a store for reading/writing the property.
 */
interface IDateInputProps {
  label: string;
  store: {
    value: number;
    setValue(val: number): void;
  }
}

interface IDateInputState {
  value: string;
}

@observer class DateInput extends React.Component<IDateInputProps, IDateInputState> {
  constructor(props: IDateInputProps) {
    super(props);
    this.state = { value: new Date(props.store.value).toDateString() };
  }

  render() {
    return (
      <div>
        <label>{this.props.label}
          <input
            value={this.state.value}
            onChange={this.onChange.bind(this)} />
        </label>
      </div>
    );
  }

  onChange(event) {
    const date = new Date(event.target.value);
    this.setState({ value: event.target.value });
    this.props.store.setValue(date.getTime());
  }
}


/**
 *  Test view
 *
 */
class TestView extends React.Component {
  render() {
    return (
      <div>
        <DateInput label="Editing the value property of store: " store={myStore}></DateInput>

        {/* How to create components for startTimestamp and endTimestamp */}
      </div>
    );
  }
};

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

Upvotes: 4

Views: 1488

Answers (1)

samb102
samb102

Reputation: 1114

The main issue come from you have store's state dependencies in your DateInput component, which make it hardly reusable. What you need is to break theses references, and rather than access store references directly from the reusable component, you have to give them in props from the parent.

Let's go.


First, if I well understood, I think that your issue can be easier if you modify your store like this :

class MyStore {
   @observable state = {
     dateInputsValues : [
                          { value: '01/01/1970', label: 'value'}, 
                          { value: '01/01/1970', label: 'startTimestamp' }, 
                          { value: '01/01/1970', label: 'endTimestamp' }
                        ]
   }
}

Now, you will be able to loop over dateInputsValues in your DateInput and avoid code repetition.

Then, rather than passing the whole store, why don't you just pass props with the observables you need (i.e. label & value) ?  

@observer 
class TestView extends React.Component {
  render() {
    return (
      <div>
        {myStore.state.dateInputsValues.map(date => 
          <DateInput 
             label={`Editing the ${date.label} property of store: `} 
             value={date.value} 
          />
        }
      </div>
    );
  }
};

Break old references to the store in DateInput (that, as you said, make the component "tightly coupled" to the store and make it hardly reusable) . Replace them by the props we added.

Remove DateInput internal state. In the actual code, you don't need a component internal state for the moment. You can directly use the state store for this kind of scenario.

Finally, add an action method that modify the value prop as you seem to be in MobX strict mode (otherwise, you could have set the value outside an action)

@observer 
class DateInput extends React.Component<IDateInputProps, IDateInputState> {    
  render() {
    return (
      <div>
        <label>{this.props.label}
          <input
            value={this.props.value}
            onChange={this.onChange} />
        </label>
      </div>
    );
  }

  onChange = event => {
    const date = new Date(event.target.value);
    this.setDateInputValue(date.getTime());
  }

  @action
  setDateInputValue = val => this.props.value = val
}

Upvotes: 4

Related Questions