How should I use React to display global error messages? - reactjs

I have an App component at the top level, and then within that component, I have one called ErrorMessages.
I put my App component's state into a context, and then elsewhere in my app, if an error occurs, I set errors on the App component's context.
The App component will then pass the errors to ErrorMessages via prop, and then the error messages will display.
The problem is, I can't seem to clear out the error messages.
What I've tried so far is, using the cleanup mechanism of useEffect to clear the errorMessages.
Say I have a component called UserRegistration, the user doesn't enter an e-mail address, and I set an error "email address required", this displays just fine.
Now, when UserRegistration is unmounted, I want to clear the error messages out of the App context.
How might I achieve this?
This is my app component:
class App extends React.Component {
// For errors in the format of:
// [{message: "first_name missing..."}, {message: "Email address in use..."}]
setErrorMessages = (errors) => {
this.setState({errors}) }
// Other messages not in the format above. Can set "alert-warning" or
// "alert-success" type messages.
setStatusMessage = (statusType, statusMessage) => {
this.setState({statusType, statusMessage}) }
clearAllMessages = () => {
this.setState({errors: [], statusType: null, statusMessage: null}) }
setAuthToken = () => {}
setLoaded = (val) => { this.setState({loaded: val}) }
state = {
user: {},
loaded: false,
errors: [],
statusType: null,
statusMessage: null,
setErrorMessages: this.setErrorMessages,
clearAllMessages: this.clearAllMessages,
setStatusMessage: this.setStatusMessage,
setLoading: this.setLoading,
setLoaded: this.setLoaded,
setAuthToken: this.setAuthToken,
}
render() {
return (
<div className="App container">
<React.StrictMode>
<AppContext.Provider value={this.state}>
<BrowserRouter>
{this.state.statusMessage ?
<div className={"alert alert-" + this.state.statusType}>{this.state.statusMessage}</div> : null}
<ErrorMessages list={this.state.errors} />
{!this.state.loaded ? <LoaderWidget /> : null }
<IssueBrowser />
</BrowserRouter>
</AppContext.Provider>
</React.StrictMode>
</div>
);
}
}
If I use the cleanup mechanism of useEffect in a lower level component, I get the "maximum depth exceeded" error, and the page hangs.
Here's the component I'm calling useEffect in:
const UserRegistration = (props) => {
const appContext = useContext(AppContext)
const history = useHistory();
const registerUser = (event) => {
event.preventDefault()
appContext.setLoaded(false)
let payload = {
first_name: event.target.first_name.value,
last_name: event.target.last_name.value,
email_address: event.target.email_address.value,
password: event.target.password.value }
Users.create(payload)
.then(response => {
appContext.setStatusMessage("success", "User successfully registered.")
appContext.setLoaded(true)
history.push('/')
})
.catch(error => {
if (error.response.data.messages) { appContext.setErrorMessages(error.response.data.messages) }
appContext.setLoaded(true)
})
}
useEffect(() => {
return function cleanup() {
appContext.clearAllMessages()
// this just seems to hang, the app goes into an infinite loop at this point and freezes.
}
})
return (
<form onSubmit={registerUser}>
<div className="form-group">
<label htmlFor="first_name">First Name:</label>
<input name="first_name" type="text" className="form-control" />
</div>
<div className="form-group">
<label htmlFor="last_name">Last Name:</label>
<input name="last_name" type="text" className="form-control" />
</div>
<div className="form-group">
<label htmlFor="email_address">Email Address:</label>
<input name="email_address" type="text" className="form-control" />
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input name="password" type="password" className="form-control" />
</div>
<div className="form-group">
<label htmlFor="password">Confirm Password:</label>
<input name="password" type="password" className="form-control" />
</div>
<div className="form-group">
<input className="btn btn-primary form-control" type="submit" value="Register" />
</div>
</form>
)
}

Use events! Send a cleanAllErrors when the user closes the event.
In the same way listen for errors in the ErrorHandlingComponent and display errors.
If you want to make errors disapear after a timeout that is fine. For certain errors that is ok. Same here send a message. And if you want user interaction on some important error, send the message in the onClose of the error label!
Cheers

The reason why you're getting maximum depth exceeded is because your useEffect hook inside the UserRegistration component runs every time new props are received or you get new data through hooks.
This happens when your cleanup function gets triggered because it causes an update to your context and therefore causing an infinite re-render.
You should pass an empty dependency array so that the useEffect hook will behave similarly to a componentWillUnmount lifecycle method in a class component:
const UserRegistration = (props) => {
// ...
useEffect(() => {
return function cleanup() {
appContext.clearAllMessages();
}
// b/c of the empty dependency array
// this hook will only run once when
// the component renders for the first time, and
// it will run the cleanup function once,
// when it unmounts
}, []);
}

