In order to maximize functionality of our new product using JavaScript, we have implemented an Authentication module that manages a tokenPromise which is updated upon user logins or token refreshes. It seems imperative to allow for mutation in this process.
Rather than placing the tokenPromise at the module level, I opted to create a class solely dedicated to high-level functions that restrict how the state can be mutated. Other helper functions, which are pure and do not require state mutation, remain outside the class. This strategy greatly aids in understanding when the member might undergo changes as it is closely associated with all operations capable of changing it.
I have yet to come across similar patterns - is this approach considered best practice, or should we explore other options? Below is the class containing the mutable data, which is exported from Authentication.ts.
export default class Authentication {
public static async getAuthToken(): Promise<string> {
if (!this.tokenPromise || await hasExpired(this.tokenPromise)) {
// Either we've never fetched, memory was cleared, or token expired
this.tokenPromise = getUpdatedTokenPromise();
}
return (await this.tokenPromise).idToken;
}
public static async logOut(): Promise<void> {
this.tokenPromise = null;
await LocalStorage.clearAuthCredentials();
// Simply restart to log out for now
RNRestart.Restart();
}
private static tokenPromise: Promise<IAuthToken> | null;
}
// Afterwards, at the module level, we define all helper functions that do not require mutating this module's state - such as getUpdatedAuthToken(), etc.
An underlying principle appears to be: maintain objects with mutable state as concise as possible, exposing only high-level compact methods for state mutation (e.g. logOut and refreshAuthToken, rather than get/set authToken).