fullstackopen
Part2 Commwserver

Communicating With Server

Part A - Rendering a collection, modules

I've done this multiple times, but say we have a array or an object. And we want to display it or something. The course talks over how to parse thru that using map and how the key attribute should not be array indexes, because it will cuase errors in the long run.

https://robinpokorny.medium.com/index-as-a-key-is-an-anti-pattern-e0349aece318 (opens in a new tab)

Starting with the following App.js file:

const App = (props) => {
  const { notes } = props
 
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        <li>{notes[0].content}</li>
        <li>{notes[1].content}</li>
        <li>{notes[2].content}</li>
      </ul>
    </div>
  )
}
 
export default App

The index.js file is as follows:

import React from 'react'
import ReactDOM from 'react-dom/client'
 
import App from './App'
 
const notes = [
  {
    id: 1,
    content: 'HTML is easy',
    important: true
  },
  {
    id: 2,
    content: 'Browser can execute only JavaScript',
    important: false
  },
  {
    id: 3,
    content: 'GET and POST are the most important methods of HTTP protocol',
    important: true
  }
]
 
ReactDOM.createRoot(document.getElementById('root')).render(
  <App notes={notes} />
)

Every note contains a boolean value marking weather the note has been categorized as important or not, and also a unique id.

Currently a single note is rendered by accessing the objects in the array and indexing a hard-coded number. This isnt practical and can be improved by using the map function

notes.map(note => <li>{note.content}</li>)

Which can be then placed inside a ul tag

const App = (props) => {
  const { notes } = props
 
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => <li>{note.content}</li>)}
      </ul>
    </div>
  )
}

At this point try running the code and you get an error.

Key attribute

Each note created must also contain a unique key value. Adding the key below:

const App = (props) => {
  const { notes } = props
 
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <li key={note.id}>
            {note.content}
          </li>
        )}
      </ul>
    </div>
  )
}

In your own words, try to explain what is going on.

Here is our App.js file in which we are taking in some props from the object notes. Returning div incapsulating a h1, and an unordered list. The unordered list is taking each exisitng notes, maping over each individual one. And returning a li element with the note.id and note.content for each one.

Map

This Generates an error as well in the console, do to the next topic.

Anti-pattern: Array Indexes as Keys

<ul>
  {notes.map((note, i) => 
    <li key={i}>
      {note.content}
    </li>
  )}
</ul>

Generating a value of an index where the note resides is not good practice and can create undesired problems.

Interesting side note that I read in the article linked, stated that the correct practice is to reach for a existing stable ID inside your list item. So it does have errors durring the render period in react. Medium (opens in a new tab)

Refactoring Modules

const Note = ({ note }) => {
  return (
    <li>{note.content}</li>
  )
}
 
const App = ({ notes }) => {
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
    </div>
  )
}

Here we are using destructuring again and we are and now we have a seperate display of a single note in its own component. A whole React application can be written in a single file. Although it is not very practical.

Common practice is to declare each component in its own file as an ES6-modules.

In smaller applications, components are used placed in a director called components, which is turn placed within the src

Then we proceed towards creating the Note component in its own component.

Exisiting Code on can be found on Github.

We will create a directory called components for our application and place a file named Note.js inside, its contents is as follows

const Note = ({ note }) => {
  return (
    <li>{note.content}</li>
  )
}
 
export default Note

The last line of the module exports the declared module, the variable Note.

Now the file that is using the component - App.js - can import the module:

import Note from './components/Note'
 
const App = ({ notes }) => {
  // ...
}

The component exported by the module is now available for use in the variable Note, just as it was earlier.

When importing our own components, their location must be given in relation to the importing file

Exercise 2.1 - 2.5

Finishing the code from exercises 1.1 - 1.5

The component structure of the application should be like such

App
  Course
    Header
    Content
      Part
      Part

Starting of with the App.js:

import Course from "./components/Course";
 
