smithery.ai

navigation-patterns

WHAT: React Navigation patterns with useNavigationHeader and type-safe route enums.

First seen Apr 6, 2026

Installation

$ npx skills add https://smithery.ai

Summary

  • WHAT: React Navigation patterns with useNavigationHeader and type-safe route enums.
  • WHEN: configuring screen headers, creating header buttons, navigating with parameters.
  • KEYWORDS: navigation, useNavigationHeader, route enum, header, navigate, params, React Navigation, goBack, headerLeft.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 18,705 B
  • docs SUMMARY.md 316 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Navigation Patterns

Core Principles

Use useNavigationHeader for all header configuration. It automatically applies Zest theme styling (colors, fonts, spacing) and uses useLayoutEffect internally to prevent header flicker.

Define routes as enums for type safety. Enums prevent typos, enable IDE autocomplete, and make route refactoring safer.

Why: Consistent navigation patterns improve UX, ensure design system compliance, and provide compile-time type safety.

When to Use This Skill

Use these patterns when:

  • Configuring screen headers with titles and buttons
  • Creating custom header buttons (close, save, menu)
  • Navigating between screens with parameters
  • Defining routes for navigation stacks
  • Implementing conditional header UI based on state
  • Testing navigation behavior
  • Ensuring consistent theming across all headers

useNavigationHeader Hook

Basic Header Configuration

Use useNavigationHeader to configure screen headers with automatic Zest theme styling.

import { useNavigation } from '@libs/navigation';
import { useNavigationHeader } from '@libs/navigation-header';
import { useT9n } from '@libs/localization';

const ProductDetailsScreen = () => {
  const navigation = useNavigation();
  const { translateRaw } = useT9n('product-details');

  useNavigationHeader({
    navigation,
    options: {
      headerTitle: translateRaw('product-details.header.title'),
    },
  });

  return <View>{/* content */}</View>;
};

Why: useNavigationHeader automatically applies Zest theme styling and uses useLayoutEffect internally to prevent header flicker.

Production Example: git-resources/shared-mobile-modules/src/modules/store/screens/upsell/UpsellModule.tsx:71

Automatic Theme Styling

The hook applies default styling from Zest theme automatically:

// Default styles applied by useNavigationHeader (you don't write this)
const defaultOptions: NativeStackNavigationOptions = {
  headerShown: true,
  headerBackVisible: true,
  headerTitleAlign: 'left',
  headerBackButtonDisplayMode: 'minimal',
  headerTitleStyle: {
    fontFamily: theme.global.fontFamily.headline,
    fontSize: theme.global.fontSize.headline.headlineMd,
    color: theme.alias.color.neutral.foreground.inverse,
  },
  headerStyle: {
    backgroundColor: theme.alias.color.brand.background.default,
  },
  headerTintColor: theme.alias.color.neutral.foreground.inverse,
};

Why: Automatic theming ensures all headers follow the design system without manual color/font configuration.

Custom Header Title Component

Render custom header title component instead of string:

import { View } from 'react-native';
import { Text } from '@zest/react-native';

const SocialRecipeBridgeScreen = () => {
  const navigation = useNavigation();
  const { translateRaw } = useT9n('social-recipe-bridge');

  const renderHeader = useCallback(() => {
    return (
      <View style={styles.headerContainer}>
        <Text type="headline-lg" style={styles.headerTitle}>
          {translateRaw('social-recipe-bridge.screen.header.title')}
        </Text>
        <View style={styles.tag}>
          <Text type="body-xs-regular">Beta</Text>
        </View>
      </View>
    );
  }, [translateRaw, styles]);

  useNavigationHeader({
    navigation,
    options: {
      headerTitle: renderHeader,
    },
  });

  return <View>{/* content */}</View>;
};

Why: Custom header components enable complex layouts like badges, tags, or multi-line titles.

Production Example: git-resources/shared-mobile-modules/src/modules/social-recipe-bridge/screens/social-recipe-bridge/SocialRecipeBridgeScreen.tsx:195

Custom Header Buttons

Header Button Pattern

Create header buttons using TouchableOpacity with Zest Icons and memoize with useCallback.

import { TouchableOpacity } from 'react-native';
import { Icon } from '@zest/react-native';
import { useT9n } from '@libs/localization';
import { useNavigationHeader } from '@libs/navigation-header';

