diceline-chartmagnifiermouse-upquestion-marktwitter-whiteTwitter_Logo_Blue

Today I Learned

React custom hook for form state management

Goal: defining a custom useForm hook for managing the state of our forms. This can have some initial state, mainly used for precompleting update forms, but can also provide empty form fields. 




Problem: when we want to operate on a precompleted form, altough we pass all the data for our fields, the form is not being precompleted and its fields are empty.

export default function useForm(initial = {}) {
  const [inputs, setInputs] = useState(initial);



  const handleChange = (e) => {
    let { value, name, type } = e.target;

    if (type === 'number') {
      value = parseInt(value);
    }
    if (type === 'file') {
      [value] = e.target.files;
    }

    setInputs({
      ...inputs,
      [name]: value,
    });
  };

  const resetForm = () => {
    setInputs(initial);
  };

  const clearForm = () => {
    const blankState = Object.fromEntries(
      Object.entries(inputs).map(([key, value]) => [key, ''])
    );

    setInputs(blankState);
  };

  return {
    inputs,
    clearForm,
    resetForm,
    handleChange,
  };
}


This is what happens:

  1. We pass an initial state object which is undefined (server-side rendered) until the GQL query loads.
  2. This initial object (which is undefined) populates the form fields making them empty.
  3. After the query loads, the initial state object is repassed to the useForm hook, but the DOM is not rerendered => a possible solution is to make use of the useEffect() hook for forcing rerendering.
  4. We cannot watch for changes directly on the initial object and reassign it using setInputs, because it triggers the useEffect callback once again and again and again when altering its value, causing an infinite loop.
  5. The solution is to watch for changes on a string joined by the values of the initial object. When that changes from undefined to the GraphQL query results, the useEffect callback is called and it initializes and rerenders the fields correspondingly.


An example implementation could be:

const initialValues = Object.values(initial).join('');
useEffect(() => {
  setInputs(initial);
}, [initialValues]);



Now the form precompletion works fine using our custom useForm() hook.

JavaScript event processing and bubbling

JavaScript event listeners and handlers can have some unnatural behaviour if we are not aware of how things work behind the hood. Let's understand what these unexpected behaviours could be and why things actually happen as they happen.

Let's pretend that we have the following DOM structure with 3 divs: grandparent, parent and child.

<div class="grandparent"></div>
  <div class="parent">
    <div class="child"></div>
  </div>
</div>

All 3 divs have their custom event handlers, like follows:

grandparent.addEventListener('click', () =>  console.log('Grandparent'));
parent.addEventListener('click', () =>  console.log('Parent'));
child.addEventListener('click', () =>  console.log('Child'));

If we click on the inner div (the child div), we would expect to call the child div's event handler, but actually we will see in the console that all 3 event handlers have been called, in the following order:

Child
Parent
Grandparent

Let's understand why this happens. Mainly, there are 3 phases in processing JavaScript events:

  1. Capturing phase
  • the DOM tree is parsed downwards until the caller element is found. Throughout its way, if there are nodes that explicitly require event execution in the capturing phase, their event handlers will be executed right away.
  1. Targeting phase
  • the caller element that triggered the event is being targeted
  1. Bubbling phase
  • the DOM tree is now parsed upwards until finding the document element, starting from the targeted/caller element. If on the way up we find elements with event handlers, they will be automatically called.

If we want to stop this behaviour of calling parent elements event handlers, we can use event.stopPropagation() on our last event handler that we want to be executed.

Regex simplicity vs normal

There are some cases when making use of regular expressions (regex) can exempt a lot of fuss from a programmers head. For example, let's compare the complexity between traditional logic and regex, in a function that excludes the vowels from a given string:

Traditional way:

function removeVowel(str) {
  let strList = str.split('');
  for (let i = 0; i < str.length; i++) {
    let char = str[i].toLowerCase();
    if (char == "a" || char == "e" || char == "i" || char == "o" || char == "u") {
      strList[i] = '';
    }
  }
  return strList.join('');
}

Regex Way:

function removeVowel(str){
  return str.replace( /[aeiou]/ig, '')
}

Add event listener on page resize

There is a special event listener in javascript that is called whenever a page resize is detected. The event listener is named 'resize' and can be used in the following way:

function onFrameResize(){
  console.table(
    {
      Height: window.innerHeight,
      Width: window.innerWidth  
    }
  );
}

window.addEventListener("resize", onFrameResize);

"window.innerHeight" and "window.innerWidth" are functions to access the width and the height of the frame.

This functionality will come in handy when working with responsive components.