xBlog

BLOG

Pro tips using localStorage

Pro tips using localStorage
Mohamed EL AYADI

Mohamed EL AYADI

22 April, 2021 Β· 4min πŸ“–

As a front-end developer, you will have to use the browser's storage someday.

In this post, we will explore some pro tips that we learned through our journey.

Disclaimer

For the rest of this post, we will consider that we are building a cart and we will persist it in localStorage. The cart will hold a list of products and their details, plus the amount and quantity.

The user will be able to add and remove items, and obviously see the list of the current items in the cart.


Include a version in your keys

The first and most important tip in the list is to include a version in your storage key: after users start using the application and add items to the cart, your code change, and you deliver new versions periodically. What if the structure that you save into localStorage changes (you have a 100% chance it will occur, one day!)?

When this happens, and when your newly delivered app tries to read the cart from the storage, it will encounter an old structure that may cause the app to break, or to crash entirely (ie: when you try to read a variable that you introduced in version 1.2 but the user hasn't visited since 0.5, and did not empty the cache... your app will go boom in this case).

To solve this, append a version to your storage key.

const CART_STORAGE_KEY = 'cart-1.1.2';

// later
localStorage.getItem(CART_STORAGE_KEY);
// if your user is able to have multiple carts simultaneousely (maybe by switching profile)
// then you need another information to construct the storage key

So, the idea is that each time our code tries to read the from local storage, it reads the version that goes with it! and nothing else. If there is an old structure, you may think of two options: remove it or migrate it.

Keep track of the recent versions

To minimize the storage taken on the user's side, you should always remove entries that correspond to the older version of your code. To do that, keep track of an array of recent versions that should be removed (why not just one ? because you may perform multiple deployments while the user hasn't visited and refreshed the website's code ;) so, keep enough versions).

const PREVIOUS_CART_KEYS = Object.freeze(["cart-1.1.0", "cart-1.1.1"]);

// later
PREVIOUS_CART_KEYS.forEach(legacyKey => {
  window.localStorage.removeItem(legacyKey);
});

Keep the storage synchronized

The user may open multiple tabs displaying the elements in the cart, then removes or adds an element: All tabs should display the same thing at the same time.

To achieve this, you should be aware of updates that occur in the storage in other tabs: subscribe to the window's storage event and reload your data (please be aware that the storage event is not triggered in the document that causes it to fire).

window.addEventListener("storage", loadStorageData);
// and also, don't forget the unsubscription
window.removeEventListener("storage", loadStorageData);

Validate additions to storage

For a smoother developer experience, every addition to the storage should be validated. This is to keep data consistency and warn as early as possible (while typing code) about bugs:


function addElement(element) {
  validateElement(element);
}
// ...
function validate(element) {
  if (__DEV__) {
    validateAmount(element);
    validateProduct(element);
    validateDisplaySupport(element);
    validateUniqueIdentifier(element);
    validateSerialization(element, props);
  }
}
// ...
function validateAmount(el) {
  invariant(el.amount, "amount is required");
  // ... other rules
}
function validateUniqueIdentifier(el) {
  invariant(el.uniqueIdentifier, "uniqueIdentifier is required");
}
function validateDisplaySupport(el) {
  const productCode = el.product?.code;
  invariant(DISPLAY_SUPPORT[productCode], `${productCode} is not supported`);
}
function validateProduct(el) {
  invariant(el.product, "product is required");
  invariant(el.product.name, "product name is required");
  invariant(el.product.logo, "product logo is required");
  invariant(el.product.category, "product category is required");
}
function validateSerzialization(el, props) {
  return props.forEach(prop => {
    invariant(isSerializable(el[prop]), `${prop} should be serializable`);
  });
}
function isSerializable(value) {
  return isEqual(value, JSON.parse(JSON.stringify(value)));
}

These tips helped us catch countless bugs in development mode, along with smoother user and developer experience, and practically no bug in production in cart!


Let's now briefly explore a cart provider in a real-world react application, and see how our engineers designed it:

The first thing was to create a react context, and a hook that returns its value:


export const CartContext = React.createContext(null);

export function useCartContext() {
  const contextValue = React.useContext(CartContext);
  invariant(contextValue, `You are not inside a <CartContext.Provider />`);
  return contextValue;
  // with no types support in your javascript project
  // you may want to destruct the context value in DEV mode
  // so you will have a better IDE auto-completion support
}

The context value contains the following properties:

  • cart: the cart instance (we will talk about it in a second);
  • addItem: adds an item to the cart, and obviously perform lots of checks;
  • addItems: adds an array of items, performing the same checks;
  • removeItem , removeItems and removeAllItems removes an item or multiple items from the cart, or all of them;

So, basically while developing, all you need to do is to grab the utility from the cart context that fits your needs and use it.

The cart instance in our case, provided maps of the items in the cart, while grouping and performing some business logic. Along with some functions that filter and calculate things related to business needs.

Besides the cart, we had a bigger abstraction in the project that made necessary the presence of a cartConfig: A plain JavaScript object that contains information about the rendering, processing, and deletion of items from the cart.

The following snippets illustrate how we use the cart context at various places of the app:


import useCartContext from '../path/to/module';

// later

const { removeItem } = useCartContext();
<button onClick={() => removeItem(id)}>Remove</button>

// ...
const { addItems } = useCartContext();
function addItemsToCart() {
  addItems(selectedItems.map(prepareToCart);
}
<button onClick={addItemsToCart}>Add to cart</button>

// ...
const { removeAllItems, cart } = useCartContext();
const formattedItems = React.useMemo(() => cart.getFormattedItems(), [cart]);

<button onClick={removeAllItems}>Empty cart</button>
{formattedItems.map(/*...mapper func*/)}


The thing is as a developer, you have elementary operations that act differently in development mode to help you stay in the right direction and answer the business needs correctly. The version tag will prevent your code from loading any stale elements in the local storage, which will eliminate this kind of bugs.

Conclusion

As already mentioned, these tips helped us achieve great consistency and confidence while working with localStorage with really Zero bugs in large-scale production regarding the reading/writing of elements.

If you know any great tips that should be added to the list, please don't hesitate to reach out to me or any other member of the team, it will be added along with credits.

Mohamed EL AYADI

Mohamed EL AYADI

Hi, I'm Mohamed and I am a software engineer. I like to build efficient and secure apps. I am a java and javascript developer.

Tags:

signature

Mohamed EL AYADI has no other posts

Aloha from xHub team πŸ€™

We are glad you are here. Sharing is at the heart of our core beliefs as we like to spread the coding culture into the developer community, our blog will be your next IT info best friend, you will be finding logs about the latest IT trends tips and tricks, and more

Never miss a thing Subscribe for more content!

πŸ’Ό Offices

We’re remote friendly, with office locations around the world:
🌍 Casablanca, Agadir, Valencia, Quebec

πŸ“ž Contact Us:

🀳🏻 Follow us:

Β© XHUB. All rights reserved.

Made with πŸ’œ by xHub

Terms of Service