0

I have a parent component which maps through my array and prints out various iterations of my Child components.

I'm trying to access the state in the parent component but can't figure out how to lift the correct state of "inputValue" from the child and have it in the parent instead.

Ideally I'd like the inputValue and isValidated state living in the PARENT so I can use it for various form-based functions.

Parent Component:

import React, { useState } from 'react';
import { formFieldsData } from './FormFields';
import Input from './Input';

export default function Form() {

    return (
        <form>
            {formFieldsData.map((item) => (
                <Input
                    key={item.id}
                    id={item.id}
                    label={item.label}
                    type={item.type}
                    placeholder={item.placeholder}
                />
            ))}
        </form>
    )
}

Child component:

import React, { useState } from 'react';
import styles from './forms.module.scss';
import RangeInput from './RangeInput';

export default function Input({ type, id, placeholder = '', label, props }) {
    const [inputValue, setInputValue] = useState('');
    const [isValidated, setIsValidated] = useState(false);
    const isRangeInput = type === 'range';

    const handleChange = (e) => {
        setInputValue(e.target.value)
        if(inputValue.length >  0 || type === 'date') {
            setIsValidated(true)
        }
    }

    return (
        <div className={styles.form__row}>
            <label htmlFor={id}>{label}: {inputValue}</label>
            {isRangeInput
                ? <RangeInput id={id} onChange={(e) => handleChange(e)} />
                : <input
                    required
                    type={type}
                    id={id}
                    name={id}
                    placeholder={placeholder}
                    className={styles.input}
                    value={inputValue}
                    onChange={(e) => handleChange(e)}
                />
            }
            <button type="submit" id="formSubmit" onClick={() => alert('button click catched')} disabled={!isValidated}>Submit!</button>
        </div>
    )
}
Connor
  • 21
  • 5

3 Answers3

0

The easiest way for this is to manage your state in the parent. Then you just need to pass a function to your child as a prop to update the parent' state. Here is one of my previous answer related to this problem and demonstrating how to apss data from child to parent.

Note that you should do this kind of prop drilling only when it's a direct child. If your tree expand between parent and child then a context/state management would be more apropriate.

Quentin Grisel
  • 4,794
  • 1
  • 10
  • 15
0

a) Just add the following function in your parent component.

const inputDataHandler = (inputData) => {
    console.log(inputData);
}

b) Now, pass the inputDataHandler function using props to your child component.

<Input
  key={item.id}
  id={item.id}
  label={item.label}
  type={item.type}
  placeholder={item.placeholder}
  inputDataHandler = {inputDataHandler}
/>

c) Now, just call the inputDataHandler function with the arguments from child component by the handleChange function. Arguments can be value/state etc. Here, I have passed "inputValue" as argument.

const handleChange = (e) => {
    setInputValue(e.target.value)
    if(inputValue.length >  0 || type === 'date') {
        setIsValidated(true)
    }
    props.inputDataHandler(inputValue);
}

The concept is, you write & pass the function as props to the child, when child calls that function with arguments, those arguments are passed to parent component. In this way, a value can be passed from child to parent. This concept is called "Lifting state up".

rifat1747
  • 1
  • 1
0

To remove some of the complexity you could combine your existing form field array with your state - for each object in the array specify the id, type, a unique name, value, and valid properties, and initialise your form state with it.

You can then iterate over that array as you're currently doing, and pass down those properties to the Input component. To ensure you can update the state in the parent component you also pass down a handler from the parent to the child (here: handleChange).

When an input changes it calls handleChange. The handler destructures the name, type, and value values, and then sets a new state by iterating over the previous state. If the changed input's name matches the iterated object's name then you return a new object formed from the old object but with an updated value and valid value (you can use a separate function to determine whether that field type/value is valid).

In the input component you can use the valid value to add a class to style the element.

As an extra feature you can toggle the submit button on/off by calling a function to return a boolean value on whether any of the valid values of the fields are false.

const { useEffect, useState } = React;

function Example({ initialState }) {

  // Set the form state using the initial state array
  const [ form, setForm ] = useState(initialState);

  // Simple validation checking for the various
  // input types
  function isValid(type, value) {
    if (type === 'text' && value.length > 0) return true;
    if (type === 'range' && Number(value) > 0) return true;
    if (type === 'number' && Number(value) > 0) return true;    
    return false;
  }

  // When any of the inputs change...
  // ...destructure its name, type, and value
  // and then create an updated state by mapping
  // over the previous state - if the name from the
  // changed input matches the current iterated field
  // return an updated field object, otherwise return
  // the current field object. Note you can change the
  // validity value of the by calling the isValid function 
  function handleChange(e) {
    const { name, type, value } = e.target;
    setForm(prev => {
      return prev.map(field => {
        if (field.name === name) {
          return {
            ...field,
            value,
            valid: isValid(type, value)
          };
        }
        return field;
      });
    });
  }

  // Log the form state
  function handleSubmit() {
    console.log(form);
  }

  // Return false (button disabled) if some of the
  // fields are invalid
  function isButtonDisabled() {
    return form.some(field => !field.valid);
  }

  // `map` over the form state passing down a handler
  return (
    <form>
      {form.map(({ id, ...rest }) => {
        return (
          <Input
            key={id}
            {...rest}
            handleChange={handleChange}
          />
        );
      })}
      <button
        type="button"
        disabled={isButtonDisabled()}
        onClick={handleSubmit}
      >Submit form
      </button>
    </form>
  );

}

function Input(props) {

  // Get all the props (including the value and the handler
  const { type, name, value, handleChange, valid } = props;

  // Form a class string depending on the valid value
  const cls = ['input', !valid && 'invalid'].join(' ');

  return (
    <input
      className={cls}
      name={name}
      type={type}
      value={value}
      onChange={handleChange}
    />
  );

}

const formFields = [
  { id: 1, type:"text", name: "name", value: '', valid: false },
  { id: 2, type:"range", name: "range", value: 0, valid: false },
  { id: 3, type:"number", name: "age", value: 0, valid: false }  
];

const node = document.getElementById('root');
const root = ReactDOM.createRoot(node);
root.render(<Example initialState={formFields} />);
.input { font-size: 1.5rem; }
.invalid { border-color: red; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.min.js"></script>
<div id="root"></div>
Andy
  • 61,948
  • 13
  • 68
  • 95