JSFiddle - React, Tailwind, and code Playground

HTML

<table border="1">
    <thead>
        <tr>
            <td>
                <strong>#</strong>
                <span data-bind="click: sorter('id', true)">asc</span>
                <span data-bind="click: sorter('id', false)">desc</span>
            </td>
            <td>
                <strong>First</strong>
                <span data-bind="click: sorter('firstName', true)">asc</span>
                <span data-bind="click: sorter('firstName', false)">desc</span>
            </td>
            <td>
                <strong>Last</strong>
                <span data-bind="click: sorter('lastName', true)">asc</span>
                <span data-bind="click: sorter('lastName', false)">desc</span>
            </td>
            <td>
                <strong>Email</strong>
            </td>
            <td>
                <strong>City</strong>
                <span data-bind="click: sorter('city', true)">asc</span>
                <span data-bind="click: sorter('city', false)">desc</span>
            </td>
        </tr>
    </thead>
    <tbody data-bind="foreach: items">
        <tr>
            <td data-bind="text: id"></td>
            <td data-bind="text: firstName"></td>
            <td data-bind="text: lastName"></td>
            <td data-bind="text: emailAddress"></td>
            <td data-bind="text: city"></td>
        </tr>
    </tbody>
</table>

<p>
    Sorters:
    <!-- ko foreach: sortersObservable -->
        <span data-bind="text: $data[0]"></span>: <span data-bind="text: $data[1]"></span>
    <!-- /ko -->
</p>

JavaScript

function Item(id, firstName, lastName, emailAddress, city) {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
    this.emailAddress = emailAddress;
    this.city = city;
}

function compareValues(ascending, value1, value2) {
    value1 = value1 || null;
    value2 = value2 || null;
            
    if (value1 === value2) {
        return 0;
    }
    
    if (value1 === null && value2 !== null) {
        return ascending ? -1 : 1;
    }
    
    if (value2 !== null && value1 === null) {
        return ascending ? 1 : -1;
    }
    
    if (typeof value1 === 'number') {
        return ascending ? value1 - value2 : value2 - value1;
    }
    
    value1 = value1.toString();
    value2 = value2.toString();
        
    if (ascending) {
        return value1 > value2 ? 1 : value1 < value2 ? -1 : 0;
    }
    
    return value1 < value2 ? 1 : value1 > value2 ? -1 : 0;
}

var model = {
    items: ko.observableArray(),
    
    sortersObservable: ko.observableArray(),
    
    sorter: function (property, ascending) {
        return function () {
            var list = model.sorter.list = model.sorter.list || {},
                s,
                i;
            
            if (list[property] === ascending) {
                delete list[property];
            } else {
                list[property] = ascending;
            }
            
            var s = [], i = 0;
            
            for (i in list) {
                s.push([i, list[i]]);
            }
            
            model.sortersObservable(s);
        }
    }
};

model.items.push(new Item(1, 'Jane', 'Doe', '[email protected]', 'Los Angeles'));
model.items.push(new Item(2, 'John', 'Doe', '[email protected]', 'Los Angeles'));
model.items.push(new Item(3, 'John', 'Bloggs', '[email protected]', 'San Francisco'));
model.items.push(new Item(4, 'Happy', 'Citizen', '[email protected]', 'San Francisco'));
model.items.push(new Item(5, 'Ham', 'Burger',...