Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to properly handle subscriptions depending on property

I have a global service widgetService which holds data for a number of widgets, each identified by a widgetID. Each widget's data can change at any time. I want to display a widget with a React component, say WidgetReactComponent.

The react component shall take a widget ID as property and get the information to display from the widget service. A widget's data can be queried from the widget service with the method getWidgetData(widgetID). And in order to be able to publish data changes, it also offers two methods: addListenerForWidget(widgetID, listener) and removeListenerForWidget(widgetID, listener).

When assuming that the property is set once and never changed, this can be achieved like this, following React's recommendations:

class WidgetReactComponent extends Component {
    constructor() {
        super();
        this.state = {
            data: widgetService.getWidgetData(this.props.widgetID)
        };
        this._onDataChange = this._onDataChange.bind(this);
    }

    _onDataChange(newData) {
        this.setState({data: newData});
    }

    componentDidMount() {
        // React documentation: "This method is a good place to set up any subscriptions."
        widgetService.addListenerForWidget(this.props.widgetID, this._onDataChange);
    }

    componentWillUnmount() {
        // React documentation: "Perform any necessary cleanup in this method, such as [...] cleaning up any subscriptions that were created in componentDidMount()."
        widgetService.removeListenerForWidget(this.props.widgetID, this._onDataChange);
    }

    render() {
        return <div className="Widget">{this.state.data.stuff}</div>;
    }
}

The component may then be used like this:

<ReactWidgetComponent widgetID={17} />

However, the widgetID property may change at any time, and the component has to handle this in order to function properly under all circumstances. By react's recommendation, this should be handled by setting the state based on properties using the static getDerivedStateFromProps function. But since it is static, I do not have access to the component and cannot change the listeners accordingly.

One way to work around this would be to store the widgetID in the state, and then use the lifecycle method componentDidUpdate to detect the change, like this:

constructor() {
    super();
    this._onDataChange = this._onDataChange.bind(this);
}

static getDerivedStateFromProps(nextProps) {
    return {
        widgetID: nextProps.widgetID,
        data: widgetService.getWidgetData(nextProps.widgetID)
    };
}

componentDidUpdate(prevProps, prevState) {
    if (prevState.widgetID !== this.state.widgetID) {
        widgetService.removeListenerForWidget(prevState.widgetID, this._onDataChange);
        widgetService.addListenerForWidget(this.state.widgetID, this._onDataChange);
    }
}

However, componentDidUpdate won't be called when shouldComponentUpdate returns false. This doesn't feel like a safe way of doing this. Also I believe the listeners will be wrong for the entire timespan between the property change and the update's completion. How could I safely implement this?

like image 623
Felk Avatar asked May 06 '18 20:05

Felk


Video Answer


1 Answers

You don't need to store widgetID in state, you can compare prevProps with this.props:

componentDidUpdate(prevProps, prevState) {
  if (prevProps.widgetID !== this.props.widgetID) {
    widgetService.removeListenerForWidget(prevProps.widgetID, this._onDataChange);
    widgetService.addListenerForWidget(this.props.widgetID, this._onDataChange);
  }
}

You will also need to add the listener in componentDidMount as componentDidUpdate is not called on first rendering:

componentDidMount() {
  widgetService.addListenerForWidget(this.props.widgetID, this._onDataChange);
}

Regarding your concerns:

componentDidUpdate won't be called when shouldComponentUpdate returns false

From the docs:

Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props.

So if you decided to not update the component when this.props.widgetID changes, then you are violating the assumption/purpose of shouldComponentUpdate and should not expect your widget listener to be updated.

A lot of things will not work as intended if you misuse shouldComponentUpdate anyway (eg. component not updated to reflect new data), so relying on an API being used correctly as per official docs is a necessity to achieve simplicity, rather than something to be avoided.

the listeners will be wrong for the entire timespan between the property change and the update's completion

By this logic, when you update some displayed data in an event handler, you can also claim that the data displayed is wrong for the entire timespan between the event and the re-rendering. You can even claim that your text editor is displaying the wrong data between the time you press a keyboard key and rendering of the key on the screen.

like image 94
Roy Wang Avatar answered Oct 23 '22 16:10

Roy Wang