const SocialRecipeBridgeScreen = () => {
  const navigation = useNavigation();
  const { translateRaw } = useT9n('social-recipe-bridge');

  const handleClose = useCallback(() => {
    navigation.goBack();
  }, [navigation]);

  const renderCloseButton = useCallback(
    () => (
      <TouchableOpacity
        onPress={handleClose}
        testID="close-button"
        accessibilityRole="button"
        accessibilityLabel={translateRaw(
          'social-recipe-bridge.screen.header.close.alt_text'
        )}
      >
        <Icon
          icon="CloseOutline24"
          color="alias.color.neutral.foreground.inverse"
          altText={translateRaw(
            'social-recipe-bridge.screen.header.close.alt_text'
          )}
        />
      </TouchableOpacity>
    ),
    [handleClose, translateRaw]
  );

  useNavigationHeader({
    navigation,
    options: {
      headerTitle: translateRaw('social-recipe-bridge.screen.header.title'),
      headerLeft: renderCloseButton,
    },
  });

  return <View>{/* content */}</View>;
};

Why: useCallback memoization prevents unnecessary re-renders when navigation options update. Zest Icons ensure consistent iconography.

Production Example: git-resources/shared-mobile-modules/src/modules/social-recipe-bridge/screens/social-recipe-bridge/SocialRecipeBridgeScreen.tsx:156

Multiple Header Buttons

Add both left and right header buttons:

const SocialRecipeBridgeScreen = () => {
  const navigation = useNavigation();
  const { translateRaw } = useT9n('social-recipe-bridge');

  const handleClose = useCallback(() => {
    navigation.goBack();
  }, [navigation]);

  const handleMenuButtonPress = useCallback(() => {
    setIsMenuVisible(true);
  }, []);

  const renderCloseButton = useCallback(
    () => (
      <TouchableOpacity
        onPress={handleClose}
        testID="close-button"
        accessibilityRole="button"
        accessibilityLabel={translateRaw('...close.alt_text')}
      >
        <Icon
          icon="CloseOutline24"
          color="alias.color.neutral.foreground.inverse"
          altText={translateRaw('...close.alt_text')}
        />
      </TouchableOpacity>
    ),
    [handleClose, translateRaw]
  );

  const renderMenuButton = useCallback(
    () => (
      <TouchableOpacity
        onPress={handleMenuButtonPress}
        testID="menu-button"
        accessibilityRole="button"
        accessibilityLabel={translateRaw('...menu.alt_text')}
      >
        <Icon
          icon="EllipsesOutline24"
          color="alias.color.neutral.foreground.inverse"
          altText={translateRaw('...menu.alt_text')}
        />
      </TouchableOpacity>
    ),
    [handleMenuButtonPress, translateRaw]
  );

  useNavigationHeader({
    navigation,
    options: {
      headerTitle: translateRaw('screen.title'),
      headerLeft: renderCloseButton,
      headerRight: renderMenuButton,
    },
  });
};

Why: Separate useCallback functions for each button enable independent memoization and testing.

Production Example: git-resources/shared-mobile-modules/src/modules/social-recipe-bridge/screens/social-recipe-bridge/SocialRecipeBridgeScreen.tsx:175

Conditional Header Buttons

Disable or change header buttons based on component state:

const EditRecipeScreen = () => {
  const navigation = useNavigation();
  const [isFormValid, setIsFormValid] = useState(false);
  const { translateRaw } = useT9n('recipe');

  const handleSave = useCallback(() => {
    if (isFormValid) {
      saveRecipe();
    }
  }, [isFormValid, saveRecipe]);

  const renderSaveButton = useCallback(
    () => (
      <TouchableOpacity
        onPress={handleSave}
        testID="save-button"
        disabled={!isFormValid}
        accessibilityRole="button"
        accessibilityLabel={translateRaw('recipe.header.save.alt_text')}
      >
        <Icon
          icon="CheckmarkOutline24"
          color={
            isFormValid
              ? 'alias.color.neutral.foreground.inverse'
              : 'alias.color.neutral.foreground.disabled'
          }
          altText={translateRaw('recipe.header.save.alt_text')}
        />
      </TouchableOpacity>
    ),
    [handleSave, isFormValid, translateRaw]
  );

  useNavigationHeader({
    navigation,
    options: {
      headerTitle: translateRaw('recipe.header.title'),
      headerRight: renderSaveButton,
    },
  });

  return <View>{/* form */}</View>;
};

Why: Conditional styling and disabled state provide clear visual feedback about action availability.

Route Type Safety

Route Enums

Define route names as enums for type safety and autocomplete:

// modules/store/stacks/store/routes.ts
export enum StoreStackRoutes {
  Storefront = 'Storefront',
  Upsell = 'Upsell',
  ProductDetails = 'ProductDetails',
  Cart = 'Cart',
  OrderConfirmation = 'OrderConfirmation',
  Promotion = 'Promotion',
}