Related

How to fetch specific JSON data using an ID (React Hooks)

I want to get a specific item from a json using its unique ID but with the function that I have created I do not get any data. This is the function:
export function getPost(id) {
return fetch("http://localhost:3004/edit/"+id)
.then(data => data.json())
}
And this is the page where I want to print the item. The ID comes from another page and it's shown in the url, where I get it thanks to useParams:
interface IPost {
id: number;
title: string;
author: string;
content: string;
}
const Edit: React.FC = () => {
const [post, setPost] = useState<IPost>();
const {id} = useParams();
// Not working
getPost(id)
.then(items => {
setPost(items)
})
return (
<div className="containerHomepage">
<form className="formulari">
<div className="containerBreadCrumb">
<ul className="breadCrumb">
<li>Posts</li>
{/* THIS SHOWS AN ERROR */}
{post.author}
</ul>
</div>
<div className="containerTitleButton">
<input
className=""
type="text"
placeholder='Post title'
name="title"
// onChange={handleInputChange}
></input>
<button
className="button"
type="submit"
>Save</button>
</div>
<div className="containerEdit">
<input
className="editAuthor"
type="text"
placeholder='Author'
name="author"
// onChange={handleInputChange}
></input>
<input
className="editContent"
type="textarea"
placeholder='Content'
name="content"
// onChange={handleInputChange}
></input>
{/* <div className="errorEmpty">{error}</div> */}
</div>
</form>
</div>
);
};
// ========================================
export default Edit;
Throws an error in "{post.author}", and I guess that it's something wrong with my function "getPost".
Since you initialize post to undefined:
const [post, setPost] = useState<IPost>();
trying to access properties of it will throw:
{post.author}
Your TypeScript should have warned you about this - it's good to fix TypeScript warnings before running apps for real to avoid runtime errors. Check that the object exists before trying to access properties on it.
{post?.author}
There's no issue with your getPost function, except for the fact that you should probably only call it once, when the component mounts, not every time it re-renders.
useEffect(() => {
getPost(id).then(setPost);
}, []);
I'd also recommend not ignoring errors - catch them to avoid unhandled rejections.
useEffect(() => {
getPost(id).then(setPost).catch(handleError);
}, []);

How can I reset the form values in a React component with a button press?

