Changelog

New updates and improvements to Expo and EAS.


Introducing Notification Center

Sep 12, 2023 by

Steven Songqi Pu

Notification Center

You have a new notification! You can now see the latest updates from your personal and organization accounts in the new website notification center.

You can visit the notification center by clicking the bell icon in the navigation bar. Currently only build limit threshold notifications are supported but more types of notifications will be added in the future.

Pinning Projects in Dashboard

Aug 25, 2023 by

Avatar of Tomasz Czajecki

Tomasz Czajecki

Pinning Projects in Dashboard

Do you love experimenting but want to have your main projects always at hand? Now you can pin them to the top of the list so they are always there when you need them!

To get started, visit your projects list, click on the three vertical dots icon and select "Pin project" option. You can also unpin the already pinned project via this actions menu.

Understand your users and grow your app with EAS Insights

Aug 22, 2023 by

Avatar of Chris Walter

Chris Walter

Statistics and graphs showing how users by platform for an example app

In an effort to make it easier to develop and grow your apps, we're announcing a new suite of analytics tools called EAS Insights. Our goal with EAS Insights is to provide developers with the information they need to better understand their users, assess their app's performance, and feel confident in their direction.

As of today, we've begun introducing project-level Insights for all Expo developers. These features offer a preview of what we're working on, and we will continue adding new features and functionality over time.

In order to use EAS Insights, simply add the expo-insights module to your project. From there, we'll gather some basic data and offer breakdowns on the /insights page of your project on expo.dev.

If you have questions about EAS Insights or any feedback about these preview features, let us know.

Preview: fully customizable builds on EAS Build

Aug 10, 2023 by

Avatar of Szymon Dziedzic

Szymon Dziedzic

A custom build job running in sequential steps on EAS Build.

It’s extremely satisfying when you eas build on a new project and it Just Works — at Expo, we know what we need to do to build Android and iOS React Native apps, and we handle all of that automatically for you. In many cases, this is all a project ever needs; but, not always.

We’ve come across a number of cases where developers have asked to replace single steps in their build process with their own implementations (such as for installing dependencies for Rush or Nx monorepos) or even replace the majority of the build process in order to build for another platform (such as Electron app or one of the many TV platforms). We’ve also heard from developers that want to use EAS Build to run their test suite, so that they could consolidate their CI into a single service.

To enable all of these use cases and more, today we are releasing a preview of our new fully customizable build process. You can use it by creating a yml file in .eas/build and pointing to that file from the config field on your build profile. For example, you can configure a build profile to run tests with the following:

eas.json

{
  "build": {
    "test": {
      "config": "test.yml",
      "withoutCredentials": true
    },
    // other build profiles...
  }
}

.eas/build/test.yml

build:
  name: Run tests
  steps:
    - eas/checkout
    - eas/install_node_modules
    - run:
        name: Unit tests
        command: |
          echo "Running tests..."
          npm test

When you run eas build -p android --profile test, you'll see the following:

Screenshot of EAS Build running a test using the given configuration

Reusing existing build steps

If you needed to rewrite the entire build process from scratch as soon as you wanted to replace a single step, this wouldn’t be particularly useful. Each step in the standard build process can be used in your custom build definition. In the above test.yml example, we are using eas/checkout to check out the repository and eas/install_node_modules to re-use the same logic for installing Node modules (select the correct package manager, handle monorepos appropriately, etc).

For example, the following steps will create a development build for Android:

build:
  name: Development build - Android
  steps:
    - eas/checkout
    - eas/use_npm_token
    - eas/install_node_modules
    - eas/prebuild
    - eas/run_gradle
    - eas/find_and_upload_build_artifacts

And the equivalent for iOS:

build:
  name: Development build - iOS
  steps:
    - eas/checkout
    - eas/use_npm_token
    - eas/install_node_modules
    - eas/prebuild
    - run:
        name: Install pods
        working_directory: ./ios
        command: pod install
    - eas/generate_gymfile_from_template
    - eas/run_fastlane
    - eas/find_and_upload_build_artifacts

Example configurations for store-ready builds are available in the eas-custom-builds-example repository: Android, iOS.

You can learn more about the provided reusable steps and others in the “Built-in EAS functions” documentation. You can also fork these steps in your own JavaScript / TypeScript functions, or write your own build steps from scratch.

