← Back to all posts

Jotai: The Only State Management Library You Need for React Native

EhsanBy Ehsan
11 min read
React NativeState ManagementJotaiAtomic State ManagementMMKVPerformance OptimizationReact HooksMobile Development

Introduction

State management in React Native has always been messy. Redux requires too much boilerplate. Context API causes unnecessary re-renders. MobX feels like magic until it doesn't. Zustand is good, but still not quite right.

Then I found Jotai, and I haven't looked back.

Jotai is an atomic state management library that's simple, performant, and actually fun to use. Built on top of React Hooks, it feels natural if you're already familiar with useState and useEffect. I've been using it over the last couple of months and I can confidently say: this is the only state management solution you need.

Let me show you why.

What Is Jotai?

Jotai (Japanese for "state") is a primitive and flexible state management library. Instead of managing a giant global state tree like Redux, you work with independent atoms—small pieces of state that components can subscribe to.

import { atom, useAtom } from 'jotai';

// Create an atom
const playCountAtom = atom(0);

// Use it in a component
function MusicPlayer() {
  const [playCount, setPlayCount] = useAtom(playCountAtom);

  return (
    <View>
      <Text>Plays: {playCount}</Text>
      <Button onPress={() => setPlayCount(c => c + 1)} title="Play" />
    </View>
  );
}

Look at that. No providers (well, technically optional), no boilerplate, no action creators, no reducers. Just atoms and hooks. It feels like useState, but works globally across your app.

Why Jotai Beats Everything Else

I've used every popular state management solution in React Native. Here's why Jotai wins.

1. Zero Boilerplate

Redux:

// Action types
const INCREMENT = 'INCREMENT';

// Action creators
const increment = () => ({ type: INCREMENT });

// Reducer
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    default:
      return state;
  }
};

// Store setup
const store = createStore(counterReducer);

// Usage in component
const count = useSelector(state => state);
const dispatch = useDispatch();
dispatch(increment());

Jotai:

const countAtom = atom(0);

// Usage in component
const [count, setCount] = useAtom(countAtom);
setCount(c => c + 1);

The difference is night and day. Jotai gets out of your way and lets you write code.

2. No Unnecessary Re-renders

Context API has a fundamental problem: when context value changes, every component using that context re-renders, even if they only care about a small piece of the state.

// Context API problem
const AppContext = createContext();

function Parent() {
  const [user, setUser] = useState({});
  const [settings, setSettings] = useState({});

  return (
    <AppContext.Provider value={{ user, settings, setUser, setSettings }}>
      <UserProfile />  {/* Re-renders when settings change */}
      <SettingsPanel /> {/* Re-renders when user changes */}
    </AppContext.Provider>
  );
}

With Jotai, components only re-render when the specific atoms they subscribe to change.

// Jotai solution
const userAtom = atom({});
const settingsAtom = atom({});

function UserProfile() {
  const [user] = useAtom(userAtom); // Only re-renders when user changes
  return <Text>{user.name}</Text>;
}

function SettingsPanel() {
  const [settings] = useAtom(settingsAtom); // Only re-renders when settings change
  return <Text>{settings.theme}</Text>;
}

Performance is built-in. You don't have to think about it.

3. Derived State Is Beautiful

One of Jotai's best features is derived atoms. You can create atoms that compute values based on other atoms.

const songsAtom = atom([
  { title: "Song A", duration: 180 },
  { title: "Song B", duration: 240 },
  { title: "Song C", duration: 200 },
]);

// Derived atom - automatically updates when songsAtom changes
const totalDurationAtom = atom((get) => {
  const songs = get(songsAtom);
  return songs.reduce((sum, song) => sum + song.duration, 0);
});

const songCountAtom = atom((get) => get(songsAtom).length);

// Use them like normal atoms
function PlaylistStats() {
  const [totalDuration] = useAtom(totalDurationAtom);
  const [songCount] = useAtom(songCountAtom);

  return (
    <View>
      <Text>{songCount} songs</Text>
      <Text>{Math.floor(totalDuration / 60)} minutes total</Text>
    </View>
  );
}

Derived atoms automatically recompute when dependencies change. No selectors, no memoization headaches—it just works.

4. Write Atoms Are Powerful

You can create atoms with custom write logic. This is perfect for complex state updates.

const playlistAtom = atom([]);

// Write atom - custom logic for adding songs
const addSongAtom = atom(
  null, // read function (not used)
  (get, set, newSong) => {
    const playlist = get(playlistAtom);
    // Don't add duplicates
    if (!playlist.find(s => s.id === newSong.id)) {
      set(playlistAtom, [...playlist, newSong]);
    }
  }
);

// Usage
function AddSongButton({ song }) {
  const [, addSong] = useAtom(addSongAtom);

  return (
    <Button onPress={() => addSong(song)} title="Add to Playlist" />
  );
}

Write atoms encapsulate logic and keep your components clean.

5. Async Is First-Class

Handling async operations in Redux requires middleware like Redux Thunk or Redux Saga. In Jotai, async just works. For more advanced server state management with caching and background refetching, you might also want to explore React Query.

const userAtom = atom(async () => {
  const response = await fetch('https://api.example.com/user');
  return response.json();
});

// Use it like any other atom
function UserProfile() {
  const [user] = useAtom(userAtom);

  return <Text>Welcome, {user.name}</Text>;
}

You can also create async write atoms for side effects:

const saveSettingsAtom = atom(
  null,
  async (get, set, newSettings) => {
    set(settingsAtom, newSettings);
    await fetch('https://api.example.com/settings', {
      method: 'POST',
      body: JSON.stringify(newSettings),
    });
  }
);

No extra libraries, no middleware configuration. Just write async functions.

6. TypeScript Support Is Excellent

Jotai is written in TypeScript, and the type inference is fantastic.

interface Song {
  id: string;
  title: string;
  artist: string;
  duration: number;
}

const songsAtom = atom<Song[]>([]);

// TypeScript knows the type automatically
const songTitlesAtom = atom((get) => {
  const songs = get(songsAtom); // Type: Song[]
  return songs.map(s => s.title); // Type: string[]
});

Types just work. No fighting with the type system.

Real-World Example: Music Player State

Here's how I structure state for a music player app with Jotai:

import { atom } from 'jotai';

// Base atoms
const currentSongAtom = atom(null);
const isPlayingAtom = atom(false);
const volumeAtom = atom(0.8);
const playlistAtom = atom([]);
const currentTimeAtom = atom(0);

// Derived atoms
const currentSongDurationAtom = atom((get) => {
  const song = get(currentSongAtom);
  return song?.duration || 0;
});

const progressPercentageAtom = atom((get) => {
  const currentTime = get(currentTimeAtom);
  const duration = get(currentSongDurationAtom);
  return duration > 0 ? (currentTime / duration) * 100 : 0;
});

const nextSongAtom = atom((get) => {
  const playlist = get(playlistAtom);
  const currentSong = get(currentSongAtom);
  const currentIndex = playlist.findIndex(s => s.id === currentSong?.id);
  return playlist[currentIndex + 1] || null;
});

// Write atoms for actions
const playNextSongAtom = atom(
  null,
  (get, set) => {
    const nextSong = get(nextSongAtom);
    if (nextSong) {
      set(currentSongAtom, nextSong);
      set(currentTimeAtom, 0);
      set(isPlayingAtom, true);
    }
  }
);

const togglePlayAtom = atom(
  null,
  (get, set) => {
    set(isPlayingAtom, !get(isPlayingAtom));
  }
);

Look how clean that is. Each piece of state is independent, but they compose together perfectly.

Persistence with MMKV

One missing piece in Jotai is built-in persistence. But there's a perfect solution: react-native-mmkv.

MMKV is a fast, lightweight storage library. It's significantly faster than AsyncStorage and perfect for storing Jotai atoms.

Setting Up MMKV

npm install react-native-mmkv
# or
yarn add react-native-mmkv

For iOS, install pods:

cd ios && pod install

Creating Persistent Atoms

Here's how to make atoms persist to storage:

import { atom } from 'jotai';
import { MMKV } from 'react-native-mmkv';

const storage = new MMKV();

// Helper to create persistent atoms
function atomWithMMKV(key, initialValue) {
  // Try to get stored value
  const storedValue = storage.getString(key);

  const baseAtom = atom(
    storedValue ? JSON.parse(storedValue) : initialValue
  );

  return atom(
    (get) => get(baseAtom),
    (get, set, newValue) => {
      set(baseAtom, newValue);
      storage.set(key, JSON.stringify(newValue));
    }
  );
}

// Usage
const userPreferencesAtom = atomWithMMKV('userPreferences', {
  theme: 'dark',
  notifications: true,
});

const playlistAtom = atomWithMMKV('playlist', []);

Now your atoms automatically persist to storage. When the app restarts, the state is restored.

Why MMKV Over AsyncStorage

MMKV is way faster than AsyncStorage:

MMKV read:  0.003ms
AsyncStorage read: 2.5ms

MMKV write: 0.005ms
AsyncStorage write: 3.2ms

That's 500-800x faster. For state management where you might save on every change, this matters.

MMKV is also synchronous, which makes the code simpler:

// AsyncStorage (async, annoying)
const value = await AsyncStorage.getItem('key');

