Next Isomorphic Cookies
Using cookies in NextJS made easy!
Seamless integration with SSG and SSR, while avoiding hydration mismatches.
- Completely avoids hydration mismatches
- Works seamlessly with SSG, SSR or CSR
- Initializes state from cookies as soon as possible (in the server, when using SSR and immediately after the first render when using SSG)
- Out-of-the-box Typescript support
Usage
- Wrap your
App
at _app
:
import type { AppProps } from "next/app";
import { withCookiesAppWrapper } from "next-isomorphic-cookies";
const MyApp = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
export default withCookiesAppWrapper(MyApp);
- (Optional) Wrap
getServerSideProp
(when using SSR):
const Page = () => {
};
export const getServerSideProps: GetServerSideProps =
withCookiesGetServerSidePropsWrapper(async (context) => {
});
export default Page;
- Call
useCookieState
:
import { useCookieState } from "next-isomorphic-cookies";
const FoodTypeCookieKey = "FoodType";
const foodTypes = ["Japanese", "Mexican", "Italian"];
export const FoodTypeFilter = () => {
const foodTypeInitializer = (storedFoodType: string) => {
const defaultFoodType = foodTypes[0];
const cookieNotSet = storedFoodType === undefined;
if (cookieNotSet) {
return defaultFoodType;
}
const storedFoodTypeIsNotAvaialble = !foodTypes.includes(storedFoodType);
if (storedFoodTypeIsNotAvaialble) {
return defaultFoodType;
}
return storedFoodType;
};
const { value: foodType, setValue: setFoodType } = useCookieState<string>(
FoodTypeCookieKey,
foodTypeInitializer
);
return (
<div>
<label>Food Type</label>{" "}
<select
value={value}
onChange={(event) => {
// Automatically persists value
// in the cookie
setFoodType(event.target.value);
}}
>
<option value="Japanese">Japanese</option>
<option value="Mexican">Mexican</option>
<option value="Italian">Italian</option>
</select>
</div>
);
};
API
withCookiesAppWrapper
You must wrap your App at _app
with this function.
Example:
import type { AppProps } from "next/app";
import { withCookiesAppWrapper } from "next-isomorphic-cookies";
const MyApp = ({ Component, pageProps }: AppProps) => {
return <Component {...pageProps} />;
};
export default withCookiesAppWrapper(MyApp);
withCookiesGetServerSidePropsWrapper
Wraps getServerSideProps
to make cookies available to components rendering in the server.
If you do not wrap getServerSideProps
with this function, either because you're using SSG, or because you simply forgot, the only thing that will happen is that the state that relies on cookies to be initialized will be synced with cookies after the first render, but it won't break anything.
Example:
const Page = () => {
};
export const getServerSideProps: GetServerSideProps =
withCookiesGetServerSidePropsWrapper(async (context) => {
});
export default Page;
useCookieState
Can be called inside any component, as long as you've wrapped your App with withCookiesAppWrapper
.
export type UseCookieState<T> = (
key: string,
initializer: (storedValue: T | undefined) => T,
options?: UseCookieStateOptions<T>
) => {
value: T;
setValue: Dispatch<SetStateAction<T>>;
retrieve: (options?: {
/**
* Transforms the cookie value before
* setting value to it.
*
* Defaults to identity function.
*/
deserializer?: (storedValue: T | undefined) => T;
}) => void;
store: (
value: T,
options?: {
/**
* js-cookie attributes
*/
attributes?: CookieAttributes;
/**
* Transforms the value before
* it is stored.
*
* Defaults to identity function.
*/
serializer?: (storedValue: T | undefined) => T;
}
) => void;
clear: () => void;
isSyncing: boolean;
};
type UseCookieStateOptions<T> = {
storeOnSet?: StoreOnSetOption<T>;
};
type StoreOnSetOption<T> =
| true
| false
| {
attributes?: CookieAttributes;
serializer?: (value: T) => T;
};
interface CookieAttributes {
expires?: number | Date | undefined;
path?: string | undefined;
domain?: string | undefined;
secure?: boolean | undefined;
sameSite?: "strict" | "Strict" | "lax" | "Lax" | "none" | "None" | undefined;
}
useCookie
A "lower-level" hook, that can be used in case you want to manage state yourself.
export type UseCookie = <T>(key: string) => {
retrieve: () => T | undefined;
store: (data: T, attributes?: CookieAttributes) => void;
clear: (attributes?: CookieAttributes) => void;
needsSync: boolean;
};
Example:
const { retrieve, needsSync } = useCookie("SomeCookie");
const [value, setValue] = useState(needsSync ?? retrieve());
useSyncWithCookie((storedValue) => {
setValue(storedValue);
});
TODO
useSyncWithCookie
To be used in conjunction with useCookie
to deal with state synchronization after hydration.
type UseSyncWithCookie = <T>(key: string, sync: (cookieValue: T | undefined) => void)
TODO
Motivation
When using NextJS (or any kind of server rendering), our React components end up getting rendered in two very different environments: browser and server.
So, if you have a React component that reads values from cookies while rendering, while it works fine when rendering in the browser, it'll crash your application when rendering in the server:
const getCookie = (key: string) => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return JSON.parse(parts.pop().split(";").shift());
}
};
const FoodTypeCookieKey = "FoodType";
const foodTypes = ["Japanese", "Mexican", "Italian"];
export const FoodTypeFilter = () => {
const [foodType, setFoodType] = useState(getCookie(FoodTypeCookieKey));
return (
<div>
<label>Food Type</label>{" "}
<select
value={value}
onChange={(event) => {
setFoodType(event.target.value);
}}
>
<option value="Japanese">Japanese</option>
<option value="Mexican">Mexican</option>
<option value="Italian">Italian</option>
</select>
</div>
);
};
And then probably, you might try something like this:
const FoodTypeCookieKey = "FoodType";
const foodTypes = ["Japanese", "Mexican", "Italian"];
const isServer = typeof document === "undefined";
export const FoodTypeFilter = () => {
const initialFoodType = isServer
? foodTypes[0]
: getCookie(FoodTypeCookieKey);
const [foodType, setFoodType] = useState(initialFoodType);
return (
<div>
<label>Food Type</label>{" "}
<select
value={value}
onChange={(event) => {
setFoodType(event.target.value);
}}
>
<option value="Japanese">Japanese</option>
<option value="Mexican">Mexican</option>
<option value="Italian">Italian</option>
</select>
</div>
);
};
But then you're gonna get a hydration mismatch, because the HTML that was generated in the server must match exactly the HTML that is generated on the client in the first render.
Finally, you remember that useEffect
only runs in the client, and that runs after the render phase:
const FoodTypeCookieKey = "FoodType";
const foodTypes = ["Japanese", "Mexican", "Italian"];
const isServer = typeof document === "undefined";
export const FoodTypeFilter = () => {
const [foodType, setFoodType] = useState(foodTypes[0]);
useEffect(() => {
setFoodType(getCookie(FoodTypeCookieKey));
}, []);
return (
<div>
<label>Food Type</label>{" "}
<select
value={value}
onChange={(event) => {
setFoodType(event.target.value);
}}
>
<option value="Japanese">Japanese</option>
<option value="Mexican">Mexican</option>
<option value="Italian">Italian</option>
</select>
</div>
);
};
After a little bit more of head-scratching, you're probably gonna think:
But, wait a moment, if I use SSR, won't I have access to the request and thus, to the cookies? So why can't I use the cookie value while rendering on the server?
And you're right, you absolutely can, the only nuisance is that the only place you have access to cookies is inside getServerSideProps
, so you have to pass the cookie value all the way down to your component, or use the Context API:
type PageProps = {
cookies: Record<string, string>;
};
const Page = ({ cookies }: PageProps) => {
return (
<CookiesContext.Provider value={cookies}>
{/* ... */}
</CookiesContext.Provider>
);
};
export const getServerSideProps = async (context) => {
return {
props: {
cookies: context.req.cookies,
},
};
};
const FoodTypeCookieKey = "FoodType";
const foodTypes = ["Japanese", "Mexican", "Italian"];
const isServer = typeof document === "undefined";
export const FoodTypeFilter = () => {
const cookies = useContext(CookiesContext);
const [foodType, setFoodType] = useState(
JSON.parse(cookies[FoodTypeCookieKey])
);
return (
<div>
<label>Food Type</label>{" "}
<select
value={value}
onChange={(event) => {
setFoodType(event.target.value);
}}
>
<option value="Japanese">Japanese</option>
<option value="Mexican">Mexican</option>
<option value="Italian">Italian</option>
</select>
</div>
);
};
However, now your FoodTypeFilter
component cannot be used in pages that use SSG anymore, because we do not have access to the request in SSG (after all, it renders the page in build time), so cookies
will be undefined, which will crash your application.
And we could go on, but the point is, with next-isomorphic-cookies
you can absolutely forget about all of these issues, you don't even need to know whether your component is going to be used in a page that uses SSR, or SSG, because we take care of everything for you.
How It Works
TODO
Cookbook
TODO