const App = () => {
  const courses = [
    {
      name: "Half Stack application development",
      id: 1,
      parts: [
        {
          name: "Fundamentals of React",
          exercises: 10,
          id: 1,
        },
        {
          name: "Using props to pass data",
          exercises: 7,
          id: 2,
        },
        {
          name: "State of a component",
          exercises: 14,
          id: 3,
        },
        {
          name: "Redux",
          exercises: 11,
          id: 4,
        },
      ],
    },
    {
      name: "Node.js",
      id: 2,
      parts: [
        {
          name: "Routing",
          exercises: 3,
          id: 1,
        },
        {
          name: "Middlewares",
          exercises: 7,
          id: 2,
        },
      ],
    },
  ];
 
  return (
    <div>
      <h1>Web development curriculum</h1>
      {courses.map((course) => (
        <Course key={course.id} course={course} />
      ))}
    </div>
  );
};
 
export default App;

Right now this courses array is acting like a json request that we will fetch from a server.

And the return statment returns an h1 and a Course component for every course that exists. This allows the program to always work no matter how many courses we add or remove.

Each Course is passed in with a key, which is a course id and course

Now created in a separate component folder we have a Course.js:

const Course = ({ course }) => {
    return (
      <div>
        <Header name={course.name} />
        <Content parts={course.parts} />
        <Total parts={course.parts} />
      </div>
    );
  };
  
  const Header = ({ name }) => {
    return <h2>{name}</h2>;
  };
  
  const Content = ({ parts }) => {
    return (
      <div>
        {parts.map((part) => (
          <Part key={part.id} name={part.name} exercises={part.exercises} />
        ))}
      </div>
    );
  };
  
  const Part = ({ name, exercises }) => {
    return (
      <p>
        {name} {exercises}
      </p>
    );
  };
  
  const Total = ({ parts }) => {
    const total = parts.reduce((sum, part) => {
      return sum + part.exercises;
    }, 0);
  
    return <strong>total of {total} exercises</strong>;
  };
  
  export default Course;

Remember Each course has a total of 3 sections, Header, Content and Total

Starting with the Course component itself, we take in a course object as props and we return a div containing a Header, Content and Total components.

Content takes in the course.parts object and maps thru each part of an individual course, and creates a Parts component. Which contains a key, a name and the number of exercises.

The part component just takes in the name and number of exercises as props and returns the name and number of exercises wrapped in a p tag

Next the Total component takes in parts as a prop, and then calculates the total number of exercises for each course using the array function reduce.

Part B - Forms

Expanding the application in the previous sections Then we add an HTML form to the component that will be used to add new notes. The prevent default function, is there to prevent a reload.

HTMLFormElement (opens in a new tab)

How do we access the data contained in the forms input element? Thats where controlled components come in.

Controlled components

React docs- Controlling input with a state variable (opens in a new tab)

Creating a peice of state called newNote, creates errors for us and the function doesn't work as expected. And we get an error saying we provided a value prop to a form field without an onChange handler. Thus causing the value to be only a read-only field

Since we assigned a piece of the App commponents state as the value attribute of the input element, the App component now controls the behavior of the input element.

To enable editing of the input element, we have to register an event handler that sychronizes the changes made to the input within the components state. This can be done with a handleNoteChange function

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(
    'a new note...'
  ) 
 
  // ...
 
  const handleNoteChange = (event) => {
    console.log(event.target.value)
    setNewNote(event.target.value)
  }
 
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note => 
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleNoteChange}
        />
        <button type="submit">save</button>
      </form>   
    </div>
  )
}

If we run the program we see that the input we are typing is getting put out due to the console log we have. And we we check the React Dev tools we also see we have a new state and it tracking every letter we input.

Now the App components newNote state reflects the current value of the input, we can complete the addNote function for created enw notes

const addNote = (event) => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    important: Math.random() < 0.5,
    id: notes.length + 1,
  }
 
  setNotes(notes.concat(noteObject))
  setNewNote('')
}

First, we create a new object for the note called noteObject that will recieve its content from the components newNote state. The unique identifier id is generated based on the total number of notes. This works because our notes are never deleted.

The new note is added to the list of notes using the concat (opens in a new tab) array method.

This method does not mutate the original notes array, but rather creates a new copy of the array with the new item added to the end. This is very important since we must never mutate state directly in React!

Filtering Displayed Elements

Adding new functionality to our application that allows us to only view the important notes. Lets add a piece of state to the App compponent that keeps track of which notes should be displayed

Then lets change the component so that it stores a list of all the notes to be displayed in the notesToShow variable. The items on the list depend on the state of the component itself