// modules/social-recipe-bridge/types.ts
export enum SocialRecipeBridgeStackRoutes {
  SocialRecipeBridge = 'SocialRecipeBridge',
  CookbookFaq = 'CookbookFaq',
  RecipeDetail = 'RecipeDetail',
  EditRecipe = 'EditRecipe',
}

Why: Enums prevent typos, enable IDE autocomplete, and make route refactoring safer.

Production Example: git-resources/shared-mobile-modules/src/modules/store/stacks/store/routes.ts:1

Typed Navigation

Use route enums with navigation for type-safe navigation calls:

import { useNavigation } from '@libs/navigation';
import { SocialRecipeBridgeStackRoutes } from '../../types';

const CookbookScreen = () => {
  const navigation = useNavigation();

  const handleLearnMorePress = useCallback(() => {
    navigation.navigate(SocialRecipeBridgeStackRoutes.CookbookFaq);
  }, [navigation]);

  return (
    <Button onPress={handleLearnMorePress}>
      Learn More
    </Button>
  );
};

Why: TypeScript catches invalid route names at compile time, preventing runtime navigation errors.

Production Example: git-resources/shared-mobile-modules/src/modules/social-recipe-bridge/screens/social-recipe-bridge/SocialRecipeBridgeScreen.tsx:133

Navigation with Parameters

Pass typed parameters when navigating:

import { StoreStackRoutes } from '@modules/store/stacks/store/routes';

const ProductList = () => {
  const navigation = useNavigation();

  const handleProductPress = useCallback(
    (productId: string, position: number) => {
      navigation.navigate(StoreStackRoutes.ProductDetails, {
        planId: planId,
        deliveryId: weekId,
        productId: productId,
        source: SOURCE.LIST,
        position,
        category: 'market',
      });
    },
    [navigation, planId, weekId]
  );

  return <ProductList onPress={handleProductPress} />;
};

Why: Typed parameters ensure required navigation data is provided and prevent runtime errors from missing parameters.

Production Example: git-resources/shared-mobile-modules/src/modules/store/screens/upsell/UpsellModule.tsx:120

Accessing Route Parameters

Use route params in the destination screen:

import { useRoute } from '@libs/navigation';
import type { StoreStackNavigationProp } from '@modules/store/stacks/store/types';

const ProductDetailsScreen = ({
  route: {
    params: { productId, planId, deliveryId, source, position, category },
  },
}: StoreStackNavigationProp<StoreStackRoutes.ProductDetails>) => {
  // Use typed parameters
  const product = useProductQuery(productId);

  return <ProductDetails product={product} />;
};

Why: Typed route props ensure type safety for screen parameters and document expected props.

Navigation Patterns

Going Back

Use navigation.goBack() for simple back navigation:

const DetailScreen = () => {
  const navigation = useNavigation();

  const handleClose = useCallback(() => {
    navigation.goBack();
  }, [navigation]);

  return <CloseButton onPress={handleClose} />;
};

Why: goBack() maintains navigation history and works with native back gestures.

Production Example: git-resources/shared-mobile-modules/src/modules/social-recipe-bridge/screens/social-recipe-bridge/SocialRecipeBridgeScreen.tsx:112

Nested Stack Navigation

Navigate within nested stacks using route enums:

// HomeStack includes Store screens via createStoreStackScreens
const HomeScreen = () => {
  const navigation = useNavigation();

  const handleShopPress = useCallback(() => {
    // Navigate to Store screen within Home stack
    navigation.navigate(StoreStackRoutes.Storefront, {
      categoryId: 'market',
      selectedWeek: currentWeek,
    });
  }, [navigation, currentWeek]);

  return <ShopButton onPress={handleShopPress} />;
};

Why: Nested navigation allows screens to be shared across multiple stacks while maintaining correct navigation state.

Navigation with Callbacks

Pass callback functions as route parameters for parent screen updates:

const RecipeListScreen = () => {
  const navigation = useNavigation();
  const { refetch } = useGetExternalRecipesInfinite({});

  const handleRecipePress = useCallback(
    (recipe: ExternalRecipeListItem) => {
      navigation.navigate(SocialRecipeBridgeStackRoutes.RecipeDetail, {
        recipeId: recipe.id,
        // Pass refetch so detail screen can update list after deletion
        onRecipeDeleted: refetch,
      });
    },
    [navigation, refetch]
  );

  return <RecipeList onPress={handleRecipePress} />;
};

Why: Callback parameters enable child screens to trigger parent screen updates (e.g., refetch after deletion).

Production Example: git-resources/shared-mobile-modules/src/modules/social-recipe-bridge/screens/social-recipe-bridge/SocialRecipeBridgeScreen.tsx:240

