JavascriptD_870

When localStorage and cookies reach their limits, when their thread blocking nature slow your app down, it's time to reach for IndexDB. But why live with the low-level API that can be clumsy to get the hang of? Reach for PouchDB and start taking advantage of this asynchronous, performant browser storage today.

There are many ways to store data client-side, and we covered localStorage, sessionStorage, and cookies in Part 1 of this article, but that’s not where it ends! Modern browsers come with another approach to client-side data storage, providing asynchronous reads and writes with an albeit slightly confusing interface. What am I talking about? No, not Web SQL, which has been deprecated and should not be used. I’m talking about IndexedDB!

In this article, we’ll investigate how to use IndexedDB (along with the PouchDB library) to store “draft” notes that can be later synced to our API to be stored permanently. Accidentally close your browser? Never fear! The draft has been safely tucked away in IndexedDB.

The source code for this project can be found at https://github.com/leighhalliday/leigh-notes-web. The final version of this app is located at https://leigh-notes-web.herokuapp.com/.

Difficult Interface

I mentioned above that IndexedDB is difficult to work with, and even this explanation uses a slightly modified API (that has been promisified) to interact with it. Because of this, many people have created libraries built on top of IndexedDB to simplify things. Popular libraries include localForage, Dexie.js, IDB-keyval, and the library we’ll be covering in this article, PouchDB.

Connecting to the DB

Similar to how backend/server-side code must connect to the database, we must connect to our PouchDB database as well. Because this is client-side specific code and Next.js also runs on the server, I have a check in place to only return a connection to the database if we are in the browser.

There can be multiple database connections, so here we are saying to connect to one called “notes,” which will be created if it doesn’t exist yet.

// pages/index.js
import PouchDB from "pouchdb";

const getDb = () => {
  if (process.browser) {
    return new PouchDB("notes", { auto_compaction: true });
  }
};

So far the component we are building which renders the notes looks like this:

class Index extends React.Component {
  // A special function in Next.js which provides initial props to
  // the "Page" Component before it renders.
  static async getInitialProps({ token }) {
    return { token };
  }

  // Our default state
  state = {
    draftNotes: {},
    savedNotes: {}
  };

  // Let's set up the DB connection
  db = getDb();

  // remaining functions including render
}

Reading the Notes

When our component mounts, we want to load the draftNotes from PouchDB and populate the state with them.

componentDidMount() {
  this.loadDraftNotes(); // data comes from PouchDB
  this.loadSavedNotes(); // data comes from API
}

The loadDraftNotes function first reads allDocs from the DB which gives us back a result object. After extracting the doc (the actual note) from each row, and then reversing them to have the latest draft note at the top of the list, we can place them into the state.

loadDraftNotes = async () => {
  const result = await this.db.allDocs({ include_docs: true });
  const draftNotes = result.rows.map(row => row.doc).reverse();
  this.setState({ draftNotes });
};

Adding and Updating Notes

When creating a new note, we must have a unique _id column that is required by PouchDB… we’re using an ISO string of the current date in this case. First, we’ll append the draftNotes state, adding our new note to the beginning of the array, and after that we can call the db.put(note) function which inserts the new note into the database.

createNote = () => {
  const note = {
    _id: new Date().toISOString(),
    body: ""
  };

  const draftNotes = [note, ...this.state.draftNotes];
  this.setState({ draftNotes });

  this.db.put(note);
};

Updating is slightly more complicated than inserting, but isn’t too bad! Given the index of the note in our draftNotes array, and the updated body, we can first update the state (with a little help from the immutable library Immer).

After updating the state, we’ll use the index to find the _id property, needed by PouchDB to read it from the database. After reading the note from the database, we can update its body property, and then call put to update the database with our changes.

Even though we used the put function for both inserting a new note and updating an existing note, PouchDB is smart enough to know which one you’re trying to do because of its unique _id attribute.

updateNote = async (index, body) => {
  this.setState(
    produce(draft => {
      draft.draftNotes[index].body = body;
    })
  );

  const noteId = this.state.draftNotes[index]._id;
  const note = await this.db.get(noteId);
  note.body = body;
  this.db.put(note);
};

Removing Notes