...
  const notesToShow = showAll
    ? notes
    : notes.filter(note => note.important === true)
 
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...
    </div>
  )

Explaining the notesToshow variable:

  const notesToShow = showAll
    ? notes
    : notes.filter(note => note.important === true)
 

This is rather compact and essentially what we are doing here is creating a variable notesToShow to filter weather or not to display only notes that are tagged important or not.

The definition used the conditional operator, the basic syntax of the operator functions as follows:

const result = condition ? val1 : val2

the result variable will be set to the value of val1 if condition is true. If the condition is false, the result variable will be set to the value of val2.

If the value showAll is false, the notesToShow variable will be assigned to a list that only contains the important property set to true. The filtering is done with the help of the array filter method:

notes.filter(note => note.important === true)

Next the functionality

-- To be finished... forms section --

Exercise 2.6 - 2.10

Note: the way to start server is "npm start"

2.6 Was completed by simple debugging and comparing example to my problem

2.7 Problem - prevent the user from being able to add names that already exist in the phonebook.

  • Figure out where the comparison of the two object needs to take place

Ended up creating a variable that uses the .some() method https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some (opens in a new tab)

2.8 Problem - Expand your application by allowing users to add phone numbers to the phone book. You will need to add a second input element to the form (along with its own event handler):

2.9 Problem - Implement a search field that can be used to filter the list of people by name:

  • Creating a search query, requires state, event handler, and a function to filter the array
  const filteredPersons = persons.filter((person) =>
    person.name.toLowerCase().includes(searchQuery.toLowerCase())
  );

2.10 Problem - If you have implemented your application in a single component, refactor it by extracting suitable parts into new components. Maintain the application's state and all event handlers in the App root component.

const App = () => {
  // ...
 
  return (
    <div>
      <h2>Phonebook</h2>
 
      <Filter ... />
 
      <h3>Add a new</h3>
 
      <PersonForm 
        ...
      />
 
      <h3>Numbers</h3>
 
      <Persons ... />
    </div>
  )
}

So we need 3 seperate components: 'Filter', 'PersonForm' and 'PersonList'.

Part C - Getting data from server

This section will be client-side (broswer) functionality. We will go in to a direction in which we familiarize ourselves with how the code executing in the broswer communicates with the backend.

In this section we will be using a JSON server to act as our server.

https://github.com/typicode/json-server (opens in a new tab)

Above is the github about JSON Server. "Get a full fake REST API with zero coding in less than 30 seconds"

Instructions to install JSON server locally

Going forward the main principle is that we will save notes to the server, or technically saving them to the json-server. React code fetches notes from server and renders them to screen. When a new note is added, it persist in "memory"

json-server stores all the data in the db.json file, which resides on server. In the real world it would be store in some database.

We will get more familiar with the princiles of implimenting server-side functionality in more detail in part 3

The broswer as a runtime enviorment

Goes over using the outdated way of fetching data that was used in part 0 using XMLHttRequest.

npm

In this course we will be using the axios library for communication between the browser and server.

Most JS projects are defined using the node package manager, aka npm. Dependencies are just the external libraries the project has.

Axios can be installed via the command line

npm install axios

We get an error when trying to start the server. It happens due to the fact that the server cannot bind to port 3001 because it is already occupied by a previously started json-server.

npm install axios
npm install json-server --save-dev

There is a difference in the parameters. Axios is installed as a runtime dependency of the aplication, because the execution of the program requires the existence of the library.

On the other hand, json-server was installed as a development dependency using save dev, since the program itself does not require it. It is used for assistance durring software development.

Command to start server now

npm run server

Axios and promises

Going forward json-server is assumed to be running in port 3001

Adding the following code in index.js:

import axios from 'axios'

const promise = axios.get('http://localhost:3001/notes')
console.log(promise)

const promise2 = axios.get('http://localhost:3001/foobar')
console.log(promise2)

From my understanding we are importing a library axios, and creating 2 promises.

The result should be printed in the console.

Axios method get returns a promise.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises (opens in a new tab)

