6

I'm trying to create a Form component in react, kind of like Formik, but way simpler.

The way I'm thinking involves adding a onChange handler to all the children. I am doing this with children.map(). It works, however I get a key warning

Warning: Each child in a list should have a unique "key" prop.

I know there's no way to suppress this, so maybe there's a better approach to create this Form component? Also, how should I approach the case when the <input> is not a direct child?

Edit: I know how to avoid the problem, I mainly want the best way to approach this, including cases of nested inputs.

Here is how I want to use it:

<Form>
  <label htmlFor="owner">Owner</label>
  <input
    type="text"
    name="owner"
    id="owner"
  />
  <label htmlFor="description">Description</label>
  <input
    type="text"
    name="description"
    id="description"
  />
  <input
    type="submit"
    value="Submit"
  />
</Form>

and here is my code:

import React from 'react';
    
class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {}
    this.handleInputChange = this.handleInputChange.bind(this);
  }
    
  handleInputChange(event) {
    const target = event.target;
    const value =
      target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    console.log(`${name} : ${value}`)
    this.setState({
      [name]: value
    });
  }
    
  render() {
    return (
      <form>
        {this.props.children.map((child) => {
          if (child.type === "input") {
            return (
              <input
                onChange={this.handleInputChange}
                {...child.props}
              />
            )
          }
        })}
      </form>
    )
  }
}
    
export default Form;
Ledorub
  • 354
  • 3
  • 9
Lorenzo
  • 2,160
  • 12
  • 29
  • You might want to consider using context instead, if you are worried about inputs not being direct children. You cannot reliably traverse further down than children from a component, but you can inject context at any level. – Wolfie Jun 15 '19 at 14:21
  • Possible duplicate of [Understanding unique keys for array children in React.js](https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js) – Anurag Srivastava Jun 15 '19 at 14:22
  • @AnuragSrivastava I do indeed know how to avoid the warning, I'm asking for a better way to actually eliminate the problem, plus also dealing with nested inputs – Lorenzo Jun 15 '19 at 18:23

2 Answers2

4

If you use a render prop, you won't run into the unique "key" prop issue at all (This is also how Formik is implemented).

Your component would be easy to set up to pass handleChange to its children as a render prop, and this would also not require you to have input as a direct child.

class Form extends Component {
  ...
  handleInputChange() {...}

  render() {
    // Note that we call children as a function,
    // passing `handleChangeInput` as the argument.
    // If you want to pass other other things to the
    // children (handleSubmit, values from state), just
    // add them to the argument you're passing in.
    this.props.children({this.handleInputChange});
  }
}

Here's how you use it:

<Form>
  // Notice that <Form> needs its immediate child to be
  // a function, which has your handler as the argument:
  {({handeInputChange}) => {
    return (
      <form>
        <input type="text" name="owner" onChange={handleInputChange} />
        <input type="checkbox" name="toggle" onChange={handleInputChange} />
        <div>
          // inputs can be nested in other elements
          <input name=“inner” onChange={handleInputChange} />
        <div>
      <form>
    )  
  }}
</Form>

EDIT: You mentioned in a comment that you didn't want to explicitly pass the handler to each of your inputs. Another way to achieve this is with React Context, with a Provider in your Form, and each input wrapped in a consumer:

const FormContext = React.createContext();

const FormInput = (props) => {
  const {handleInputChange} = useContext(FormContext);
  return <input handleInputChange={handleInputChange} {...props} />
}

class Form extends Component {
  ...
  handleInputChange() {...}

  render() {
    // Pass anything you want into `value` (state, other handlers),
    // it will be accessible in the consumer 
    <Provider value={{ handleInputChange: this.handleInputChange }}>
      <form>
        {this.props.children}
      </form>
    </Provider>
  }
}

// Usage:
<Form>
  <FormInput type="text" name="owner" />
  <FormInput type="submit" name="submit" />
  <div>
      <FormInput type="checkbox" name="toggle" />
  </div>
</Form>

In fact, Formik has this option as well, with either the Field component, or the connect function.

helloitsjoe
  • 6,264
  • 3
  • 19
  • 32
  • The problem is that I wanted to avoid all the boilerplate of having the handlers in every input, and just use `Form` as a wrapper component – Lorenzo Jun 15 '19 at 18:13
  • kinda like formik where you only provide the handler to the form and not every input – Lorenzo Jun 15 '19 at 18:21
  • Ah, I understand now. You could do this with Context, but your inputs would need to be wrapped in a context consumer. I'll update my answer with that approach. – helloitsjoe Jun 15 '19 at 18:29
  • 1
    Thanks, that's exactly what I needed – Lorenzo Jun 15 '19 at 20:13
1

i think this is what you need, already you could add child index as key since there order won't change, and reduce here is not returning null in the array in case the type of the child is not input, map+filter could resolve it also:

class Form extends React.Component {
  constructor(props) {
    super(props);
    this.state = {};
    this.handleInputChange = this.handleInputChange.bind(this);
    // this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === "checkbox" ? target.checked : target.value;
    const name = target.name;
    console.log(`${name} : ${value}`);
    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        {this.props.children.reduce((childrenAcc, child, index) => {
          if (child.type === "input") {
            return [
              ...childrenAcc,
              <input
                key={index}
                onChange={this.handleInputChange}
                {...child.props}
              />
            ];
          }
          return childrenAcc;
        }, [])}
      </form>
    );
  }
}

function App() {
  return (
    <Form>
      <label htmlFor="owner">Owner</label>
      <input type="text" name="owner" />
      <label htmlFor="description">Description</label>
      <input type="text" name="description" />
      <input type="submit" value="Submit" />
    </Form>
  );
}

check this sandbox .

Lafi
  • 1,310
  • 1
  • 15
  • 14
  • Indeed it works, could even use `map(child, index)`, however it does not work in case of nested elements? – Lorenzo Jun 15 '19 at 18:19
  • @LefiTarik true that, however I do think using context works better, so that's the accepted answer. I still up voted yours as it's a good, working answer – Lorenzo Jun 20 '19 at 15:43
  • @Lorenzo yes i agree with that unless the use case is simple, but in other use cases using the context API is not flexible enough and this pattern could solve complex problems without getting into RenderProp hell for exp (when there is multiple providers), so it depends on the problem to solve and i recommend your solution for simplicity. – Lafi Jun 20 '19 at 16:10