Testing Navigation

Mock Navigation in Tests

Mock navigation hooks for unit tests:

import { render, fireEvent } from '@testing-library/react-native';

const mockNavigation = {
  navigate: jest.fn(),
  goBack: jest.fn(),
  setOptions: jest.fn(),
};

jest.mock('@libs/navigation', () => ({
  useNavigation: () => mockNavigation,
}));

jest.mock('@libs/navigation-header', () => ({
  useNavigationHeader: jest.fn(),
}));

describe('ProductListScreen', () => {
  it('navigates to product details on press', () => {
    const { getByTestId } = render(<ProductListScreen />);

    fireEvent.press(getByTestId('product-card-123'));

    expect(mockNavigation.navigate).toHaveBeenCalledWith(
      StoreStackRoutes.ProductDetails,
      expect.objectContaining({
        productId: '123',
      })
    );
  });
});

Why: Mocking navigation enables testing navigation logic without actual navigation behavior.

Common Mistakes to Avoid

Don't hardcode theme values:

// ❌ Wrong - Hardcoded colors/fonts break theming
navigation.setOptions({
  headerTitleStyle: {
    color: '#FFFFFF',
    fontSize: 18,
    fontFamily: 'Inter-Bold',
  },
});

Do use useNavigationHeader:

// ✅ Correct - Theme values applied automatically
useNavigationHeader({
  navigation,
  options: {
    headerTitle: translateRaw('screen.title'),
  },
});

Don't forget to memoize header buttons:

// ❌ Wrong - Recreates function on every render
const renderButton = () => (
  <TouchableOpacity onPress={handlePress}>
    <Icon icon="PlusOutline24" />
  </TouchableOpacity>
);

Do memoize with useCallback:

// ✅ Correct - Stable reference across renders
const renderButton = useCallback(
  () => (
    <TouchableOpacity onPress={handlePress}>
      <Icon icon="PlusOutline24" />
    </TouchableOpacity>
  ),
  [handlePress]
);

Don't use string literals for routes:

// ❌ Wrong - No type safety
navigation.navigate('ProductDetails', { id: '123' });

Do use route enums:

// ✅ Correct - Type-safe, autocomplete, refactorable
navigation.navigate(StoreStackRoutes.ProductDetails, {
  productId: '123',
});

Don't use useEffect for navigation options:

// ❌ Wrong - Can cause header flicker
useEffect(() => {
  navigation.setOptions({ title: 'Products' });
}, [navigation]);

Do use useNavigationHeader:

// ✅ Correct - useLayoutEffect runs before paint
useNavigationHeader({
  navigation,
  options: {
    headerTitle: translateRaw('products.title'),
  },
});

Don't miss accessibility props:

// ❌ Wrong - Missing accessibility
<TouchableOpacity onPress={handleClose}>
  <Icon icon="CloseOutline24" />
</TouchableOpacity>

Do add accessibility:

// ✅ Correct - Complete accessibility
<TouchableOpacity
  onPress={handleClose}
  testID="close-button"
  accessibilityRole="button"
  accessibilityLabel={translateRaw('close.alt_text')}
>
  <Icon
    icon="CloseOutline24"
    color="alias.color.neutral.foreground.inverse"
    altText={translateRaw('close.alt_text')}
  />
</TouchableOpacity>

Quick Reference

Basic header configuration:

useNavigationHeader({
  navigation,
  options: {
    headerTitle: translateRaw('screen.title'),
  },
});

Header buttons:

const renderButton = useCallback(
  () => (
    <TouchableOpacity onPress={handlePress}>
      <Icon icon="PlusOutline24" />
    </TouchableOpacity>
  ),
  [handlePress]
);

useNavigationHeader({
  navigation,
  options: {
    headerLeft: renderButton,
    headerRight: renderAnotherButton,
  },
});

Route enums:

export enum MyStackRoutes {
  ScreenOne = 'ScreenOne',
  ScreenTwo = 'ScreenTwo',
}

Navigate with params:

navigation.navigate(MyStackRoutes.ScreenTwo, {
  id: '123',
  source: 'list',
});

Access params:

const MyScreen = ({
  route: { params: { id, source } },
}: MyStackNavigationProp<MyStackRoutes.ScreenTwo>) => {
  // Use id, source
};

Go back:

const handleClose = useCallback(() => {
  navigation.goBack();
}, [navigation]);

Key Libraries:

  • React Navigation 7.0.13
  • React Native 0.75.4
  • @zest/react-native 1.3.1

For production examples, see [references/examples.md](references/examples.md).