text: If updating our component state whiles our database changes. The purpose is mainly to help you understand how to work with data offline, especially when working on Progressive Web Apps. So, let's get started!
  We'll be building a simple restaurant menu app with different meals. We'll only add the functionality to add and remove meals from the menu. Here's the sandbox of the app:
  I've divided the process into 5 main parts:
  react-parcel-app-boilerplateon GitHub.
  MenuContextProviderand
  MenuContextto handle the IndexedDB database, which will have only one object store,
  Menuand
  Mealcomponents.
  removeMealfeature.
  addMealfeature.
  Important Note: This isn't an introductory article to React and IndexedDB. I'm assuming you are familiar with both technologies, and that you're here to learn how to use both side-by-side.
  So,
  react-parcel-app-boilerplate is my recent repository on GitHub meant to help set up React for immediate prototyping. So, instead of using
  create-react-app which I think is too bulky, as compared to this boilerplate, we'd be using
  react-parcel-app-boilerplate for simplicity by taking advantage of Parcel, the zero-configuration web application bundler.
  Open your command-line program and navigate to the directory in which you want to create the app, and, assuming you already have Git installed, run the following command:
  git clone
  Navigate into the project directory:
  cd react-parcel-app-boilerplate
  Now, assuming you're using NPM, run:
  npm start
  ... and wait for Parcel to bundle the project and spin up a local development server, opening your default browser automatically.
  In the
  App.jsx file in the
  src/ folder, remove the unnecessary code so it becomes as simple as this:
  import React, { Suspense } from "react"; // Suspense for loading effect function App() { return ( <> </> ); } export default App;
  You should clean the
  css/styles.css file too.
  MenuContextProvider
  The reason I'm choosing to use context is because I want to make a centralised store, on which the
  Menu and
  Meal components can all depend when accessing the IndexedDB
  menu object store. We'll create those components soon.
  src/component/directory, create a file named
  MenuContextProvider.jsx, with the code:
  import React, { useState } from 'react'; function MenuContextProvider() { return (); } export default MenuContextProvider;
  tiny-indexed-db, and copy the
  unique-string.jsand
  tiny-idb.jsinto a new folder
  libin the
  src/directory.
  unique-string.jsis used to generate a unique string, which will be used as the ID of meals in the
  menuobject store of our IndexedDB database we're yet to create. You could could use UUID if you want, but I'll be using
  unique-string.js.
  tiny-idb.jsis a small Promise-based library on top of the
  window.indexedDBobject, instead of using the traditional
  onsuccessand
  onerrorevents handlers. Again, you can use
  idbif you prefer it.
  indexedDBDatabase
  Inside the
  MenuContextProvider.jsx file we just created, just after you've imported
  React, { useState }, import the
  DB object and
  UniqueString class from the
  tiny-idb.js and
  unique-string.js files, respectively, from the
  src/lib/ folder. We'll be using them for our
  indexedDB database in our app. In the
  MenuContextProvider.jsx file, after you've made your imports, create the
  setUpDatabase function:
  import DB from '../lib/db'; import UniqueString from '../lib/unique-string'; // an instance of the UniqueString class, // for generating unique strings, // which we'll use as ID for the data to be stored const uid = new UniqueString(); // this will initialize the database if it isn't set up yet async function setUpDatabase() { ; // create a DB with the name MenuDatabase and version 1 await DB.createDB("MenuDatabase", 1, [ // list of object stores to create after the DB is created { // name of first object store name: "menu", // keyOptions of the object store config: { keyPath: "mealID" }, // list of data (meals) to store after store is created data: [ { mealID: uid.generate(), name: "Jollof Rice" }, { mealID: uid.generate(), name: "Banku with Okro Stew" }, ], }, ]); }
  I think the code above is self explanatory, but let me brush through. We're using a promise-based syntax with
  indexedDB thanks to the
  tiny-idb.js library.
  DB.createDB()method creates an
  indexedDBdatabase with the name
  MenuDatabase, at version
  1. The third argument, which is an array, is an array of objects defining a number of object stores to be created once the database is created. In our case, we create one object store with name
  menu, which uses
  mealIDproperty to uniquely identify an object added to the store, defined with the
  configproperty. The
  dataproperty is an array of objects that should be added to the object store once it is successfully created. Here we have two objects each with different names and unique IDs generated by
  uid.generate(). Please note that, the
  dataarray was added for testing purposes. If you use it and later empty your object store, when the page is loaded again, the objects defined will be added again. Please read the description of both libraries being used.
  We'll call this function at the appropriate time.
  MenuContext
  Before we go ahead and create our context, we'll need a suspender, for data fetching. You can implement yours, or you could copy and paste this in a file called
  suspender.js in the
  src/lib/ directory:
  const promiseSuspender = (wrappedPromise) => { let status = "pending"; let result = null; const suspender = wrappedPromise .then((resolvedResult) => { status = "success"; result = resolvedResult; }) .catch((error) => { status = "error"; result = error; }); return { read() { switch (status) { case "pending": throw suspender; case "error": return result; default: return result; } }, }; }; const suspender = (promise) => { return { data: promiseSuspender(promise), }; }; export default suspender;
  And, please import
  suspender in the
  MenuContextProvider.jsx file:
  import suspender from "../lib/suspender";
  After creating the
  setUpDatabase() function, write the following line of code to create a context:
  const MenuContext = React.createContext();
  Now, let's re-write our
  MenuContextProvider component:
  // function to get all meals in the DB async function getAllMealsFromDB() { // now, initialise the database await setUpDatabase(); // open the database & grab the database object let db = await DB.openDB("MenuDatabase", 1); // create a transaction on the db // and retrieve the object store const menuStore = await DB.transaction( db, // transaction on our DB ["menu"], // object stores we want to transact on "readwrite" // transaction mode ).getStore("menu"); // retrieve the store we want // grab all meals from the menuStore let allMeals = await DB.getAllObjectData(menuStore); // return the allMeals array return allMeals; } // using suspender to get all meals from database const resource = suspender(getAllMealsFromDB()); // The component itself function MenuContextProvider({ children }) { // reading data from suspender const meals = resource.data.read(); // state to store list of meals in the object Store // set state to be the data retrieved from database const [mealsList, setMealsList] = useState(meals); // return the context provider passing the mealsList state // and its updator function as values of the context return ( <MenuContext.Provider value={{ mealsList, setMealsList }}> {children} </MenuContext.Provider> ); } // export MenuContext also export { MenuContext };
  Once again, self-explanatory code.
  Menuand
  MealComponents
  So, before we go ahead and use our
  MenuContextProvider, let's ensure we have the the
  Menu and
  Meal components implemented. The
  component will be displaying the list of meals in the database whilst the
  Meal component will be the JSX representing each meal. Create two files
  Menu.jsx and
  Meal.jsx in the
  src/components directory.
  MenuComponent
  In your
  Menu.jsx file, implement the component like so:
  import React, { useContext } from "react"; import { MenuContext } from "./MenuContextProvider"; // calling in the MenuContext import Meal from "./Meal"; // we'll create this component soon function Menu() { // grabbing the mealsList list from the MenuContext // which has access to to the data in the MenuContextProvider state // in the future const { mealsList, setMealsList } = useContext(MenuContext); // an array to store JSX of all meals in DB const mealsOnMenu = mealsList.map((meal) => { // return a Meal component for each meal object in DB return <Meal key={meal.mealID} meal={meal} />; }); // render list of all meals return <div className="menu">{mealsOnMenu}</div>; } export default Menu;
  MealComponent
  Let's define our
  Meal component: in the
  Meal.jsx file inside the
  src/components directory, insert this code:
  import React from "react"; // grabbing the meal object passed from the context // as a prop function Meal({ meal }) { // return a JSX representation of the data. // we gave the outer DIV an id, set to the mealID // of the meal object in the object store. // we'll use this to query the object store later. return ( <div className="meal" id={meal.mealID}> <div className="meal-info"> <p>{meal.name}</p> </div> <div className="meal-actions"> <button className="btn-remove">Remove</button> </div> </div> ); } export default Meal;
  Now, we can preview our app. But before that, we need to re-write our
  App.jsx file:
  import React, { Suspense } from "react"; import MenuContextProvider from "./components/MenuContextProvider"; import Menu from "./components/Menu"; function App() { return ( <> // show "Loading..." whiles we load data from database <Suspense fallback={<p>Loading...</p>}> <MenuContextProvider> <Menu /> </MenuContextProvider> </Suspense> </> ); } export default App;
  Please note that, Suspense is currently an experimental feature in React. At any moment, the React team could change its API and that'll mean, applications using it will break, including what we're building. I'm using it because, like the Suspense API, our app is also experimental.
  Okay. Go back to your command-line; if you have terminated the development server, run
  npm start again to see the app. It should look like this:
  It looks ugly. We'll write some CSS later to polish it.
  removeMealFeature
  Each
  Meal component has a button that says "Remove", but at the moment we can't remove anything. That's about to change.
  The
  Menu component has its parent JSX element to be a DIV with a
  menu class. We'll write a function that handles a click event on that DIV. Wait, why not the
  <button>? Our database may contain a hundred
  Meal components, each having a button. Adding an event listener on all of them is bad for performance. When a button is clicked, the event bubbles up. So, the button triggers the event, and then its parent
  <div className="meal-actions"> element, to the
  <div className="meal"> element, which also bubbles up to the
  <div className="menu">, and on and on...
  We'll take advantage of this and implement the event on the
  menu element rather. So, in the
  Menu component, we'll create a
  removeMeal function inside the
  Menu component which will perform the delete command. Since, it'll be the event handler, we'll use it to find the value of the
  id attribute on the
  meal element whose button was clicked.
  Since, we'll be using the
  tiny-idb.js library with
  indexedDB, let's import it.
  import DB from '../lib/tiny-idb.js';
  Let's implement the
  removeMeal function:
  // destructure the target property from the event object const removeMeal = async ({ target }) => { // check if the target is a button element // with an class of of "btn-remove" let isRemoveButton = target.tagName === "BUTTON" && /btn-remove/g.test(target.className); if (isRemoveButton) { // if true, get the id of its grandparent element ;) const mealElement = target.parentElement.parentElement; let id = mealElement.getAttribute("id"); // open the database const db = await DB.openDB("MenuDatabase", 1); // create an indexedDB transaction and grab the object store const menuStore = await DB.transaction( db, ["menu"], "readwrite" ).getStore("menu"); // pass in the targeted ID and delete it from the database await DB.deleteObjectData(menuStore, id); } };
  Attach an
  onClick event to the
  menu DIV; in the
  Menu component, return this JSX rather:
  return ( <div className="menu" onClick={(e) => removeMeal(e)}> {mealsOnMenu} </div> );
  When the button is clicked, you don't see the UI change because we haven't updated the state of the
  MenuContextProvider. However, the update appears in the database. In Chrome, check it by going to developer tools (press
  F12) > Application tab > IndexedDB (on the left side-nav) > MenuDatabase > menu.
  As you can see, after I clicked on the button of the meal with name "Banku with Okro Stew", it was removed from the database.
  So, how do we fix this? Simple! The
  DB.deleteObjectData() method that we used returns an array containing two objects:
  So, in our
  removeMeal function, instead of this line:
  await DB.deleteObjectData(menuStore, id);
  ... we'll write this:
  // destructuring the returned array const [deleted, remaining] = await DB.deleteObjectData(menuStore, id);
  Now that we have the remaining meals in the
  menu object store, we can update the state of our
  MenuContextProvider, which should cause the
  MenuContextProvider component to re-render, and since the
  Menu component is nested in it, it should also re-render, getting provided with the updated state.
  Do you remember we passed the
  setMealsList function (which updates the
  mealsList state of the context) along with the
  mealsList in the value of the context provider? Here's that line:
  <MenuContext.Provider value={{ mealsList, setMealsList }}>
  The
  setMealsList function is used to update the state of the context. Do you also remember we destructured it when we used the
  useContext hook, in the
  Menu component?
  const { mealsList, setMealsList } = useContext(MenuContext);
  It's time to give it a purpose. Just after the line in the
  removeMeal handler where we delete the meal object from the object store, write this code to update the context state:
  // update context state with remaining objects in object store setMealsList(remaining);
  Now, when a button is clicked, the UI is updated, but don't celebrate yet. Try deleting all the items in the database. Refresh the page. You realise that the meals we just deleted have been added again. Well, that's because when we were setting up the database, we told
  indexedDB to automatically add two meals to the
  menu object store. Do you remember we set up some initial objects to be added once the database is set, in the
  setUpDatabase() function, inside the
  MenuContextProvider.jsx file:
  // create a DB with the name MenuDatabase and version 1 await DB.createDB("MenuDatabase", 1, [ // list of object stores to create after the DB is created { ... // list of data (meals) to store after store is created data: [ { mealID: uid.generate(), name: "Jollof Rice" }, { mealID: uid.generate(), name: "Banku with Okro Stew" }, ], }, ]);
  This happens because the
  tiny-idb library checks whether or not, there are objects in the database with the same
  mealID as the ones it's about to add, if there are, it skips and doesn't add them, else it goes ahead and adds them to the object store. So, when you delete all the objects from the store, and then reload the page, the initialisation process happens again. How do we handle this? We'd have to remove the initial data that'll be added when the page is loaded, but we'll do that after we add the
  addMeal feature.
  Let's do that!
  addMealfeature
  Before we begin writing some logic, let's first of all, create another component file,
  AddMeal.jsx, in the
  src/components folder. We'll create a form for the user to input data
  import React, { useState } from "react"; function AddMeal() { // state for the input field in the form const [mealName, setMealName] = useState(""); // handler for when input field value changes const handleMealNameChange = (e) => { // value of input field const value = e.target.value; // update mealName state with the new value in input field setMealName(value); }; return ( // a form to be used to add the meal <form className="add-meal"> {/* input field to keep track of the name of the new meal */} <input type="text" id="mealName" required </form> ); } export default AddMeal;
  Import
  AddMeal in
  App.jsx and put the
  <AddMeal /> component in the
  <MenuContextProvider> component, on top of the
  <Menu /> component:
  import React, { Suspense } from "react"; import MenuContextProvider from "./components/MenuContextProvider"; import Menu from "./components/Menu"; import AddMeal from "./components/AddMeal"; function App() { return ( <> <Suspense fallback={<p>Loading...</p>}> <MenuContextProvider> <AddMeal /> {/* like this */} <Menu /> </MenuContextProvider> </Suspense> </> ); } export default App;
  In order to add the new meal to the
  MealContext and update the whole UI, we need to add a submit event listener to the
  <form> element. Then, we can write our logic to add the new meal to the database. To do this, let's import some modules:
  import { MenuContext } from './MenuContextProvider'; import UniqueString from '../lib/unique-string.js'; import DB from '../lib/tiny-idb.js';
  Okay, inside the
  AddMeal component, we need to get the function that updates the context state. We can access it by using the
  useContext hook from the
  MenuContext. Remember to
  import { useContext } from 'react'.
  // getting the context state updator function // we won't need the mealsList state itself // so we're ignoring it with the underscore (_) const { _, setMealsList } = useContext(MenuContext);
  Ensure that the code above is written inside the
  AddMeal component.
  Now, create this onsubmit handler in the
  AddMeal component:
  // the form submit handler async function handleAddNewMeal(e) { // prevent the page from loading when form is submitted e.preventDefault(); // open Database const db = await DB.openDB("MenuDatabase", 1); // create a transaction and grab the menu store const menuStore = await DB.transaction( db, ["menu"], "readwrite" ).getStore("menu"); // add the data, and grab the updated meals const newMealsInStore = await DB.addObjectData(menuStore, { // set a unique ID mealID: new UniqueString().generate(), // set name to be value of mealName state name: mealName, }); // set the context state // to have the updated items in menu store setMealsList(newMealsInStore); // after updating the context state, reset the input field's value setMealName(""); }
  Attach an
  onSubmit event listener on the form and assign it to the
  handleAddNewMeal handler we just created:
  <form className="add-meal" onSubmit={handleAddNewMeal}>
  Go ahead and test it. Yep, it works! :D
  Before I conclude this article, let's go back inside the
  MenuContextProvider.jsx file and edit the
  setUpDatabase() function. Remove the
  data array from the object that initialises the
  menu object store.
  return await DB.createDB("MenuDatabase", 1, [ // list of object stores to create after the DB is created { // name of first object store name: "menu", // keyOptions of the object store config: { keyPath: "mealID" } }, ]);
  Open the developer tools and delete the entire database, then refresh the page to initialise the database again. This time, we won't have any objects in the database when rendering for the first time:
  A blank menu list? Yikes! Let's create a fallback which will be rendered if there are no meals in the menu store. Do this in the
  Menu.jsx file.
  function NoMealsFallback() { return ( <div className="fallback"> <h1>:(</h1> <h3>There are no meals on your menu.</h3> <p>Use the form above to add some.</p> </div> ); }
  In the
  Menu component, instead of rendering this:
  // render array of all meals return ( <div className="menu" onClick={(e) => removeMeal(e)}> {mealsOnMenu} </div> );
  ... let's conditionally render it only if there's a meal on the menu.
  // if mealsList is empty render fallback // else render the menu return mealsList.length === 0 ? ( <NoMealsFallback /> ) : ( <div className="menu" onClick={(e) => removeMeal(e)}> { mealsOnMenu } </div> );
  At the moment, our app looks horrible. Let's add some stylesheets in the
  src/css/styles.css file:
  body { font-family: Cambria, Georgia, serif; font-weight: 200; margin: 0; } .add-meal { display: flex; justify-content: center; align-items: center; flex-direction: row; padding: 20px 0; background: rgb(245, 245, 245); margin-bottom: 20px; } .add-meal * { font-family: Cambria, Georgia, serif; font-weight: 200; } .add-meal input { padding: 10px 15px; background: rgb(241, 241, 241); border: 0; outline: 1px solid rgb(224, 224, 224); font-size: 1.2rem; box-sizing: border-box; } .add-meal input[type="text"] { margin-right: 5px; } .add-meal input[type="text"]:focus { background: #fff; } .add-meal input[type="submit"] { background: rgb(66, 66, 66); color: #fff; cursor: pointer; } .add-meal input[type="submit"]:hover { background: rgb(82, 81, 81); } .fallback { outline: 1px solid rgb(224, 224, 224); max-width: 400px; text-align: center; padding: 50px 0; margin: 10% auto; } .fallback h1, .fallback h3 { color: rgb(66, 66, 66); } .fallback h1 { margin: 0; } .menu { padding: 20px 0; display: flex; flex-direction: column; justify-content: space-between; align-items: stretch; align-content: center; } .meal { font-size: 1.2rem; display: flex; justify-content: space-between ; align-items: center; min-width: 70% ; padding: 25px; background: rgb(231, 231, 231); align-self: center; } .meal:nth-child(odd) { background: rgb(247, 247, 247); margin-bottom: 10px; } .btn-remove { padding: 10px 15px; border: 0; outline: 1px solid rgb(224, 224, 224); font-size: 1rem; box-sizing: border-box; background: rgb(66, 66, 66); color: #fff; cursor: pointer; } /* animation for when a meal is removed */ .remove-meal { animation: removeMeal 0.3s linear; animation-fill-mode: forwards; } @keyframes removeMeal { from { min-width: 30%; opacity: 0.5; } to { min-width: 10%; opacity: 0; } }
  The
  styles.css file has already been imported in the
  index.jsx file, so there's no need to re-import it.
  You can see that getting to the end of the
  styles.css file, we've defined an animation for when a meal element is being removed from the DOM. This won't work yet. In our
  removeMeal() function, in the
  Menu component, just before we update the context state with the new data (which unmounts the element immediately from the DOM):
  // update context state with remaining objects in object store setMealsList(remaining);
  ... our animation plays in 300 milliseconds (0.3 seconds), so in order to see the animation in action, we have to delay the state update for 300 milliseconds. So, in 300 milliseconds, our little animation will play and fade out the removed element. Then, after that, we'll go ahead and update the context state.
  setTimeout() will help us delay the state update:
  // animate the meal element // we defined the mealElement already, // to contain the grandparent of the button which was clicked mealElement.classList.add("remove-meal"); // after 300 milliseconds (0.3 seconds) // update context state with remaining objects in object store setTimeout(() => setMealsList(remaining), 300);
  Remember to do this in the
  removeMeal() function inside the
  Menu component. Here's how our app now looks:
  Menu app with no meals
  Menu app after meals have been added
  Here's the complete sandbox of the toy app we just created:
  Task: Add another button and use it to update already added meals individually. You can also add a remove-all-meals button to clear all the meals from the data store.
  In case you find it hard working with my
  tiny-idb.js library, make sure to check out its
  README.md file on the GitHub repo and learn more about it.
  If you have any ideas and you'd want to add some features to the
  react-parcel-app-boilerplate or
  tiny-indexed-db, feel free to contribute.
  If you like this article, please react to it, share, and follow me on Hashnode and or Twitter, and comment if you have any questions.
  Brazilian full stack developer, tale writer and aspiring designer
  I'm a React enthusiast. So, for a future guide in new self challenges, I bookmarked this amazing article. Thanks for sharing!