(if you're using a component framework such as react-native-web, replace <p> by <Text> or whatever relevant)
Try it! Run npm start from the root, to run both the front end and the back end. It should work just like it used to.
You can try to change the url in the fetch function, and see the error messages pop up (you may have to scroll up or down to read it, depending where you put it).
Implementing the CRUD methods in the React Component
Now, let's implement the other methods. We're going to just follow, again, the same protocol we had in the controller, and the rest interface. This is not necessary, but makes it easier to understand.
We're going to need:
getContact
deleteContact
updateContact
createContact
getContactsList
The item #5, getContactsList, already exists; it is just running automatically inside onComponentDidMount. We're going to take it out of there. Take everything inside of componentDidMount and put it in a method getContactsList, written like so:
...getContactsList= () =>// everything that was in componentDidMount before...
Then:
...componentDidMount(){this.getContactsList()}...
This way, our code works exactly the same as before, but we've made getContactsList reusable. We can now call it from other places than componentDidMount.
Swell. Let's implement the skeleton of the other methods. They all follow the same pattern:
SOME_METHOD=async () => {try{// request something from the rest interface:constresponse=awaitfetch('http://localhost:8080/contacts/SOME_COMMAND')// transform the http response to json (try to parse it as json):constanswer=awaitresponse.json()if(answer.success){if (answer.success){// if successful, do something to the state// maybe manipulate the cached contacts in memory to replicate// the database changes, or display a message, etcthis.setState({ SOME_KEY:answer.result }) }else{// set an error in the statethis.setState({error_message:answer.message}) } } }catch(err){this.setState({error_message:err.message}) }}
Below is the full implementation of all the methods.
This is going to be long, but if you look at the methods closely, you will notice they actually do not differ greatly from each other.
...getContact=async id => {// check if we already have the contactconstprevious_contact=this.state.contacts_list.find( contact =>contact.id === id );if (previous_contact) {return; // do nothing, no need to reload a contact we already have }try {constresponse=awaitfetch(`http://localhost:8080/contacts/get/${id}`);constanswer=awaitresponse.json();if (answer.success) {// add the user to the current list of contactsconstcontact=answer.result;constcontacts_list= [...this.state.contacts_list, contact];this.setState({ contacts_list }); } else {this.setState({ error_message:answer.message }); } } catch (err) {this.setState({ error_message:err.message }); } };deleteContact=async id => {try {constresponse=awaitfetch(`http://localhost:8080/contacts/delete/${id}` );constanswer=awaitresponse.json();if (answer.success) {// remove the user from the current list of usersconstcontacts_list=this.state.contacts_list.filter( contact =>contact.id !== id );this.setState({ contacts_list }); } else {this.setState({ error_message:answer.message }); } } catch (err) {this.setState({ error_message:err.message }); } };updateContact=async (id, props) => {try {if (!props ||!(props.name ||props.email)) {thrownewError(`you need at least name or email properties to update a contact` ); }let url="";const{name,email}= props;if(name && email) { url=`http://localhost:8080/contacts/update/${id}?name=${name}&email=${email}`; }if(name) { url=`http://localhost:8080/contacts/update/${id}?name=${name}`; }if(email) { url=`http://localhost:8080/contacts/update/${id}?email=${email}`; }constresponse=awaitfetch( url );constanswer=awaitresponse.json();if (answer.success) {// we update the user, to reproduce the database changes:constcontacts_list=this.state.contacts_list.map(contact => {// if this is the contact we need to change, update it. This will apply to exactly// one contactif (contact.id === id) {constnew_contact= { id:contact.id, name:props.name ||contact.name, email:props.email ||contact.email };return new_contact; }// otherwise, don't change the contact at allelse {return contact; } });this.setState({ contacts_list }); } else {this.setState({ error_message:answer.message }); } } catch (err) {this.setState({ error_message:err.message }); } };createContact=async props => {try {if (!props ||!(props.name &&props.email)) {thrownewError(`you need both name and email properties to create a contact` ); }const { name,email } = props;constresponse=awaitfetch(`http://localhost:8080/contacts/new/?name=${name}&email=${email}` );constanswer=awaitresponse.json();if (answer.success) {// we reproduce the user that was created in the database, locallyconstid=answer.result;constcontact= { name, email, id };constcontacts_list= [...this.state.contacts_list, contact];this.setState({ contacts_list }); } else {this.setState({ error_message:answer.message }); } } catch (err) {this.setState({ error_message:err.message }); } };getContactsList=async order => {try {constresponse=awaitfetch(`http://localhost:8080/contacts/list?order=${order}` );constanswer=awaitresponse.json();if (answer.success) {constcontacts_list=answer.result;this.setState({ contacts_list }); } else {this.setState({ error_message:answer.message }); } } catch (err) {this.setState({ error_message:err.message }); } };componentDidMount() {this.getContactsList(); }...
This is a lot to take in, so take a moment to observe each function individually. I don't advise copy pasting, but rather to copy each function manually, so you can understand what it does.
note: of course, we could abstract away the repeating parts, such as fetch...then...catch.... For example, we could have a function that goes:
request=async url => {try{constresult=fetch(`//localhost:8080/contacts/${url}`)constanswer=result.json()if(answer.success){returnanswer.result }else{this.setState({ error_message:answer.message }); } }catch (error){this.setState({ error_message:error.message }); }}deleteContact=async id => {constresult=awaitthis.request(`delete/${id}`)constcontacts_list=this.state.contacts_list.filter( contact =>contact.id === id );// remove the user from the current list of usersthis.setState({ contacts_list });};
But for the moment, we keep things stupid and simple.
We might, later, discover we need to handle specific methods in specific ways (for example, checking that the user is allowed to pursue the action, or needing to display a confirmation dialog). At that point, having a lot of abstraction would make things harder. This is an example of early optimization, and is something to generally avoid.
Thus, we'll keep things stupid and simple. We can add all the indirection we want later ("indirection" is the process of adding pathways to the code, which abstracts away things, but makes each function less readable by itself, because you need to jump around to understand it).
Very well, we will now proceed to try those methods from the user facing React.
We will create four buttons, which will each call a method, to test it. Somewhere in render, write:
note: With react-native-web you should use onPress instead of onClick, and use title="" instead of writing the text in the button's children
You can try the buttons. However, of course, some of them you can only try once. For example, when you press delete, you will need to provide another id next time, since the id you deleted was, well, deleted. But that's ok. You can manually change the parameters, test again. Once you are satisfied, remove the buttons. We will implement the actual behavior properly in the next steps.
First, let's try to create new contacts. We're going to need two text inputs. Import TextInput if you're using react-native-web, or simply use an <input type="text"> if you're using regular html. Then, in render:
... But our component is cluttered enough with all those CRUD methods, so we're creating those handlers directly in-place.
note: this means the handlers are re-created every time a render occurs, which will come with a performance penalty; but we can always create those handlers later if we want to.
don't forget to add the keys name and email to state
...state = {... name:"", email:"",}...
Then, add a form, which, when submit, runs the function onSubmit (which doesn't exist yet, we will create it).
...onSubmit= (evt) => {// stop the form from submitting:evt.preventDefault()// extract name and email from stateconst { name,email } =this.state// create the contact from mail and emailthis.createContact({name, email})// empty name and email so the text input fields are resetthis.setState({name:"", email:""}) }...
The whole set of changes looks like so:
...onSubmit= () => {// extract name and email from stateconst { name,email } =this.state// create the contact from mail and emailthis.createContact({name, email})// empty name and email so the text input fields are resetthis.setState({name:"", email:""}) }render() {const { contacts_list,error_message } =this.state;return (... <formclassName="third"onSubmit={this.onSubmit}> <inputtype="text"placeholder="name"onChange={evt =>this.setState({ name:evt.target.value })}value={this.state.name} /> <inputtype="text"placeholder="email"onChange={evt =>this.setState({ email:evt.target.value })}value={this.state.email} /> <div> <inputtype="submit"value="ok" /> <inputtype="reset"value="cancel"className="button" /> </div> </form>... ); }...
Whoa! We can add things to our database now. Did you try the form?
Let's think about where the other things would fit:
we want a delete button next to each item
we want an input field next to each item so we can edit it
Let's start with the delete button. Change the contact list loop to: