ClaudeExpoMar 31, 2026, 1:30 PM

How Fig keeps millions eating safely with a five-engineer team and Expo

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

claudeenmodel: claude-sonnet-4-20250514

Fig's Mobile App Success with Expo SDK and Five-Engineer Team

Key Points

  • Five-engineer team serves millions of users with Expo SDK
  • Migrated from community packages to Expo for better reliability
  • EAS Updates replaced AppCenter for critical patch deployment

Summary

Fig, a dietary restriction and allergy management app serving millions of users, demonstrates how a small five-engineer team leverages Expo SDK and EAS Updates to build and maintain a high-performance React Native application. The team migrated from standard React Native to increasingly adopt Expo tools for improved reliability and faster development cycles.

Key Points

  • expo-image: Migrated from react-native-svg to expo-image for static icon rendering, improving performance by letting native libraries handle SVG rendering instead of creating component trees
  • expo-location: Replaced @react-native-community/geolocation with expo-location for restaurant search functionality, implementing dual coordinate fetching (approximate and accurate) with intelligent caching
  • expo-store-review: Quickly replaced broken react-native-in-app-review when iOS 18 deprecated SKStoreReviewController, ensuring continued app store rating functionality
  • EAS Updates: Transitioned from Microsoft's deprecated AppCenter to Expo's OTA update system for critical JavaScript patches and experimental feature testing
  • Maintenance Benefits: React Native version upgrades became significantly easier and more reliable as the team adopted more Expo packages

Full Translation

Translations

A translation section that keeps the flow of the original article.

claudejamodel: claude-sonnet-4-20250514

Figが5人のエンジニアチームとExpoで数百万人の食の安全を守る方法

Figが5人のエンジニアチームとExpoで数百万人の食の安全を守る方法

ユーザー • 2026年3月31日 • 5分で読める

Jake Lynch ゲスト著者
Taylor Harrison ゲスト著者

Figは食事制限のある数百万人が安全な食品を見つけるのを支援しています。5人のチームがExpo SDKとEAS Updateを使用して、確実に配信し、迅速に開発を進める方法をご紹介します。

Fig(Food is Good)は、あらゆる食事制限やアレルギーを持つ人々が実際に食べられる食品を見つけ、健康に深刻な影響を与える可能性のある反応を避けるのを支援しています。成分をスキャンして安全な製品を発見することから、レストランが特定のニーズにどの程度対応しているかを確認することまで、Figは複雑な食事ニーズを持つ人々にとって食事をより簡単で安全にします。

数百万人(Figチームのほとんどを含む!)がFigを頼りに安全で確信の持てる食品選択を行っています。昨年、私たちはExpo App Awardのファイナリストになりました。Expoチームは私たちのアプリのネイティブな感触とスナッピーな機能性を評価してくれたので、このブログではそれについて説明します。

なぜExpoなのか?

Figは標準的なReact Nativeアプリとして始まりましたが、開発を加速し信頼性を向上させるためにExpoを徐々に採用してきました。私たちは5人のエンジニアからなる小さなチームなので、複雑さに時間を無駄にする余裕はありません。

壊れて保守されなくなったコミュニティパッケージに何度か痛い目に遭った後、時間をかけてSDKを統合することにしました。私たちはライブラリを信頼するようになり、将来的にはさらに多くを統合する予定です。

昨年MicrosoftがAppCenterを廃止した際、重要なJavaScriptパッチのためのover-the-airアップデートもExpoのEASシステムに移行しました。

Expo SDKがFigの成功に不可欠だった理由

Figが小さなチームで迅速にパフォーマンスの高いアプリを開発するのに役立ったExpo SDKパッケージの例をいくつか紹介します:

expo-image

expo-imageにより、Figは画像とSVGを効率的にレンダリングして、コミュニティメンバーにとってアプリ体験を親しみやすくできます。

標準のReact Native ImageコンポーネントはSVGをサポートしていないため、最初はすべてのSVGレンダリングニーズ、特にアイコンにreact-native-svgを使用していました。SVG文字列をSvgXmlに渡すことで、stroke、stroke-width、fillなどの属性の設定が非常に簡単になりました。

