#100DaysOfMERN - Day 20

Subscribe to my newsletter and never miss my upcoming articles

I'm still working on my Boring Book App. After covering GET, POST and DELETE requests, this one will be about a PUT request, and it turned out that it was an interesting challenge. The request itself was easy, because I've already learned everything I need in previous posts, which is just:

  • how to set up a server route to a specific book
  • how to read/write data to a file

The interesting part was the React Frontend, and creating a good UI - in a React way. But first, what is a PUT request anyway?

✏ PUT requests

This kind of request is used to edit an already existing entry, or to submit a new resource/book to a specific address. The difference between POST and PUT is that the former makes a request to the general /api/books/ address, and lets the server handle where to put the new data (in my example, the server goes through the already existing books array, and assigns an ID to the new book, based on the IDs that are already present).

A PUT request however always goes to a specific ID like /api/books/3, and updates/creates this and only this resource.

While researching, I also learned a fancy new word: Idempotence. GET, PUT and DELETE requests are idempotent operations, which means that it makes no difference for the state of the data if I make one request or many identical requests in a row.

This is trivially true for GET, as GET doesn't change any data at all. I could, of course, I could tell my server to delete the whole database when someone makes a GET request, but I'd be violating the basic rules of a REST API, and those rules are kind of helpful and should be followed, right?

Anyway. Multiple PUT requests that all update a specific resource are also considered idempotent, because even if I make different updates each time, the data will still have one entry for this resource, and although its content may change, the fact that there's still only one resource at this one address is always true. Similar for DELETE - I do change the state of the data with the first request, but after that, I can make as many more DELETE requests to this resource as I want, I might get an error because it doesn't exist anymore, but the state of the system will remain the same for each subsequent request.

POST is the only operation that, if performed multiple times in a row, is likely to make changes to the system each time. In my book app, even if I submitted the same book over and over again, the server would create a new ID for each one and push it into the data array.

So much for theory, back to code.

✏ The Backend

I'll start with the Backend this time, and set up the route. Like in a DELETE request, the URL goes to the specific book I'd like to edit. The ID of the book will come from the request.params object, and the new book title will come from the request.body object.

To update the books list, I'll map over them, and if the ID matches, I'll return a new object where I spread all the old values in, and update the title.

Once that list is updated, I can write it to the data file, and return the updated list to the Frontend:

server.js

app.put('/api/books/:bookID', (request, response) => {
    const id = request.params.bookID;
    const newTitle = request.body.title;

    books = books.map(b => b.id === id ? {...b, title:newTitle} : b)
    fs.writeFileSync(src, JSON.stringify(books))
    response.json(books)
})

For the Frontend, I'll now need a function that makes a request to this URI *) and sends the new book title.

*) (Note that I'm using the term URI instead of URL. If you'd like to fall into a deep deep rabbit hole, I recommend researching the difference - after hours of reading, you might find that you're still as confused as before, and that nitpicks about which is the correct term are absolutely pointless and even annoying, and in the end decide that you can probably use the terms interchangeably in 99% of cases. I'll deal with this problem by randomly and carelessly switching between both from now on.)


✏ The Current Frontend

To make it a little easier to talk about it, I'll start with a screenshot of the current state of the App:

book-app-frontend.jpg

And this is the current code in the render method:

App.js

<div className="app">
    <div className="book-list">
        <ul>
            {books.map(b => {
                return (
                    <li key={b.id}>
                        <span><b>{b.id}: </b>{b.title}</span>
                        <button
                            type="button"
                            value={b.id}
                            onClick={handleDeleteBtn}
                        >delete
                        </button>
                    </li>
                )
            })}
        </ul>
    </div>
    <form>
        <div className="wrapper">
            <input type="text" id="input-title" value={title} onChange={handleTitleInput}/>
            <button type="submit" onClick={handleSubmitBtn}>submit</button>
        </div>
    </form>
</div>

What I'd like to do now is:

  • add an "edit" button next to the "delete" button for each book
  • if the "edit" button is clicked, the <span> that's displaying the title should become an <input> field, pre-populated with the current title. Also, the "edit" button should change to a "save" button
  • a click on the "save" button should trigger the PUT request
  • a click somewhere on the body should cancel the "edit" operation and change the app back to just displaying the book list

Screenshot from me from the future, where I've already implemented the above:

book-app-frontend-2.jpg

I've done such things a thousand times in Vanilla JS, and how it's done there is always the first thing that comes to mind, but I'm getting better at thinking in React. One good rule of thumb is that whenever I think I need document.querySelector(), I'm doing it wrong, and should instead use state and conditional rendering.


✏ New State Variables

At the moment, I have two state variables:

  • books: The array of books that I'm fetching from the Backend
  • title: The title of a new book that is entered into the <input> field at the bottom

I'll now add two more variables editID and editTitle:

  • editID: will store the current ID, if someone clicks on the "edit" button of a book
  • editTitle: will control the <input> field that gets rendered instead of the <span>

So this is state:

const [books, setBooks] = useState([]);
const [title, setTitle] = useState('');
const [editID, setEditID] = useState('');
const [editTitle, setEditTitle] = useState('');

