htmlreactjsinputcode-reuse

Creating a truly reusable react input component using the value attribute


I'm fairly new to react, and while creating my first website, I found myself often needing to reuse an input component. The problem I'm having is balancing reusability and readability of the code. Let me explain. I've found that the more I try to make the input component reusable the more props I need to pass especially functions as props. I've been following this post which helped a lot: How to create a reusable input field using react?.

As the post suggests, I did the following

import {React, useState} from 'react'
import './custominputfield.css'

const CustomInputField = ({value, label, name, placeholder, type, onChange}) => {
  //If value exists and is > placeholder's lengths use the value's length  
  const widthOfInput = (value && value.length > placeholder.length) ? value.length : placeholder.length

  return (
    <div className="custom-input-field-container">
        {label && <label>{label}</label>}
        <div className="custom-input-container">
            <input 
                value= {value}
                type = {type}
                name={name}
                placeholder = {placeholder}
                onChange={e => onChange(e)}
                style={ {width: widthOfInput + 6 + "ch"}}
            />
        </div>
    </div>
  )
}

export default CustomInputField

And in the parent component I basically have an array of rows, where each row contains several of these input components:

const [gameResults, setGameResults] = useState(results)
//Create a new row
function addNewPlayerRow() {
      // Generate a unique ID for the new row by finding the max of all the ids including 0 and adding 1 to it
      const newRowId = Math.max(...gameResults.map((row) => row._id), 0) + 1;
      //Create a new row with empty results 
      const newRow = {
        _id: newRowId,
        username : '', 
        cashIn : '',
        cashOut : '',
        balance : '',
      }
      // Update the state to include the new row
      setGameResults((prevRows) => [...prevRows, newRow]);
    }

This is how I update the gameResults variable:

const handleInputChange = (e, id) => {
        const { name, value } = e.target;

        setGameResults(prevResults => (prevResults.map(result => (
            result._id === id ? ({
                ...result,
                [name] : value,
            }) : (
                result
            )
        ))))
    }

And it works! But my question is why? In the input component we set the input's value to the prop value which is always initialised to '' (empty string). So it seems that the child needs the parent to get its value but the parent needs the child in order to update the gameResults object. A dependency loop? I would expect the value in the handleInputChange function to just always return the empty string but somehow it returns the correct value. Anyways, I'd also appreciate any advice on cleaning up this code and making it more reusable.


Solution

  • This is a very typical setup. Yes, there is a dependency here between the child and the parent. The main thing to recognize is that the child's (the input component's) job is to:

    When a user inputs a new value, the child calls the onChange with that new value. The parent's onChange function updates the "value" variable, which gets sent back to the child. The child now displays the new value.

    Note what this means: the input field is showing the PARENT's value. It is NOT showing the user's input (!!!) When the user types "a", the UI does not display that character. Instead it fires the onChange function, which passes the newly inputted value (not displayed!!) to the function, which sets the new input ("a") into the state variable, which gets passed back to the child as value which then updates the UI with this new value.....only after all of that does the UI now show "a".

    Whew!

    You could prove this out by modifying the onChange function such that it stores some different value into the state variable than what the user typed in. For example, you could do:

    const handleInputChange = (e, id) => {
      const { name, value } = e.target;
    
      const newValue = value.replaceAll("a", "XXX");
    
      setGameResults(prevResults => (prevResults.map(result => (
        result._id === id ? ({
            ...result,
            [name] : newValue,
          }) : (
            result
          )
      ))))
    }
    

    and see how the UI behaves when you type "a" into the input field.