js-cookie vs idb-keyval vs localforage vs store2 vs redux-persist vs store
Client-Side Data Persistence Strategies: Cookies, LocalStorage, and IndexedDB
js-cookieidb-keyvallocalforagestore2redux-persiststoreSimilar Packages:

Client-Side Data Persistence Strategies: Cookies, LocalStorage, and IndexedDB

This comparison evaluates six distinct approaches to persisting data in the browser. js-cookie specializes in HTTP cookie management for server interaction and small state. store, store2, and localforage provide unified APIs over localStorage and sessionStorage, with localforage uniquely offering an asynchronous fallback to IndexedDB for larger datasets. idb-keyval is a minimal wrapper specifically for IndexedDB, bypassing synchronous storage limits. Finally, redux-persist is not a storage engine itself but a middleware layer that automates saving and rehydrating Redux state using any of the other engines. Choosing the right tool depends on data size, synchronicity requirements, and whether you need direct server access via cookies or complex client-side database features.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
js-cookie31,537,96422,56925.9 kB104 months agoMIT
idb-keyval9,898,4383,25255.8 kB253 months agoApache-2.0
localforage8,340,14325,801-2515 years agoApache-2.0
store22,697,6951,94289.4 kB52 years agoMIT
redux-persist1,708,47212,951-07 years agoMIT
store826,34513,981-999 years agoMIT

Client-Side Persistence: Choosing the Right Storage Engine

Persisting data in the browser is a fundamental requirement for modern web apps, whether you're saving a user's theme preference, caching API responses, or managing an offline-first database. The ecosystem offers a range of tools, from simple wrappers around localStorage to full asynchronous IndexedDB clients. Let's break down how idb-keyval, js-cookie, localforage, redux-persist, store, and store2 solve these problems differently.

📦 Storage Backend: Where Does the Data Live?

The most critical decision is where your data physically resides. This dictates size limits, performance, and whether the server can see the data.

js-cookie writes to HTTP Cookies.

  • Data is sent to the server with every request.
  • Strictly limited to ~4KB per cookie.
  • Ideal for auth tokens and server-read settings.
// js-cookie: Sets a cookie visible to the server
import Cookies from 'js-cookie';

Cookies.set('session_id', 'abc123', { expires: 7, path: '/' });
const value = Cookies.get('session_id');

store, store2, and localforage (by default) use localStorage.

  • Data stays on the client; never sent to the server automatically.
  • Limit is usually ~5-10MB.
  • Synchronous access blocks the main thread during read/write.
// store2: Writes to localStorage synchronously
import store from 'store2';

store.set('user_prefs', { theme: 'dark' });
const prefs = store.get('user_prefs');

localforage can also use IndexedDB or WebSQL.

  • If configured, it handles large blobs and bigger datasets asynchronously.
  • Falls back to localStorage if needed.
// localforage: Configures IndexedDB as the driver
import localforage from 'localforage';

localforage.config({
  driver: localforage.INDEXEDDB,
  name: 'myAppDB'
});

await localforage.setItem('large_image', blobData);

idb-keyval uses IndexedDB exclusively.

  • Designed for large data (hundreds of MBs) and binary blobs.
  • Fully asynchronous; does not block the UI.
// idb-keyval: Direct IndexedDB access with Promises
import { set, get } from 'idb-keyval';

await set('avatar_blob', imageFile);
const avatar = await get('avatar_blob');

redux-persist is Engine Agnostic.

  • It doesn't store data itself; it delegates to a storage engine (usually localStorage).
  • It sits between your Redux store and the browser's storage.
// redux-persist: Configures a storage engine (default is localStorage)
import { persistStore } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // uses localStorage

const persistor = persistStore(store);

⚡ Sync vs Async: Blocking the Main Thread

How your code waits for data determines your app's responsiveness. Synchronous APIs are easier to write but can cause "jank" with large data. Asynchronous APIs require await but keep the UI smooth.

store, store2, and js-cookie are Synchronous.

  • You get the value immediately.
  • Dangerous if storing large JSON strings, as parsing blocks rendering.
// store: Synchronous read
const data = store.get('heavy_data'); // Blocks thread while parsing JSON
console.log(data);

idb-keyval and localforage are Asynchronous.

  • They return Promises. You must use async/await or .then().
  • Prevents UI freezing during heavy I/O operations.
// idb-keyval: Asynchronous read
const data = await get('heavy_data'); // Non-blocking
console.log(data);

redux-persist handles Rehydration Asynchronously.

  • When the app loads, it fetches data from storage and merges it into Redux.
  • Your components might render before the data is ready unless you pause rendering.
// redux-persist: Waiting for rehydration
import { PersistGate } from 'redux-persist/integration/react';

<PersistGate loading={<Loading />} persistor={persistor}>
  <App />
