Skip to content

echowaves/expo-masonry-layout

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

20 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

expo-masonry-layout

Expo Masonry Layout Demo

A high-performance masonry layout component for React Native and Expo applications

โœจ Used in production by WiSaw - a location-based photo sharing app

npm version npm downloads license

โœจ 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
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;
  [key: string]: any;
}

MasonryRenderItemInfo

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

๐ŸŽฏ 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


Made with โค๏ธ by Echowaves Corp.
Powering beautiful photo experiences in WiSaw and beyond

About

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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published