Mastering State Management in React with Jotai & TypeScript: A Comprehensive Guide

Pawan GangwaniPawan Gangwani
5 min read

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

  1. Set up a Jotai store:

     // src/utils/jotaiStore.ts
     import { getDefaultStore } from 'jotai';
    
     const jotaiStore = getDefaultStore();
     export default jotaiStore;
    
  2. 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);
     }
    
  3. Using the function:

    You can now call incrementCounter() from anywhere in your app to increment counterAtom 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.

0
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.