A promise is an object representing the eventual completion or failure of a asynchronous operation. A promise can have 3 distinct states.

  1. The promise is pending: Means the final value (one of the folowing two) is not available yet

  2. The promise is fulfilled: Means that the operation has been completed and the final value is available which generally is a successful operation. This state is also called resolved

  3. The promise is rejected: Means that an error prevented the final value from being determined which generally represents a failed operation

To access the result of the operation represented by the promise, we must register an event handler to the promise. Do this using a then method.

The JavaScript runtime enviornment calls the callback function registered by the then method providing it with a response object as a parameter. The response object contains all the essential data related to the response of an HTTP GET request. Which includes the returned data, status code and headers.

axios
  .get('http://localhost:3001/notes')
  .then(response => {
    const notes = response.data
    console.log(notes)
  })

Effect-Hooks

With the Effect Hook inserted in the code, the following is printed to the console:

render 0 notes
effect
promise fulfilled
render 3 notes

website initially rendered, effect is after rendering, axios.get initiates the fetching of data from the server, as well as registers the following function as an event handler for the operation:

response => {
  console.log('promise fulfilled')
  setNotes(response.data)
}

When the data arrives from the server, the JavaScript runtime calls the function registered as the event handler, which prints promise and fulfilled to the console and stores the notes recieve from the server in to the state using the function setNotes (response.data).

As always, a call to state-updating function triggers the re-rendering of the component. Render 3 notes is printed to the console and the notes are fetched from the server and are rendered.

Rewriting the code a bit differently

const hook = () => {
  console.log('effect')
  axios
    .get('http://localhost:3001/notes')
    .then(response => {
      console.log('promise fulfilled')
      setNotes(response.data)
    })
}
 
useEffect(hook, [])

useEffect takes two parameters. The first is a function, the effect itself.

The second parameter of useEffect is used to specify how often the effect is run. If empty array, then the effect is only run along the first render of the component.

Another way we could have written the code is shown below:

useEffect(() => {
  console.log('effect')
 
  const eventHandler = response => {
    console.log('promise fulfilled')
    setNotes(response.data)
  }
 
  const promise = axios.get('http://localhost:3001/notes')
  promise.then(eventHandler)
}, [])

A reference to an event handler function is assigned to the variable eventHandler. The promise returned by the get method of Axios is stored in the variable promise. The registration of the callback happens by giving the eventHandler variable, referring to the event-handler function, as a parameter to the then method of the promise. It isn't usually necessary to assign functions and promises to variables, and a more compact way of representing things, as seen further above, is sufficient.

We still run to a problem wiht our application, when adding new notes they are not stored on the server.

The development runtime enviornment

The configuration for the whole application has steadly grown more complex. Lets review what happens and where.

The JavaScript code making up our React application is run in the broswer. The broswer getes the JavaScript from the React dev server, which is the application that runs after running the command npm start. The dev-server transforms the JavaScript into a format understood by the browser. Among other things, it stiches together JavaScript from different files into one file. The React application running in the browswer fetches the JSON formated data from json-server runnign on port 3001 on the machine. The server we query the data from - json-server - gets its data from the file db.json.

At this point in the development, all the parts of the application reside on the localhost, in the next part we will deploy it to the internet.

Exercise 2.11

The Phonebook Step6

We continue with developing the phonebook. Store the initial state of the application in the file db.json, which should be placed in the root of the project.

{
  "persons":[
    { 
      "name": "Arto Hellas", 
      "number": "040-123456",
      "id": 1
    },
    { 
      "name": "Ada Lovelace", 
      "number": "39-44-5323523",
      "id": 2
    },
    { 
      "name": "Dan Abramov", 
      "number": "12-43-234345",
      "id": 3
    },
    { 
      "name": "Mary Poppendieck", 
      "number": "39-23-6423122",
      "id": 4
    }
  ]
}

http://localhost:3001/persons

is the website, persons is regarding to the name of the object itself.

Reminder in a real world scenario data would be stored in some sort of database. We will get in to server-side functionality in more detail in the next part

Installing axios

npm install axios

Installing json-server

npm install json-server --save-dev

Using Effect-hooks

Part D - Altering data in server

In this section we will look at some conventions used by json-server and REST APIs in general. In particular, we will be taking a look at the conventional use of routes, aka URLs and HTTP request types, in REST.

REST