When rendering the book list, I'll use the editID to determine what to render for that particular entry. If the book's ID is equal to the editID, I'll render an input field with the title and a "save" button, otherwise, a span with the title and an "edit" button.

Here's just the .map function, I've edited out "obsolete" attributes like type="text" to keep it readable:

{books.map(b => {

    const isEdit = editID === b.id;

    return (
        <li key={b.id}>
            <span>
                <b>{b.id}: </b>
                {isEdit
                    ? <input value={editTitle} onChange={handleTitleChange}/>
                    : <span>{b.title}</span>
                }
            </span>

            {isEdit
                ? <button value={b.id} onClick={handleSaveBtn}>save</button>
                : <button value={b.id} onClick={handleEditBtn}>edit</button>
            }

            <button value={b.id} onClick={handleDeleteBtn}>delete</button>
        </li>
    )
})}

✏ Event Handlers

handleTitleChange

This one just controls the input field:

const handleTitleChange = e => setEditTitle(e.target.value);

handleEditBtn

If clicked, it'll

  • write the ID of the book I'm currently trying to edit into editID
  • get the corresponding title from the books array and write it into editTitle (which will prepopulate the input field with the title)
function handleEditBtn(e){
    const id = e.target.value;
    setEditID(id);

    const titleToEdit = books.find(b => b.id === id).title;
    setEditTitle(titleToEdit)
}

handleSaveBtn

This will finally trigger my PUT request. It'll

  • get the book ID from the button's value, in order to build the correct URL for the route
  • create a new object holding the new title, to be sent as the request body
  • make the PUT request with axios.put(url, body)
  • clear editID and editTitle to reset the app back to just displaying the list
function handleSaveBtn(e){
    const id = e.target.value;
    const bookUrl = `${url}/${id}`;

    const newTitle = {title: editTitle};
    axios.put(bookUrl, newTitle).then(res => setBooks(res.data));

    setEditID('');
    setEditTitle('')
}

And that's it! The Backend already knows how to handle the incoming request, and my Boring Book App is hereby finished. Except one final function to cancel an edit by clicking somewhere on the app - that'll be a simple onClick event on the whole app container. If the click happened anywhere but a button or an input field, it'll reset the app by setting the state of editID and editTitle to an empty string:

function handleClickOff(e){
    if (!e.target.closest('button') && !e.target.closest('input')){
        setEditID('');
        setEditTitle('')
    }
}

return (
    <div className="app" onClick={handleClickOff}>
        {/* .... */}
    </div>
)

✏ Wrapping it Up

This mini-series within the series was quite fun, I've built the world's most boring application with less functionality than a ToDo-App, but I've gained a firm unterstanding of what's happening with the different request types of a CRUD application.

To really master this and make it stick, it'll take a few more projects, but I've still got 80 days left and enough ideas. For example, I could make a Weight and Fitness Tracker App, maybe with data visualisation using React 3D, to stop me from body-shaming myself, or a #100DaysOfMERN App, where I can store a number of keywords for each article, search through them, keep track of topics I haven't covered yet, etc. In fact I just fell in love with the idea, because my list of previous articles at the bottom of each blog grows longer and longer. But I'm going to do that with a database instead of a file, so I'm not yet ready for it.

Some other loose threads that are hanging around here: There's the FullStackOpen course, where the last wall I hit was that I couldn't figure out how to deploy an app with Heroku, and the MERN ecommerce Udemy course, which I stopped at the point where we installed Postman, because I had no clue what I'm installing that for. Roughly, the next step should involve connecting to a MongoDB database, but I'll fill a couple of gaps first that are still on the list, before I move on (see below).


✏ Recap

I've learned

  • how to make a PUT request to update an already existing entry

✏ Next:

  • building a server with Vanilla JS instead of Express
  • handling promises with async/await

✏ Thanks for reading!

I do my best to thoroughly research the things I learn, but if you find any errors or have additions, please leave a comment below, or @ me on Twitter. If you liked this post, I invite you to subsribe to my newsletter. Until next time 👋


✏ Previous Posts

  • Day 1: Introduction, Node.js, Node.js in the terminal
  • Day 2: npm, node_modules, package.json and package-lock.json, local vs global installation of packages
  • Day 3: Create a React app without create-react-app, Webpack, Babel
  • Day 4: npx and cowsay
  • Day 5: npm vs. npx, npm audit, semantic versioning and update rules
  • Day 6: Call stack, event loop, JavaScript engine, JavaScript runtime
  • Day 7: Call stack and event loop in Node.js, setImmediate()
  • Day 8: setImmediate(), process.nextTick(), event loop phases in Node.js
  • Day 9: Network requests with XMLHttpRequest and callbacks
  • Day 10: Promises
  • Day 11: Network requests with XMLHttpRequest and Promises
  • Day 12: React Quiz App part 1
  • Day 13: React Hangman
  • Day 14: FullStackOpen course 1: details of GET and POST requests, request headers
  • Day 15: React Hangman: Trigger fetch with click event callback vs useEffect
  • Day 16: REST API and CRUD
  • Day 17: Boring Book App part 1: React Frontend, Express Backend, GET requests, CORS
  • Day 18: Boring Book App part 2: POST request, File System API
  • Day 19: Boring Book App part 3: Request Parameters, DELETE request

No Comments Yet