I'm using the below Contact component to handle a simple form in react to send an email. I want the sendEmail function to also clear the form fields but I've tried referencing them through form.current and that doesnt seem to be working. Is there another way I need to reference them to set the values? I also tried e.target.reset() and it didn't seem to do anything.
import React, { useRef } from 'react';
import emailjs from 'emailjs-com';
const Contact = () => {
const form = useRef();
const sendEmail = (e) => {
e.preventDefault();
const serviceId='...';
const templateId='...';
const userId='...';
emailjs.sendForm(serviceId, templateId, form.current, userId)
.then((result) => {
console.log(result.text);
}, (error) => {
console.log(error.text);
});
};
return (
<form onSubmit={sendEmail} ref={form}>
<div className="field half first">
<label htmlFor="name">Name</label>
<input type="text" name="fromName" id="name"/>
</div>
<div className="field half">
<label htmlFor="email">Email</label>
<input type="text" name="fromEmail" id="email"/>
</div>
<div className="field">
<label htmlFor="message">Message</label>
<textarea name="message" id="message" rows="4"
placeholder = "..."></textarea>
</div>
<ul className="actions">
<li>
<input type="submit" value="Send Message" className="special"/>
</li>
</ul>
</form>
);
};
export default Contact
There are so many ways to doing this, basically on how the component has been structured to collect the data. However, following your pattern of arrangement, why don't you set a boolean variable or something coercible to a boolean value (value initially set to false) that is made aware (i.e value changes to true or 'sent') when the email has been successfully sent.
import {useState} from "react";
const [emailStatus, setEmailStatus] = useState("Not sent");
Then use the useEffect-hook to re-render the component, whenever the emailStatus variable changes to true or 'sent' and thereby clear-out the form values.
Hence, in your function:
emailjs.sendForm(serviceId, templateId, form.current, userId)
.then((result) => {
.... any other logic
console.log(result.text);
setEmailStatus('sent');
................
useEffect(() => {
//clear form fields here
}, [emailStatus]);
return(
.....
.....
);

Keep getting max update exceeded error but cannot seem to find error in code

I have made forms like this before but I seem to be missing something in this one. I keep getting the error "maximum update depth exceeded error" but I dont see where I am goin wrong and I've spent too much time looking at it. I already tried to change my onChange to include an arrow because others have suggested to do so , but when that happens I cant type in the input boxes. like so
onChange={()=>this.handleChange("username")}
I should note that I only get the error when I try to register the user and not when I type into the input. Here is the full error as well.
at checkForNestedUpdates (react-dom.development.js:23804)
at scheduleUpdateOnFiber (react-dom.development.js:21836)
at Object.enqueueSetState (react-dom.development.js:12468)
at Router.Component.setState (react.development.js:366)
at react-router.js:75
at listener (history.js:156)
at history.js:174
at Array.forEach (<anonymous>)
at Object.notifyListeners (history.js:173)
at setState (history.js:562)
Here is my code, please help.
import React from "React"
class Splash extends React.Component{
constructor(props) {
super(props)
this.state = this.props.user;
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
this.props.clearErrors();
}
handleSubmit(event) {
event.preventDefault();
this.props.signUp(this.state);
}
handleChange(field) {
return (e) => {
this.setState({ [field]: e.currentTarget.value })
};
}
render() {
return (
<div className="splash-background">
<div className="modal-screeen">
<form className="modal" onSubmit={this.handleSubmit}>
<h2 className="welcom-text"></h2>
<input className="user-input" type="text" placeholder="Name" onChange={this.handleChange("name")} value={this.state.name}/>
<input className="user-input" type="text" placeholder="Email" onChange={this.handleChange("email")} value={this.state.email}/>
<input className="user-input" type="text" placeholder="Create Username" onChange={this.handleChange("username")} value={this.state.username}/>
<input className="user-input" type="password" placeholder="Create Password" onChange={this.handleChange("password")} value={this.state.password}/>
<button>Sign Up</button>
</form>
</div>
</div>
);
}
}
export default Splash
import { connect } from "react-redux";
import { signup, login, clearErrors } from "../../actions/session_actions.js";
import Splash from "./splash";
const mapStateToProps = ({ errors }) => {
return {
errors: errors.session,
user: {
username: "",
password: "",
name:"",
email: "",
},
};
};
const mapDispatchToProps = (dispatch) => {
return {
signUp: (user) => dispatch(signup(user)),
login: (user) => dispatch(login(user)),
clearErrors: () => dispatch(clearErrors()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Splash);
I believe the problem here is the implementation of redux and react state. If you're using redux to manage the form state then I don't think there is a need to also manage that same state with react.
Try something like this, but keep in mind this code isn't tested.
class Splash extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
this.props.clearErrors();
}
handleSubmit(event) {
event.preventDefault();
this.props.signUp(this.props.user);
}
handleChange(e) {
// here you would have another action to update redux state depending
// on which input has changed. You can grab the input name via e.target.name
}
render() {
return (
<div className="splash-background">
<div className="modal-screeen">
<form className="modal" onSubmit={this.handleSubmit}>
<h2 className="welcom-text"></h2>
<input
className="user-input"
type="text"
placeholder="Name"
name="name"
onChange={this.handleChange}
value={this.props.user.name}
/>
<input
className="user-input"
type="text"
placeholder="Email"
name="email"
onChange={this.handleChange}
value={this.props.user.email}
/>
<input
className="user-input"
type="text"
placeholder="Create Username"
name="username"
onChange={this.handleChange}
value={this.props.user.username}
/>
<input
className="user-input"
type="password"
placeholder="Create Password"
name="password"
onChange={this.handleChange}
value={this.props.user.password}
/>
<button>Sign Up</button>
</form>
</div>
</div>
);
}
}
export default Splash;
When it comes to form data, I find it's easier to manage just with react state. Generally redux is used to manage state that is shared across the whole application/multiple components.
The problem was actually in my route util file. I had an infinite loop of rerouting!

Setting the default value of an input field after data is retrieved causes the content to overlap and the "onChange" event not to be triggered

I have an "edit category" component in my React application.
The ID is passed through the URL.
When the component is mounted, the action "fetchCategory" is called, which updates the props on the component with the current category.
I have a form which I want to be pre-populated, which I'm currently doing using the defaultValue on the input.
However, this isn't reflected on the state and the label for the text field overlaps the input field.
Any help would be greatly appreciated. I'll leave snippets of my code below which could help with understanding what I'm trying to do.
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchCategory } from "../../store/actions/categoryActions";
class AddOrEditCategory extends Component {
componentDidMount() {
this.props.fetchCategory(this.props.match.params.id);
if (this.props.match.params.id) {
this.setState({
_id: this.props.match.params.id
});
}
}
handleSubmit = e => {
e.preventDefault();
console.log(this.state);
};
handleChange = e => {
this.setState({
[e.target.id]: e.target.value
});
};
render() {
const addingNew = this.props.match.params.id === undefined;
return (
<div className="container">
<h4>{addingNew ? "Add category" : "Edit category"}</h4>
<form onSubmit={this.handleSubmit}>
<div className="input-field">
<input
type="text"
id="name"
defaultValue={this.props.category.name}
onChange={this.handleChange}
/>
<label htmlFor="name">Category name</label>
</div>
<div className="input-field">
<input
type="text"
id="urlKey"
onChange={this.handleChange}
defaultValue={this.props.category.urlKey}
/>
<label htmlFor="urlKey">URL Key</label>
</div>
<button className="btn">{addingNew ? "Add" : "Save"}</button>
</form>
</div>
);
}
}
const mapStateToProps = state => {
return {
category: state.categoryReducer.category
};
};
export default connect(
mapStateToProps,
{ fetchCategory }
)(AddOrEditCategory);
EDIT: Included whole component as requested
You need to replace the 'defaultValue' attribute with 'value' in the inputs.
You are using a controlled vs uncontrolled component. You dont need to use defaultValue.
You can set the initial values on the promise success for fetchCategory
componentDidMount() {
this.props.fetchCategory(this.props.match.params.id).then(response => {
// Set the initial state here
}
}
OR in
componentWillReceiveProps(nextProps) {
// Compare current props with next props to see if there is a change
// in category data received from action fetchCategory and set the initial state
}
React docs
<form onSubmit={this.handleSubmit}>
<div className="input-field">
<input
type="text"
id="name"
onChange={this.handleChange}
value={this.state.name} //<---
/>
<label htmlFor="name">Category name</label>
</div>
<div className="input-field">
<input
type="text"
id="urlKey"
onChange={this.handleChange}
value={this.state.urlKey}
/>
<label htmlFor="urlKey">URL Key</label>
</div>
<button className="btn">{addingNew ? "Add" : "Save"}</button>
</form>

How to initialize form data using async data

experts!
I make web service using react.
I want to make page to modify user info.
I can receive user data and set data to input's value.
But, react occur warning it.
Warning: A component is changing an uncontrolled input of type text to be controlled. ~~
I think. I did something wrong. when componentDidMount().
I want to know, how to initialize form data using async data.
Sorry about my poor english skill.
This code is part of my code.
export class UpdatedUser {
#observable private _name: string;
#observable private _phone: string;
#observable private _email: string;
// skip setters, getters.
}
interface MyPageComponentProps extends RouteComponentProps<{}> {
global?: GlobalService;
}
interface MyPageComponentState {
user: UpdatedUser;
}
#inject('global')
#observer
class MyPageComponent extends React.Component<MyPageComponentProps, MyPageComponentState> {
constructor(props: MyPageComponentProps) {
super(props);
this.state = {
user: new UpdatedUser(),
};
}
componentDidMount() {
if (this.props.global) {
userService.getUser(this.props.global.loginedInfo.idx + '').subscribe((res: any) => {
if (res.result === 'success') {
this.setState((prev: MyPageComponentState) => ({
user: update(prev.user, {$set: {
name: res.memberInfo.name,
phone: res.memberInfo.phone,
email: res.memberInfo.email,
} as UpdatedUser})
}));
} else {
alert(res.msg);
}
});
}
}
render() {
return (
<form className="user-info" onSubmit={this.updateUser}>
<h3 className="title">Modify User Info</h3>
<div className="form-group">
<label htmlFor="name" className="form-label">name</label>
<input type="text" id="name" className="form-control" value={this.state.user.name} onChange={this.onChangeInfo} />
</div>
<div className="form-group">
<label htmlFor="phone" className="form-label">phone</label>
<input type="text" id="phone" className="form-control" value={this.state.user.phone} onChange={this.onChangeInfo} />
</div>
<div className="form-group">
<label htmlFor="email" className="form-label">email</label>
<input type="text" id="email" className="form-control" value={this.state.user.email} onChange={this.onChangeInfo} />
</div>
<div className="btn-group">
<button type="submit" className="btn raised primary">수정</button>
<button className="btn raised secondary" onClick={() => this.props.history.goBack()}>취소</button>
</div>
</form>
);
}
export default MyPageComponent;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
The warning you get is about controlled and uncontrolled components. Basically, you can work with elements such as input, textarea and select having the state in the internal state of a React component or keeping it in the DOM.
In your code, you are keeping the UpdatedUser info in the local state and propagating its data with value and onChange, so you are controlling the inputs. However, where is your onChange callback? It does not exist. Try adding that method to your class and it should work. Take a look at the documentation about controlled components
The alternative approach is having uncontrolled inputs, in which case, you need to pass a defaultValue prop to your inputs instead of value and onChange, like this:
<input
defaultValue="Bob"
type="text"
ref={(input) => this.input = input}
/>
If the updateUser method gets triggered, you can simply get the new values from your ref elements:
updateUser(event) {
event.preventDefault()
console.log('New value: ' + this.input.value)
this.props.onSubmit(this.input.value)
}

Resources