</PersistGate>

🔑 Data Structure: Simple Keys vs Namespaces

Organizing data prevents collisions, especially in large apps or when sharing storage across micro-frontends.

js-cookie, idb-keyval, and standard store use a Flat Key-Value structure.

  • You manage namespaces manually (e.g., prefixing keys).
  • Simple, but prone to naming conflicts in large teams.
// idb-keyval: Manual namespacing
await set('app_user_profile', profileData);
await set('app_settings_theme', themeData);

store2 has built-in Namespaces.

  • Creates isolated areas for different parts of your app.
  • Cleaner than manual string concatenation.
// store2: Using namespaces
const userStore = store.namespace('user');
const settingsStore = store.namespace('settings');

userStore.set('profile', { name: 'Alice' });
settingsStore.set('theme', 'dark');
// Keys in localStorage become 'user.profile' and 'settings.theme'

localforage supports Instances.

  • Similar to namespaces but allows different configurations per instance (e.g., one DB for images, one for text).
// localforage: Creating distinct instances
const imagesDB = localforage.createInstance({ name: 'images' });
const textDB = localforage.createInstance({ name: 'text' });

await imagesDB.setItem('logo', logoBlob);

redux-persist uses Reducer Keys.

  • Automatically maps storage keys to your Redux reducer structure.
  • You can whitelist or blacklist specific reducers from being saved.
// redux-persist: Persisting specific reducers
import { persistReducer } from 'redux-persist';

const persistedReducer = persistReducer(
  {
    key: 'root',
    whitelist: ['auth', 'preferences'] // Only save these slices
  },
  rootReducer
);

🔄 Automation: Manual vs Integrated

Do you want to call save() every time data changes, or should it happen automatically?

store, store2, js-cookie, localforage, and idb-keyval require Manual Management.

  • You explicitly decide when to read and write.
  • Gives you full control over debouncing and error handling.
// store2: Manual update on event
button.addEventListener('click', () => {
  store.set('last_action', Date.now());
});

redux-persist provides Automatic Persistence.

  • It subscribes to the Redux store and saves state on every change.
  • Reduces boilerplate but requires careful configuration to avoid performance hits on frequent updates.
// redux-persist: Automatic saving on state change
// No manual save call needed; it watches the store dispatches
const store = configureStore({ reducer: persistedReducer });

🗑️ Deprecation and Maintenance Status

It is vital to choose a library that will be supported in the future.

  • store (store.js): While still functional, it is considered legacy. The community has largely moved to store2, which offers better TypeScript support, namespaces, and active maintenance. Avoid store for new greenfield projects.
  • localforage: Still widely used, but note that its WebSQL backend is deprecated in browsers. It relies heavily on IndexedDB now, which is good, but idb-keyval is often preferred for pure IndexedDB tasks due to smaller bundle size.
  • redux-persist: Actively maintained and the standard for Redux persistence, though developers should be aware of the shift towards React Context and server-state tools (like TanStack Query) which sometimes reduce the need for complex Redux persistence.

📊 Summary: Which Tool for Which Job?

Featurejs-cookiestore2idb-keyvallocalforageredux-persist
BackendCookiesLocalStorageIndexedDBIndexedDB/LocalStorageConfigurable
Sync/AsyncSyncSyncAsyncAsyncAsync (Rehydration)
Max Size~4KB~5MBUnlimited*Unlimited*Depends on Engine
Server Access✅ Yes❌ No❌ No❌ No❌ No
Best ForAuth, SessionSettings, CacheBlobs, Large DataLegacy, Large DataRedux State

* Subject to browser disk quota policies.

💡 The Big Picture

js-cookie is your go-to for anything the server needs to see, like authentication tokens. Keep it lightweight.

store2 is the modern replacement for store. Use it for simple, synchronous user preferences and small caches where you don't want to deal with Promises.

idb-keyval is the specialist for heavy lifting. If you are building an offline-first app storing images, audio, or massive JSON logs, this is the most efficient, modern choice.

localforage acts as a bridge. Use it if you need the power of IndexedDB but want an API that feels like localStorage, or if you need to support very old browsers gracefully.

redux-persist is the glue for Redux apps. It saves you from writing boilerplate but adds complexity to your store setup. Only use it if you are deeply invested in the Redux ecosystem.

Final Thought: Don't over-engineer. If you just need to remember a user's dark mode preference, store2 or even native localStorage is enough. If you are building the next Google Docs offline, reach for idb-keyval.

