Rebuilding Emojinate


This is a somewhat technical post about building Emojinate using React Native on the Expo platform. This project is open source, so you can check out the source code.

You can also read more about the origins of Emojinate.

From Meteor to React Native

The 2015 web application version of Emojinate was built using Meteor and it came with a sharing and voting system that provided users with real-time updates. These features required a hosted database for user-management and authentication which cost money to maintain.

I need Emojinate to be simple and free for everyone to use for as long as possible, so I removed the sharing and voting systems to eliminate any operating costs. I also need Emojinate’s main focus to be on writing; the voting system proved to be a distraction. Lastly, I need Emojinate to work offline. I often want to use it in places where I don’t have great internet connection.

Based on these requirements, I determined that Meteor was no longer the right tool and decided to switch to React Native.

Using Expo

Expo is a platform for managing your React Native application builds. It’s a great tool for simplifying the React Native development process, though you will find some people who prefer the use of React Native CLI. As this is a very simple project without the requirements of any native modules, Expo was the right choice.

Expo Router

If you’re coming from the React web-development world, you’re probable used to React Router to do client-side routing to different pages of your application. In React Native the concept is similar; instead of routing pages you’re routing screens, which can stack on top of (or below) each other.

This project uses Expo Router to navigate between screens, though Expo supports React Navigation as well. Expo Router admittedly has some weirdness, and many people prefer to use React Navigation. Nonetheless, I decided to “embrace the ecosystem” and use Expo Router.

Layouts

The main app/_layout.tsx file is our application’s entry point. It contains the <KyselyProvider> component, which is used to connect to the database and run any SQL migrations if necessary.

export default function RootLayout() {
  const { background } = useThemeColors();

  return (
    <KyselyProvider<Database>
      database={DATABASE_NAME}
      autoAffinityConversion
      debug={__DEV__ ? true : false}
      onInit={async (database) => {
        try {
          await getMigrator(database).migrateToLatest();
        } catch (err) {
          console.error({ err });
        }
      }}
    >
      <SafeAreaView
        style={{ flex: 1, backgroundColor: background, paddingBottom: 10 }}
      >
        <Stack
          screenOptions={{
            headerShown: false,
            headerTitleStyle: {
              fontFamily: "NotoSans-Regular",
              fontWeight: "bold",
            },
          }}
        >
          <Stack.Screen name="(main)" />
        </Stack>
      </SafeAreaView>
    </KyselyProvider>
  );
}

It also provides the main <Stack> of our application, which is just one group of screens called (main).

The app/(main)/_layout.tsx file is the layout for the (main) stack. It consists of 4 screens: index, settings, list.

Lastly, there is a /app/(main)/post/[id].tsx file which is the layout for the post screen, used to display a single post.

Index Screen

This is the main screen that shows the list of Emoji and has two text inputs: one for the story title and one for the story content.

The <EmojiList/> component is wrapped inside of a <RefreshControl>.

...
<ScrollView
  style={styles.emojiContainer(themeColors)}
  refreshControl={
    <RefreshControl
      refreshing={refreshing}
      onRefresh={() => {
        setRefreshing(true);
        store.resetEmojis();
        setRefreshing(false);
      }}
    />
  }
>
  <EmojiList />
</ScrollView>
...

When the <RefreshControl/> is pulled down, store.resetEmojis() is called. This is a mobx action that updates our application’s state. Let’s take a look at the store.

Application State

Application state is managed using the mobx library. The /app/(main)/_layout.tsx file contains the React Context that provides the store to the application.

The Store

The store is responsible for keeping track of all of our application’s state. It consists of Observable values can be observed by different parts of our application; when an observable value changes, any component that is observing it will be recomputed.

// state/store.ts
export class Store {
  @observable emojiLength = 5;
  @observable visibleEmoji = [] as Emoji[];
  @observable posts = [] as Post[];
  @observable excludedGroups = defaultExcludedGroups;
  @observable emojiGroupNames = emojiGroupNames;

  constructor() {
    makeObservable(this);
    this.fetchSettings();
    this.fetchPosts();
  }

  // actions and views are defined here
  ...

}

Here we define some values that we want to be able to use in different parts of the application.

emojiLength: The number of randomly generated emojis to show on the main screen.

visibleEmoji: This is an array of the Emoji that are visible on the screen. Each Emoji consists of a base and shortcode property.

posts: A users saved stories. These are fetched from the database when the application loads. this.fetchPosts() calls an action that queries the local database and writes the response into this observable value. More detail on fetchPosts() and actions in general below.

excludedGroups: User’s can exclude groups of Emoji from being randomly selected. This is an array of excluded group names. ‘Flags’ and ‘Symbols’ are excluded by default.

emojiGroupNames: A list of all the group names found in the emoji.json file. Doesn’t really need to be observable and the value never changes.

Then the store is initialized in the app/_layout.tsx file, the actions in the contructor are called.

Actions

Here is what the fetchSetting action looks like.

// state/store.ts

export class Store {
  ...

  @action
   async fetchSettings() {
     const settings = await fetchSettings();
     if (settings) {
       runInAction(() => {
         this.emojiLength = settings.count;
         this.excludedGroups = settings.excludedGroups;
         this.resetEmojis();
       });
     }
   }

  ...
}


// data/database.ts
export const fetchSettings = async () => {
  return db.selectFrom("settings").selectAll().executeTakeFirst();
};

Note that runInAction is used because this is an asynchronous function. There should only be one row in the settings table, so we use executeTakeFirst in the fetchSettings function.

The fetchPosts action is similar to fetchSettings.

Computed Views

mobx has a nice feature called computed values; these are values that are derived from values in your application’s store. When the underlying store values change, the computed values will get recomputed automatically; any component that is observing the computed value will be updated.

An example of a computed view is the postsByDate view:

//state/store.ts
@computed
get postsByDate() {
  return this.posts.slice().sort((a, b) => {
    return (
      new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
    );
  });
}

When the application loads, fetchPosts is called and writes the response to the posts observable value, represented by this.posts. We must use slice() to create a copy of the array because sort mutates the original array, which is not allowed as all values in mobx are immutable by default.

Now, whenever a new post is created and this.posts is updated, the postsByDate view will be recomputed, and the list screen will update.

Using The Store

When the app loads, the store initializes and is put inside of a React Context.

// state/store.ts
const store = new Store();
export const StoreContext = React.createContext<Store>(store);
export const useStore = () => React.useContext(StoreContext);

We can now import useStore inside of components that need be able to interact with the store.

// components/EmojiList.tsx
export default observer(function EmojiList() {
  const store = useStore();
  const { visibleEmoji } = store;

  return (
    <Flex
      direction="row"
      align="center"
      justify="center"
      style={styles.container}
    >
      <EmojiText emojis={visibleEmoji} style={styles.emoji} />
    </Flex>
  );
});

Note that the EmojiList function component is wrapped in an observer, which is imported from mobx-react-lite. The observer tracks that we are using the visibleEmoji value from our store (which is a computed value!). Under the hood, this component will now update whenever visibleEmoji changes in the store.

If you recall back up in the Index Screen section, EmojiList is wrapped in a RefreshControl; when the RefreshControl is refreshed (the user pull down the screen), the store.resetEmojis() action is called. This updates visibleEmoji, and the EmojiList component is updated.

The benefit of using a global store and sticking it inside of a React Context is that we don’t have to pass any props or do any prop-drilling. Notice that EmojiList has no props!

Wrap Up

Overall this was a great project to work on and a lot of fun to rebuild. React Native and Expo are great tools, and I’m thankful to all of the open-source maintainers out there.

As a reminder, the code to this project is available on GitHub. If you want to download the production versions, you can do so from the Play Store or the App Store.