A maintained, New Architecture–only cookie manager for React Native, implemented as a TurboModule with Swift on iOS and Kotlin on Android. It is a successor to @react-native-cookies/cookies: legacy method names and call signatures remain available, while v7 intentionally changes architecture support, validation, and error codes.
Starting with
v7.0.0, this package supports only React Native's New Architecture. Projects that still require the legacy bridge should stay onv6.x.
The package works in bare React Native apps and in Expo Dev Builds (custom native builds).
This package is based on the public API and behavior of @react-native-cookies/cookies. Big thanks to the upstream maintainers and contributors for the original implementation and long-term work on the project.
Install with your package manager:
# Bun
bun add @preeternal/react-native-cookie-manager
# Yarn
yarn add @preeternal/react-native-cookie-manager
# npm
npm install @preeternal/react-native-cookie-managerReact Native still selects CocoaPods by default. This library keeps its podspec, so existing apps and React Native versions before 0.87 continue to install it normally:
cd ios && bundle exec pod installReact Native 0.87 added experimental, opt-in SwiftPM integration. This library
ships a compatible Package.swift; CocoaPods remains the default and supported
production path.
To migrate an app once:
cd ios
npx react-native spm --deintegrateAfter a fresh clone or in CI, generate the SwiftPM workspace before building:
cd ios
npx react-native spmEvery native dependency must have a compatible Package.swift. If a dependency
does not provide one, generate it with npx react-native spm scaffold and keep
the manifest in a package-manager patch.
The SwiftPM commands and generated layout are experimental in React Native 0.87 and may change in later releases. Do not use this integration in production yet. See the React Native 0.87 release notes.
React Native stores response cookies automatically. Make the request with your HTTP client, then read cookies matching the URL:
import CookieManager from '@preeternal/react-native-cookie-manager';
const url = 'https://example.com/login';
await fetch(url);
// axios alternative: await axios(url);
const cookies = await CookieManager.get(url);Standard React Native networking handles cookies by default. Credentials options are only needed if your client configuration explicitly disables cookie handling. get() only reads the native cookie store; it does not make a request.
If a custom client or Axios adapter does not use React Native's native cookie handling, getCookieHeader(url) returns a ready-to-use Cookie request-header value. Do not add it to standard Fetch/Axios requests, where native networking already attaches cookies.
The upstream-compatible getFromResponse(url) remains available but is deprecated: it performs a separate GET, follows redirects, and updates the cookie store without options for headers, authentication, timeout, or cancellation. Prefer the flow above to avoid a duplicate request and its side effects.
On iOS, “the WebKit store” below means WKWebsiteDataStore.default().httpCookieStore, the app's default persistent store.
// Foundation on iOS; options are optional
await CookieManager.set('https://example.com', {
name: 'session',
value: 'abc123',
domain: 'example.com',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
});
// iOS: write to the WebKit store
await CookieManager.set(
'https://example.com',
{ name: 'web_session', value: 'abc123', path: '/', secure: true },
{ iosCookieStore: 'webKit' }
);
const cookies = await CookieManager.get('https://example.com');
// Preserve cookies that share a name but differ by domain or path
const cookieVariants = await CookieManager.getAsArray('https://example.com');
// iOS only: get all cookies
const allCookies = await CookieManager.getAll();
// Clear cookies named "session"
await CookieManager.clearByName('https://example.com', 'session');
// Clear Foundation on iOS; clear the shared store on Android
await CookieManager.clearAll();
// Clear Foundation and the WebKit store on iOS
await CookieManager.clearAllStores();
// Remove session cookies from both iOS stores; shared Android store
await CookieManager.removeSessionCookies();
await CookieManager.removeSessionCookies({ iosCookieStore: 'both' }); // explicit equivalent
// iOS: limit session cleanup to one store when needed
await CookieManager.removeSessionCookies({ iosCookieStore: 'webKit' });Use the change listener to invalidate application state after Foundation or the WebKit store changes—for example, when a login completes inside a WebView:
import CookieManager, {
type IOSCookieStore,
} from '@preeternal/react-native-cookie-manager';
import { useEffect } from 'react';
import { Platform } from 'react-native';
const url = 'https://example.com/account';
useEffect(() => {
if (Platform.OS !== 'ios') {
return;
}
const subscription = CookieManager.addCookieChangeListener(
({ iosCookieStore }) => {
refreshCookies(iosCookieStore)
.then((snapshot) => {
updateAuthState(snapshot);
})
.catch((error) => {
console.error('Failed to refresh cookies after invalidation', error);
});
}
);
return () => subscription.remove();
}, []);
async function refreshCookies(iosCookieStore: IOSCookieStore) {
// Read cookies matching one URL from the store that changed.
const matchingCookies = await CookieManager.get(url, { iosCookieStore });
const sessionCookie = matchingCookies.session;
// Or read every cookie in that store.
const allCookiesInChangedStore = await CookieManager.getAll({
iosCookieStore,
});
// When application state depends on both stores, read them separately.
const [allFoundationCookies, allWebKitCookies] = await Promise.all([
CookieManager.getAll({ iosCookieStore: 'foundation' }),
CookieManager.getAll({ iosCookieStore: 'webKit' }),
]);
return {
sessionCookie,
allCookiesInChangedStore,
allFoundationCookies,
allWebKitCookies,
};
}The event payload is only { iosCookieStore: 'foundation' | 'webKit' }. It is an invalidation signal: native stores may coalesce notifications, so one event is not guaranteed for every cookie mutation and no cookie delta is provided. Choose the narrowest useful follow-up read: select one cookie from get(), preserve same-name variants with getAsArray(), read the changed store with getAll() / getAllAsArray(), or call those methods once per store when both snapshots are required. Results from Foundation and WebKit remain separate. The listener observes Foundation and the default persistent WebKit store; custom or non-persistent WKWebsiteDataStore instances are outside its scope.
Cookie change subscriptions are iOS-only. Calling addCookieChangeListener() on Android throws an error with code: 'not_supported' because the public Android WebView cookie store has no global change observer.
set() validates the complete structured cookie before touching a native store. Validation is enabled by default on both platforms. It rejects malformed names, paths, and expiry dates, together with control characters or field delimiters that could change the cookie's structure. Printable values do not require percent-encoding or base64url merely because they contain spaces, Unicode, quotes, commas, backslashes, or =.
During migration from v6, { validate: false } temporarily restores native-store handling for compatibility-sensitive input. For example, v6 treated an unparseable expires value as a session cookie:
import CookieManager, {
isCookieManagerError,
type Cookie,
} from '@preeternal/react-native-cookie-manager';
const cookieFromBackend: Cookie = {
name: 'session',
value: '<token supplied by the backend>',
path: '/',
// set() requires an ISO 8601 expiry date.
expires: '2032-06-09 10:18:14 UTC',
};
try {
await CookieManager.set('https://example.com', cookieFromBackend);
} catch (error) {
if (isCookieManagerError(error) && error.code === 'invalid_cookie') {
console.warn('Structured cookie rejected', {
code: error.code,
cookieName: cookieFromBackend.name,
path: cookieFromBackend.path,
hasExpires: cookieFromBackend.expires !== undefined,
backendOperation: 'create-session',
});
}
}
// Use only after confirming that validation caused the migration failure.
await CookieManager.set('https://example.com', cookieFromBackend, {
validate: false,
});The validate field is deprecated and will be removed in the next major version, when validation becomes unconditional. Do not automatically retry every invalid_cookie rejection with validation disabled. Log the stable code and safe source context, fix the producer or backend, and then remove the escape hatch. Never log a cookie value, raw Set-Cookie header, authentication token, or session identifier. Messages are diagnostic text and must not be used for branching.
Structural safety checks remain active when validate: false. CR, LF, NUL, other ASCII controls, and ; in a structured value still reject. Android WebView accepts only a raw Set-Cookie string and treats the first ; as an attribute separator, even inside quotes. A literal semicolon therefore cannot be preserved without encoding agreed by the application and backend; the library never encodes values implicitly.
setFromResponse() is an advanced API for importing a raw Set-Cookie header from a custom HTTP client that does not share React Native's cookie store. It is normally unnecessary with Fetch or Axios. Call it once for each Set-Cookie header value.
await CookieManager.setFromResponse(
'https://example.com',
'session=abc123; Path=/; Secure; HttpOnly'
);Semicolons in this raw API delimit real attributes; they do not escape a literal semicolon inside a value. Empty headers and headers containing CR, LF, or NUL reject before reaching the native store.
Legacy method names and positional boolean overloads remain source-compatible with @react-native-cookies/cookies. The New Architecture requirement, default validation, and stable error taxonomy are intentional v7 behavior changes.
| Method | Platforms | Description |
|---|---|---|
addCookieChangeListener(listener): EventSubscription |
iOS | Subscribes to invalidations from Foundation and the WebKit store. Native observers are shared across JS subscribers and stop after the last subscription is removed. Android throws not_supported. |
set(url, cookie, options?): Promise<boolean> |
iOS, Android | Validates and stores a cookie, including sameSite and relative maxAge. On iOS, omitted options select Foundation; { iosCookieStore: 'webKit' } selects the WebKit store. options.validate is a temporary deprecated v7 migration escape hatch. |
get(url, options?): Promise<Cookies> |
iOS, Android | Reads matching cookies without making a request. On iOS, omitted options select Foundation; { iosCookieStore: 'webKit' } selects the WebKit store. |
getAsArray(url, options?): Promise<ReadonlyArray<Cookie>> |
iOS, Android | Reads matching cookies without collapsing cookies that share a name. Store selection matches get(). |
getCookieHeader(url, options?): Promise<string> |
iOS, Android | Returns the selected store's matching cookies as a Cookie request-header value, or an empty string. |
clearAll(options?): Promise<boolean> |
iOS, Android | Clears the shared Android store or the selected iOS store. |
clearAllStores(): Promise<boolean> |
iOS, Android | Clears the shared Android store, or Foundation and the WebKit store on iOS; resolves true after native completion. |
getAll(options?): Promise<Cookies> |
iOS | Reads all cookies from the selected iOS store. |
getAllAsArray(options?): Promise<ReadonlyArray<Cookie>> |
iOS | Reads the selected iOS store without collapsing cookies that share a name. |
clearByName(url, name, options?): Promise<boolean> |
iOS, Android | Clears same-name cookies from the selected iOS store, or variants applicable to url in the shared Android store. |
flush(): Promise<void> |
iOS, Android | Explicit Android persistence barrier for external shared-store changes. Android mutations already flush automatically; this method is a no-op on iOS. |
removeSessionCookies(options?): Promise<boolean> |
iOS, Android | Removes cookies without an expiry date and reports whether any were removed; includes both iOS stores by default. |
setFromResponse(url, cookieHeader): Promise<boolean> |
iOS, Android | Imports one raw Set-Cookie header value; uses Foundation on iOS. |
getFromResponse(url): Promise<Cookies> |
iOS, Android | Deprecated; performs a GET and updates Foundation on iOS. |
All single-store methods use the same optional { iosCookieStore } selector. Omitting options or omitting the field selects Foundation. The legacy positional useWebKit overloads remain available throughout v7 but are deprecated; replace true with { iosCookieStore: 'webKit' } and false with { iosCookieStore: 'foundation' } or omitted options. On iOS each call selects one store and never combines them. Android ignores iosCookieStore because WebView and native share a single store.
removeSessionCookies() clears both iOS stores by default. Pass { iosCookieStore: 'both' } to state that scope explicitly, or select 'foundation' / 'webKit' to limit cleanup to one store. Android ignores this iOS-only option.
On Android, clearByName() relies on GET_COOKIE_INFO support in the device's Android System WebView provider. It rejects with not_supported on devices with an older provider. The method clears every same-name domain/path variant visible to the supplied URL. A cookie restricted to /account is not visible from a / URL, so use a matching path (and multiple calls for unrelated paths). On iOS, the method clears same-domain variants across all paths in the selected store.
Native failures use a small platform-neutral set of stable codes. Use isCookieManagerError() before reading code:
import CookieManager, {
isCookieManagerError,
} from '@preeternal/react-native-cookie-manager';
try {
await CookieManager.set(url, cookie);
} catch (error) {
if (isCookieManagerError(error)) {
switch (error.code) {
case 'domain_mismatch':
// The cookie domain cannot be set from this URL.
break;
case 'invalid_cookie':
// Fix the structured cookie input.
break;
}
}
}| Code | Meaning |
|---|---|
invalid_url |
The supplied URL failed the parsing or host check required by that operation. |
invalid_cookie |
Cookie input cannot be represented or accepted as a cookie. |
domain_mismatch |
The cookie domain does not match the URL host or one of its parent domains. |
not_supported |
The requested capability is unavailable on this platform or native provider. |
storage_error |
The native cookie store could not complete a read, write, deletion, or persistence operation. |
network_error |
The deprecated getFromResponse() request failed. No other method performs network I/O. |
The human-readable message and any native cause are diagnostic details and are not stable API. Do not branch on their contents.
v7 does not add a global HTTP(S)-only check to every store operation. URL acceptance otherwise remains delegated to the platform store for compatibility with v6. The deprecated network method still requires an HTTP(S) URL.
type Cookie = {
name: string;
value: string;
path?: string;
domain?: string;
version?: string;
expires?: string; // ISO 8601 string, e.g. 2015-05-30T12:30:00.00-05:00
secure?: boolean;
httpOnly?: boolean;
sameSite?: 'lax' | 'strict' | 'none';
maxAge?: number; // set() only: relative lifetime in whole seconds
};
type IOSCookieStore = 'foundation' | 'webKit';
type IOSCookieStoreOptions = {
iosCookieStore?: IOSCookieStore;
};
type SetCookieOptions = IOSCookieStoreOptions & {
/** @deprecated Temporary v7 migration escape hatch. */
validate?: boolean;
};
type RemoveSessionCookiesOptions = {
// Defaults to 'both'.
iosCookieStore?: IOSCookieStore | 'both';
};maxAge takes precedence over expires; 0 or a negative value expires the cookie immediately. Native stores expose the resulting absolute expires date when reading, not the original maxAge. sameSite: 'none' requires secure: true. The iOS HTTPCookie model represents this unrestricted policy as no explicit SameSite value, so reads may omit sameSite after setting 'none'.
partitioned is intentionally not a structured field: creating a partitioned cookie requires top-level site context that this API's cookie URL cannot express consistently across Android and iOS. Prefer receiving it from the server or setting it inside the relevant WebView context.
Cookies is keyed by cookie name, so get() and getAll() retain only the last item when multiple cookies share a name. This legacy behavior is preserved for upstream compatibility. Use getAsArray() or getAllAsArray() when domain/path variants must remain separate.
On Android, metadata is populated when the device's Android System WebView provider supports GET_COOKIE_INFO. Devices with an older provider fall back to legacy name/value parsing, so domain, path, expires, and sameSite may be unavailable, while secure and httpOnly should not be treated as authoritative.
- iOS has two stores:
NSHTTPCookieStorage(used by URLSession) andWKHTTPCookieStore(used by WKWebView /react-native-webview). - Pass
{ iosCookieStore: 'webKit' }toset()to use the default WKWebView cookie store. For network-only flows, omit options or select'foundation'to useNSHTTPCookieStorage. - To apply a single-store method to both stores, call it once with
{ iosCookieStore: 'foundation' }(or omitted options) and once with{ iosCookieStore: 'webKit' }. Results are returned separately and are not merged. getCookieHeader(url, { iosCookieStore: 'webKit' })filters cookies from the WebKit store by domain, path,Secure, and expiry. A URL alone cannot reproduce WebKit'sSameSite, partition, or third-party request context, so do not treat it as the exact header of an embedded WebView request.- Use
clearAllStores()when logout must clear both app-accessible stores. The library cannot access a non-persistent or custom store owned by a specific WebView. - On Android the flag is ignored; WebView and native use the same store.
Warning
On Android, react-native-webview's incognito mode currently clears the shared app-wide cookie store, including cookies used by React Native networking. Avoid it when your app relies on authenticated native requests. See react-native-webview#3988.
Android uses androidx.webkit:webkit:1.16.0 by default. Most applications do not need to configure it. Bare React Native apps can override the requested version in android/gradle.properties:
react_native_cookie_manager_webkit_version=1.16.0An existing shared override in the root android/build.gradle is also honored, including when it configures react-native-webview:
rootProject.ext.webkitVersion = "1.16.0"The package-specific gradle.properties value takes precedence when both are present. Expo apps can set it during prebuild:
{
"expo": {
"plugins": [
[
"@preeternal/react-native-cookie-manager",
{ "androidWebkitVersion": "1.16.0" }
]
]
}
}Versions older than 1.6.0 are unsupported because the library compiles against CookieManagerCompat.getCookieInfo(). Gradle may select a higher compatible version when another dependency requires it.
- A cookie is persistent only when the server supplies
Expires/Max-Age, or whenset()receivesexpires/maxAge. Native stores enforce expiration;flush()does not extend a cookie's lifetime or turn a session cookie into a persistent one. - On iOS, Foundation persistent cookies survive without a WebView. There is no public iOS flush API, so
flush()is a no-op. - On Android, the library automatically flushes the shared WebView cookie store after its mutations. Current WebView implementations may also restore session cookies—cookies without an expiry—after a process restart.
A persistent cookie written with { iosCookieStore: 'webKit' } survives process termination only if a normal, non-incognito WKWebView using the default data store was mounted before the process ended. A normally mounted react-native-webview satisfies this requirement; installing the package without mounting a WebView does not.
After a cold start, mount the WebView before reading cookies from the previous app session with get(), getAsArray(), getCookieHeader(), getAll(), or getAllAsArray() using { iosCookieStore: 'webKit' }. Do the same before clearByName(url, name, { iosCookieStore: 'webKit' }) when deleting a persistent cookie from the previous session, because this method first reads the store to find matching cookies.
Full cleanup with clearAll({ iosCookieStore: 'webKit' }) or clearAllStores() can run before a WebView is mounted because it clears WebKit website data directly. removeSessionCookies() can also run before mounting; iOS session cookies are process-scoped and are not expected to survive a restart.
If the app never creates a WebView, use the Foundation store instead by omitting options or selecting { iosCookieStore: 'foundation' }.
On Android, mutation methods automatically flush before their Promises resolve. Calling flush() immediately after awaiting set(), setFromResponse(), getFromResponse(), clearByName(), clearAll(), clearAllStores(), or removeSessionCookies() is redundant. Use it only as an explicit persistence barrier after the shared Android store was changed outside this library.
The library intentionally does not maintain a separate cookie backup or silently replay cookies on startup. That could resurrect expired or logged-out authentication state and would require the application to choose appropriate secure storage. Prefer server-defined persistent cookies; call removeSessionCookies() before the first request or WebView load when the application requires a clean session on launch.
example/uses CocoaPods on iOS and is also the Android example.example-spm/uses React Native 0.87 SwiftPM autolinking without CocoaPods.
Both apps demonstrate store selection, iOS invalidations, stable errors, structured validation, native smoke checks, and persistence across an app restart.
MIT
Made with create-react-native-library