How to Choose: js-cookie vs idb-keyval vs localforage vs store2 vs redux-persist vs store

  • js-cookie:

    Choose js-cookie when your data needs to be read by the server (via HTTP headers) or when you require strict expiration handling and path/domain scoping. It is the standard choice for authentication tokens, session IDs, and user preferences that must persist across subdomains. Do not use it for large client-side data, as cookies are sent with every request and have strict size limits (typically 4KB).

  • idb-keyval:

    Choose idb-keyval when you need to store large binary blobs (like images or audio) or massive datasets that exceed localStorage limits. It is ideal for scenarios where you can work with asynchronous Promises and need the performance of IndexedDB without the verbosity of the native API. Avoid this if you need synchronous access or simple key-value pairs for small configuration settings.

  • localforage:

    Choose localforage if you need a simple, synchronous-looking API but must support large data sizes or older browsers that lack robust IndexedDB support. It automatically selects the best available storage backend (IndexedDB, WebSQL, or localStorage). This is the best fit for legacy projects or applications that need to scale storage capacity without refactoring the entire data access layer to be asynchronous.

  • store2:

    Choose store2 when you need a lightweight, synchronous wrapper around localStorage or sessionStorage with added features like namespaces and event hooks. It is perfect for simple user preferences, feature flags, or caching small API responses where asynchronous code would add unnecessary complexity. It offers a cleaner API and better TypeScript support than the original store package.

  • redux-persist:

    Choose redux-persist if your application uses Redux and you want to automatically persist the store to local storage without writing boilerplate save/load logic. It is essential for maintaining user state across page reloads in complex Redux architectures. Note that you must pair this with a storage engine (like redux-persist/lib/storage which uses localStorage, or a custom IndexedDB engine) to function.

  • store:

    Choose store (often imported as store.js) only for maintaining legacy codebases that already depend on it. It provides a basic synchronous wrapper around localStorage with JSON serialization. For new projects, prefer store2 or native APIs, as store has seen less active modernization compared to its successors and lacks some of the namespace features found in store2.

README for js-cookie

JavaScript Cookie CI npm size jsDelivr Hits

A simple, lightweight JavaScript API for handling cookies, client-side.

👉👉 If you're viewing this at https://github.com/js-cookie/js-cookie, you're reading the documentation for the main branch. View documentation for the latest release. 👈👈

Installation

NPM

npm i js-cookie

The npm package has a module field pointing to an ES module variant of the library, mainly to provide support for ES module aware bundlers, whereas its browser field points to an UMD module for full backward compatibility.

Not all browsers support ES modules natively yet. For this reason the npm package/release provides both the ES and UMD module variant and you may want to include the ES module along with the UMD fallback to account for this:

CDN

Alternatively, include js-cookie via jsDelivr CDN.

Basic Usage

Import the library:

import Cookies from 'js-cookie'
// or
const Cookies = require('js-cookie')

Create a cookie, valid across the entire site:

Cookies.set('name', 'value')

Create a cookie that expires 7 days from now, valid across the entire site:

Cookies.set('name', 'value', { expires: 7 })

Create an expiring cookie, valid to the path of the current page:

Cookies.set('name', 'value', { expires: 7, path: '' })

Read cookie:

Cookies.get('name') // => 'value'
Cookies.get('nothing') // => undefined

Read all visible cookies:

Cookies.get() // => { name: 'value' }

Note: It is not possible to read a particular cookie by passing one of the cookie attributes (which may or may not have been used when writing the cookie in question):

Cookies.get('foo', { domain: 'sub.example.com' }) // `domain` won't have any effect...!

The cookie with the name foo will only be available on .get() if it's visible from where the code is called; the domain and/or path attribute will not have an effect when reading.

Delete cookie:

Cookies.remove('name')

Delete a cookie valid to the path of the current page:

Cookies.set('name', 'value', { path: '' })
Cookies.remove('name') // fail!
Cookies.remove('name', { path: '' }) // removed!

IMPORTANT! When deleting a cookie and you're not relying on the default attributes, you must pass the exact same path, domain, secure and sameSite attributes that were used to set the cookie:

Cookies.remove('name', { path: '', domain: '.yourdomain.com', secure: true })

Note: Removing a nonexistent cookie neither raises any exception nor returns any value.

Namespace conflicts

If there is any danger of a conflict with the namespace Cookies, the noConflict method will allow you to define a new namespace and preserve the original one. This is especially useful when running the script on third party sites e.g. as part of a widget or SDK.

// Assign the js-cookie api to a different variable and restore the original "window.Cookies"
var Cookies2 = Cookies.noConflict()
Cookies2.set('name', 'value')

Note: The .noConflict method is not necessary when using AMD or CommonJS, thus it is not exposed in those environments.

Encoding

This project is RFC 6265 compliant. All special characters that are not allowed in the cookie-name or cookie-value are encoded with each one's UTF-8 Hex equivalent using percent-encoding. The only character in cookie-name or cookie-value that is allowed and still encoded is the percent % character, it is escaped in order to interpret percent input as literal. Please note that the default encoding/decoding strategy is meant to be interoperable only between cookies that are read/written by js-cookie. To override the default encoding/decoding strategy you need to use a converter.

