Skip to content

NativeWind + Zustand + React Query

Here’s a production-ready setup for combining Expo + Bun with:

  • 🎨 NativeWind (styling)
  • 🧠 Zustand (state)
  • ⚑ TanStack Query (API + caching)

This is structured exactly how senior teams ship scalable apps.


Terminal window
bun add nativewind react-native-reanimated react-native-safe-area-context
bun add zustand @tanstack/react-query axios
bun add -d tailwindcss

Then:

Terminal window
bunx tailwindcss init

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,tsx}",
"./src/**/*.{js,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};

module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['nativewind/babel'],
};
};

import { View, Text } from 'react-native';
export default function Home() {
return (
<View className="flex-1 items-center justify-center bg-white">
<Text className="text-xl font-bold text-blue-500">
Hello NativeWind πŸ‘‹
</Text>
</View>
);
}

import { create } from 'zustand';
type AuthState = {
user: null | { id: string; name: string };
setUser: (user: AuthState['user']) => void;
logout: () => void;
};
export const useAuthStore = create<AuthState>((set) => ({
user: null,
setUser: (user) => set({ user }),
logout: () => set({ user: null }),
}));

import { useAuthStore } from '@/store/authStore';
const user = useAuthStore((state) => state.user);

import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
staleTime: 1000 * 60 * 5, // 5 mins
},
},
});

import axios from 'axios';
export const apiClient = axios.create({
baseURL: 'https://api.example.com',
timeout: 10000,
});
apiClient.interceptors.request.use((config) => {
// attach token here
return config;
});

import { apiClient } from '@/services/api/client';
export const getUser = async () => {
const { data } = await apiClient.get('/user');
return data;
};

import { useQuery } from '@tanstack/react-query';
import { getUser } from './api';
export const useUser = () => {
return useQuery({
queryKey: ['user'],
queryFn: getUser,
});
};

import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from '@/lib/react-query';
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Stack />
</QueryClientProvider>
);
}

import { View, Text, Button } from 'react-native';
import { useUser } from '@/features/user/hooks';
import { useAuthStore } from '@/store/authStore';
export default function Profile() {
const { data, isLoading } = useUser();
const setUser = useAuthStore((s) => s.setUser);
if (isLoading) return <Text>Loading...</Text>;
return (
<View className="flex-1 p-4 bg-white">
<Text className="text-xl font-bold">
{data?.name}
</Text>
<Button
title="Save to Store"
onPress={() => setUser(data)}
/>
</View>
);
}

βš™οΈ 7. Recommended Add-ons (Optional but Pro)

Section titled β€œβš™οΈ 7. Recommended Add-ons (Optional but Pro)”
PurposeLibrary
Formsreact-hook-form + zod
Storageexpo-secure-store / mmkv
Icons@expo/vector-icons
Animationsreact-native-reanimated
Environmentexpo-constants

Terminal window
bun install
bunx expo start

  • Keep API logic inside features, not global
  • Use React Query for server state ONLY
  • Use Zustand for UI/global state
  • Don’t overuse global store β†’ prefer feature isolation
  • Add error boundaries + retry UI early

If you want next step, I can build you:

  • πŸ”₯ A complete starter repo (copy-paste ready)
  • 🧠 Auth system (JWT + refresh + secure storage)
  • πŸ“¦ Offline-first setup (React Query persistence + caching)
  • πŸš€ CI/CD with Expo EAS + environments