简体   繁体   中英

React conditionally rendering

I have a simple form build from React. Upon submission, if the error comes back, I wish to render an extra div to show error on the form.

Right now I got it to work but I do not like the solution. My solution is based on the knowledge that the component will only re-render if the state changes inside the render function (In this case this.state.errorMessages). So I had to explicitly put the if condition inside the render function like so

renderError() {
    var errArr = [];
    for (var key in this.state.errorMessages) {
        errArr = [...errArr, ...this.state.errorMessages[key]];
    }
    return (
        <div className="alert alert-danger">
            {errArr.map((err) => {
                return <p>{err}</p>
            })}
        </div>
    )
}

renderForm() {
    return (
        <form onSubmit={this.handleRegisterFormSubmit}>
            <div className="form-group">
                <label>First Name</label>
                <input type="text" className="form-control" name="name" placeholder="Name" value={this.state.firstName} onChange={this.handleFirstNameChange} required/>
            </div>
            <div className="form-group">
                <label>Last Name</label>
                <input type="text" className="form-control" name="lastName" placeholder="Last Name" value={this.state.lastName} onChange={this.handleLastNameChange} required/>
            </div>
            <div className="form-group">
                <label>Email address</label>
                <input type="email" className="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email" value={this.state.email} onChange={this.handleEmailChange} />
                <small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
            </div>
            <div className="form-group">
                <label>Password</label>
                <input type="password" className="form-control" name="password" placeholder="Password" value={this.state.password} onChange={this.handlePasswordChange}/>
            </div>
            <div className="form-group">
                <label>Password Confirmation</label>
                <input type="password" className="form-control" name="password_confirmation" placeholder="Password Confirmation" value={this.state.passwordConfirmation} onChange={this.handlePasswordConfirmationChange}/>
            </div>
            <div>
                <button type="submit" className="btn btn-primary">Submit</button>
                <button type="button" className="btn btn-danger" onClick={this.handleCancelClick}>Cancel</button>
            </div>
        </form>
    )
}

render () {
    if (!this.state.errorMessages) {
        return (
            <div>
                {this.renderForm()}
            </div>
        )
    } else {
        return (
            <div>
                {this.renderForm()}
                {this.renderError()}
            </div>
        )
    }
}

I don't really like this approach as this could get nasty if I have more condition to re-render. I'm hoping there is a solution along the line of not having much logic in the actual render function and have that extracted out. For example...

renderError() {
    if (!this.state.errorMessages) {
        return;
    }
    var errArr = [];
    for (var key in this.state.errorMessages) {
        errArr = [...errArr, ...this.state.errorMessages[key]];
    }
    return (
        <div className="alert alert-danger">
            {errArr.map((err) => {
                return <p>{err}</p>
            })}
        </div>
    )
}

render () {
    <form onSubmit={this.handleRegisterFormSubmit}>
        <div className="form-group">
            <label>First Name</label>
            <input type="text" className="form-control" name="name" placeholder="Name" value={this.state.firstName} onChange={this.handleFirstNameChange} required/>
        </div>
        <div className="form-group">
            <label>Last Name</label>
            <input type="text" className="form-control" name="lastName" placeholder="Last Name" value={this.state.lastName} onChange={this.handleLastNameChange} required/>
        </div>
        <div className="form-group">
            <label>Email address</label>
            <input type="email" className="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email" value={this.state.email} onChange={this.handleEmailChange} />
            <small id="emailHelp" className="form-text text-muted">We'll never share your email with anyone else.</small>
        </div>
        <div className="form-group">
            <label>Password</label>
            <input type="password" className="form-control" name="password" placeholder="Password" value={this.state.password} onChange={this.handlePasswordChange}/>
        </div>
        <div className="form-group">
            <label>Password Confirmation</label>
            <input type="password" className="form-control" name="password_confirmation" placeholder="Password Confirmation" value={this.state.passwordConfirmation} onChange={this.handlePasswordConfirmationChange}/>
        </div>
        <div>
            <button type="submit" className="btn btn-primary">Submit</button>
            <button type="button" className="btn btn-danger" onClick={this.handleCancelClick}>Cancel</button>
        </div>
        {this.renderError}
    </form>
}

This throws an error because it complains that this.renderError should be called as a function. But when I put it as this.renderError() The error will never render because it does not get automatically called when errors come back.

-----------Update----------

Alternatively, why can't I do something like the following

render () {
    <div>
        <form onSubmit={this.handleRegisterFormSubmit}>
            ...
        </form>
        {if (this.state.errorMessages) {this.renderError()}}
    </div>
}

