This document contains Guidelines and Good Practices applied by the AE Mobile Team at [e-spres-oh]. Some of them are very general (applicable on any project), others are specific to our team structure and technology stack. Avoid the Cargo Cult before applying them on your projects.
βοΈ IMPORTANT:
- this document includes Guidelines, not Dogmas;
- motivated exceptions are always understandable;
- this is a living document, so any piece of content is subject to change for the better.
Table of contents:
- π§ Pragmatism
- π Code style
- π Folder structure
- π¦ Component structure
- π¬ Code Reviews and Feedback
- π Issues
- π¨ Commits
- π Code Comments
- π Refactoring
- β Tests
- 𧩠Types
- π Leadership
- π§ Mindset
Don't overthink. Don't predict the future. Don't over-engineer.
-
𧬠Avoid creating the wrong abstractions
Evaluate only the current needs when implementing a new feature. Create an abstraction only when you have more than one use-case. Let the Desire Lines unravel themselves. -
π³ Re-evaluate all use-cases when changing an existing abstraction, or feature
Always question the high inertia of abstractions. When you need to extend existing abstractions or features with additional functionality, don't focus only on your specific use-case. Consider all the use-cases and re-think the implementation if needed. -
πΈ Avoid boiling frogs and fix broken windows
Always refactor when something doesn't feel right. Keep the entropy in check. -
βοΈ Improve constantly, but not indefinitely
Keep the right balance between Kaizen and Wabi-sabi.
IMPORTANT:
If adapting a new use-case would require bigger rewrites that are cumbersome, add a // @todo
comment + an Issue with all the details, and reference the Issue
in the @todo
. Discuss it and plan it accordingly.
Coding style is (and will probably always be) a strongly debatable subject, in contrast with its uselessness. No matter what the style is, it should be 100% consistent across the entire team.
-
Use Prettier for code formatting
No more manual formatting, no more debates on "how the code should look". Delegate this task to the tooling. -
Set your editor to Format on Save
Unformatted code should never reach the remote repository. One way to prevent that is to format the code before you save. Having your editor/IDE do that for you is infallible. -
Make sure your editor/IDE is using the local
.prettierrc
settings
We all must use the same formatting rules. Saving an unmodified file should make no changes to it.
These guidelines are influenced by our technology choices.
1. π Each Screen should have its folder
-
Each
Screen
represents aRoute
in the application. -
Each
Screen
has its separate folder, that contains anySubComponents
used by thatScreen
. -
Screens are flattened, not nested and they should not reflect the routing. Routing is often not a tree, but a graph. Changing the routing should not require you to change the folder structure.
2. π Reusable components should be placed in src/components
folder
A reusable component is any component that is used in more than one Screen. They are split into 2 categories:
-
Generic components, that don't know anything about the business domain.
Examples:Text
,Button
,Input
. -
Business components, that contain some knowledge about the business domain.
Examples:ReservationHeader
,CarCard
,PriceButton
.
3. π Use file suffixes
Specific files such as Services
, Stores
, Screens
should have the proper suffix appended to their filename.
This communicates better the role of a file, especially when you have different files with the same name. It also helps to distinguish the opened files in your editor/IDE or when you're searching for them.
src/
βββ services/
β βββ AuthService.ts
βββ stores/
β βββ index.ts
β βββ UserStore.ts
βββ screens/
βββ User/
βββ UserScreen.tsx
βββ Profile.tsx
-
The
Screen
suffix helps identify which is the top-level Component and which are the SubComponents, from a Screen folder. -
The
Service
orStore
suffix helps differentiate between components and other business logic files, ie:User.tsx
component andUserStore.ts
.
All components should follow a basic structure, as suggested below:
// imports
import React from 'react'
import { useTranslation } from 'react-i18next'
import Text from '../../components/Text';
// constants
const HEIGHT = 56;
// interface
type Props = {
title: string;
icon?: IconEnum;
}
// named export
export function Component(props: Props) {
// declarations
const { title, icon } = props;
const [count, setCount] = useState(1);
const navigation = useNavigation();
// effects
useEffect(() => {
increment(count);
}, [count]);
// return render content
return (
<View style={styles.container}>
<View>{renderHeader()}</View>
<View>{renderBody()}</View>
</View>
)
// partial renderers
function renderHeader() {}
function renderBody() {}
// local functions (closures)
function callSomeAction() { // button handler }
function getSomeValue() {}
// end component
}
// styles
const styles = StyleSheet.create({
container: {...}
})
// local functions (pure)
function getSomePureValue() {}
Some things to keep in mind:
-
Avoid verbose Component
interface
:- use
Props
if the interface is private. There's no need to use a different name for each component. One file should contain only one component andProps
communicates perfectly its purpose. - prepend the component name, like
ButtonProps
, if the interface is exported, because it will be imported in other components that have their ownProps
interface. - avoid the Hungarian notation, like
IProps
orIButtonProps
.
- use
-
Prefer function declarations over expressions:
Usefunction increment() {}
instead ofconst increment = () => {}
. This better communicates that it's afunction
and has the benefit of being hoisted, so you can call it before you define it. -
Return component structure as soon as possible:
The primary purpose of a component is its markup & structure. That should be visible as soon as possible when you open the file. -
Prefix
render
to partial rendering functions:
Functions that returnJSX
should be prefixed withrender
to better communicate their purpose. They should also be placed right below the return statement, and before other functions that don't returnJSX
. -
Place closure functions at the end of the component:
Functions that close over props or state are less important, so they should be placed at the end of the component. -
Place pure functions outside the component:
Pure functions, can easily be moved outside the component, making extraction and testing much easier. -
Extract any
magic values
as constants:
Magicnumbers
andstrings
should be extracted asnamed constants
. If they're used in other files as well, debate whether toexport
it, or tomove
it somewhere else. -
Don't prefix functions with
_
:
In function components, everything declared inside the component is private. Onlyexported
expressions become public. Prefixing functions with"_"
is useless.
π¬ Code Reviews can be performed in 2 ways:
- On request, by creating a Pull Request, or
- Unrequested, by checking the git history.
π The Feedback you give could fall into 2 categories and should be treated accordingly:
- Objective feedback should always be accompanied by explanations;
- Subjective feedback should be highlighted (ie. "If I were you, I would do this...", "My opinion is that...", etc).
It's important what you say, but equally important how you say it.
π€ Guidelines for giving feedback:
-
Be polite when you give feedback, especially when it's not requested. Choose your words carefully.
-
Don't ask absurd things during code reviews. Ask only for meaningful changes that you would do yourself.
-
Try to understand why a weird specific approach was taken, before trashing it.
-
Ask questions when a piece of code looks weird. You might not understand the bigger picture.
-
Give props when you encounter any piece of code that tingle your heart, or mind.
π₯ Guidelines for receiving feedback:
-
Be polite when you receive feedback, especially when you asked for it. Accept the feedback with an open mind and an open heart.
-
Don't be defensive when you receive feedback you don't like. Don't take it personally. You got feedback for the code that you wrote, not for the person that you are.
-
Explain when you don't agree with the feedback you receive. It's OK to disagree. It's NOT OK to be disagreeable.
-
Show your appreciation when your reviewer finds a bug, or a problem with the code you wrote.
When you add a new Issue, you make it public for everyone on the team. That's why it should contain all the knowledge and the information that somebody else would require to understand what the problem is.
-
π― Short and concise title
Treat Issues like functions or commits. Their title should clearly and concisely communicate the problem. You have plenty of space to add lengthy descriptions and explanations in the body. -
π· Use labels for categorization
Labels are a great way to add predefined context to Issues, to keep the title concise. -
π Bugs should have a test case
Before you create a bug issue, make sure that bug is reproducible in certain situations. When you report a bug, it's necessary to include all the details required for somebody else to be able to reproduce it:- iOS-specific, Android-specific or both;
- simulator or real device;
- relevant application state: guest or user, filled in profile or not, online or offline, search location, etc.
-
π¨ Format the description
Plain text is harder to read than formatted text. Use markdown to format your content accordingly. You won't receive any praise for formatting the content, but you will annoy somebody if you didn't.- use bold to highlight important content;
- use lists whenever you have to enumerate more than 1 item;
- use quotes when you cite something;
- use inline or block code to format code-related content;
- use links to refer to external sources;
- use mentions to ping other team members;
- reference other issues;
- use task lists to track progress, etc.
-
Commits should be self-contained and highly cohesive
All changes in a commit should be related to the message of the commit. Avoid adding unrelated changes. If you revert a commit, it should only undo what the commit message describes. -
Avoid merge commits on
develop
Fetch/pull before making a commit. After committing, push immediately to remote. Merge commits are accepted on branches, because we're squashing them anyway. -
Avoid temporary commits on
develop
If you feel like committing, but the only message you can think of is "Temp commit" or "WIP some feature", just don't. Commit when you have a stable and clear implementation, or create a branch + Pull Request.
π Commit messages
Commit subject should be short and clear. Don't explain what the problem was; explain what the commit is doing.
-
Use the imperative mood in the message
Commit subjects should complete the sentence:"If applied, this commit will your commit subject here"
Examples: "Replace icons file" or "Prevent location search when offline", etc.
-
Commit subject should only explain the
what
Avoid explainingwhy
orhow
in the commit subject. It should only describe what the commit is doing. -
Commit body can explain the
why
Sometimes it helps to highlight why we created the commit. This can be described in the commit body. -
Don't explain the
how
in the commit message
There's no need to describe how you've implemented a specific commit. You have the code diff for that.
π Comments in code should generally be avoided. Usually, there are better alternatives:
-
What a function/constant does
Instead of explaining the name of an identifier, better rename it to be self-explanatory. Yes, naming things is difficult, but the exercise pays off. -
Magic values
Instead of explaining what a magic number or string is, extract it to a constant and give it a self-explanatory name. -
Commented out code
Instead of commenting code, just remove it. You won't need it. And if you do, you can write it again, even better than the first time. And if you can't write it again, you have the git history.
π Comments could help in some situations:
-
Explaining the "why"
Sometimes we have to implement intentionally bad code, or take some unorthodox approaches. In such situations, comments help to document why such decisions were taken. Think of it like a written Mea culpa.If possible, add a test to ensure it doesn't get unfixed by mistake.
-
Describing complex code
Not all code can be clean, simple and intuitive. At times, we might write strange or weird code that is not intuitive, nor self-explanatory, especially when we have to deal with complex requirements. For instance, Regexes are inherently obscure and difficult to understand. In these situations, using comments to describe what does the code do, would help you (or somebody else) when reading it.
Consider refactoring as a normal routine, like cleaning or maintenance:
-
β° Every day
Refactorings like Renaming, Extracting, Inlining, Moving, Deleting should be performed every time you encounter a better approach. They should be applied toconstants
,variables
,props
,types
,interfaces
,functions
,arguments
,components
,files
,folders
, etc. -
π Every sprint
Whenever something feels wrong or it becomes difficult to explain to someone else, that's a code smell. Discuss it and plan the proper refactoring. -
βοΈ Remove unused code
You won't need it later. You might think you will, but foreseeing hasn't been proven yet as a human skill. Even Wikipedia has little information about it. -
π Code smells
Spotting refactoring opportunities gets easier and easier once we learn to identify the various code smells which signal that our code starts to rot.
Put all the necessary effort to make your tests stupidly simple, that anybody can understand without reading the source of the system under test.
-
Don't test the tools
Be pragmatic, test only your own code, not the tools, frameworks, and libraries that you use. -
Make sure you cover all code branches
Usually you'll need at least one test for eachif
statement,ternary
orlogical
operators like&&
or||
. Additional tests might be required to cover some combinations. -
Use code coverage only as a double check
A high coverage doesn't really mean anything. A low coverage, on the other hand, means that you don't have enough tests. Use coverage only to identify what you've missed. Don't use it as a metric for how good your tests are. -
When testing collections, 2 items are enough
Usually, if a collection implementation works with 2 items, it will work with more than 2 as well. If your logic doesn't specifically expect more than 2 items, there's no need to go above and beyond. -
Use dummy data when input is irrelevant
When you pass some input or stub something, and you don't care what that data is, use something as close to "nothing", such as:0
for numbers,""
for strings,[]
for Arrays or{}
for Objects. -
Avoid concrete input, if not used in asserts, or not relevant for the test
Passingadmin123
as a username, orUnited States
as a country, could raise the question: "Would the test pass if I put a different value?". Avoid this question by making them explicit.// β don't expect(getProfile({ country: 'United States', username: 'admin123' })).toEqual(...) // β do expect(getProfile({ country: 'Any Country', username: 'any_username' })).toEqual(...)
-
Describe test input & stubs
Similar to magic values extract hard-coded values to constants that describe what they are:// β don't expect(getCountry('40')).toEqual('Romania'); // β do const ROMANIA_COUNTRY_CODE = '40'; expect(getCountry(ROMANIA_COUNTRY_CODE)).toEqual('Romania');
// β don't expect(login('admin', '123456')).toEqual(true); // β do const VALID_CREDENTIALS = { user: 'admin', pass: '123456' }; expect(login(VALID_CREDENTIALS.user, VALID_CREDENTIALS.pass)).toEqual(true);
// β don't expect(login('asdfghjkl', 'zxcvbnm')).toEqual(false); // β do expect(login('invalid_user', 'invalid_pass')).toEqual(false);
We use TypeScript
because of the following main reasons:
-
Communicates structure & design
Very useful for deeply nested data structures likeArrays
andObjects
, to have IntelliSense on what properties are available and what their type is. It also forces you to think more deeply about code design and domain modelling. -
Documents function contracts
You get out-of-the-box documentation onfunction arguments
and theirreturn type
, easily finding out how to call a function/method and what to expect from it. -
Enforces contracts
Let the type checker verify that you don't have any compile errors. Extremely useful in refactorings to not miss any potential errors. -
Pit of Success It doesn't let us fall into the Pit of Despair, like JavaScript or other dynamic languages do. Instead, it guides us and helps us to fall into the Pit of Success.
π¦ Type everything as strictly as possible:
-
Convert
Strings
toEnums
Strings are difficult to change, refactor, and discover their accepted values. In isolated situations, where you don't need to re-use it in other files, you could also go for aUnion
. Otherwise, useEnum
.// β don't function setDirection(direction: string) {} setDirection('up') // π better function setDirection(direction: 'up' | 'down') {} setDirection('up') // β best enum Direction { UP = 'up', DOWN = 'down', } function setDirection(direction: Direction) {} setDirection(Direction.UP)
-
Define
types
forObjects
An arbitrary object structure doesn't communicate anything. You can't re-use it, you don't know what it represents. Putting the extra effort to define atype
for your object, forces you to deeply think about code design & structure. You might realize that you end up having multiple definitions for the same concept.// β don't const user = {...} // β do type User = {...} const user: User = {...}
-
Specify items type for
Arrays
An array is usually a collection of a certain type. Specify what that type is.// β don't const usersList = [] // β do const usersList: User[] = [] // or const usersList: Array<User> = []
-
Define boundary
types
forresponses
&payloads
When fetching data from a server or sending data to a server, TS cannot infer the types. That's the boundary of your system. To have atype-safe
system you need to ensure that your system starts and ends with the right types.Add suffixes for types used for boundary requests:
- for data on
GET
requests, add theResponse
suffix, ie.LocationsResponse
; - for payload on
POST
/PUT
/DELETE
requests, add thePayload
suffix, ie.ReservationPayload
.
- for data on
-
Avoid type assertions
Forcing something to be something else is a code smell and it should trigger the alarm that there might be better alternatives. However, at the boundaries of the application likeHTTP requests
or3rd party libraries
, where proper typing is not available, casting might be necessary.// β this might be a lie & not type-checked const style = {...} as ViewStyle // β this will be properly type-checked const style: ViewStyle = {...}
// β don't const user = { address: {} as Address } // β do const EMPTY_ADDRESS: Address: {...} const user = { address: EMPTY_ADDRESS }
// β don't const [item, setItem] = useState(Enum.FIRST as string); // β do const [item, setItem] = useState<Enum>(Enum.FIRST);
-
Avoid
any
type completely
Usingany
might seem like an easy escape hatch, but it completely removes the type-checking and type-safety which is the main reason we use TypeScript in the first place. Use theunknown
type instead, along with proper type narrowing.// β don't const data: any = fetch("/api"); // this is not type-checked data.map(item => item.id); // β do const data: unknown = fetch("/api"); // we need to narrow down the type if (Array.isArray(data)) { data.map((item: MyItem) => item.id); }
There is no designated Team, or Tech Lead as a role. Leaders exist only as a trait. This enables the team mindset and avoids pulling ranks.
It's nice to be important, but it's more important to be nice.
-
Responsibility lies on the team
We are a self-organizing agile team. If somebody fucks up, the team fucks up. You don't have a specific person to blame. It's every member's responsibility to make the necessary efforts that the team doesn't fuck up: do, learn, and help. -
Everybody has something to say
Without a hierarchy, we are all equal. Anybody can make mistakes, anybody can (and should) point out these mistakes. If there is something you believe that needs to change, bring it up, and let's talk about it. -
Decisions are driven by reason
Nobody can impose anything. Anybody can bring proposals to the table. When you don't agree with something, motivate it.
Software development goes beyond frameworks, libraries, programming paradigms, and technical challenges. The right mindset is a valuable trait, that will benefit you and your teammates at any stage of the project, and on any project.
-
π Be responsible
Taking responsibility for your actions and your code doesn't mean that "others will have someone to blame". It means that "you don't have someone else to blame".Ignorance (as in not knowing about something) can be understandable, but it's not an excuse. Consider this when:
- you re-use or copy-paste someone else's code, that turns out to be bad;
- you follow someone's suggestion and it turns out it's not a good idea;
- you get feedback after your PR was approved;
- you install a dependency that might be harmful, etc.
- π€ΈββοΈ Be agile
Nothing is set in stone. Anything can change. Resistance is futile. Be comfortable with changing features, even late in the development process. Don't get emotionally attached to your code. For this you need to:- write your code easy to change;
- make sure your code is easy to remove.
- π© Understand your managers, clients & users
Depeche Mode puts it perfectly in Walking In My Shoes. Before you judge and trash a decision from your Client or Manager, or feedback from your Users, try walking in their shoes. Understand their context and the reason for their decisions or feedback. If you think they're wrong, prove them wrong.- being a manager yourself and having to talk directly to clients (pushing back, contradicting them, or negotiating features and deadlines) will help you enjoy the comfort of not having to wear their shoes;
- being a client yourself and witnessing what it takes to communicate your needs to a development team, will help you better understand the other side;
- being a user of the software you're building, will help you better understand its flaws.
- ποΈ Get involved
Replace "Let's do that" with "Let me do that". When you want to propose something new: take the lead, be the driver, show the others how, why, and what to do. Don't just launch ideas.
- π Be reasonable
No one is perfect, and that's perfectly fine.- be reasonable with yourself: you will make mistakes and those mistakes will and should be highlighted by your teammates;
- be reasonable with others: they will also make mistakes and you should highlight those mistakes in the most friendly possible way.