Skip to main content

🧠 State Management

Shipnative employs a powerful and efficient state management strategy using a combination of Zustand for global client-side state and React Query for server-side state and data fetching. This approach provides a clear separation of concerns, optimizes performance, and simplifies data management in your application.

Zustand: Global Client-Side State

Zustand is a small, fast, and scalable bear-bones state-management solution. It’s ideal for managing UI state, user preferences, and any other client-side data that doesn’t require server interaction.

Key Features

  • Simple API: Easy to learn and use, with a minimal boilerplate.
  • Hooks-based: Integrates seamlessly with React’s functional components.
  • Performant: Renders components only when necessary, optimizing performance.
  • Scalable: Suitable for small to large applications.

Usage Pattern

  1. Define Your Store: Create a store using create from zustand.
    // apps/app/app/stores/counterStore.ts
    import { create } from 'zustand'
    
    interface CounterState {
      count: number
      increment: () => void
      decrement: () => void
    }
    
    export const useCounterStore = create<CounterState>((set) => ({
      count: 0,
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
    }))
    
  2. Use in Components: Access state and actions using the custom hook.
    // apps/app/app/screens/CounterScreen.tsx
    import { View, Text, Button } from 'react-native'
    import { useCounterStore } from '@/stores/counterStore'
    export const CounterScreen = () => {
      const { count, increment, decrement } = useCounterStore()
    
      return (
        <View>
          <Text>Count: {count}</Text>
          <Button title="Increment" onPress={increment} />
          <Button title="Decrement" onPress={decrement} />
        </View>
      )
    }
    

Best Practices with Zustand

  • Atomic Stores: Create small, focused stores for specific pieces of state.
  • Selectors: Use selectors to extract only the necessary parts of the state, preventing unnecessary re-renders.
  • Immutability: Always update state immutably (e.g., using spread syntax).

React Query: Server-Side State & Data Fetching

React Query (also known as TanStack Query) is a powerful library for managing, caching, synchronizing, and updating server state in your React Native applications. It handles the complexities of data fetching, leaving you with more time to focus on your UI.

Key Features

  • Automatic Caching: Caches fetched data, reducing redundant network requests.
  • Background Refetching: Automatically refetches stale data in the background.
  • Optimistic Updates: Provides a smooth user experience by updating the UI before the server responds.
  • Error Handling: Built-in mechanisms for handling and retrying failed requests.
  • Pagination & Infinite Scrolling: Simplifies complex data fetching patterns.

Usage Pattern

  1. Define Your Query Function: A simple async function that fetches data.
    // apps/app/app/services/api.ts
    import { supabase } from './supabase'
    
    export const fetchPosts = async () => {
      const { data, error } = await supabase.from('posts').select('*')
      if (error) throw error
      return data
    }
    
    export const addPost = async (newPost: { title: string; content: string }) => {
      const { data, error } = await supabase.from('posts').insert(newPost).select().single()
      if (error) throw error
      return data
    }
    
  2. Use in Components with useQuery or useMutation:
    // apps/app/app/screens/PostsScreen.tsx
    import { View, Text, FlatList, Button, ActivityIndicator } from 'react-native'
    import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
    import { fetchPosts, addPost } from '@/services/api'
    export const PostsScreen = () => {
      const queryClient = useQueryClient()
      const { data: posts, isLoading, isError, error } = useQuery({
        queryKey: ['posts'],
        queryFn: fetchPosts,
      })
    
      const addPostMutation = useMutation({
        mutationFn: addPost,
        onSuccess: () => {
          queryClient.invalidateQueries({ queryKey: ['posts'] }) // Refetch posts after successful addition
        },
      })
    
      if (isLoading) return <ActivityIndicator />
      if (isError) return <Text>Error: {error?.message}</Text>
    
      return (
        <View>
          <FlatList
            data={posts}
            keyExtractor={(item) => item.id.toString()}
            renderItem={({ item }) => <Text>{item.title}</Text>}
          />
          <Button
            title="Add New Post"
            onPress={() => addPostMutation.mutate({ title: 'New Post', content: 'Hello from React Query!' })}
            disabled={addPostMutation.isPending}
          />
        </View>
      )
    }
    

Best Practices with React Query

  • Query Keys: Use descriptive and consistent query keys for effective caching and invalidation.
  • Query Invalidation: Invalidate queries after mutations to ensure your UI reflects the latest server state.
  • Optimistic Updates: Implement optimistic updates for a snappier user experience, especially for actions like β€œliking” a post.
  • Error Boundaries: Use React Error Boundaries to gracefully handle data fetching errors.

Combining Zustand and React Query

Zustand and React Query complement each other perfectly:
  • Zustand: Manages transient UI state (e.g., modal open/close, form input values before submission, theme preferences).
  • React Query: Manages persistent server data (e.g., lists of items, user profiles fetched from an API, authentication status from a backend).
By separating these concerns, you achieve a cleaner, more performant, and easier-to-maintain codebase.

Resources