At some point you’ll want to save your draft notes to the server, something in this example I called sync. For each note we’ll want to:

  1. Post the note via AJAX to our API, saving it in our production database
  2. Remove the note from the local PouchDB

After we have gone through all the notes, we can call loadDraftNotes and loadSavedNotes again to refresh the state.

syncNotes = async () => {
  const promises = this.state.draftNotes
    .filter(note => note.body)
    .map(async note => {
      await this.postNoteToApi(note);
      await this.deleteNote(note);
    });

  await Promise.all(promises);

  this.loadDraftNotes();
  this.loadSavedNotes();
};

There’s a lot of promises going on here! What you end up with is an array of them, and using await Promise.all(promises) you can wait for all of them to resolve. Let’s take a look at what postNoteToApi(note) does. It’s a fairly straightforward POST AJAX request using Axios to the API, passing along the JWT token in the Authorization header so it knows who we are.

postNoteToApi = note => {
  const { token } = this.props;
  return Axios.post(
    "https://leigh-notes-api.herokuapp.com/notes",
    {
      note: { body: note.body }
    },
    { headers: { Authorization: `Bearer: ${token}` } }
  );
};

Once we’ve posted the note to the API, we can remove it from our local PouchDB, which is what deleteNote(note) does:

deleteNote = async note => {
  const doc = await this.db.get(note._id);
  await this.db.remove(doc);
};

The Notes UI

We’ve looked a lot at the individual functions so far, but haven’t yet taken a peek at what the UI looks like, what actually gets rendered.

render() {
  const { token } = this.props;
  const { draftNotes, savedNotes } = this.state;

  return (
    <Layout token={token}>
      <h1>Notes!</h1>

      <div className="actions">
        <button onClick={this.createNote}>New</button>
        {draftNotes.length > 0 && (
          <button onClick={this.syncNotes}>Save Drafts</button>
        )}
      </div>

      <ul className="notes">
        {draftNotes.map((note, index) => (
          <li className="note" key={note._id}>
            <DraftNote note={note} index={index} updateNote={this.updateNote} />
          </li>
        ))}

        {savedNotes.map(note => (
          <li className="note" key={note.id}>
            <span className="note-status">
              {note.createdAt.substring(0, 10)}
            </span>
            <p>{note.body}</p>
          </li>
        ))}
      </ul>
    </Layout>
  );
};

The DraftNote component referenced above is a small component to help with rendering a form for each of the draft notes:

const DraftNote = ({ note, index, updateNote }) => (
  <form>
    <span className="note-status">draft</span>
    <textarea
      value={note.body}
      onChange={event => {
        const body = event.target.value;
        updateNote(index, body);
      }}
    />
  </form>
);

Used by Twitter

The next time you’re on Twitter, go into the developer tools and into Application, where you’ll see IndexedDB as one of the Storage options on the left. Poke around a little bit and see how they use IndexedDB to cache some user information for direct messages. Looks like I found Wes Bos in mine!

twitter-indexeddb

{
conversations: ["815246-119841821"]; id: "815246"; id_str: "815246"; name: "Wes Bos"; name_lowercase: "wes bos"; profile_image_url_https: "https://pbs.twimg.com/profile_images/877525007185858562/7G9vGTca_normal.jpg"; protected: false; screen_name: "wesbos"; verified: true; }

Conclusion

We were able to use IndexedDB (with the help of PouchDB) to create functionality to add and edit draft notes and later synchronize them back to the API / server. We could have probably done something similar using localStorage, but IndexedDB has performance gains given that all of its functions happen asynchronously.

For More Info on Building Great Web Apps

Want to learn more about creating great web apps? It all starts out with Kendo UI - the complete UI component library that allows you to quickly build high-quality, responsive apps. It includes everything you need, from grids and charts to dropdowns and gauges.

Learn More about Kendo UI

Get a Free Trial of Kendo UI



leigh-halliday
About the Author

Leigh Halliday

Leigh Halliday is a full-stack developer specializing in React and Ruby on Rails. He works for FlipGive, writes on his blog, and regularly posts coding tutorials on YouTube.

Related Posts

Comments

Comments are disabled in preview mode.