expo-imageでSVGをレンダリングするFigの設定画面

しかし、Software Mansionのこのブログ投稿を読んだ後、SVGからコンポーネントツリーを作成することにはコストがかかり、静的アイコンには過剰であることがわかりました。

アイコンをexpo-imageに移行してネイティブライブラリがSVGレンダリングを処理できるようにし、SVG内部を操作/アニメーション化する必要がある場合のみreact-native-svgを使用するようにしました。

expo-imageのSVG変更機能は限定的(tintColorはすべての非透明ピクセルの色を設定)なので、stroke-widthなどを変更する必要がある場合はいくつかのSVGを追加でバンドルすることになりますが、パフォーマンスのトレードオフは価値があると考えています。

react-native-svg:

// svgを文字列としてインポート
import thumbsUpIcon from '@assets/icons/thumbs-up.svg';
import { iconDefault } from '@utils/colors.util';

const Icon: FunctionComponent<IconProps> = ({
  iconSize,
  color,
  rotation,
  transform,
  height,
  width,
  ...otherProps
}) => {
  const parsedIconSize = !height && !width ? getIconSize(iconSize) : undefined;
  const sizeProps: { height?: NumberProp; width?: NumberProp } = {};
  
  if (parsedIconSize || height) {
    sizeProps.height = parsedIconSize ?? height;
  }
  if (parsedIconSize || width) {
    sizeProps.width = parsedIconSize ?? width;
  }
  
  // svg文字列はotherPropsの`xml`を通じて渡される
  return (
    <SvgXml
      color={color ?? iconDefault}
      transform={rotation ? [{ rotate: `${rotation}deg` }] : transform}
      {...sizeProps}
      {...otherProps}
    />
  );
};

expo-image:

import { Image } from 'expo-image';
import styled, { css } from 'styled-components/native';

// アイコンは他の静的アセットと同様にassetsフォルダから`require`される
export const iconAssets = {
  thumbsUp: require('@assets/icons/thumbs-up.svg'),
  ...
} as const;

export const StyledImage = styled(Image)<{
  width?: number;
  height?: number;
  rotation?: number;
}>`
  ${({ width }) => width !== undefined && css`width: ${width}px;`}
  ${({ height }) => height !== undefined && css`height: ${height}px;`}
  ${({ rotation }) => !!rotation && css`transform: rotate(${rotation}deg);`}
`;

const Icon: FunctionComponent<IconProps> = ({
  iconSource: iconName,
  iconSize,
  height,
  width,
  rotation,
  color,
  ...otherProps
}) => {
  const parsedIconSize = !height && !width ? getIconSize(iconSize) : undefined;
  
  // アイコンはiconAssetsキーで参照され、ネイティブ画像ライブラリがレンダリングを処理
  return (
    <StyledImage
      tintColor={color}
      source={typeof iconName === 'object' ? iconName : iconAssets[iconName]}
      width={parsedIconSize ?? width}
      height={parsedIconSize ?? height}
      rotation={rotation}
      contentFit="contain"
      {...otherProps}
    />
  );
};

expo-location

Figはレストラン検索機能の位置データ取得にexpo-locationを使用しています。より積極的に保守されているライブラリから位置データをより細かく制御するために、@react-native-community/geolocationから移行しました。

  • 概算座標とより正確な座標を同時に取得
  • 十分に新しい場合はキャッシュされた位置データを利用
  • これにより、マップビューとレストラン検索に近くのレストランを迅速に表示できる

位置情報はコンテキストに保存され、アプリ全体で使用されます。特に新しいレストラン検索機能で活用されています。

ユーザーの位置情報を要求するFigの画面 | 位置情報が有効になった後の対応するMapBoxビュー

import * as Location from 'expo-location';

// キャッシュされた位置の最大有効期間(ミリ秒)
const locationMaximumAge = 3 * 60 * 1000; // 3分
const locationRequiredAccuracy = 11; // メートル、LocationAccuracy.Highのすぐ上
const approximateLocationRequiredAccuracy = 3001; // メートル、LocationAccuracy.Lowestのすぐ上

