Mastering State Management in React with Jotai & TypeScript: A Comprehensive Guide
This guide covers:
Basic Atoms
Dependent Atoms
Async Atoms with
loadable
Scoped Providers
Accessing Jotai Atoms Outside Components
Prerequisites
You’ll need:
TypeScript set up in your React project.
Install Jotai with
npm install jotai jotai/utils
.
Setting Up
Organize the project with a scalable folder structure.
src/
│
├── atoms/
│ ├── counterAtom.ts # Basic counter atom
│ ├── dependentAtom.ts # Dependent atom example
│ ├── dropdownAtoms.ts # Async atoms with loadable
│ ├── scopedCounterAtom.ts # Scoped counter atom
│
├── components/
│ ├── Counter.tsx # Component for basic atom
│ ├── DependentCounter.tsx # Component for dependent atom
│ ├── AsyncDropdown.tsx # Component for async dropdowns
│ ├── ScopedCounter.tsx # Component for scoped counters
│
└── App.tsx # Main app file
Basic Atom (Primitive Atom)
Step 1: Define the Atom in TypeScript
// src/atoms/counterAtom.ts
import { atom } from 'jotai';
export const counterAtom = atom<number>(0);
Step 2: Create the Counter Component
// src/components/Counter.tsx
import React from 'react';
import { useAtom } from 'jotai';
import { counterAtom } from '../atoms/counterAtom';
const Counter: React.FC = () => {
const [count, setCount] = useAtom(counterAtom);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
</div>
);
};
export default Counter;
Step 3: Use the Counter Component in App.tsx
// src/App.tsx
import React from 'react';
import Counter from './components/Counter';
const App: React.FC = () => {
return (
<div>
<h1>Jotai Basics</h1>
<Counter />
</div>
);
};
export default App;
Creating Dependent Atoms
Step 1: Define Dependent Atom (dependentAtom.ts)
// src/atoms/dependentAtom.ts
import { atom } from 'jotai';
import { counterAtom } from './counterAtom';
export const doubleCounterAtom = atom((get) => get(counterAtom) * 2);
Step 2: Create Dependent Counter Component
// src/components/DependentCounter.tsx
import React from 'react';
import { useAtom } from 'jotai';
import { doubleCounterAtom } from '../atoms/dependentAtom';
const DependentCounter: React.FC = () => {
const [doubleCount] = useAtom(doubleCounterAtom);
return (
<div>
<h2>Double Count: {doubleCount}</h2>
</div>
);
};
export default DependentCounter;
Step 3: Add to App.tsx
// src/App.tsx
import React from 'react';
import Counter from './components/Counter';
import DependentCounter from './components/DependentCounter';
const App: React.FC = () => {
return (
<div>
<h1>Jotai Basics</h1>
<Counter />
<DependentCounter />
</div>
);
};
export default App;
Async Atom with loadable
for Cascaded Dropdowns
Using loadable
, we can manage async atoms without needing Suspense
, which is ideal for showing loading states directly within the component.
Step 1: Define Async Atoms with loadable
(dropdownAtoms.ts)
// src/atoms/dropdownAtoms.ts
import { atom } from 'jotai';
import { loadable } from 'jotai/utils';
export const countryAtom = atom<string | null>(null);
export const stateAtom = loadable(
atom(async (get) => {
const selectedCountry = get(countryAtom);
if (!selectedCountry) return [];
const response = await fetch(`/api/states?country=${selectedCountry}`);
return response.json();
})
);
export const cityAtom = loadable(
atom(async (get) => {
const selectedState = get(stateAtom)?.data;
if (!selectedState) return [];
const response = await fetch(`/api/cities?state=${selectedState}`);
return response.json();
})
);
Step 2: Create the Async Dropdown Component
// src/components/AsyncDropdown.tsx
import React from 'react';
import { useAtom } from 'jotai';
import { countryAtom, stateAtom, cityAtom } from '../atoms/dropdownAtoms';
const AsyncDropdown: React.FC = () => {
const [country, setCountry] = useAtom(countryAtom);
const [states] = useAtom(stateAtom);
const [cities] = useAtom(cityAtom);
return (
<div>
<h2>Cascaded Dropdowns</h2>
<label>
Country:
<select onChange={(e) => setCountry(e.target.value)}>
<option value="">Select Country</option>
<option value="usa">USA</option>
<option value="canada">Canada</option>
</select>
</label>
<label>
State:
{states.state === 'loading' ? (
<p>Loading states...</p>
) : (
<select>
<option value="">Select State</option>
{states.data.map((state) => (
<option key={state.id} value={state.name}>
{state.name}
</option>
))}
</select>
)}
</label>
<label>
City:
{cities.state === 'loading' ? (
<p>Loading cities...</p>
) : (
<select>
<option value="">Select City</option>
{cities.data.map((city) => (
<option key={city.id} value={city.name}>
{city.name}
</option>
))}
</select>
)}
</label>
</div>
);
};
export default AsyncDropdown;
Step 3: Add Async Dropdown to App.tsx
// src/App.tsx
import React from 'react';
import AsyncDropdown from './components/AsyncDropdown';
const App: React.FC = () => {
return (
<div>
<h1>Async with Jotai</h1>
<AsyncDropdown />
</div>
);
};
export default App;
Using Scoped Providers
Scoped providers are helpful for creating isolated instances of atoms.
Step 1: Define Scoped Atom (scopedCounterAtom.ts)
// src/atoms/scopedCounterAtom.ts
import { atom } from 'jotai';
export const scopedCounterAtom = atom<number>(0);
Step 2: Scoped Component (ScopedCounter.tsx)
// src/components/ScopedCounter.tsx
import React from 'react';
import { useAtom } from 'jotai';
import { scopedCounterAtom } from '../atoms/scopedCounterAtom';
const ScopedCounter: React.FC = () => {
const [count, setCount] = useAtom(scopedCounterAtom);
return (
<div>
<h2>Scoped Count: {count}</h2>
<button onClick={() => setCount((prev) => prev + 1)}>Increment</button>
</div>
);
};
export default ScopedCounter;
Step 3: App with Scoped Providers
// src/App.tsx
import React from 'react';
import { Provider } from 'jotai';
import ScopedCounter from './components/ScopedCounter';
const CounterScope1 = Symbol('CounterScope1');
const CounterScope2 = Symbol('CounterScope2');
const App: React.FC = () => {
return (
<div>
<h1>Scoped Providers with Jotai</h1>
<Provider scope={CounterScope1}>
<ScopedCounter />
</Provider>
<Provider scope={CounterScope2}>
<ScopedCounter />
</Provider>
</div>
);
};
export default App;
Accessing Jotai Atoms Outside Components (Final Section)
Sometimes, you might need to interact with atoms outside of React components—perhaps in utility functions or side effects. Using the getDefaultStore
method, you can directly get
or set
values in Jotai atoms.
Example: Using get
and set
Outside a Component
Set up a Jotai store:
// src/utils/jotaiStore.ts import { getDefaultStore } from 'jotai'; const jotaiStore = getDefaultStore(); export default jotaiStore;
Create a utility function that uses the store:
// src/utils/incrementCounter.ts import jotaiStore from './jotaiStore'; import { counterAtom } from '../atoms/counterAtom'; export function incrementCounter() { const currentCount = jotaiStore.get(counterAtom); jotaiStore.set(counterAtom, currentCount + 1); }
Using the function:
You can now call
incrementCounter()
from anywhere in your app to incrementcounterAtom
without directly involving React components.
Conclusion
With Jotai and TypeScript, you can build a finely-tuned state management layer that’s both minimal and powerful. This guide has covered the essentials, from basic and dependent atoms to asynchronous handling with loadable
and using atoms outside components. Now you’re equipped to harness Jotai’s flexibility for creating stateful, reactive apps in a way that’s both scalable and efficient.
Subscribe to my newsletter
Read articles from Pawan Gangwani directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Pawan Gangwani
Pawan Gangwani
I’m Pawan Gangwani, a passionate Full Stack Developer with over 12 years of experience in web development. Currently serving as a Lead Software Engineer at Lowes India, I specialize in modern web applications, particularly in React and performance optimization. I’m dedicated to best practices in coding, testing, and Agile methodologies. Outside of work, I enjoy table tennis, exploring new cuisines, and spending quality time with my family.