Writing build steps in JavaScript / TypeScript

If you’d like to fork a default build step, or if you just want to use JavaScript or TypeScript instead of Bash because the logic for a step is complex, you can run npx create-eas-build-function@latest .eas/build/my-new-function to create a new function (we suggest creating the function in the .eas/build directory, next to your YAML configuration — but you can put the functions anywhere in your repository).

After you’ve defined your function, you can refer to it from your build configuration YAML file. Refer to the README.md in the generated directory for more information about how to use it. Your configuration will look something like this:

eas.json

{
  "build": {
    "test": {
      "config": "test.yml",
      "withoutCredentials": true
    },
    // other build profiles...
  }
}

.eas/build/test.yml

build:
  name: Run tests
  steps:
    - eas/checkout
    - eas/install_node_modules
    - run:
        name: Unit tests
        command: npm run test
    - my-new-function

functions:
    my-new-function:
        name: my-new-function
        path: ./my-new-function

.eas/build/my-new-function/src/index.ts

import { BuildStepContext } from '@expo/steps';

async function myFunction(ctx: BuildStepContext): Promise<void> {
  ctx.logger.info('Hello from my TypeScript function!');
}

export default myFunction;

Learn more about creating EAS Build functions with TypeScript.

Current limitations

  • These features are currently in preview and are likely to change: APIs may change as we continue to iterate on them and on the the system as a whole. We expect custom builds to reach General Availability (GA) by late 2023 / early 2024.
  • EAS Build pricing model is not yet adapted to new use cases enabled by custom builds: while in preview, builds with custom configurations will be priced the same as standard builds. As the feature approaches GA, we will roll out pricing that is more appropriate to the usage patterns. We understand that for many folks this uncertainty may be a blocker for beginning to invest in adopting custom builds, and we will do our best to communicate the pricing model as soon as possible.
  • Artifacts can only be uploaded once during a build job: it is currently only possible to upload files once for each artifact type (application archive, generic build artifacts). So if you want to upload multiple artifacts, you will need to do that after all of the artifacts are ready, rather than as part of the steps where those artifacts are prepared. Example of uploading artifacts.
  • Reusable step for caching dependencies and outputs is not ready yet: we don’t yet expose a re-usable step for caching files that would speed up subsequent build runs.
  • Classic Update is not supported: the eas/configure-eas-update-if-installed step only supports EAS Update. Example of configuring EAS Update.
  • Build jobs nominally tied to target platforms: custom builds are still identified as either Android or iOS builds — this may not be true for many use cases, such as if you are running unit tests for multiple platforms or building for a different platform. For now, perform an “Android” build if you would like to use a Linux worker, and “iOS” builds if you’d like to use a macOS worker.
  • GitHub integration not ready yet: custom build jobs can’t yet be triggered from the EAS GitHub integration.

Try it out and give us feedback!

This new feature helps bring EAS Build more in alignment with the spirit of how we think about building apps at Expo — our tools and services give you great defaults out of the box, and when you need to, you can opt-out of any those default choices and customize any part of your experience without having to face a sudden complexity cliff. Keep what works for you, change what doesn’t.

Some ideas for areas we’d like feedback on:

  • There are small differences between the default build process and the equivalent composed of assembling the required steps (Android example, iOS example). Some are intentional, such as not supporting classic update configuration, and others might not be. Please report any discrepancies that you encounter that impact your project.
  • Try out the reusable eas/ functions. Are they the right level of abstraction? Should they accept additional inputs for further customization, or provide additional outputs for use in subsequent steps?
  • Every team has their own unique culture and preferences around tools and workflows, let us know if we’re missing something or could improve the APIs to better fit your use cases.

Feel free to send us feedback on Discord, @expo, Threads, or Bluesky.

A special mention goes out to Dominik Sokal, who built out the foundation of custom builds.

Proof of concept: expo-sqlite integration with CR-SQLite

Aug 10, 2023 by

Alan Hughes

CR-SQLite syncing two devices with a todo list

We're releasing an alpha version of the expo-sqlite library that includes a proof-of-concept iOS integration with CR-SQLite, an SQLite extension that "allows merging different SQLite databases together that have taken independent writes" that makes up part of the Vulcan toolchain.