// MMKV (sync, clean)
const value = storage.getString('key');

The Complete Setup

Here's my standard Jotai + MMKV setup for React Native apps:

1. Install dependencies

npm install [email protected] [email protected]
cd ios && pod install

2. Create storage utility

// lib/storage.js
import { atom } from 'jotai';
import { MMKV } from 'react-native-mmkv';

export const storage = new MMKV();

export function atomWithMMKV(key, initialValue) {
  const storedValue = storage.getString(key);

  const baseAtom = atom(
    storedValue ? JSON.parse(storedValue) : initialValue
  );

  return atom(
    (get) => get(baseAtom),
    (get, set, newValue) => {
      set(baseAtom, newValue);
      storage.set(key, JSON.stringify(newValue));
    }
  );
}

3. Create your atoms

// atoms/index.js
import { atom } from 'jotai';
import { atomWithMMKV } from '../lib/storage';

// Non-persistent atoms (temporary state)
export const currentSongAtom = atom(null);
export const isPlayingAtom = atom(false);
export const currentTimeAtom = atom(0);

// Persistent atoms (saved to storage)
export const userAtom = atomWithMMKV('user', null);
export const settingsAtom = atomWithMMKV('settings', {
  theme: 'dark',
  notifications: true,
});
export const playlistAtom = atomWithMMKV('playlist', []);

4. Use in components

// components/MusicPlayer.js
import { useAtom } from 'jotai';
import { currentSongAtom, isPlayingAtom, playlistAtom } from '../atoms';

function MusicPlayer() {
  const [currentSong] = useAtom(currentSongAtom);
  const [isPlaying, setIsPlaying] = useAtom(isPlayingAtom);
  const [playlist] = useAtom(playlistAtom);

  return (
    <View>
      <Text>{currentSong?.title || 'No song playing'}</Text>
      <Text>Playlist: {playlist.length} songs</Text>
      <Button
        onPress={() => setIsPlaying(!isPlaying)}
        title={isPlaying ? 'Pause' : 'Play'}
      />
    </View>
  );
}

That's it. Clean, simple, performant.

When Jotai Might Not Be Enough

I want to be honest—Jotai isn't always the answer. There are cases where you might need something else:

You're building a huge enterprise app:

  • Redux with Redux Toolkit might be better for massive state trees
  • You might need strict patterns and middleware
  • Time-travel debugging could be valuable

You need DevTools:

  • Redux DevTools are more mature

You're primarily dealing with server state:

But for client state, Jotai is perfect for 99% of React Native apps. It's fast, simple, and gets out of your way. You can even combine it with React Query—use Jotai for client state and React Query for server state.

Why I'll Never Go Back

After using Jotai in production, I can't imagine going back to Redux or Context API:

It's faster to write - No boilerplate means I ship features faster

It's easier to understand - New developers on my team pick it up immediately

It's more performant - Atomic subscriptions mean fewer re-renders

It scales well - I've used it in small apps and large ones. It handles both perfectly

It just feels right - Working with Jotai is actually enjoyable

Getting Started

If you're starting a new React Native project, try Jotai. Here's the minimal setup:

# Install Jotai
npm install jotai

# Install MMKV for persistence
npm install react-native-mmkv
cd ios && pod install

Create a few atoms, use them in your components, and see how it feels. I bet you'll love it as much as I do.

Final Thoughts

State management doesn't have to be complicated. You don't need reducers, action creators, middleware, or complex patterns.

Jotai gives you simple, atomic state that works exactly how you'd expect. Add MMKV for persistence, and you have a complete state management solution that's fast, lightweight, and a pleasure to use.

I've tried everything else. This is what I'm sticking with.

Frequently Asked Questions

Jotai vs Redux: Which should I choose?

Choose Jotai for simpler apps with less boilerplate. Choose Redux if you need Redux DevTools, time-travel debugging, or your team already knows Redux well. For most React Native apps, Jotai is faster to work with.

Can I use Jotai with React Query?

Yes! Use Jotai for client state (UI state, user preferences) and React Query for server state (API data, caching). They complement each other perfectly and work great together.

How does Jotai compare to Zustand?

Both are excellent. Jotai uses atomic state (many small atoms), Zustand uses store-based state (one or more stores). Jotai has better React integration and automatic re-render optimization. Choose based on preference—both are lightweight and fast.

Does Jotai affect performance?

No, it improves performance. Jotai's atomic subscriptions mean components only re-render when their specific atoms change, not when any state changes. This is more efficient than Context API or basic Redux setups.

How do I debug Jotai atoms?

Install the Jotai DevTools extension or use console.log in atom updates. You can also use React DevTools to inspect atom values. Atoms are just React state under the hood, so standard debugging techniques work.

Resources