Warm tip: This article is reproduced from stackoverflow.com, please click
javascript knockout.js reactjs

Using React components inside KnockoutJS

发布于 2020-05-05 11:32:25

We have a big web application mostly built using KnockoutJS. I'm looking at if there is a possibility to migrate to React, but in a way that require rewriting the entire application. My idea was to use a bottom-up approach: start by replacing the basic building blocks one by one.

I was inspired by some prior work to call ReactDOM.render inside a KnockoutJS binding handler:

ko.bindingHandlers.react = {
    init() {
        return {controlsDescendantBindings: true};
    },
    update(element, valueAccessor) {
        const {component, props} = valueAccessor();
        ReactDOM.render(React.createElement(component, props), element);
    }
};

Knockout has its own dependency tracking system, so it will call the update method whenever some data changes.

It works perfectly, and the component is re-rendered to reflect any changes to data. However, it breaks down when data is updated inside a React event handler. E.g., this component does not work as expected:

const Input = function ({value}) {
    return <input type="text" 
                  value={value()} 
                  onChange={e => value(e.target.value)}/>;
}

Note: value in this case is a ko.observable, and calling it like in the event handler will cause the bindingHandler's update method to be called, which in turn calls ReactDOM.render. However, this render only works once, after that the component stops updating.

The issue is demonstrated in this CodePen. Click the box and try to type something. One update goes through, after than, the component's function stops being called.

Edit: I believe the issue is that the second call to ReactDOM.render (when the value is updated by the user in the onChange handler) is not synchronous. This means that Knockout's dependency detection cannot work and any subsequent calls no longer call the update method of the binding handler.

Can this be circumvented somehow?

Questioner
waxwing
Viewed
24
waxwing 2020-02-25 23:34

As I guessed, the issue seems to be that ReactDOM.render is asynchronous in some cases - in this case when called from an event handler in React.

This means that if you dereference any observables that you depend on in the update method itself, Knockout's dependency tracking mechanism works as expected. This is why the modification Gawel1908 proposed makes it work - not because the value is "reset", but because props.value is dereferenced.

I decided to instead use a convention: always unwrap any such observables in the valueAccessor itself:

<div data-bind="react: { component: Input, props: { value: val(), setValue: val }}">
</div>

And don't unwrap it in the component:

const Input = function ({value, setValue}) {
  return <input 
           type="text" 
           value={value} 
           onChange={e => setValue(e.target.value)}/>;
}

Updated, working codepen.