We're also releasing an example project that uses CR-SQLite with TinyBase, a reactive datastore for local first apps. It demonstrates what is known as local-first architecture. For a comprehensive look at this approach and why we think it matters, check out Johannes Schickling's "Local-first app development" talk at App.js 2023, the Ink & Switch "Local-first software" essay that coined the term, and the local-first community at LFW.dev.

Convergent, Replicated, SQLite (CR-SQLite)

CR-SQLite is a run-time loadable extension for SQLite and libSQL. It allows merging different SQLite databases together that have taken independent writes.

With CR-SQLite, your users can all write to their own app's local SQLite database while offline. They can then come online and merge their databases together, without conflict.

In other (much more technical) words, CR-SQLite adds multi-master replication and partition tolerance to SQLite via conflict-free replicated data types (CRDTs) and/or causally ordered event logs.

For more information, check out the CR-SQLite README.

Integrating expo-sqlite and CR-SQLite

CR-SQLite adds functionality that allows us to request a set of changes from an SQLite database, and then we can insert those changes to another copy of the SQLite database, likely running on another device.

It works in two steps:

  1. Convert tables we would like to sync to CRRs (conflict-free replicated relations) with the following command:
    SELECT crsql_as_crr('todo');
    
  2. Then, request the changes from the database, send them to our server, and insert them in to the receiving peer's database:
    SELECT * FROM crsql_changes
    

The results can be merged in any order (they are commutative) and the databases will always converge on the same state given the same set of operations.

Example project

The example project is a todo list: expo/todo-sync-example. There are two parts:

  1. Client: A React Native app that uses expo-sqlite and TinyBase to store the user's todo list.
  2. Server: A Node server that uses PartyKit to open a websocket and listen for changes on any of the connected clients. When it receives a change, they are pushed to the other clients which will merge them into their local databases.

To run the example project, clone the example repo, check out the initial-poc branch, and run yarn to install its dependencies (this project is a Yarn Classic workspace).

After that, start the server:

cd apps/server
yarn start

Then, start the app:

cd apps/mobile
npx expo run:ios

We now have the app running in the iOS simulator. Let's start a second instance of the app on another simulator so that we can see the sync in action:

npx expo run:ios -d # Select another device from the prompt

Make some changes, add and delete todos, mark them as complete, or delete everything. No matter which device you use, you will see both stay in sync.

How it works

The first step is to set up the TinyBase persister.

// App.tsx
import { useCreatePersister } from 'tinybase/lib/ui-react';
import { createExpoSqlitePersister } from './app/store';

function TodoList() {
  // ...

  useCreatePersister(
    store,
    (store) =>
      createExpoSqlitePersister(store, db, {
        mode: 'tabular',
        tables: {
          load: { todo: { tableId: 'todo', rowIdColumnName: 'id' } },
          save: { todo: { tableName: 'todo', rowIdColumnName: 'id' } },
        },
      }),
    [db],
    async (persister) => {
      await persister.startAutoLoad();
      await persister.startAutoSave();
    }
  );
}

The createExpoSqlitePersister() function allows TinyBase to interact with the underlying data store, which is expo-sqlite in this case. As we make changes to our store, changes will be persisted in the local SQLite database. This is everything we need to set up persisting our data locally.

Next, we need to notify the server of our changes. We'll use the useSync() hook and the onDatabaseChange() listener provided by expo-sqlite.

First, create a socket:

// apps/mobile/app/useSync.ts
import { useEffect, useRef } from "react";
import PartySocket from "partysocket";

export function useSync() {
  const socket = useRef(createPartySocket()).current;
  // ...
}

Then let's connect our server:

// apps/mobile/app/useSync.ts

export function useSync() {
  // ...
  useEffect(() => {
    const handleMessage = (e: MessageEvent<string>) => {
      if (!syncEnabled) return;
      handleMessageAsync(e);
    };

    socket.addEventListener("message", handleMessage);

    if (syncEnabled) {
      // Send an init message to get the latest changes
      socket.send("init");
    }

    return () => {
      socket.removeEventListener("message", handleMessage);
    };
  }, [socket, syncEnabled]);
}

async function handleMessageAsync(e: MessageEvent<string>) {
  const data = JSON.parse(e.data);
  const rows = data[0].rows;

  for (const row of rows) {
    const { pk, ...rest } = row;
    const sql = `INSERT INTO crsql_changes ("table", 'pk', 'cid', 'val', 'col_version', 'db_version', 'site_id') VALUES (?, ${pk}, ?, ?, ?, ?, ?)`;
    try {
      await db.execAsync(
        [
          {
            sql,
            args: Object.values(rest),
          },
        ],
        false
      );
    } catch (e) {
      console.log(e);
    }
  }
}

We register a handler for the message event, and when we receive it, we insert the results into our database. Finally, we use another effect to set up the onDatabaseChange() event listener so that we are notified when the database has changed. When we receive an update event, we request our changes from the crsql_changes table and send the results. Allowing enabling and disabling of the sync is optional. Also, note that the queries used here will be improved in future versions so users won't have to know about these details.

// apps/mobile/app/useSync.ts

export function useSync() {
 // ...
 useEffect(() => {
    const maybeSendChanges = async () => {
      if (syncEnabled) {
        const changes = await requestChanges();
        socket.send(JSON.stringify(changes));
      }
    };

    // Subscribe to changes
    const subscription = db.onDatabaseChange(async (result) => {
      if (result.tableName.includes("__crsql_")) return;
      maybeSendChanges();
    });

    // Also maybe send them right away, in case changes happened while sync was
    // disabled
    maybeSendChanges();

    return () => subscription.remove();
  }, [syncEnabled]);
}

async function requestChanges() {
  return await db.execAsync(
    [
      {
        sql: `SELECT "table", quote(pk) as pk, cid, val, col_version, db_version, site_id FROM crsql_changes WHERE db_version > -1`,
        args: [],
      },
    ],
    false
  );
}

Implementation note: a real-world application would rarely want to use WHERE db_version > -1 because this will select the entire set of changes from the crsql_changes table, rather than only the changes that have been applied since the most recent sync (for example: WHERE db_version > ?last_sent_version). We also left out WHERE site_id IS NULL, which is likely be used in order to ensure we only select changes that occurred on the local client, rather than re-sending changes received from a recent sync. The code is simplified here for the sake of this proof of concept, where we have not set up an state persistence on the sync server. We'll continue to iterate on the main branch to create an example that better represents a real app.

What's next

We plan to begin investing heavily in our SQLite bindings, expo-sqlite, working with Matt Wonlaw on integrating seamlessly with CR-SQLite, and coordinating with James Pearce on a TinyBase persister. We're big believers in local-first architecture, and you should expect to see more work from us in this space in the future.

We'll continue to evolve this example to address many of the current limitations and to push more of the generic implementation details into related libraries. In particular, we plan to update it to:

  • Support the CR-SQLite extension on Android.
  • Incrementally sync changes between clients, rather than syncing the entire set of changes on each message.
  • Incrementally write to SQLite from TinyBase.
  • Demonstrate persistence on the syncing server, and examples of how you can leverage hosted SQLite services.

Send us your feedback on Discord, @expo, Threads, or Bluesky.

More

Proof of concept: Expo CLI Dev Tools Plugins

Aug 10, 2023 by

Avatar of Kudo Chien

Kudo Chien

Expo Orbit: Download and launch builds

Aug 9, 2023 by

Avatar of Gabriel Donadel

Gabriel Donadel

Rollouts for EAS Update

Aug 8, 2023 by

Avatar of Quinlan Jung

Quinlan Jung

useUpdates() API for expo-updates

Aug 8, 2023 by

Avatar of Doug Lowder

Doug Lowder

Single Sign-On (SSO)

Aug 8, 2023 by

Avatar of Will Schurman

Will Schurman

New Dashboard Tables and More Timeline Activities

Aug 2, 2023 by

Avatar of Tomasz Czajecki

Tomasz Czajecki

Enforcement of EAS Free plan limits

Aug 1, 2023 by

Avatar of Chris Jensen

Chris Jensen

Expo VS Code theme

Jul 24, 2023 by

Avatar of Bartosz Kaszubowski

Bartosz Kaszubowski

Link your GitHub repo with Expo

Jul 19, 2023 by

Avatar of Juwan Wheatley

Juwan Wheatley

Improved Expo Docs organization

Jul 5, 2023 by

Avatar of Aman Mittal

Aman Mittal

App.js 2023

May 3, 2023 by

Avatar of Jon Samp

Jon Samp

Changes summary Q4 2022

Jan 31, 2023 by

Avatar of Jon Samp

Jon Samp