npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

expo-masonry-layout

v1.2.0

Published

High-performance masonry layout component for React Native and Expo applications

Downloads

162

Readme

expo-masonry-layout

✨ Features

  • 🚀 High Performance: Uses VirtualizedList for optimal performance with large datasets
  • 📱 Responsive: Automatically adapts to screen size and orientation changes
  • 🎨 Flexible: Supports custom aspect ratios and layout configurations
  • 🔄 Interactive: Built-in pull-to-refresh and infinite scroll support
  • 📐 Smart Layout: Intelligent row-based masonry with justified alignment
  • 🎯 TypeScript: Full TypeScript support with comprehensive types
  • Optimized: Minimal re-renders with memoized calculations

🌟 Real-world Usage

This component is actively used in production by:

  • WiSaw - A location-based photo sharing mobile app that displays thousands of user-generated photos in a beautiful masonry layout. WiSaw demonstrates the component's ability to handle large datasets with smooth scrolling and optimal performance.

The screenshot above is taken directly from the WiSaw app, showcasing real-world usage with actual user photos.

🚀 Installation

npm install expo-masonry-layout
# or
yarn add expo-masonry-layout


## 📖 Quick Start

```tsx
import React from 'react';
import { View, Image, Text } from 'react-native';
import ExpoMasonryLayout from 'expo-masonry-layout';

const MyMasonryGrid = () => {
  const data = [
    { id: '1', uri: 'https://example.com/image1.jpg', width: 300, height: 400 },
    { id: '2', uri: 'https://example.com/image2.jpg', width: 400, height: 300 },
    { id: '3', uri: 'https://example.com/image3.jpg', width: 300, height: 300 },
    // ... more items
  ];

  const renderItem = ({ item, dimensions }) => (
    <View style={{ width: dimensions.width, height: dimensions.height }}>
      <Image
        source={{ uri: item.uri }}
        style={{ width: '100%', height: '100%' }}
        resizeMode="cover"
      />
    </View>
  );

  return (
    <ExpoMasonryLayout
      data={data}
      renderItem={renderItem}
      spacing={6}
      keyExtractor={item => item.id}
    />
  );
};

🖼️ Using with Expo Cached Image

For better performance with remote images, we recommend using expo-cached-image alongside the masonry layout:

npm install expo-cached-image
# or
yarn add expo-cached-image

Here's how to integrate it:

import React from 'react';
import { View, Dimensions } from 'react-native';
import ExpoMasonryLayout from 'expo-masonry-layout';
import { CachedImage } from 'expo-cached-image';

const CachedMasonryGrid = () => {
  const data = [
    { id: '1', uri: 'https://example.com/image1.jpg', width: 300, height: 400 },
    { id: '2', uri: 'https://example.com/image2.jpg', width: 400, height: 300 },
    { id: '3', uri: 'https://example.com/image3.jpg', width: 300, height: 300 }
    // ... more items
  ];

  const renderItem = ({ item, dimensions }) => (
    <View style={{ width: dimensions.width, height: dimensions.height }}>
      <CachedImage
        source={{ uri: item.uri }}
        style={{ width: '100%', height: '100%' }}
        resizeMode="cover"
        cacheKey={`masonry-${item.id}`} // Unique cache key
        placeholderContent={
          <View
            style={{
              flex: 1,
              backgroundColor: '#f0f0f0',
              justifyContent: 'center',
              alignItems: 'center'
            }}
          />
        }
      />
    </View>
  );

  return (
    <ExpoMasonryLayout
      data={data}
      renderItem={renderItem}
      spacing={6}
      keyExtractor={(item) => item.id}
    />
  );
};

Benefits of Using Expo Cached Image:

  • Automatic Caching: Images are cached locally after first load
  • Placeholder Support: Shows placeholder while loading
  • Better Performance: Reduces network requests for repeated views
  • Memory Management: Efficient image memory handling
  • Progressive Loading: Smooth loading experience

Performance Tips with Cached Images:

  1. Use Unique Cache Keys: Ensure each image has a unique cacheKey prop
  2. Optimize Image Sizes: Use appropriately sized images for your layout
  3. Implement Placeholders: Provide placeholder content for better UX
  4. Clear Cache When Needed: Implement cache clearing for updated content
// Example with cache management
import { CachedImage } from 'expo-cached-image';

const clearImageCache = async () => {
  await CachedImage.clearCache();
};

// Clear cache for specific images
const clearSpecificCache = async (imageId) => {
  await CachedImage.clearCache(`masonry-${imageId}`);
};

🔧 Advanced Usage

Here's a comprehensive example inspired by the WiSaw app implementation:

import React, { useState, useCallback } from 'react';
import { TouchableOpacity, Image, Text, View } from 'react-native';
import ExpoMasonryLayout, { MasonryRenderItemInfo } from 'expo-masonry-layout';

// Example data structure similar to WiSaw's photo feed
const PhotoMasonryGrid = () => {
  const [photos, setPhotos] = useState(initialPhotos);
  const [refreshing, setRefreshing] = useState(false);
  const [loading, setLoading] = useState(false);

  // Photo item renderer similar to WiSaw's implementation
  const renderPhotoItem = useCallback(
    ({ item, dimensions }: MasonryRenderItemInfo) => (
      <TouchableOpacity
        style={{
          width: dimensions.width,
          height: dimensions.height,
          borderRadius: 12,
          overflow: 'hidden',
          backgroundColor: '#f0f0f0',
          shadowColor: '#000',
          shadowOffset: { width: 0, height: 2 },
          shadowOpacity: 0.1,
          shadowRadius: 4,
          elevation: 3
        }}
        onPress={() => handlePhotoPress(item)}
        activeOpacity={0.9}
      >
        <Image
          source={{ uri: item.imageUrl }}
          style={{
            width: '100%',
            height: '85%'
          }}
          resizeMode="cover"
          loadingIndicatorSource={require('./placeholder.png')}
        />
        <View
          style={{
            position: 'absolute',
            bottom: 0,
            left: 0,
            right: 0,
            backgroundColor: 'rgba(0,0,0,0.7)',
            padding: 8
          }}
        >
          <Text
            style={{
              color: 'white',
              fontSize: 12,
              fontWeight: '600'
            }}
            numberOfLines={1}
          >
            📍 {item.location}
          </Text>
          <Text
            style={{
              color: 'rgba(255,255,255,0.8)',
              fontSize: 10,
              marginTop: 2
            }}
          >
            ❤️ {item.likes} • 👤 {item.username}
          </Text>
        </View>
      </TouchableOpacity>
    ),
    []
  );

  const handlePhotoPress = useCallback((photo) => {
    // Navigate to photo detail view (like in WiSaw)
    console.log('Photo pressed:', photo.id);
  }, []);

  const handleRefresh = useCallback(async () => {
    setRefreshing(true);
    try {
      // Fetch fresh photos from your API
      const freshPhotos = await fetchLatestPhotos();
      setPhotos(freshPhotos);
    } catch (error) {
      console.error('Error refreshing photos:', error);
    } finally {
      setRefreshing(false);
    }
  }, []);

  const handleLoadMore = useCallback(async () => {
    if (loading) return;

    setLoading(true);
    try {
      // Load more photos for infinite scroll
      const morePhotos = await fetchMorePhotos(photos.length);
      setPhotos((prevPhotos) => [...prevPhotos, ...morePhotos]);
    } catch (error) {
      console.error('Error loading more photos:', error);
    } finally {
      setLoading(false);
    }
  }, [photos.length, loading]);

  return (
    <ExpoMasonryLayout
      data={photos}
      renderItem={renderPhotoItem}
      spacing={8}
      maxItemsPerRow={2} // WiSaw uses 2 columns for optimal photo viewing
      baseHeight={200}
      keyExtractor={(item) => item.id}
      refreshing={refreshing}
      onRefresh={handleRefresh}
      onEndReached={handleLoadMore}
      onEndReachedThreshold={0.2}
      aspectRatioFallbacks={[0.7, 1.0, 1.3, 1.6]} // Common photo ratios
      style={{ backgroundColor: '#f8f9fa' }}
      contentContainerStyle={{ padding: 8 }}
      showsVerticalScrollIndicator={false}
      initialNumToRender={8}
      maxToRenderPerBatch={10}
      windowSize={15}
    />
  );
};

🔧 VirtualizedList Pass-Through

The component now supports passing any VirtualizedList prop directly to the underlying implementation. This gives you full control over scrolling behavior, performance tuning, and platform-specific features:

import React, { useCallback } from 'react';
import ExpoMasonryLayout from 'expo-masonry-layout';

const AdvancedMasonryGrid = () => {
  const handleScroll = useCallback((event) => {
    console.log('Scroll position:', event.nativeEvent.contentOffset.y);
  }, []);

  const handleScrollBeginDrag = useCallback(() => {
    console.log('User started scrolling');
  }, []);

  return (
    <ExpoMasonryLayout
      data={photos}
      renderItem={renderPhotoItem}
      spacing={8}
      maxItemsPerRow={2}

      {/* VirtualizedList props passed through */}
      onScroll={handleScroll}
      onScrollBeginDrag={handleScrollBeginDrag}
      scrollEventThrottle={16}
      showsVerticalScrollIndicator={true}
      bounces={true}
      scrollEnabled={true}
      nestedScrollEnabled={true} // Android
      maintainVisibleContentPosition={{
        minIndexForVisible: 0,
        autoscrollToTopThreshold: 100,
      }}

      {/* Performance tuning */}
      initialNumToRender={10}
      maxToRenderPerBatch={5}
      windowSize={10}
      removeClippedSubviews={true}
      updateCellsBatchingPeriod={50}

      {/* Infinite scroll */}
      onEndReached={loadMoreData}
      onEndReachedThreshold={0.2}

      {/* Pull to refresh */}
      refreshing={isRefreshing}
      onRefresh={handleRefresh}
    />
  );
};

📋 API Reference

Props

The component extends React Native's VirtualizedListProps and accepts all VirtualizedList properties in addition to the masonry-specific props below:

Masonry-Specific Props

| Prop | Type | Default | Description | | ------------------------ | --------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------ | | data | MasonryItem[] | required | Array of items to display | | renderItem | (info: MasonryRenderItemInfo) => ReactElement | required | Function to render each item | | spacing | number | 6 | Space between items in pixels | | maxItemsPerRow | number | 6 | Maximum number of items per row | | baseHeight | number | 100 | Base height for layout calculations | | aspectRatioFallbacks | number[] | [0.56, 0.67, 0.75, 1.0, 1.33, 1.5, 1.78] | Fallback aspect ratios | | preserveItemDimensions | boolean | false | Whether to respect exact item dimensions when provided | | getItemDimensions | (item: MasonryItem, index: number) => { width: number; height: number } \| null | undefined | Function to calculate custom dimensions for items | | keyExtractor | (item: MasonryItem, index: number) => string | (item, index) => item.id \|\| index | Extract unique key for each item |

VirtualizedList Props

All VirtualizedList props are supported and passed through to the underlying implementation, including:

  • Performance: initialNumToRender, maxToRenderPerBatch, windowSize, updateCellsBatchingPeriod, removeClippedSubviews
  • Scrolling: onScroll, onScrollBeginDrag, onScrollEndDrag, onMomentumScrollBegin, onMomentumScrollEnd, scrollEventThrottle
  • Interaction: onEndReached, onEndReachedThreshold, refreshing, onRefresh, scrollEnabled, bounces
  • Styling: style, contentContainerStyle, showsVerticalScrollIndicator
  • Platform: nestedScrollEnabled (Android), scrollIndicatorInsets (iOS)

🔷 Types

MasonryItem

interface MasonryItem {
  id: string;
  width?: number;
  height?: number;
  preserveDimensions?: boolean;
  [key: string]: any;
}

MasonryRenderItemInfo

interface MasonryRenderItemInfo {
  item: MasonryItem;
  index: number;
  dimensions: {
    width: number;
    height: number;
    left: number;
    top: number;
  };
}

� Custom Dimensions

The library supports multiple ways to override the automatic dimension calculation:

1. Per-Item Dimension Preservation

Set preserveDimensions: true on individual items to use their exact width and height:

const dataWithExactSizes = [
  {
    id: '1',
    width: 300,
    height: 200,
    preserveDimensions: true, // This item will be exactly 300x200
    imageUrl: 'https://example.com/image1.jpg'
  },
  {
    id: '2',
    width: 400,
    height: 300,
    // No preserveDimensions flag - will be auto-calculated
    imageUrl: 'https://example.com/image2.jpg'
  }
];

2. Global Dimension Preservation

Use the preserveItemDimensions prop to respect exact dimensions for all items that have width and height:

<ExpoMasonryLayout data={data} preserveItemDimensions={true} renderItem={renderItem} />

3. Custom Dimension Function

Use getItemDimensions for dynamic dimension calculation:

const getCustomDimensions = (item, index) => {
  // Make every 5th item extra wide
  if (index % 5 === 0) {
    return { width: 300, height: 150 };
  }

  // Featured items get special treatment
  if (item.featured) {
    return { width: 250, height: 200 };
  }

  // Return null for auto-calculation
  return null;
};

<ExpoMasonryLayout data={data} getItemDimensions={getCustomDimensions} renderItem={renderItem} />;

4. Mixed Layout Strategy

Combine all approaches for maximum flexibility:

const mixedData = [
  {
    id: '1',
    width: 200,
    height: 300,
    preserveDimensions: true // Exact size
  },
  {
    id: '2',
    featured: true // Will use getItemDimensions
  },
  {
    id: '3',
    width: 400,
    height: 300 // Will be auto-calculated unless preserveItemDimensions=true
  }
];

<ExpoMasonryLayout
  data={mixedData}
  preserveItemDimensions={false}
  getItemDimensions={(item, index) => {
    if (item.featured) return { width: 250, height: 200 };
    return null;
  }}
  renderItem={renderItem}
/>;

Priority Order:

  1. getItemDimensions function result (highest priority)
  2. preserveDimensions: true on item + item's width/height
  3. preserveItemDimensions: true prop + item's width/height
  4. Auto-calculated from aspect ratio (lowest priority)

�🎯 Performance Tips

  1. Provide Image Dimensions: Include width and height in your data items for optimal layout calculation
  2. Memoize Render Function: Use useCallback for your renderItem function
  3. Optimize Images: Use appropriate image sizes and consider lazy loading
  4. Key Extractor: Provide a stable keyExtractor function
  5. Batch Size: Adjust maxToRenderPerBatch based on your item complexity

🧮 Layout Algorithm

The component uses a sophisticated row-based masonry algorithm:

  1. Row Filling: Items are added to rows based on available width
  2. Height Normalization: All items in a row are scaled to the same height
  3. Width Justification: The entire row is scaled to fill the available width
  4. Vertical Positioning: Items are vertically centered within their row

This approach ensures:

  • Consistent row heights for smooth scrolling
  • Optimal use of screen space
  • Predictable layout behavior
  • Excellent performance with virtualization

📄 License

MIT

🤝 Contributing

Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.

📞 Support