JSFiddle - React, Tailwind, and code Playground

HTML

<table class="table">
  <tbody>
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
     <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </tbody>
</table>

CSS

.table {
  table-layout: fixed;
  width: 100%;
}

.table td {
  border: 1px solid #000000;
  height: 30px;
}

.table td:not(.is-edit):hover {
  border-color: red;
  cursor: pointer;
}

.table td input {
  box-sizing: border-box;
  height: 100%;
  width: 100%;
}

JavaScript

const table = document.querySelector('.table');
const rows = table.querySelectorAll('tr');

const STORAGE_KEY = 'table-data';

const currentData = (() => {
  const json = window.localStorage.getItem(STORAGE_KEY);
  
  if (json) {
  	try {
    	const data = JSON.parse(json);
      return data;
    } catch (e) {}
  }

  return {};
})();

const saveData = () => {
	window.localStorage.setItem(STORAGE_KEY, JSON.stringify(currentData));
};

const handleCellClick = (key, node) => {
	if (node.classList.contains('is-edit')) {
  	return;
  }

	const input = document.createElement('input');
  input.type = 'text';
  input.value = currentData[key] || '';
  
  input.onblur = () => {
  	currentData[key] = input.value;
    saveData();
    
  	node.replaceChild(document.createTextNode(input.value), input);
    node.classList.remove('is-edit');
  };
  
  if (node.firstChild) {
  	node.replaceChild(input, node.firstChild);
  } else {
  	node.appendChild(input);
  }
  
  
  node.classList.add('is-edit');
};

rows.forEach((rowItem, rowIndex) => {
  const cells = rowItem.querySelectorAll('td');
  
  cells.forEach((cellItem, cellIndex) => {
  	const key = `${rowIndex}-${cellIndex}`;

		cellItem.addEventListener('click', () => {
    	handleCellClick(key, cellItem);
    });
    
    cellItem.textContent = currentData[key] || '';
  });
});