UNPKG

@epic-web/config

Version:

Reasonable ESLint configs for epic web devs

1,964 lines (1,544 loc) 44.2 kB
# Epic Programming Style Guide This style guide is a collection of recommendations for writing code that is easy to understand, maintain, and scale. It goes hand-in-hand with the [Epic Programming Principles](https://www.epicweb.dev/principles) and the [Epic Web Config](https://github.com/epicweb-dev/config). This is an opinionated style guide that's most useful for people who: 1. Don't have a lot of experience writing code and want some guidance on how to write code that's easy to understand, maintain, and scale. 2. Have experience writing code but want a set of standards to align on for working in a team. Much of this is subjective, but most opinions are thought through and based on years of experience working with large codebases and teams. Note: Not every possible formatting opinion is mentioned because they are handled automatically by [Prettier](https://prettier.io) anyway. ## JavaScript This section will include TypeScript guidelines as well. ### Variables #### References Use `const` by default. Only use `let` when you need to reassign. Never use `var`. Remember that `const` does not mean "constant" in the sense of "unchangeable". It means "constant reference". So if the value is an object, you can still change the properties of the object. #### Naming conventions Use descriptive, clear names that explain the value's purpose. Avoid single-letter names except in small loops or reducers where the value is obvious from context. ```tsx // ✅ Good const workshopTitle = 'Web App Fundamentals' const instructorName = 'Kent C. Dodds' const isEnabled = true const sum = numbers.reduce((total, n) => total + n, 0) const names = people.map((p) => p.name) // ❌ Avoid const t = 'Web App Fundamentals' const n = 'Kent C. Dodds' const e = true ``` Follow [the naming cheatsheet](https://github.com/kettanaito/naming-cheatsheet) by [Artem Zakharchenko](https://github.com/kettanaito) for more specifics on naming conventions. #### Constants For truly constant values used across files, use uppercase with underscores: ```tsx const BASE_URL = 'https://epicweb.dev' const DEFAULT_PORT = 3000 ``` ### Objects #### Literal syntax Use object literal syntax for creating objects. Use property shorthand when the property name matches the variable name. ```tsx // ✅ Good const name = 'Kent' const age = 36 const person = { name, age } // ❌ Avoid const name = 'Kent' const age = 36 const person = { name: name, age: age } ``` #### Computed property names Use computed property names when creating objects with dynamic property names. ```tsx // ✅ Good const key = 'name' const obj = { [key]: 'Kent', } // ❌ Avoid const key = 'name' const obj = {} obj[key] = 'Kent' ``` #### Method shorthand Use object method shorthand: ```tsx // ✅ Good const obj = { method() { // ... }, async asyncMethod() { // ... }, } // ❌ Avoid const obj = { method: function () { // ... }, asyncMethod: async function () { // ... }, } ``` > **Note**: Ordering of properties is not important (and not specified by the > spec) and it's not a priority for this style guide either. #### Accessors Don't use them. When I do this: ```ts console.log(person.name) person.name = 'Bob' ``` All I expect to happen is to get the person's name and pass it to the `log` function and to set the person's name to `'Bob'`. Once you start using property accessors (getters and setters) then those guarantees are off. ```ts // ✅ Good const person = { name: 'Hannah', } // ❌ Avoid const person = { get name() { // haha! Now I can do something more than just return the name! 😈 return this.name }, set name(value) { // haha! Now I can do something more than just set the name! 😈 this.name = value }, } ``` This violates the principle of least surprise. ### Arrays #### Literal syntax Use Array literal syntax for creating arrays. ```tsx // ✅ Good const items = [1, 2, 3] // ❌ Avoid const items = new Array(1, 2, 3) ``` #### Filtering falsey values Use `.filter(Boolean)` to remove falsey values from an array. ```tsx // ✅ Good const items = [1, null, 2, undefined, 3] const filteredItems = items.filter(Boolean) // ❌ Avoid const filteredItems = items.filter( (item) => item !== null && item !== undefined, ) ``` #### Array methods over loops Use Array methods over loops when transforming arrays with pure functions. Use `for` loops when imperative code is necessary. Never use `forEach` because it's never more readable than a `for` loop and there's not situation where the `forEach` callback function could be pure and useful. Prefer `for...of` over `for` loops. ```tsx // ✅ Good const items = [1, 2, 3] const doubledItems = items.map((n) => n * 2) // ❌ Avoid const doubledItems = [] for (const n of items) { doubledItems.push(n * 2) } ``` ```tsx // ✅ Good for (const n of items) { // ... } // ❌ Avoid for (let i = 0; i < items.length; i++) { const n = items[i] // ... } // ❌ Avoid items.forEach((n) => { // ... }) ``` ```tsx // ✅ Good for (const [i, n] of items.entries()) { console.log(`${n} at index ${i}`) } // ❌ Avoid for (const n of items) { const i = items.indexOf(n) console.log(`${n} at index ${i}`) } ``` #### Favor simple chains over `.reduce` Favor simple `.filter` and `.map` chains over complex `.reduce` callbacks unless performance is an issue. ```tsx // ✅ Good const items = [1, 2, 3, 4, 5] const doubledGreaterThanTwoItems = items.filter((n) => n > 2).map((n) => n * 2) // ❌ Avoid const doubledItems = items.reduce((acc, n) => { acc.push(n * 2) return acc }, []) ``` #### Spread to copy Prefer the spread operator to copy an array: ```tsx // ✅ Good const itemsCopy = [...items] const combined = [...array1, ...array2] // ❌ Avoid const itemsCopy = items.slice() const combined = array1.concat(array2) ``` #### Non-mutative array methods Prefer non-mutative array methods like `toReversed()`, `toSorted()`, and `toSpliced()` when available. Otherwise, create a new array. Unless performance is an issue or the original array is not referenced (as in a chain of method calls). ```tsx // ✅ Good const reversedItems = items.toReversed() const mappedFilteredSortedItems = items .filter((n) => n > 2) .map((n) => n * 2) .sort((a, b) => a - b) // ❌ Avoid const reversedItems = items.reverse() ``` #### Use `with` Use `with` to create a new object with some properties replaced. ```tsx // ✅ Good const people = [{ name: 'Kent' }, { name: 'Sarah' }] const personIndex = 0 const peopleWithKentReplaced = people.with(personIndex, { name: 'John' }) // ❌ Avoid (mutative) const peopleWithKentReplaced = [...people] peopleWithKentReplaced[personIndex] = { name: 'John' } ``` #### TypeScript array generic Prefer the Array generic syntax over brackets for TypeScript types: ```tsx // ✅ Good const items: Array<string> = [] function transform(numbers: Array<number>) {} // ❌ Avoid const items: string[] = [] function transform(numbers: number[]) {} ``` Learn more about the reasoning behind the Array generic syntax in the [Array Types in TypeScript](https://tkdodo.eu/blog/array-types-in-type-script) article by [Dominik Dorfmeister](https://github.com/tkdodo). ### Destructuring #### Destructure objects and arrays Use destructuring to make your code more terse. ```tsx // ✅ Good const { name, avatar, 𝕏: xHandle } = instructor const [first, second] = items // ❌ Avoid const name = instructor.name const avatar = instructor.avatar const xHandle = instructor.𝕏 ``` Destructuring multiple levels is fine when formatted properly by a formatter, but can definitely get out of hand, so use your best judgement. As usual, try both and choose the one you hate the least. ```tsx // ✅ Good (nesting, but still readable) const { name, avatar, 𝕏: xHandle, address: [{ city, state, country }], } = instructor // ❌ Avoid (too much nesting) const [ { name, avatar, 𝕏: xHandle, address: [ { city: { latitude: firstCityLatitude, longitude: firstCityLongitude, label: firstCityLabel, }, state: { label: firstStateLabel }, country: { label: firstCountryLabel }, }, ], }, ] = instructor ``` ### Strings #### Interpolation Prefer template literals over string concatenation. ```tsx // ✅ Good const name = 'Kent' const greeting = `Hello ${name}` // ❌ Avoid const greeting = 'Hello ' + name ``` #### Multi-line strings Use template literals for multi-line strings. ```tsx // ✅ Good const html = ` <div> <h1>Hello</h1> </div> `.trim() // ❌ Avoid const html = '<div>' + '\n' + '<h1>Hello</h1>' + '\n' + '</div>' ``` ### Functions #### Function declarations Use function declarations over function expressions. Name your functions descriptively. This is important because it allows the function definition to be hoisted to the top of the block, which means it's callable anywhere which frees your mind to think about other things. ```tsx // ✅ Good function calculateTotal(items: Array<number>) { return items.reduce((sum, item) => sum + item, 0) } // ❌ Avoid const calculateTotal = function (items: Array<number>) { return items.reduce((sum, item) => sum + item, 0) } const calculateTotal = (items: Array<number>) => items.reduce((sum, item) => sum + item, 0) ``` #### Limit single-use functions Limit creating single-use functions. By taking a large function and breaking it down into many smaller functions, you reduce benefits of type inference and have to define types for each function and make additional decisions about the number and format of arguments. Instead, extract logic only when it needs to be reused or when a portion of the logic is clearly part of a unique concern. ```tsx // ✅ Good function doStuff() { // thing 1 // ... // thing 2 // ... // thing 3 // ... // thing N } // ❌ Avoid function doThing1(param1: string, param2: number) {} function doThing2(param1: boolean, param2: User) {} function doThing3(param1: string, param2: Array<User>, param3: User) {} function doThing4(param1: User) {} function doStuff() { doThing1() // ... doThing2() // ... doThing3() // ... doThing4() } ``` #### Default parameters Prefer default parameters over short-circuiting. ```tsx // ✅ Good function createUser(name: string, role = 'user') { return { name, role } } // ❌ Avoid function createUser(name: string, role: string) { role ??= 'user' return { name, role } } ``` #### Early return Return early to avoid deep nesting. Use guard clauses: ```tsx // ✅ Good function getMinResolutionValue(resolution: number | undefined) { if (!resolution) return undefined if (resolution <= 480) return MinResolution.noLessThan480p if (resolution <= 540) return MinResolution.noLessThan540p return MinResolution.noLessThan1080p } // ❌ Avoid function getMinResolutionValue(resolution: number | undefined) { if (resolution) { if (resolution <= 480) { return MinResolution.noLessThan480p } else if (resolution <= 540) { return MinResolution.noLessThan540p } else { return MinResolution.noLessThan1080p } } else { return undefined } } ``` #### Async/await Prefer async/await over promise chains: ```tsx // ✅ Good async function fetchUserData(userId: string) { const user = await getUser(userId) const posts = await getUserPosts(user.id) return { user, posts } } // ✅ Fine, because wrapping in try/catch is annoying function sendAnalytics(event: string) { return fetch('/api/analytics', { method: 'POST', body: JSON.stringify({ event }), }).catch(() => null) } // ❌ Avoid function fetchUserData(userId: string) { return getUser(userId).then((user) => { return getUserPosts(user.id).then((posts) => ({ user, posts })) }) } // ❌ Avoid async function sendAnalytics(event: string) { try { return await fetch('/api/analytics', { method: 'POST', body: JSON.stringify({ event }), }) } catch { // ignore return null } } ``` #### Inline Callbacks Anonymous inline callbacks should be arrow functions: ```tsx // ✅ Good const items = [1, 2, 3] const doubledGreaterThanTwoItems = items.filter((n) => n > 2).map((n) => n * 2) // ❌ Avoid const items = [1, 2, 3] const doubledGreaterThanTwoItems = items .filter(function (n) { return n > 2 }) .map(function (n) { return n * 2 }) ``` #### Arrow Parens Arrow functions should include parentheses even with a single parameter: <!-- prettier-ignore --> ```tsx // ✅ Good const items = [1, 2, 3] const doubledGreaterThanTwoItems = items.filter((n) => n > 2).map((n) => n * 2) // ❌ Avoid const items = [1, 2, 3] const doubledGreaterThanTwoItems = items.filter(n => n > 2).map(n => n * 2) ``` This makes it easier to add/remove parameters without having to futz around with parentheses. ### Modules #### File Organization In general, files that change together should be located close to each other. In Breaking a single file into multiple files should be avoided unless absolutely necessary. Specifics around file structure depends on a multitude of factors: - Framework conventions - Project size - Team size Strive to keep the file structure as flat as possible. #### Module Exports Framework and other tool conventions sometimes require default exports, but prefer named exports in all other cases. ```tsx // ✅ Good export function add(a: number, b: number) { return a + b } export function subtract(a: number, b: number) { return a - b } // ❌ Avoid export default function add(a: number, b: number) { return a + b } ``` #### Barrel Files Do **not** use barrel files. If you don't know what they are, good. If you do and you like them, it's probably because you haven't experienced their issues just yet, but you will. Just avoid them. #### Pure Modules In general, strive to keep modules pure (read more about this in [Pure Modules](https://kentcdodds.com/blog/pure-modules)). This will make your application start faster and be easier to understand and test. ```tsx // ✅ Good let serverData export function init(a: number, b: number) { const el = document.getElementById('server-data') const json = el.textContent serverData = JSON.parse(json) } export function getServerData() { if (!serverData) throw new Error('Server data not initialized') return serverData } // ❌ Avoid let serverData const el = document.getElementById('server-data') const json = el.textContent export const serverData = JSON.parse(json) ``` > **Note**: In practice, you can't avoid some modules having side-effects (you > gotta kick off the app somewhere), but most modules should be pure. #### Import Conventions Import order has semantic meaning (modules are executed in the order they're imported), but if you keep most modules pure, then order shouldn't matter. For this reason, having your imports grouped can make things a bit easier to read. ```ts // Group imports in this order: import 'node:fs' // Built-in import 'match-sorter' // external packages import '#app/components' // Internal absolute imports import '../other-folder' // Internal relative imports import './local-file' // Local imports ``` #### Type Imports Each module imported should have a single import statement: ```tsx // ✅ Good import { type MatchSorterOptions, matchSorter } from 'match-sorter' // ❌ Avoid import { type MatchSorterOptions } from 'match-sorter' import { matchSorter } from 'match-sorter' ``` #### Import Location All static imports are executed at the top of the file so they should appear there as well to avoid confusion. ```tsx // ✅ Good import { matchSorter } from 'match-sorter' function doStuff() { // ... } // ❌ Avoid function doStuff() { // ... } import { matchSorter } from 'match-sorter' ``` #### Export Location All exports should be inline with the function/type/etc they are exporting. This avoids duplication of the export identifier and having to keep it updated when changing the name of the exported thing. ```tsx // ✅ Good export function add(a: number, b: number) { return a + b } // ❌ Avoid function add(a: number, b: number) { return a + b } export { add } ``` #### Module Type Use ECMAScript modules for everything. The age of CommonJS is over. ✅ Good **package.json**: ```json { "type": "module" } ``` Use **exports** field in **package.json** to explicitly declare module entry points. ✅ Good **package.json**: ```json { "exports": { "./utils": "./src/utils.js" } } ``` #### Import Aliases Use import aliases to avoid long relative paths. Use the standard `imports` config field in **package.json** to declare import aliases. ✅ Good **package.json**: ```json { "imports": { "#app/*": "./app/*", "#tests/*": "./tests/*" } } ``` ```tsx import { add } from '#app/utils/math.ts' ``` > **Note**: Latest versions of TypeScript support this syntax natively. #### Include file extensions The ECMAScript module spec requires file extensions to be included in import paths. Even though TypeScript doesn't require it, always include the file extension in your imports. An exception to this is when importing a module which has `exports` defined in its **package.json**. ```tsx // ✅ Good import { redirect } from 'react-router' import { add } from './math.ts' // ❌ Avoid import { add } from './math' ``` ### Properties #### Use dot-notation When accessing properties on objects, use dot-notation unless you can't syntactically (like if it's dynamic or uses special characters). ```tsx const user = { name: 'Brittany', 'data-id': '123' } // ✅ Good const name = user.name const id = user['data-id'] function getUserProperty(user: User, property: string) { return user[property] } // ❌ Avoid const name = user['name'] ``` ### Comparison Operators & Equality #### Triple equals Use triple equals (`===` and `!==`) for comparisons. This will ensure you're not falling prey to type coercion. That said, when comparing against `null` or `undefined`, using double equals (`==` and `!=`) is just fine. ```tsx // ✅ Good const user = { id: '123' } if (user.id === '123') { // ... } const a = null if (a === null) { // ... } if (b != null) { // ... } // ❌ Avoid if (a == null) { // ... } if (b !== null && b !== undefined) { // ... } ``` #### Rely on truthiness Rely on truthiness instead of comparison operators. ```tsx // ✅ Good if (user) { // ... } // ❌ Avoid if (user === true) { // ... } ``` #### Switch statement braces Using braces in switch statements is recommended because it helps clarify the scope of each case and it avoids variable declarations from leaking into other cases. ```tsx // ✅ Good switch (action.type) { case 'add': { const { amount } = action add(amount) break } case 'remove': { const { removal } = action remove(removal) break } } // ❌ Avoid switch (action.type) { case 'add': const { amount } = action add(amount) break case 'remove': const { removal } = action remove(removal) break } ``` #### Avoid unnecessary ternaries ```tsx // ✅ Good const isAdmin = user.role === 'admin' const value = input ?? defaultValue // ❌ Avoid const isAdmin = user.role === 'admin' ? true : false const value = input != null ? input : defaultValue ``` ### Blocks #### Use braces for multi-line blocks Use braces for multi-line blocks even when the block is the body of a single statement. ```tsx // ✅ Good if (!user) return if (user.role === 'admin') { abilities = ['add', 'remove', 'edit', 'create', 'modify', 'fly', 'sing'] } // ❌ Avoid if (user.role === 'admin') abilities = ['add', 'remove', 'edit', 'create', 'modify', 'fly', 'sing'] ``` ### Control Statements #### Use statements Unless you're using the value of the condition in an expression, prefer using statements instead of expressions. ```tsx // ✅ Good if (user) { makeUserHappy(user) } // ❌ Avoid user && makeUserHappy(user) ``` ### Comments #### Use comments to explain "why" not "what" Comments should explain why something is done a certain way, not what the code does. The names you use for variables and functions are "self-documenting" in a sense that they explain what the code does. But if you're doing something in a way that's non-obvious, comments can be helpful. ```tsx // ✅ Good // We need to sanitize lineNumber to prevent malicious use on win32 // via: https://example.com/link-to-issue-or-something if (lineNumber && !(Number.isInteger(lineNumber) && lineNumber > 0)) { return { status: 'error', message: 'lineNumber must be a positive integer' } } // ❌ Avoid // Check if lineNumber is valid if (lineNumber && !(Number.isInteger(lineNumber) && lineNumber > 0)) { return { status: 'error', message: 'lineNumber must be a positive integer' } } ``` #### Use TODO comments for future improvements Use TODO comments to mark code that needs future attention or improvement. ```tsx // ✅ Good // TODO: figure out how to send error messages as JSX from here... function getErrorMessage() { // ... } // ❌ Avoid // FIXME: this is broken function getErrorMessage() { // ... } ``` #### Use FIXME comments for immediate problems Use FIXME comments to mark code that needs immediate attention or improvement. ```tsx // ✅ Good // FIXME: this is broken function getErrorMessage() { // ... } ``` > **Note**: The linter should lint against FIXME comments, so this is useful if > you are testing things out and want to make sure you don't accidentally commit > your work in progress. #### Use @ts-expect-error for TypeScript workarounds When you need to work around TypeScript limitations (or your own knowledge gaps with TypeScript), use `@ts-expect-error` with a comment explaining why. ```tsx // ✅ Good // @ts-expect-error no idea why this started being an issue suddenly 🤷‍♂️ if (jsxEl.name !== 'EpicVideo') return // ❌ Avoid // @ts-ignore if (jsxEl.name !== 'EpicVideo') return ``` #### Use JSDoc for public APIs Use JSDoc comments for documenting public APIs and their types. ```tsx // ✅ Good /** * This function generates a TOTP code from a configuration * and this comment will explain a few things that are important for you to * understand if you're using this function * * @param {OTPConfig} config - The configuration for the TOTP * @returns {string} The TOTP code */ export function generateTOTP(config: OTPConfig) { // ... } ``` #### Avoid redundant comments Don't add comments that just repeat what the code already clearly expresses. ```tsx // ✅ Good function calculateTotal(items: Array<number>) { return items.reduce((sum, item) => sum + item, 0) } // ❌ Avoid // This function calculates the total of all items in the array function calculateTotal(items: Array<number>) { return items.reduce((sum, item) => sum + item, 0) } ``` ### Semicolons #### Don't use unnecessary semicolons Don't use semicolons. The rules for when you should use semicolons are more complicated than the rules for when you must use semicolons. With the right eslint rule ([`no-unexpected-multiline`](https://eslint.org/docs/latest/rules/no-unexpected-multiline)) and a formatter that will format your code funny for you if you mess up, you can avoid the pitfalls. Read more about this in [Semicolons in JavaScript: A preference](https://kentcdodds.com/blog/semicolons-in-javascript-a-preference). <!-- prettier-ignore --> ```tsx // ✅ Good const name = 'Kent' const age = 36 const person = { name, age } const getPersonAge = () => person.age function getPersonName() { return person.name } // ❌ Avoid const name = 'Kent'; const age = 36; const person = { name, age }; const getPersonAge = () => person.age; function getPersonName() { return person.name } ``` The only time you need semicolons is when you have a statement that starts with `(`, `[`, or `` ` ``. Instances where you do that are few and far between. You can prefix that with a `;` if you need to and a code formatter will format your code funny if you forget to do so (and the linter rule will bug you about it too). ```tsx // ✅ Good const name = 'Kent' const age = 36 const person = { name, age } // The formatter will add semicolons here automatically ;(async () => { const result = await fetch('/api/user') return result.json() })() // ❌ Avoid const name = 'Kent' const age = 36 const person = { name, age } // Don't manually add semicolons ;(async () => { const result = await fetch('/api/user') return result.json() })() ``` ### Types #### Type Inference Let TypeScript do the heavy lifting with type inference when possible: ```ts // ✅ Good function add(a: number, b: number) { return a + b // TypeScript infers return type as number } // ❌ Avoid function add(a: number, b: number): number { return a + b } ``` #### Generics Use generics to create reusable components and functions. And treat type names in generics the same way you treat any other kind of variable or parameter (because a generic type is basically a parameter!): ```tsx // ✅ Good function createArray<Value>(length: number, value: Value): Array<Value> { return Array(length).fill(value) } // ❌ Avoid function createStringArray(length: number, value: string) { return Array(length).fill(value) } ``` #### Type Assertions Avoid type assertions (`as`) when possible. Instead, use type guards or runtime validation. ```tsx // ✅ Good function isError(maybeError: unknown): maybeError is Error { return ( maybeError && typeof maybeError === 'object' && 'message' in maybeError && typeof maybeError.message === 'string' ) } // ❌ Avoid const error = caughtError as Error ``` #### Type Guards Use type guards to narrow types and provide runtime type safety. Type guards are functions that check if a value is of a specific type. The most common way to create a type guard is using a type predicate. ```tsx // ✅ Good - Using type predicate function isError(maybeError: unknown): maybeError is Error { return ( maybeError && typeof maybeError === 'object' && 'message' in maybeError && typeof maybeError.message === 'string' ) } // ✅ Good - Using type predicate with schema validation function isApp(app: unknown): app is App { return AppSchema.safeParse(app).success } // ✅ Good - Using type predicate with composition function isExampleApp(app: unknown): app is ExampleApp { return isApp(app) && app.type === 'example' } // ❌ Avoid - Not using type predicate function isApp(app: unknown): boolean { return typeof app === 'object' && app !== null } ``` Type predicates use the syntax `parameterName is Type` to tell TypeScript that the function checks if the parameter is of the specified type. This allows TypeScript to narrow the type in code blocks where the function returns true. ```tsx // Usage example: const maybeApp: unknown = getSomeApp() if (isExampleApp(maybeApp)) { // TypeScript now knows that maybeApp is definitely an ExampleApp maybeApp.type // TypeScript knows this is 'example' } ``` #### Schema Validation Use schema validation (like Zod) for runtime type checking and type inference when working with something that crosses the boundary of your codebase. ```tsx // ✅ Good const OAuthData = z.object({ accessToken: z.string(), refreshToken: z.string(), expiresAt: z.date(), }) type OAuthData = z.infer<typeof OAuthDataSchema> const oauthData = OAuthDataSchema.parse(rawData) // ❌ Avoid type OAuthData = { accessToken: string refreshToken: string expiresAt: Date } const oauthData = rawData as OAuthData ``` #### Unknown Type Use `unknown` instead of `any` for values of unknown type. This forces you to perform type checking before using the value. ```tsx // ✅ Good function handleError(error: unknown) { if (isError(error)) { console.error(error.message) } else { console.error('An unknown error occurred') } } // ❌ Avoid function handleError(error: any) { console.error(error.message) } ``` #### Type Coercion Avoid implicit type coercion. Use explicit type conversion when needed. An exception to this is working with truthiness. ```tsx // ✅ Good const number = Number(stringValue) const string = String(numberValue) if (user) { // ... } // ❌ Avoid const number = +stringValue const string = '' + numberValue if (Boolean(user)) { // ... } ``` ### Naming Conventions Learn and follow [Artem's](https://github.com/kettanaito) [Naming Cheatsheet](https://github.com/kettanaito/naming-cheatsheet). Here's a summary: ```tsx // ✅ Good const firstName = 'Kent' const friends = ['Kate', 'John'] const pageCount = 5 const hasPagination = postCount > 10 const shouldPaginate = postCount > 10 // ❌ Avoid const primerNombre = 'Kent' const amis = ['Kate', 'John'] const page_count = 5 const isPaginatable = postCount > 10 const onItmClk = () => {} ``` Key principles: 1. Use English for all names 2. Be consistent with naming convention (camelCase, PascalCase, etc.) 3. Names should be Short, Intuitive, and Descriptive (S-I-D) 4. Avoid contractions and context duplication 5. Function names should follow the A/HC/LC pattern: - Action (get, set, handle, etc.) - High Context (what it operates on) - Low Context (optional additional context) For example: `getUserMessages`, `handleClickOutside`, `shouldDisplayMessage` ### Events #### Event Constants Define event constants using a const object. Use uppercase with underscores for event names. ```tsx // ✅ Good export const EVENTS = { USER_CODE_RECEIVED: 'USER_CODE_RECEIVED', AUTH_RESOLVED: 'AUTH_RESOLVED', AUTH_REJECTED: 'AUTH_REJECTED', } as const // ❌ Avoid export const events = { userCodeReceived: 'userCodeReceived', authResolved: 'authResolved', authRejected: 'authRejected', } ``` #### Event Types Use TypeScript to define event types based on the event constants. ```tsx // ✅ Good export type EventTypes = keyof typeof EVENTS // ❌ Avoid export type EventTypes = | 'USER_CODE_RECEIVED' | 'AUTH_RESOLVED' | 'AUTH_REJECTED' ``` #### Event Schemas Define Zod schemas for event payloads to ensure type safety and runtime validation. ```tsx // ✅ Good const CodeReceivedEventSchema = z.object({ type: z.literal(EVENTS.USER_CODE_RECEIVED), code: z.string(), url: z.string(), }) // ❌ Avoid type CodeReceivedEvent = { type: 'USER_CODE_RECEIVED' code: string url: string } ``` > **Note**: This is primarily useful because in event systems, you're typically > crossing a boundary of your codebase (network etc.). #### Event Cleanup Always clean up event listeners when they're no longer needed. ```tsx // ✅ Good authEmitter.on(EVENTS.USER_CODE_RECEIVED, handleCodeReceived) return () => { authEmitter.off(EVENTS.USER_CODE_RECEIVED, handleCodeReceived) } // ❌ Avoid authEmitter.on(EVENTS.USER_CODE_RECEIVED, handleCodeReceived) // No cleanup ``` #### Event Error Handling Make certain to cover error cases and emit events for those. ```tsx // ✅ Good try { // event handling logic } catch (error) { authEmitter.emit(EVENTS.AUTH_REJECTED, { error: getErrorMessage(error), }) } // ❌ Avoid try { // event handling logic } catch (error) { console.error(error) } ``` ## React ### Avoid useEffect [You Might Not Need `useEffect`](https://react.dev/learn/you-might-not-need-an-effect) Instead of using `useEffect`, use ref callbacks, event handlers with `flushSync`, css, `useSyncExternalStore`, etc. ```tsx // This example was ripped from the docs: // ✅ Good function ProductPage({ product, addToCart }) { function buyProduct() { addToCart(product) showNotification(`Added ${product.name} to the shopping cart!`) } function handleBuyClick() { buyProduct() } function handleCheckoutClick() { buyProduct() navigateTo('/checkout') } // ... } useEffect(() => { setCount(count + 1) }, [count]) // ❌ Avoid function ProductPage({ product, addToCart }) { useEffect(() => { if (product.isInCart) { showNotification(`Added ${product.name} to the shopping cart!`) } }, [product]) function handleBuyClick() { addToCart(product) } function handleCheckoutClick() { addToCart(product) navigateTo('/checkout') } // ... } ``` There are a lot more examples in the docs. `useEffect` is not banned or anything. There are just better ways to handle most cases. Here's an example of a situation where `useEffect` is appropriate: ```tsx // ✅ Good useEffect(() => { const controller = new AbortController() window.addEventListener( 'keydown', (event: KeyboardEvent) => { if (event.key !== 'Escape') return // do something based on escape key being pressed }, { signal: controller.signal }, ) return () => { controller.abort() } }, []) ``` ### Don't Sync State, Derive It [Don't Sync State, Derive It](https://kentcdodds.com/blog/dont-sync-state-derive-it) ```tsx // ✅ Good const [count, setCount] = useState(0) const isEven = count % 2 === 0 // ❌ Avoid const [count, setCount] = useState(0) const [isEven, setIsEven] = useState(false) useEffect(() => { setIsEven(count % 2 === 0) }, [count]) ``` ### Do not render falsiness In JSX, do not render falsy values other than `null`. ```tsx // ✅ Good <div> {contacts.length ? <div>You have {contacts.length} contacts</div> : null} </div> // ❌ Avoid <div> {contacts.length && <div>You have {contacts.length} contacts</div>} </div> ``` ### Use ternaries Use ternaries for simple conditionals. When automatically formatted, they should be plenty readable, even on multiple lines. Ternaries are also the only conditional in the spec (currently) which are expressions and can be used in return statements and other places expressions are used. ```tsx // ✅ Good const isAdmin = user.role === 'admin' const access = isAdmin ? 'granted' : 'denied' function App({ user }: { user: User }) { return ( <div className="App"> {user.role === 'admin' ? <Link to="/admin">Admin</Link> : null} </div> ) } ``` ## Testing ### Test User Interactions Test components based on how users actually interact with them, not implementation details: > The more your tests resemble the way your software is used, the more > confidence they can give you. - > [Kent C. Dodds](https://x.com/kentcdodds/status/977018512689455106) ```tsx // ✅ Good test('User can add items to cart', async () => { render(<ProductList />) await userEvent.click(screen.getByRole('button', { name: /add to cart/i })) await expect(screen.getByText(/1 item in cart/i)).toBeInTheDocument() }) // ❌ Avoid test('Cart state updates when addToCart is called', () => { const { container } = render(<ProductList />) const addButton = container.querySelector('[data-testid="add-button"]') fireEvent.click(addButton) expect( container.querySelector('[data-testid="cart-count"]'), ).toHaveTextContent('1') }) ``` ### Avoid Unnecessary Mocks Only mock what's absolutely necessary. Most of the time, you don't need to mock any of your own code or even dependency code. ```tsx // ✅ Good function Greeting({ name }: { name: string }) { return <div>Hello {name}</div> } test('Greeting displays the name', () => { render(<Greeting name="Kent" />) expect(screen.getByText('Hello Kent')).toBeInTheDocument() }) // ❌ Avoid test('Greeting displays the name', () => { const mockName = 'Kent' vi.mock('./greeting.tsx', () => ({ Greeting: () => <div>Hello {mockName}</div>, })) render(<Greeting name={mockName} />) expect(container).toHaveTextContent('Hello Kent') }) ``` ### Mock External Services Use MSW (Mock Service Worker) to mock external services. This allows you to test your application's integration with external APIs without actually making network requests. ```tsx // ✅ Good import { setupServer } from 'msw/node' import { http } from 'msw' const server = setupServer( http.get('/api/user', async ({ request }) => { return HttpResponse.json({ name: 'Kent', role: 'admin', }) }), ) test('User data is fetched and displayed', async () => { render(<UserProfile />) await expect(await screen.findByText('Kent')).toBeInTheDocument() }) // ❌ Avoid test('User data is fetched and displayed', async () => { vi.spyOn(global, 'fetch').mockResolvedValue({ json: () => Promise.resolve({ name: 'Kent', role: 'admin' }), }) render(<UserProfile />) await expect(await screen.findByText('Kent')).toBeInTheDocument() }) ``` ### Use Test Function Use the `test` function instead of `describe` and `it`. This makes tests more straightforward and easier to understand. ```tsx // ✅ Good test('User can log in with valid credentials', async () => { render(<LoginForm />) await userEvent.type( screen.getByRole('textbox', { name: /email/i }), 'kent@example.com', ) await userEvent.type( screen.getByRole('textbox', { name: /password/i }), 'password123', ) await userEvent.click(screen.getByRole('button', { name: /login/i })) await expect(await screen.findByText('Welcome back!')).toBeInTheDocument() }) // ❌ Avoid describe('LoginForm', () => { it('should allow user to log in with valid credentials', async () => { render(<LoginForm />) await userEvent.type( screen.getByRole('textbox', { name: /email/i }), 'kent@example.com', ) await userEvent.type( screen.getByRole('textbox', { name: /password/i }), 'password123', ) await userEvent.click(screen.getByRole('button', { name: /login/i })) await expect(await screen.findByText('Welcome back!')).toBeInTheDocument() }) }) ``` ### [Avoid Nesting Tests](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) Keep your tests flat. Nesting makes tests harder to understand and maintain. ```tsx // ✅ Good test('User can log in', async () => { render(<LoginForm />) await userEvent.type( screen.getByRole('textbox', { name: /email/i }), 'kent@example.com', ) await userEvent.type( screen.getByRole('textbox', { name: /password/i }), 'password123', ) await userEvent.click(screen.getByRole('button', { name: /login/i })) await expect(await screen.findByText('Welcome back!')).toBeInTheDocument() }) // ❌ Avoid describe('LoginForm', () => { describe('when user enters valid credentials', () => { it('should show welcome message', async () => { render(<LoginForm />) await userEvent.type( screen.getByRole('textbox', { name: /email/i }), 'kent@example.com', ) await userEvent.type( screen.getByRole('textbox', { name: /password/i }), 'password123', ) await userEvent.click(screen.getByRole('button', { name: /login/i })) await expect(await screen.findByText('Welcome back!')).toBeInTheDocument() }) }) }) ``` ### Avoid shared setup/teardown variables ```tsx // ✅ Good test('renders a greeting', () => { render(<Greeting name="Kent" />) expect(screen.getByText('Hello Kent')).toBeInTheDocument() }) // ❌ Avoid let utils beforeEach(() => { utils = render(<Greeting name="Kent" />) }) test('renders a greeting', () => { expect(utils.getByText('Hello Kent')).toBeInTheDocument() }) ``` > **Note**: Most of the time your individual tests can avoid the use of > `beforeEach` and `afterEach` altogether and it's only global setup that needs > it (like mocking out `console.log` or setting up a mock server). ### Avoid Testing Implementation Details Test your components based on how they're used, not how they're implemented. ```tsx // ✅ Good function Counter() { const [count, setCount] = useState(0) return <button onClick={() => setCount((c) => c + 1)}>Count: {count}</button> } test('Counter increments when clicked', async () => { render(<Counter />) const button = screen.getByRole('button') await userEvent.click(button) expect(getByText('Count: 1')).toBeInTheDocument() }) // ❌ Avoid test('Counter increments when clicked', () => { const { container } = render(<Counter />) const button = container.querySelector('button') fireEvent.click(button) const state = container.querySelector('[data-testid="count"]') expect(state).toHaveTextContent('1') }) ``` ### Keep Assertions Specific Make your assertions as specific as possible to catch the exact behavior you're testing. ```tsx // ✅ Good test('Form shows error for invalid email', async () => { render(<LoginForm />) await userEvent.type( screen.getByRole('textbox', { name: /email/i }), 'invalid-email', ) await userEvent.click(screen.getByRole('button', { name: /login/i })) await expect( await screen.findByText(/enter a valid email/i), ).toBeInTheDocument() }) // ❌ Avoid test('Form shows error for invalid email', async () => { const { container } = render(<LoginForm />) await userEvent.type( screen.getByRole('textbox', { name: /email/i }), 'invalid-email', ) await userEvent.click(screen.getByRole('button', { name: /login/i })) expect(container).toMatchSnapshot() }) ``` ### Follow the Testing Trophy Prioritize your tests according to the Testing Trophy: 1. Static Analysis (TypeScript, ESLint) 2. Unit Tests (Pure Functions) 3. Integration Tests (Component Integration) 4. E2E Tests (Critical User Flows) ```tsx // ✅ Good // 1. Static Analysis function add(a: number, b: number): number { return a + b } // 2. Unit Tests test('add function adds two numbers', () => { expect(add(1, 2)).toBe(3) }) // 3. Integration Tests test('Calculator component adds numbers', async () => { render(<Calculator />) await userEvent.click(screen.getByRole('button', { name: '1' })) await userEvent.click(screen.getByRole('button', { name: '+' })) await userEvent.click(screen.getByRole('button', { name: '2' })) await userEvent.click(screen.getByRole('button', { name: '=' })) expect(getByText('3')).toBeInTheDocument() }) // 4. E2E Tests (using Playwright) await page.goto('/calculator') await expect(page.getByRole('button', { name: '0' })).toBeInTheDocument() await page.getByRole('button', { name: '1' }).click() await page.getByRole('button', { name: '+' }).click() await page.getByRole('button', { name: '2' }).click() await page.getByRole('button', { name: '=' }).click() await expect(page.getByRole('button', { name: '3' })).toBeInTheDocument() // ❌ Avoid // Don't write E2E tests for everything test('every button click updates display', () => { render(<Calculator />) // Testing every possible button combination... }) ``` ### Use Appropriate Queries Follow the query priority order and avoid using container queries: ```tsx // ✅ Good screen.getByRole('textbox', { name: /username/i }) // ❌ Avoid screen.getByTestId('username') container.querySelector('.btn-primary') ``` ### Use Query Variants Correctly Only use query\* variants for checking non-existence: ```tsx // ✅ Good expect(screen.queryByRole('alert')).not.toBeInTheDocument() // ❌ Avoid expect(screen.queryByRole('alert')).toBeInTheDocument() ``` ### Use find\* Over waitFor for Elements Use find\* queries instead of waitFor for elements that may not be immediately available: ```tsx // ✅ Good const submitButton = await screen.findByRole('button', { name: /submit/i }) // ❌ Avoid const submitButton = await waitFor(() => screen.getByRole('button', { name: /submit/i }), ) ``` ### Avoid Testing Implementation Details Test components based on how users interact with them, not implementation details: ```tsx // ✅ Good test('User can add items to cart', async () => { render(<ProductList />) await userEvent.click(screen.getByRole('button', { name: /add to cart/i })) await expect(screen.getByText(/1 item in cart/i)).toBeInTheDocument() }) // ❌ Avoid test('Cart state updates when addToCart is called', () => { const { container } = render(<ProductList />) const addButton = container.querySelector('[data-testid="add-button"]') fireEvent.click(addButton) expect( container.querySelector('[data-testid="cart-count"]'), ).toHaveTextContent('1') }) ``` ### Use userEvent Over fireEvent Use userEvent over fireEvent for more realistic user interactions: ```tsx // ✅ Good await userEvent.type(screen.getByRole('textbox'), 'Hello') // ❌ Avoid fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Hello' } }) ``` ## Misc ### File naming Use kebab-case for file names. ```tsx // ✅ Good import { HighlightButton } from './highlight-button' // ❌ Avoid import { HighlightButton } from './HighlightButton' ``` It makes things work consistently on Windows and Unix-based systems.