DEV Community

pgangwani
pgangwani

Posted on

Advanced State Management in React with Jotai (TypeScript)

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
  })
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)