In REST terminology, we refer to individual data objects, such as notes in our application, as recources. Every resource has a unique addresss associated with its URL. We would be able to locate an individual note at the recourse URL notes/3, where 3 is the id of the resource. The notes URL on the other hand would point to a resource collection containing all the notes.

Resources are fetched from the server with HTTP GET request.

Creating a new resource for storing a note is done by making an HTTP POST request to the notes URL according to the REST convention that the json-server adheres to. The data for the new note resource is sent in the body of the request.

json-server requires all data to be sent in JSON format. The data must be correctly formatted string and that the request must contain the Content-Type request header with the value application/json.

Sending Data to the Server

Adding the following changes to the event handler responsible for creating a new note:

...
  axios
    .post('http://localhost:3001/notes', noteObject)
    .then(response => {
      console.log(response)
    })
...

We create a new object for the note but omit the id property, to let the server generate ids for our resources. The object is sent to the server using the axios post method. The registered event handler logs the response that is sent back from the server to the console.

Note that we now omit the id property in the code above

The payload tab can be used to check the request data along with the response tab, which can show what was the data the server reponded with

Important detail to remember is that concat method does not change the components original state, bute instead creates a new copy of the list. Next part we will use tools like Postman that helps us to debug our server applications

Changing the Importance of Notes

Lets add a button to every note that can be used for toggling its importance

Changing the Note component:

const Note = ({ note, toggleImportance }) => {
  const label = note.important
    ? 'make not important' : 'make important'
 
  return (
    <li>
      {note.content} 
      <button onClick={toggleImportance}>{label}</button>
    </li>
  )
}

We add a button to the component and assign its event handler as the toggleImportance function passed in the components props.

Individual notes stored in the json-server backend can be modified in two different ways by making HTTP request to the the note's unique URL. We can replace the entire note with an HTTP PUT request or only change some of the note's properties with an HTTP PATCH request.

The final form of the event handler function is the following:

const toggleImportanceOf = id => {
  const url = `http://localhost:3001/notes/${id}`
  const note = notes.find(n => n.id === id)
  const changedNote = { ...note, important: !note.important }
 
  axios.put(url, changedNote).then(response => {
    setNotes(notes.map(n => n.id !== id ? n : response.data))
  })
}

Explaining the code

The first line defines the unique URL for each note resource based on its id. The array find method is used to find the note we want to modify, and we then assign it to the note variable After we create a new object that is an exact copy of the old note, apart from the important property that has the value flipped.

Promises and Errors

To allow users to delete notes, we need to clear the situation in which a user tries to change the importance of the note that has already been deleted from the system.

Common way of adding a handler for rejected promises is to use catch method.

In practice, the error handler for the rejected promises is defined like this:

axios
  .get('http://example.com/probably_will_fail')
  .then(response => {
    console.log('success!')
  })
  .catch(error => {
    console.log('fail')
  })

If the request fails, the event handler registered with the catch method gets called. The catch method is often utilized by placing in deeper within the promise chain.

Full stack developer's oath

Full stack development is extremely hard, that is why I will use all the possible means to make it easier

I will have my browser developer console open all the time

I will use the network tab of the browser dev tools to ensure that frontend and backend are communicating as I expect

I will constantly keep an eye on the state of the server to make sure that the data sent there by the frontend is saved there as I expect

I progress with small steps

I will write lots of console.log statements to make sure I understand how the code behaves and to help pinpoint problems

If my code does not work, I will not write more code. Instead, I start deleting the code until it works or just return to a state when everything was still working

When I ask for help in the course Discord or Telegram channel or elsewhere I formulate my questions properly, see here how to ask for help

Exercises 2.12-2.15

2.12 Currently, the numbers that are added to the phonebook are not saved to a backend server. Fix this situation.

2.13 Extract the code that handles the communication with the backend into its own module by following the example shown earlier in this part of the course material.

2.14 Make it possible for users to delete entries from the phonebook. The deletion can be done through a dedicated button for each person in the phonebook list. You can confirm the action from the user by using the window.confirm method

Since each individual person needs a button to delete it is best to go in to PersonList component

  • Create a button in each person
  • Create a handleDelete function

2.15 (To be continued)

Part E - Adding styles to React

Using className attribute

Improved error message

Inline Styles

CSS properties are typically written in camelCase