Skip to Content
 December 21, 2019
 5 Minutes
 Computer Science
 React, Knockout, Durandal, Aurelia

I was put onto a project at work that started around 2011, ancient by web standards. It uses Durandal, basically a SPA framework that uses Knockout as the UI library. I'm not the biggest fan of Knockout, so I decided to try to make Knockout interoperable with React, for incremental adoption. So, here are some of the steps I went through to achieve this.

Setting Up a Custom Binding

Knockout has this concept of custom bindings, these bindings can be specified in your views and allow for rendering DOM elements. So, we'll utilize this for a React binding:

// custom 'react' binding
ko.bindingHandlers.react = {
	update: render
}

function render(element, valueAccessor) {
    const { Component, ...props } = valueAccessor();

    ReactDOM.render(
    	<Component {...props} />, 
    	element
    );
}
/* '<Component {...props} />,' could be replaced by
'React.createElement(Component, props),' 
if you don't want JSX in the binding */

Before explaining how this works, here's how it can be used in a view with a corresponding view model:

<!-- Knockout view -->
<div 
    data-bind="react: { 
    	Component: ReactComponent, 
    	anyPropNameIWant: theFirstPropName,
    	aDifferentPropName: anotherProp
    }"
></div>
// Durandal/RequireJS/Knockout view model based on the Durandal starter kit's pattern
define([
    '../bindings/React', //A normal app might instead make this binding global
    './ReactComponent'
], function (reactBinding, ReactCounter) {
	var constructor = function() {
        this.ReactComponent = ReactComponent;
        this.theFirstPropName = 55;
        this.anotherProp = 'wow another prop';
    }
    
    return constructor;
});

The binding uses 2 parameters, element and valueAccessor. Where element represents the DOM element that the custom binding was placed on, in this example, it would be the div. The second parameter, valueAccessor is a function that returns the object specified in the binding, in this example it would be the object that contains the keys and corresponding values of Component, anyPropNameIWant, and aDifferentPropName.

The element Parameter

The element parameter (the div) is used by the ReactDOM.render() API to render the React component inside of the div and subsequently reconcile the component.

The valueAccessor Parameter

The valueAccessor parameter is destructured and uses the spread operator so that the prop names aren't strict or hard coded and can be anything the client of this binding wants. JSX spread is then used on the React component to apply all of the specified props.

An Aside on JSX Spread

JSX spread is consider to be an anti-pattern. To my knowledge, given we want the clients of our binding to use any name they want for their React props, JSX spread would be required unless we disallowed custom prop names in favor of something like prop1, prop2, prop3, etc. that every client of the binding must use. Personally, I value the developer experience and the less complex binding more than the additional, likely negligible re-renders.

One-way Binding

One-way binding, meaning the flow of data only goes from Knockout to React for reconciliation, works pretty seamlessly with the above custom binding. All that needs to be done is to pass the value of a Knockout observable as a prop to the custom binding, as follows:

<!-- Knockout view -->
<div 
    data-bind="react: { 
    	Component: ReactComponent, 
    	valueOfObservable: anObservable()
    }"
></div>

Where anObservable is defined in the view model to the effect of:

anObservable = ko.observable('')

In Knockout the update function in custom bindings are called on initialization and when something like an observable is updated. In this example, when anObservable is updated, by doing something like anObservable('new value').

This would cause a call to the update function and thus pass the prop(s) down to the React component for reconciliation.

The Result

Now lets see what one-way binding would actually look like:

Two-way binding example

To accomplish this the Knockout view would look something like this:

<p>Knockout textbox:&nbsp; 
    <input type="text" data-bind="textInput: aTextObservable" />
</p>

<div data-bind="react: {
		Component: OneWayBinding,
   		text: aTextObservable()
	}"
></div>

where aTextObservable is an observable defined in the Knockout view model and where the OneWayBinding React component would be:

    const OneWayBinding = ({ text }) => {
        const [displayText, setDisplayText] = React.useState(text);

        React.useEffect(() => {
            setDisplayText(text);
        }, [text]);

        return (
            <p>
                React textbox:&nbsp;
                <input 
                    type="text" 
                    value={displayText} 
                    onChange={e => setDisplayText(e.target.value)} 
             	/>
            </p>
        );
    }