export const LocationProvider: FunctionComponent<PropsWithChildren<LocationProviderProps>> = ({ children }) => {
  const [coordinates, setCoordinates] = useState<Coordinates>();
  const [approximateCoordinates, setApproximateCoordinates] = useState<Coordinates>();
  
  // 代替として、expo-locationメソッドを使用して許可を要求できます
  const {
    permissionStatus: locationPermissionStatus,
    triggerRequestPermission: triggerRequestLocationPermission,
  } = usePermission('location');
  
  const fetchCoordinates = useCallback(async (
    setNewCoords: (coords: Coordinates) => void,
    requiredAccuracy: number,
    locationAccuracy: Location.LocationAccuracy,
    maxAge?: number,
  ) => {
    try {
      // まずキャッシュされた位置を使用しようとする
      let location = await Location.getLastKnownPositionAsync({
        requiredAccuracy,
        maxAge,
      });
      
      if (!location) {
        // キャッシュされた位置がない場合、現在位置を取得(時間がかかる場合がある)
        location = await Location.getCurrentPositionAsync({
          accuracy: locationAccuracy,
        });
      }
      
      const newCoordinates = location?.coords;
      setNewCoords(newCoordinates);
      return newCoordinates;
    } catch (e) {
      logFigError('Error getting location', {
        error: e,
      });
    }
  }, [mockLatitude, mockLongitude]);
  
  const updateLocation = useCallback(async () => {
    const [, newCoordinates] = await Promise.all([
      // より高速な読み込みのために概算座標を取得
      fetchCoordinates(
        setApproximateCoordinates,
        approximateLocationRequiredAccuracy,
        Location.LocationAccuracy.Lowest,
      ),
      // より良い精度のためにより正確な座標を取得
      fetchCoordinates(
        setCoordinates,
        locationRequiredAccuracy,
        Location.LocationAccuracy.High,
        locationMaximumAge,
      ),
    ]);
    return newCoordinates;
  }, [fetchCoordinates]);
  
  useEffectOnce(locationPermissionStatus === 'granted', () => {
    updateLocation();
  });
  
  return (
    <LocationContext.Provider value={{
      coordinates,
      approximateCoordinates,
      updateLocation
    }}>
      {children}
    </LocationContext.Provider>
  );
};

expo-store-review

Figは、プラットフォームアップデートがAPIを非推奨にした際にコミュニティパッケージが壊れることで何度か痛い目に遭っています。一例として、iOS 18がSKStoreReviewControllerを非推奨にし、react-native-in-app-reviewが動作しなくなったことがありました。

expo-store-reviewにより、Figは必要なすべてのオペレーティングシステムバージョンをサポートする、ユーザーに評価とフィードバックを求める新しいライブラリを迅速に採用できました。

画面の統合は簡単で、多くのコミュニティライブラリとは異なり、ExpoのライブラリはベースとなるAPIが変更されても継続的に保守されています。

import * as StoreReview from 'expo-store-review';

if (await StoreReview.hasAction()) {
  StoreReview.requestReview();
}

ExpoのOTAアップデートで重要なパッチをプッシュ

App CenterがCodePushを2025年3月に廃止した後、Figは重要な修正と実験的体験テストを即座に配信し続けるためにExpo EAS Updatesを採用しました。これにより、コードを配信する際の信頼性を持ち、本番環境に到達したバグを迅速に対処できるようになりました。

本番ビルドには引き続きBitriseを使用していますが、必要に応じてユーザーに重要なアップデートを配信するExpoのOTAアップデートシステムを信頼できることを気に入っています。

ExpoでのFigの未来

Figは、保守負担を軽減し、ビルドの複雑さを簡素化し、プラットフォームが拡張するにつれて新しいネイティブ機能を解放するために、より多くのExpoモジュールを統合し続けます。

React Nativeバージョンのアップグレードは、かつて物事が壊れるまで避けていた恐ろしいタスクでした。より多くのExpoパッケージに移行するにつれて、アップグレードプロセスはより簡単で信頼性が高くなり、開発サイクルを無駄にすることなく、サポートされているReact Nativeバージョンを維持できるようになりました。