React Part I

We now have a controller, an http interface for it.

We need to use this from the front-end, through React.

We were already using one route, contacts/list, which gave us the list of contacts.

We need to change it slightly, because previously, the data received was a list of contacts in the form:

[ Contact, Contact, Contact, ... ]

Now it is:

{ success: true,
  result: [ Contact, Contact, Contact, ... ]
}

(or {success:false, message:"blah blah"})

So, let's change our class a bit:

Where it used to say:

...
state = {
  contacts_list:[]
}
...
 async componentDidMount(){
    const getList = async()=>
    {
        try{
          const response = await fetch('//localhost:8080/contacts/list')
          const data = await response.json()
          this.setState({contacts_list:data})
        }catch(err){
          console.log(error)
        }
    }
    getList();
  }
...

make it do:

state = {
  contacts_list:[],
  error_message:""
}
...
 async componentDidMount(){
    try{
      const response = await fetch('//localhost:8080/contacts/list')
      const answer = await response.json()
      if(answer.success){
        const contacts_list = answer.result
        this.setState({contacts_list})
      }else{
        const error_message = answer.message 
        this.setState({error_message})
      }
    }catch(err){
      this.setState({error_message:err.message})
    }
  }

Now, if the json sends back an error, it will be stored int he state.

We also changed the catch to store the error in state too.

The error is stored in the state, but we can't see it at the moment.

Let's do something in the render too, to show the error. We will add a conditional, which will show the error if it is there

  render() {
    const { contacts_list, error_message } = this.state

    return (
      ...
        { error_message ? <p> ERROR! {error_message}</p> : false }
      ...
    );
  }

(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:

  1. getContact

  2. deleteContact

  3. updateContact

  4. createContact

  5. 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:
    const response = await fetch('http://localhost:8080/contacts/SOME_COMMAND')
    // transform the http response to json (try to parse it as json):
    const answer = await response.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, etc
        this.setState({ SOME_KEY:answer.result })
      }else{
        // set an error in the state
        this.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 contact
    const previous_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 {
      const response = await fetch(`http://localhost:8080/contacts/get/${id}`);
      const answer = await response.json();
      if (answer.success) {
        // add the user to the current list of contacts
        const contact = answer.result;
        const contacts_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 {
      const response = await fetch(
        `http://localhost:8080/contacts/delete/${id}`
      );
      const answer = await response.json();
      if (answer.success) {
        // remove the user from the current list of users
        const contacts_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)) {
        throw new Error(
          `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}`;
      }

      const response = await fetch(
        url
      );
      const answer = await response.json();
      if (answer.success) {
        // we update the user, to reproduce the database changes:
        const contacts_list = this.state.contacts_list.map(contact => {
          // if this is the contact we need to change, update it. This will apply to exactly
          // one contact
          if (contact.id === id) {
            const new_contact = {
              id: contact.id,
              name: props.name || contact.name,
              email: props.email || contact.email
            };
            return new_contact;
          }
          // otherwise, don't change the contact at all
          else {
            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)) {
        throw new Error(
          `you need both name and email properties to create a contact`
        );
      }
      const { name, email } = props;
      const response = await fetch(
        `http://localhost:8080/contacts/new/?name=${name}&email=${email}`
      );
      const answer = await response.json();
      if (answer.success) {
        // we reproduce the user that was created in the database, locally
        const id = answer.result;
        const contact = { name, email, id };
        const contacts_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 {
      const response = await fetch(
        `http://localhost:8080/contacts/list?order=${order}`
      );
      const answer = await response.json();
      if (answer.success) {
        const contacts_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{
    const result = fetch(`//localhost:8080/contacts/${url}`)
    const answer = result.json()
    if(answer.success){
      return answer.result
    }else{
      this.setState({ error_message: answer.message });
    }
  }catch (error){
    this.setState({ error_message: error.message });
  }
}
deleteContact = async id => {
  const result = await this.request(`delete/${id}`)
  const contacts_list = this.state.contacts_list.filter(
    contact => contact.id === id
  );
  // remove the user from the current list of users
  this.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:

  <button onClick={() => this.getContactsList()}>
    Reload
  </button>
  <button onClick={() => this.getContact(2)}>
    Get
  </button>
  <button onClick={() => this.updateContact(2, { name: "a" })}>
    Update
  </button>
  <button onClick={() => this.deleteContact(6)}>
    Delete
  </button>
  <button onClick={() =>this.createContact({ name: "test", email: "testing" })}>
    Create
  </button>

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:

<input type="text" placeholder='name' onChange={(evt)=> this.setState({name:evt.target.value})} value={this.state.name}>

And do the same for email.

Notice that we haven't created handlers for the inputs. We could've created two handlers like so:

onNameChange = (evt) => {
  const nameInput = evt.target
  const name = nameInput.value
  this.setState({name})
}
onEmailChange = (evt) => {
  const emailInput = evt.target
  const email = emailInput.value
  this.setState({email})
}
...
<input type="text" placeholder='name' onChange={this.onNameChange} value={this.state.name}>
<input type="text" placeholder='name' onChange={this.onEmailChange} value={this.state.email}>

... 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 state
    const { name, email } = this.state
    // create the contact from mail and email
    this.createContact({name, email})
    // empty name and email so the text input fields are reset
    this.setState({name:"", email:""})
  }
...

The whole set of changes looks like so:

...
  onSubmit = () => {
    // extract name and email from state
    const { name, email } = this.state
    // create the contact from mail and email
    this.createContact({name, email})
    // empty name and email so the text input fields are reset
    this.setState({name:"", email:""})
  }
  render() {
    const { contacts_list, error_message } = this.state;

    return (
      ...
        <form className="third" onSubmit={this.onSubmit}>
          <input
            type="text"
            placeholder="name"
            onChange={evt => this.setState({ name: evt.target.value })}
            value={this.state.name}
          />
          <input
            type="text"
            placeholder="email"
            onChange={evt => this.setState({ email: evt.target.value })}
            value={this.state.email}
          />
          <div>
            <input type="submit" value="ok" />
            <input type="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:

  1. we want a delete button next to each item

  2. 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:

...
  {contacts_list.map(contact => (
    <div key={contact.id}>
      <span>
        {contact.id} - {contact.name}
      </span>
      <button onClick={() => this.deleteContact(contact.id)} className="warning">x</button>
    </div>
  ))}
...

Try to delete a few contacts. Neat!

Now, the edit. This is going to be significantly harder.

We want to:

  1. have a field for the name

  2. have a field for the mail

  3. we want both fields to be linked to the actual value (as in the contact's name should display in the field)

  4. we want to be able to reset the field and restore it to the previous value (if the user didn't press "ok")

  5. we probably want, later, to display error messages and feedback messages, and do all sort of things

  6. most likely, we will have more fields than name and email, so this is going to grow, a lot.

  7. also, we don't want this edit form to show all the time; it should show only if we are actually editing the component

That's a lot of things to do. We'd better create a whole new component for it.

Hang in there, take a breather, make some coffee, then let's move to Part 2 - Contact Component

Last updated