As we have built our app in React Native, our understanding of how to build software has evolved. As our understanding grows, newer code uses newer techniques. Older code is often left un-updated. It can be difficult to orient oneself around what the current preferred practices are.
This document is a map. Not of Eigen at a specific time, but a map of how we got here and where we want to go next. This is a living document, expected to be updated regularly, of links to:
- Example code.
- Pull requests with interesting discussions.
- Conversations on Slack.
- Blog posts.
Links should point to specific commits, and not a branch (in case the branch or file is deleted, these links should always work). But it's possible that a file is outdated, that our understanding has moved on since it was linked to; in that case, please update this document.
- Current Preferred Practices
- Use React Native for new feature development
- Leverage TypeScript to prevent runtime bugs
- Keep File Structure Organized (in progress)
- Use Relay for Network Requests
- Prefer Relay containers (Higher Order Components) over Hooks
- styled-system / styled-components
- Write unit tests for new components
- Use the Native Switchboard for Navigation (for now...)
- Analytics
- Miscellaneous
The app is written in Objective-C and Swift, with React Native added in 2016. We only ship an iOS app, and do not yet use React Native for an Android app.
Objective-C and Swift (sometimes called "Native" code) are responsible for the following parts of the app:
- Sign up/in flow ("onboarding").
- Live Auctions Integration (LAI) view controller and networking.
- The Auction view controller.
- The SwitchBoard (see "SwitchBoard" section below) to navigate between view controllers.
- The top-level tab bar, and each tab's navigation controller.
- Deep-link and notification handling (via SwitchBoard).
- Analytics for Native UI.
- Initializing the React Native runtime.
Everything else is written in React Native.
New features should be built in React Native. The React Native runtime currently requires an existing user ID and access token to be loaded, and sign up/in is still handled in Native code.
- Why Artsy uses React Native
- All React Native posts on Artsy's Engineering Blog
- Some great React Native components:
- Partner is a simple top-level component.
- PartnerShows is a fragment container that uses FlatList to paginate through Relay data.
- Search is a functional component that loads data in response to user input.
We used to have many different renderX functions throughout our components, but today we prefer to have a single render() function in a component. See this PR for our rationale and a comparison of approaches.
We use TypeScript to maximize runtime code safety. In April 2020, we adopted TypeScript's strict mode. This disables "implicit any" and require strict null checks. The change left a lot of comments like this throughout the codebase:
// @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏Our goal is to reduce the number of STRICTNESS_MIGRATION migrations checks to zero over time. We use CI tooling to require PRs never to increase the number. You can opt in to helping out by requiring all the files you change to fix all the migration comments by running the following command:
touch .i-am-helping-out-with-the-strictness-migrationEverything in src/ is React Native. Within this folder things can be a bit of a mess and we are working on improving that.
Files that export a component end in .tsx, files that don't export a component end in .ts by default.
We use PascalCase for Components and Component Folders, but keep everything else within the Component folder(eg. mutations, state, utils) camelCase. Test files follow the same pattern.
For example mutations, routes, state would be camelCase folders, while MyComponent.tsx would be a PascalCase file.
├── MyComponentFolder
│ ├── MyComponent.tsx
│ ├── MyComponent.tests.tsx
│ ├── mutations
│ | ├── mutationFunction.ts
│ ├── state
│ | ├── stateFunction.ts
│ ├── utils
│ | ├── utilFunction.ts
│ | ├── utilFunction.tests.ts
├── …
Another example is:
If we have a buttons folder which exports many button components, we keep it lowercase.
├── buttons
│ ├── RedButton.tsx
│ ├── GreenButton.tsx
│ ├── YellowButton.tsx
│ ├── buttons.tests.tsx
│ ├── buttons.stories.tsx
├── …
However, if we have a Button folder which exports only one button component, we write that with in PascalCase.
├── Button
│ ├── Button.tsx
│ ├── Button.tests.tsx
│ ├── Button.stories.tsx
Note: Updating capitalisation on folders can cause issues in git and locally so please refrain from renaming existing folders until we come up with a strategy about this. (TODO)
Data should be loaded from Metaphysics, Artsy's GraphQL server. Requests to Metaphysics should be made through Relay.
We have a preference for Relay containers due to relay-hooks hooks not being compatible with Relay containers which represent the majority of our components using Relay.
- Our use of styled-components was supplemented by styled-system in #1016.
- Example pull request migrating a component from styled-components to styled-system
Unit testing on Emission is a bit all over the place. Some top-level notes:
- We prefer
react-test-renderoverenzyme, and would ultimately like to removeenzyme. - We prefer
relay-test-utilsover our existingMockRelayRendererandrenderRelayTree. - We have native unit tests too. See
getting_started.md - We don't like snapshot tests; they produce too much churn for too little value. It's okay to test that a component doesn't throw when rendered, but use
extractText(or similar) to test the actual component tree.
Here are some great examples of what tests and test coverage should look like.
- Tests for Gene component
- Tests for Consignments submission flow
- Tests for Consignments photo-selection component interactions.
- Consignments Overview is a really complex component, so tests are broken into four test files:
- General component tests
- Analytics tests
- Local storage tests
- Image uploading tests
CollectionsRailtests demonstraterelay-test-utils.
Our React Native code ("Emission") is used by our Native code ("Eigen"). They used to be two repositories but were combined in February 2020. Traces of the separation remain. The structure we originally took is described in this blog post. Interop between JavaScript and Native can be tricky.
Most interactions are made through a "SwitchBoard" to open links. Other interactions are handled by the APIModules, for example when Eigen needs to invoke some kind of callback.
- Switchboard routes defined in Eigen
- Emission switchboard to call out to Eigen
- Callbacks between JS and native code are set up here.
See our docs on implementing analytics here