There's value in defining what we as shepherds of this project believe are important traits and practices. As we grow and welcome new members into our team it's beneficial to have a sense of what guided us towards the product being implemented the way it has been.
These values are not unbendable rules. They're an attempt to share knowledge about how the application came to be and how we currently think it should be developed. It's a non-exhaustive, living document of intentions, and beliefs.
We've chosen to focus exclusively on the engineering (i.e. how we write code) part in this document. There are other values surrounding visual design, product and project management, community etc that we're not going to consider here.
We believe in strongly typed programming languages that help us avoid mistakes that are all too easy to make. When the compiler can't help us we believe in augmenting with other kinds of static analysis. Type systems and static analysis helps us not only in the moment we're writing code but also extends further out. By codifying our intentions we help prevent the next person from making a mistake.
Our assertNever
helper lets us leverage the type system to verify exhaustiveness and get compile-time errors when that assertion fails.
Our react-readonly-props-and-state
static analysis ensures that we don't accidentally mutate state which React prohibits being mutated but isn't able to enforce due to the dynamic runtime.
We write our own type definitions when none exist.
By choosing to keep as much of our state as possible in immutable data structures we can be explicit about when and where we update it and have confidence that it's not going to change from underneath us. The last part is especially important in an application such as GitHub Desktop which has lots of state. The best way we've found so far to maintain our sanity is to be explicit about how that state flows through the app and when it's updated and immutability is one tool to help us stay on the right track.
We use read-only versions of arrays in interfaces and object as well as in function parameters. (
We prefer
const a = someCondition ? someValue : someOtherValue
over
let a = someDefaultValue
if (someCondition) {
a = someOtherValue
}
We prefer the first example to the second because it states to ourselves and future readers that you can trust a
to not change. Not only does it make that intention clear, it enforces it. In the case of let
we'd have to scan the method to verify that it doesn't get mutated and even then we can't be sure that some developer in the future adds logic which changes it. Again, we're stating our intentions and letting the type system help us keep our promises.
If the checks required to determine the result are too complicated it might be time to create another function which does the heavy lifting and returns a value that you can assign to a const
.
We believe that small, consistent, and composable methods are easier to understand, and less prone to errors than larger methods operating on data from sources other than those passed to it via arguments. You might recognize this sentiment from the concept of pure functions in functional programming. While we aren't using a language which lets us express such conditions we believe in striving towards writing methods which act on values that is provided to it via arguments and returns a consistent result based solely on those values.
If a method has to acquire data from multiple sources to then do some processing on it we prefer that the gathering of data and the processing of it is separated into two methods such that the computational part doesn't get entangled with acquisition of state.
At times we move methods out of classes or even out into their own file to reinforce that it's a function only acting on its given parameters (as opposed to instance fields or shared variables the function has closed over).
In app-menu-bar we've extracted a method called createState
from the component to live outside of the class such that we can be sure that the only thing that matters to the outcome of that function is the props object that's passed to it. By doing this we can avoid a very common example of using this.props
inside of the method when, in fact, we might want to create a state object from nextProps
or even prevProps
that was given to us from one of the React lifecycle methods.