The useEffect hook here is called when aTextObservable gets a new value (i.e. something like aTextObservable('New text')). When called it sets the the text displayed for the React textbox. The useEffect hook in this example could be omitted if you didn't want/need to be able to update the text for the React textbox through typing in it.

If you don't need to maintain local state in the React component that's based on something like a Knockout observable then you could instead do this:

const OneWayBinding = ({ text }) => <p>React text: {text}</p>

which would result in this:

Basic one-way binding example

Two-way Binding

Two-way binding, meaning:

  • Knockout passing something like an observable into a React component for reconciliation.
  • Updates inside the React component to the passed in observable also propagate back to Knockout, triggering any related events (updating the view, triggering observable subscriptions, etc.).

gets quite a bit uglier.

The Problem

We now have a problem, as stated before, the update function in Knockout custom bindings are only triggered when a property in the binding is updated. So, the problem here is how do we get changes to properties in the React component to propagate back to Knockout? The fairly straightforward approach is to pass a reference of something like an observable, as opposed to the value of the observable, into the binding so that React can update that observable, this would be done simply like this:

<!-- Knockout view -->
<div 
    data-bind="react: { 
    	Component: ReactComponent, 
    	observableReference: anObservable
    }"
></div>

As you may have noticed this still doesn't solve the problem because when the value of the observable is changed the update function will not be called again because the reference to the observable obviously will not have changed.

A Solution

My, admittedly clunky, solution is to pass in the value of the observable to the binding so the update function in the binding is actually called when the observable changes, similar to the one-way binding methodology:

<!-- Knockout view -->
<div 
    data-bind="react: { 
    	Component: ReactComponent, 
    	observableReference: anObservable,
        dummy: anObservable()
    }"
></div>

Unfortunately, with this method the dummy property duplicates data that is already present in the object, subsequently passes unused/unnecessary things into the React component, and burdens the clients of this binding with additional complexity. This was the answer I came to, but there's probably something better out there, if you have any ideas I'd liked hear about it!

With this solution the same custom React binding is used, but there's still some work to be done to improve the developer experience on the React side of things.

Custom Observable Hook

For ease of use with the passed in observable reference (anObservable from the previous example), here's a custom hook that's basically just the normal useState API:

function useObservableState(observable) { // Where observable is a reference
    const [value, setValue] = React.useState(observable())

    const updateNormalAndObservableState = state => {
        observable(state);
        setValue(state);
    }
    
    React.useEffect(() => {
        setValue(observable()); 
    }, [observable()])

    return [value, updateNormalAndObservableState];
}

The only differences, are that when:

  • You use the set function you are also updating the observable, which will also trigger events back in Knockout land.

  • The observable is updated in Knockout the useEffect hook syncs the local state in the React component with the updated observable value.

The Result

Now lets see what two-way binding would actually look like:

One-way binding example

To accomplish this the Knockout view would look something like this:

<p>Knockout textbox:&nbsp; 
    <input type="text" data-bind="textInput: aTextObservable" />
</p>

<div data-bind="react: {
 		Component: TwoWayBinding,
		textObservable: aTextObservable,
		dummy: aTextObservable()
	}"
></div>

where aTextObservable is an observable defined in the Knockout view model and where the TwoWayBinding React component would be:

const TwoWayBinding = ({ textObservable }) => {
	const [displayText, setDisplayText] = useObservableState(textObservable);

	return (
		<p>
			React textbox:&nbsp;
			<input 
				type="text" 
				value={displayText} 
				onChange={e => setDisplayText(e.target.value)} 
			/>
		</p>
	);
}

where useObservableState is the aforementioned custom hook.

Closing

Knockout was a good library for its time, but with it no longer really being maintained, having a smaller ecosystem, and lacking some of the simplicity and good ideas of React, it's a good time to at least give React a shot. I hope this post will facilitate you and/or your team to try out the future of UI programming.


Like what you see?
Have any advice or questions?
Contact Me