15
loading...
This website collects cookies to deliver better user experience
# pnpm
pnpm add jotai
# npm
npm install jotai
# Or if you're a yarn person
yarn add jotai
// index.jsx (or index.tsx)
import React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
// Jotai provider
import { Provider } from 'jotai';
ReactDOM.render(
<React.StrictMode>
<Provider>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root'),
);
Atoms are the building blocks of universe and clump together into molecules--
import { atom } from 'jotai';
const themeAtom = atom('light');
Notice I suffixed my atom name with Atom
, as in themeAtom
. It's not a rule or an official convention., I simply choose to name my atoms like this for clarity in a big project. You can name it just theme
rather than themeAtom
🙂
useState
and useContext
hooks.import { useAtom } from 'jotai';
export const ThemeSwitcher = () => {
const [theme, setTheme] = useAtom(themeAtom);
return <main>{theme}</main>;
};
import { atom, useAtom } from 'jotai';
const themeAtom = atom('light');
export const ThemeSwitcher = () => {
const [theme, setTheme] = useAtom(themeAtom);
return <main>{theme}</main>;
};
import { atom, useAtom } from 'jotai';
const themeAtom = atom('light');
export const ThemeSwitcher = () => {
const [theme, setTheme] = useAtom(themeAtom);
return (
<main>
<p>Theme is {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
</main>
);
};
localstorage
accordingly.import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';
const browser = typeof window !== 'undefined';
const localValue = browser ? localStorage.getItem('theme') : 'light';
const systemTheme =
browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// The atom to hold the value goes here
const themeAtom = atom(localValue || systemTheme);
/** Sitewide theme */
export function useTheme() {
const [theme, setTheme] = useAtom(themeAtom);
useEffect(() => {
if (!browser) return;
localStorage.setItem('theme', theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
return [theme, setTheme];
}
localValue || systemTheme
. Here's what can happen with these values:localValue = 'light'
and systemTheme = 'light', localValue || systemTheme
will turn out to be light. So, important point here: Your app in SSR will be themed with light theme, so if you prerender your app, it will end up with light theme, in terms of plain HTML. As the JavaScript loads, it will sync to the most relevant theme possible.localValue
and systemTheme
variables inside the hook? The reason: If I put them in the hook, everytime the hook is initialized in any component, or a component re-renders, this hook will run again, and will fetch these values again from localstorage and media queries. These are pretty fast, but localstorage is blocking, and when used a lot, can introduce jank. So we initialize these 2 vars once in the lifetime of the app, because we need these only to get the initial value.const [theme, setTheme] = useAtom(themeAtom);
. These will be our theme in the form of state. Themes can be modified using setTheme
.useEffect(() => {
if (!browser) return;
localStorage.setItem('theme', theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
useEffect
that runs whenever theme changes, as you can see in the array in the 2nd argument. When this runs, it checks if the code is running in the browser. If it isn't, it simply stops further execution by doing a return.<body>
, then it adds the class corresponding to the latest value of theme variable.[theme, setTheme]
pair as it is, so we can use it just like we use useState
. You could also return these as objects { theme, setTheme }
giving them explicit naming.import { atom, useAtom } from 'jotai';
import { useEffect } from 'react';
export type Theme = 'light' | 'dark';
const browser = typeof window !== 'undefined';
const localValue = (browser ? localStorage.getItem('theme') : 'light') as Theme;
const systemTheme: Theme =
browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// The atom to hold the value goes here
const themeAtom = atom<Theme>(localValue || systemTheme);
/** Sitewide theme */
export function useTheme() {
const [theme, setTheme] = useAtom(themeAtom);
useEffect(() => {
if (!browser) return;
localStorage.setItem('theme', theme);
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
return [theme, setTheme] as const;
}
atomWithStorage
localstorage
completely, both from inside the hook as well as outside.atomWithStorage
is a special kind of atom that automatically syncs the value provided to it with localstorage
or sessionStorage
(Or AsyncStorage
, if used with React Native), and picks the value upon the first load automatically! It's available in the jotai/utils module, and adds some bytes other than the 2.4KB of Jotai Core.import { useAtom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import { useEffect } from 'react';
const browser = typeof window !== 'undefined';
// The atom to hold the value goes here
const themeAtom = atomWithStorage(
'theme',
browser && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
);
/** Sitewide theme */
export function useTheme() {
const [theme, setTheme] = useAtom(themeAtom);
useEffect(() => {
if (!browser) return;
document.body.classList.remove('light', 'dark');
document.body.classList.add(theme);
}, [theme]);
return [theme, setTheme];
}
localstorage
from the code, and we have a new thing atomWithStorage
. First argument is the key to store it in localstorage
. As in, if you specified theme
as value here, you would retrieve it from localstorage using localstorage.getItem('theme')
.atomWithStorage
. Now we don't have to keep the local value storage in mind, just have to focus on our main logic and remember that this value is synchronized locally, and that's it.import { useTheme } from './use-theme';
export const ThemeSwitcher = () => {
const [theme, setTheme] = useTheme();
return (
<main>
<p>Theme is {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>Toggle Theme</button>
</main>
);
};
const [derivedValue] = useAtom(derivedAtom);
setDerivedValue
here, no setter function. We can only read this atom. Changing the atoms it is derived from will automatically update this value.const store = atom('someValue');
const store = atom((get) => get(someAtomDefinedSomewhere));
export const appsStateStore = atom({
finder: false,
launchpad: false,
safari: false,
messages: false,
mail: true,
maps: true,
photos: false,
facetime: true,
calendar: false,
});
const openAppsStore = atom((get) => {
const apps = get(openAppsStore); // Gives the raw value { finder: false, launchpad: false, ...
// Filter out the values who are marked as false
const openAppsList = Object.keys(apps).filter((appName) => apps[appName]);
return openAppsList;
});
appStateStore
, setting them to true and false, the openAppsStore
will reflect the changes and the components using this store will also be updated with new values.const xCoordinateAtom = atom(0);
const yCoordinateAtom = atom(0);
// Compose 'em all
const distanceFromOriginAtom = atom((get) =>
Math.sqrt(get(xCoordinateAtom) ** 2 + get(yCoordinateAtom) ** 2),
);
xCoordinateAtom
atom and yCoordinateAtom
, and the distanceFromOriginAtom
will update with the new values!!)It's a mathematical formula to calculate the distance of a point from the origin (0, 0). If you didn't get it, no worries, I just want to get the point across that you can compose together different atoms seamlessly. That's it! 🙂
const readWriteAtom = atom(
(get) => get(priceAtom) * 2,
(get, set, newPrice) => {
set(priceAtom, newPrice / 2);
// you can set as many atoms as you want at the same time
},
);
priceAtom
, this readWriteAtom
gets updated. You update readWriteAtom
, priceAtom
gets updated. Mindblowing, right 🤯🤯?!?Beware though: As magical as this seems, it's two-way data binding. There have been controversies in the past about it, and rightfully so, as debugging and keeping the flow of data sane becomes extremely hard with these. That's why React itself has only one-way data binding. So use this atom carefully.
const fetchCountAtom = atom(
(get) => get(countAtom),
async (_get, set, url) => {
const response = await fetch(url);
set(countAtom, (await response.json()).count);
},
);
function Controls() {
const [count, compute] = useAtom(fetchCountAtom);
return <button onClick={() => compute('http://count.host.com')}>compute</button>;
}
<Suspense fallback={<span />}>
<Controls />
</Suspense>
useTheme
hook to use this special atom. It accepts a key (The name by which it is stored in localstorage
), and the initial value. Then you change this atom, and its value will be persisted locally and picked up after the page reloads.import { atomWithStorage } from 'jotai/utils';
const darkModeAtom = atomWithStorage('darkMode', false);
sessionStorage
too, so the atom's value will be persisted until the browser is closed. Handy if you're building a banking web app, where having short sessions is preferable.setState
back to that initial value. The code would look like this 👇import { atom, useAtom } from 'jotai';
const initialValue = 'light';
const themeAtom = atom(initialValue);
function ThemeSwitcher() {
const [theme, setTheme] = useAtom(themeAtom);
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
const resetTheme = () => setTheme(initialValue);
return (
<>
<button onClick={toggleTheme}>Toggle theme</button>
<button onClick={resetTheme}>Reset theme</button>
</>
);
}
import { useAtom } from 'jotai';
import { atomWithReset, useResetAtom } from 'jotai/utils';
const themeAtom = atomWithReset('light');
function ThemeSwitcher() {
const [theme, setTheme] = useAtom(themeAtom);
const reset = useResetAtom(themeAtom);
const toggleTheme = () => setTheme(theme === 'light' ? 'dark' : 'light');
return (
<>
<button onClick={toggleTheme}>Toggle theme</button>
<button onClick={reset}>Reset theme</button>
</>
);
}
const defaultPerson = {
name: {
first: 'Jane',
last: 'Doe',
},
birth: {
year: 2000,
month: 'Jan',
day: 1,
time: {
hour: 1,
minute: 1,
},
},
};
// Original atom.
const personAtom = atom(defaultPerson);
birth.time.minute
, the whole thing is going to count as an update and all the components will re-render. This is how React works, unfortunately.selectAtom
allows you to create a derived atom with only a subpath of the whole object.const firstNameAtom = selectAtom(personAtom, (person) => person.name.first);
firstNameAtom
is a read-only derived atom that only triggers when the person.name.first
property changes, and it holds the value of person.name.first.birth.time.hour
field(By updating the whole atom with new values), and the component relying on firstNameAtom
will remain unchanged. Amazing, right?selectAtom
, which is your own version of an equality check. You can write your custom function to check the objects.const birthAtom = selectAtom(personAtom, (person) => person.birth, deepEqual);
deepEqual
is hard, so it's recommended to go with lodash-es's isEqual
function.import { isEqual } from 'lodash-es';
const birthAtom = selectAtom(personAtom, (person) => person.birth, isEqual);
If seeing lodash gives you anxiety about bundle size, I assure you, isEqual of lodash-es is tree-shakeable, just 4.4KB minified, and even smaller in gzip/brotli. So no worries 😁
import { atom } from 'jotai';
import { freezeAtom } from 'jotai/utils';
const objAtom = freezeAtom(atom({ count: 0 }));
freezeAtom
takes an existing atom and returns a new derived atom. The returned atom is "frozen" which means when you use the atom with useAtom
in components or get in other atoms, the atom value will be deeply frozen with Object.freeze
. It would be useful to find bugs where you accidentally tried to mutate objects which can lead to unexpected behavior.const dogsAtom = atom(async (get) => {
const response = await fetch('/dogs');
return await response.json();
});
const catsAtom = atom(async (get) => {
const response = await fetch('/cats');
return await response.json();
});
const App = () => {
const [dogs] = useAtom(dogsAtom);
const [cats] = useAtom(catsAtom);
// ...
};
dogsAtom
to go fetch data, return, then it will move to the next atom catsAtom
. We don't want this. Both these atoms are independent of each other, we should rather fetch them in parallel(Or concurrently if you are a hardcore JavaScripter 😉)Promise.all(...)
on these atoms. The way to do that is using the waitForAll
util.const dogsAtom = atom(async (get) => {
const response = await fetch('/dogs');
return await response.json();
});
const catsAtom = atom(async (get) => {
const response = await fetch('/cats');
return await response.json();
});
const App = () => {
const [[dogs, cats]] = useAtom(waitForAll([dogsAtom, catsAtom]));
// ...
};
await Promise.all
statement.package.json
!!!"function UpdateUser() {
const [user, setUser] = useState({
id: 23,
name: 'Luke Skywalker',
dob: new Date('25 December, 19 BBY'),
});
// Update the dob
const updateDob = () => setUser({ ...user, dob: new Date('25 November, 200ABY') });
return <button onClick={updateDob}>Update DOB</button>;
}
updateDob
method, we have to spread the original object, and pass the field we want to update. This is OK. But what if the object is many levels deep and we want to update an object very deep.user.dob = new Date('25 November, 200ABY');
state.depth1.depth2.depth3.depth4 = 'something';
import { atomWithImmer } from 'jotai/immer';
const userAtom = atomWithImmer({
id: 23,
name: 'Luke Skywalker',
dob: new Date('25 December, 19 BBY'),
});
function UpdateUser() {
const [user, setUser] = useAtom(userAtom);
// Update the dob
const updateDob = () =>
setUser((user) => {
user.dob = new Date('25 November, 200ABY');
return user;
});
return <button onClick={updateDob}>Update DOB</button>;
}
setUser
works differently. It's a callback that passes you the current value of the state. This value is a copy of the original value. You can mutate this copy as much as you want inside the callback, and finally just return it, Jotai and Immer will automatically reconcile the changes without any of the bugs that come with mutating. Freaking awesome!