This throws console error

Uncaught Error: Module build failed: SyntaxError: Unexpected token (117:13)

----------Update 2-----------

Essentially, I'm looking for a solution where inside the render function, I can easily show a whole block of code when the state changes. In Vue, I can do something like

<form>
    <input type="text">
</form>
<div v-if="hasError">
    <div class="alert alert-danger">{{something}}</div>
</div>

Can I do something as easy as this in React?

You can just use map in order to extract the error message from your object.

Here below a minimal example of form validations and errors in React. It's good to understand how it works, but for my part, I use Formik which simplifies this process.

class Test extends React.Component {
    constructor(props) {
        super(props);
        this.state = { errorMessages: {} };
    }

    handleRegisterFormSubmit = e => {
        e.preventDefault(); // don't submit the form until we run what's below
        let errorMessages = {};

        if (!this.state.lastName) errorMessages.lastName = 'You need to enter your last name';
        // And so on for each field validation

        // Do we have errors ?
        if (Object.keys(errorMessages).length > 0) {
            this.setState(errorMessages);
        } else {
            // Submit to server
        }
    };

    handleChange = e => {
        this.setState({
            [e.target.name]: e.target.value,
            errorMessages: {
                ...this.state.errorMessages,
                [e.target.name]: null // remove the error of this field if it's being edited
            }
        });
    };

    render() {
        const errArr = Object.keys(this.state.errorMessages).map(key => this.state.errorMessages[key]);
        return (
            <form onSubmit={this.handleRegisterFormSubmit}>
                <div className="form-group">
                    <label>Last Name</label>
                    <input type="text" className="form-control" name="lastName" placeholder="Last Name" value={this.state.lastName} onChange={this.handleChange} />
                </div>

                {/* ... your dom */}

                <div>
                    <button type="submit" className="btn btn-primary">
                        Submit
                    </button>
                    <button type="button" className="btn btn-danger" onClick={this.handleCancelClick}>
                        Cancel
                    </button>
                </div>

                {errArr.length > 0 && (
                    <div className="alert alert-danger">
                      {errArr.map(err => {
                        return <p>{err}</p>;
                      })}
                    </div>
                )}
            </form>
        );
    }
}

One more way to not display your alert div is with a ternary operator for your className and using bootstrap's d-none

<div className={errArr.length ? "alert alert-danger" : "d-none"}>
    {errArr.map(err => {
        return <p>{err}</p>;
    })}
</div>

I believe this is an architectural question.

Try to follow those practices:

1- Inject conditional statement inside JSX directly

2- Use functional components to render JSX not object methods


1- Inject conditional statement inside JSX directly

Bad:

if (!this.state.errorMessages) {
    return (
        <div>
            {this.renderForm()}
        </div>
    )
} else {
    return (
        <div>
            {this.renderForm()}
            {this.renderError()}
        </div>
    )
}

Good:

return <div>
            {this.renderForm()}
            {this.state.errorMessages && this.renderError()}
        </div>

2- Use functional components to render JSX not object methods

Bad:

class FormComponent {
  // ....

  renderError() {
    // Blah blah blah 
    return (
        <div className="alert alert-danger">
            {errArr.map((err) => {
                return <p>{err}</p>
            })}
        </div>
    )
   }

   render() {
     return (
       <div>
         <AnotherComponent />
         {this.renderError()}
      <div/>
     )
   }

}

then {this.renderError()}

Good

class FormComponent {
  // ....

   render() {
     return (
       <div>
         <AnotherComponent />
         {<Error />} {/* render it as componet ⚠️*/} 
      <div/>
     )
   }

}
// Build it as component outside the main component (FormComponent) ⚠️
function Error(props) { 
    return (
        <div className="alert alert-danger">
            {props.errArr.map((err) => {
                return <p>{err}</p>
            })}
        </div>
     )

 }

I spent many 2 years in React development of enterprise apps also graduating from Udacity and I am sharing my experience here. Congrats!

One would usually render items in an element representing the list with some styling applied. We also sometimes don't want this wrapping element if we don't have any items.

I've written the following component which allows conditional rendering. When the condition is false, no elements are rendered keeping the DOM clean.

export default class If extends Component<{ condition: any }, {}> {

    render() {
        return this.props.condition ? this.props.children : <Fragment></Fragment>;
    }

}

One can now simply use the component as follow:

<If condition={items.length}>
    <-- item list -->
</If>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM