
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.