Note: According to RFC 6265, your cookies may get deleted if they are too big or there are too many cookies in the same domain, more details here.

Cookie Attributes

Cookie attribute defaults can be set globally by creating an instance of the api via withAttributes(), or individually for each call to Cookies.set(...) by passing a plain object as the last argument. Per-call attributes override the default attributes.

expires

Define when the cookie will be removed. Value must be a Number which will be interpreted as days from time of creation or a Date instance. If omitted, the cookie becomes a session cookie.

To create a cookie that expires in less than a day, you can check the FAQ on the Wiki.

Default: Cookie is removed when the user closes the browser.

Examples:

Cookies.set('name', 'value', { expires: 365 })
Cookies.get('name') // => 'value'
Cookies.remove('name')

path

A String indicating the path where the cookie is visible.

Default: /

Examples:

Cookies.set('name', 'value', { path: '' })
Cookies.get('name') // => 'value'
Cookies.remove('name', { path: '' })

Note regarding Internet Explorer:

Due to an obscure bug in the underlying WinINET InternetGetCookie implementation, IE’s document.cookie will not return a cookie if it was set with a path attribute containing a filename.

(From Internet Explorer Cookie Internals (FAQ))

This means one cannot set a path using window.location.pathname in case such pathname contains a filename like so: /check.html (or at least, such cookie cannot be read correctly).

In fact, you should never allow untrusted input to set the cookie attributes or you might be exposed to a XSS attack.

domain

A String indicating a valid domain where the cookie should be visible. The cookie will also be visible to all subdomains.

Default: Cookie is visible only to the domain or subdomain of the page where the cookie was created, except for Internet Explorer (see below).

Examples:

Assuming a cookie that is being created on site.com:

Cookies.set('name', 'value', { domain: 'subdomain.site.com' })
Cookies.get('name') // => undefined (need to read at 'subdomain.site.com')

Note regarding Internet Explorer default behavior:

Q3: If I don’t specify a DOMAIN attribute (for) a cookie, IE sends it to all nested subdomains anyway? A: Yes, a cookie set on example.com will be sent to sub2.sub1.example.com. Internet Explorer differs from other browsers in this regard.

(From Internet Explorer Cookie Internals (FAQ))

This means that if you omit the domain attribute, it will be visible for a subdomain in IE.

secure

Either true or false, indicating if the cookie transmission requires a secure protocol (https).

Default: No secure protocol requirement.

Examples:

Cookies.set('name', 'value', { secure: true })
Cookies.get('name') // => 'value'
Cookies.remove('name')

partitioned

Either true or false, indicating if the cookie opts into partitioned storage. Must also set secure: true.

Default: No partitioned storage.

Examples:

Cookies.set('name', 'value', { partitioned: true })
Cookies.get('name') // => 'value'
Cookies.remove('name')

sameSite

A String, allowing to control whether the browser is sending a cookie along with cross-site requests.

Default: not set.

Note that more recent browsers are making "Lax" the default value even without specifying anything here.

Examples:

Cookies.set('name', 'value', { sameSite: 'strict' })
Cookies.get('name') // => 'value'
Cookies.remove('name')

Setting up defaults

const api = Cookies.withAttributes({ path: '/', domain: '.example.com' })

Converters

Read

Create a new instance of the api that overrides the default decoding implementation. All get methods that rely in a proper decoding to work, such as Cookies.get() and Cookies.get('name'), will run the given converter for each cookie. The returned value will be used as the cookie value.

Example from reading one of the cookies that can only be decoded using the escape function:

document.cookie = 'escaped=%u5317'
document.cookie = 'default=%E5%8C%97'
var cookies = Cookies.withConverter({
  read: function (value, name) {
    if (name === 'escaped') {
      return unescape(value)
    }
    // Fall back to default for all other cookies
    return Cookies.converter.read(value, name)
  }
})
cookies.get('escaped') // 北
cookies.get('default') // 北
cookies.get() // { escaped: '北', default: '北' }

Write

Create a new instance of the api that overrides the default encoding implementation:

Cookies.withConverter({
  write: function (value, name) {
    return value.toUpperCase()
  }
})

TypeScript declarations

npm i @types/js-cookie

Server-side integration

Check out the Servers Docs

Contributing

Check out the Contributing Guidelines

Releasing

Releasing should be done via the Release GitHub Actions workflow, so that published packages on npmjs.com have package provenance.

GitHub releases are created as a draft and need to be published manually! (This is so we are able to craft suitable release notes before publishing.)

Supporters

Many thanks to BrowserStack for providing unlimited browser testing free of cost.

Authors