"Any fool can write code that a computer can understand. Good programmers write code that humans can understand."
β Martin Fowler
A structured, scalable, and opinionated style guide for building maintainable React applications with TypeScript. This guide ensures consistency, clarity, and best practices across projects.
- Philosophy
- Folder Structure
- Component Structure
- Functions & Utilities
- GraphQL Queries
- Feature Flags
- Types & Interfaces
- Comments & Documentation
- Contributing
- License
- References & Inspirations
This style guide is designed to ensure consistency, readability, and maintainability in React + TypeScript projects. By following a structured approach, we aim to reduce cognitive load, improve collaboration, and make codebases easier to scale.
-
Minimal Mental Overhead
Code should be easy to scan and understand without requiring excessive comments or context switching. Developers should be able to predict where things are located and how they are structured. -
Predictability
Every file and component follows the same structure, reducing ambiguity. Naming conventions, folder structures, and function placements should remain consistent across the entire codebase. -
Clarity Over Flexibility
While flexibility can be useful, clarity is prioritized. The goal is not to support every possible way of writing code but to ensure that code is uniform and easy to maintain. -
Encapsulation
Each feature should be self-contained, meaning components, hooks, and utilities related to a feature should live in the same folder. This improves modularity and reduces cross-dependencies. -
No Unnecessary Abstraction
Over-engineering leads to harder-to-read code. We avoid unnecessary wrapper functions, excessive prop drilling, and premature optimizations unless there is a clear need for them. -
Early Returns for Simplicity
When dealing with conditionals, we return early to reduce nesting and improve readability. -
Separation of Concerns
Logic, UI, and state management should be properly separated to improve maintainability. Business logic should live in hooks or utility functions rather than in the UI layer.
By following this guide, teams can write cleaner, more scalable, and easier-to-maintain code. The focus is on consistency, clarity, and minimal cognitive load while following modern React + TypeScript best practices.
A structured, feature-based folder organization ensures scalability, maintainability, and readability. This structure keeps related files encapsulated while providing clear separation between shared logic and feature-specific implementations.
-
Feature-based structure (
pages/featureName/
)- Each feature has its own folder inside
pages/
.- Example:
pages/profile/
contains all Profile-related logic.
- Example:
- Hooks related to a specific feature must be placed inside
hooks/
within that featureβs folder.- Example:
pages/profile/hooks/useGetProfileQuery.ts
for a Profile-specific query. - Hooks shared across multiple features should remain in
common/hooks/
.
- Example:
- Recommended depth: While there's no strict limit, keeping features within three levels
(
pages/profile/common/ProfileHero/
) improves maintainability.
- Each feature has its own folder inside
-
common/
for shared logic- Stores shared UI components, hooks, and utilities.
- Example:
common/hooks/useFlag.ts
is reusable across multiple features.
-
Application-wide configurations in
config/
- This folder is only for external integrations (e.g., Google Maps, Firebase, Analytics).
- Example:
config/apollo/ApolloProvider.tsx
.
-
Global constants & utils (if needed) in
constants/
- This folder is for app-wide constants and utilities that are used in multiple features.
- If a constant or utility is used in more than one feature, move it here.
- Example:
constants/guideUtils.ts
containsgetGuideDetailsUrl
since it is used in multiple places (e.g., dashboard & profiles). - Feature-specific constants and utils should remain inside the feature folder (e.g.,
pages/profile/profileConstants.ts
).
-
Assets Handling
- Fonts remain in
assets/fonts/
for styling purposes. - Images belong in a separate repository, not within
src/
.
- Fonts remain in
-
GraphQL Queries/Mutations stay in the feature root (if needed)
- Example:
useGetProfileQuery.ts
,useCreateProfileMutation.ts
. - Makes auditing API calls easier without deep nesting.
- Example:
-
Standardized
index.ts
Usage- Wherever applicable, folders should contain an
index.ts
file to simplify imports. - This allows cleaner imports and prevents deep import paths.
- Example:
Instead of:
import { ProfileHero } from 'src/pages/profile/common'
import { ProfileHero } from 'src/pages/profile/common/ProfileHero/ProfileHero'
- Example:
- Recommended for:
- Feature directories (
pages/profile/index.ts
) - Common utilities and hooks (
common/hooks/index.ts
) - Nested components (
pages/profile/common/ProfileHero/index.ts
)
- Feature directories (
index.ts
files can improve import readability, but excessive use of barrel files can introduce problems such as unintended re-exports, circular dependencies, and inefficient bundling. This is especially relevant in SSR frameworks like Remix.
- Wherever applicable, folders should contain an
Using barrel files (index.ts
) can simplify imports and improve readability, but they should be used with caution. Overuse can lead to unintended re-exports, circular dependencies, and performance issues in certain frameworks like Remix.
- β
When grouping related exports within a feature (
pages/profile/index.ts
). - β
When creating a clean API for shared utilities (
common/hooks/index.ts
). - β When improving import readability by reducing deep paths.
- β If the file contains a large number of exports, making tree-shaking less effective.
- β If the file is frequently updated, causing unnecessary rebuilds.
- β When using Remix and other SSR frameworks, as barrel files can cause issues with bundling.
- β While
index.ts
files help simplify imports, overusing them as barrel files (re-exporting everything) can lead to unintended re-exports, circular dependencies, and inefficient bundling. This is particularly problematic in SSR frameworks like Remix, where improper tree-shaking can increase load times.
- Prefer explicit imports over
import * as X
to avoid unintended re-exports. - Keep barrel files feature-scoped rather than app-wide.
- Be mindful of how tree-shaking works in the bundler to avoid unnecessary imports.
β Avoid wildcard (*) imports as they increase bundle size, prevent effective tree-shaking, and can introduce unnecessary dependencies.
- Import unnecessary code, even if unused.
- Make code harder to track, increasing debugging complexity.
import * as utils from βcommon/utilsβ
β Prefer named imports for clarity and tree-shaking
import { formatDate, getUserProfile } from βcommon/utilsβ
app/
βββ routes/
βββ src/
βββ assets/
β βββ fonts/
β β βββ roboto-bold.woff
β β βββ roboto-bold.woff2
βββ common/
β βββ components/
β βββ hooks/
β β βββ index.ts
β β βββ useFlag.ts
βββ config/ # External service integrations only
β βββ analytics/
β β βββ index.ts
β β βββ useLucencyNumber.ts
β βββ apollo/
β β βββ ApolloProvider.tsx
β β βββ index.ts
βββ constants/ # Global constants/utils (if used in multiple features)
β βββ guideUtils.ts # Example: Used in multiple features
β βββ index.ts
β βββ user.ts
βββ pages/
β βββ guide/
β β βββ __tests__/
β β β βββ __mocks__/
β β β β βββ index.ts
β β β β βββ guideMock.ts
β β β βββ Guide.test.tsx
β β β βββ guideUtils.test.ts
β β βββ common/
β β β βββ __tests__/
β β β β βββ GuideBadge.test.tsx
β β β βββ GuideHero/
β β β β βββ __tests__/
β β β β β βββ GuideHero.test.tsx
β β β β βββ GuideHero.tsx
β β β β βββ GuideHeroLoading.tsx
β β β β βββ index.ts
β β β βββ GuideBadge.tsx
β β β βββ GuideLoading.tsx
β β β βββ index.ts
β β βββ hooks/
β β β βββ index.ts
β β β βββ useCreateGuideMutation.ts
β β β βββ useGetGuideQuery.ts
β β β βββ useUpdateGuideMutation.ts
β β βββ Guide.tsx
β β βββ index.ts # For cleaner imports
β β βββ guideConstants.ts (if needed)
β β βββ guideUtils.ts (if needed)
β β βββ types.ts (if needed)
β βββ profile/
β β βββ __tests__/
β β β βββ __mocks__/
β β β β βββ index.ts
β β β β βββ profileMock.ts
β β β βββ Profile.test.tsx
β β β βββ profileUtils.test.ts
β β βββ common/
β β β βββ __tests__/
β β β β βββ ProfileHero.test.tsx
β β β βββ ProfileHero/
β β β β βββ ProfileHero.tsx
β β β β βββ ProfileHeroLoading.tsx
β β β β βββ index.ts
β β β βββ ProfileLoading.tsx
β β β βββ ProfileSidebar/
β β β β βββ ProfileSidebar.tsx
β β β β βββ ProfileSidebarLoading.tsx
β β β β βββ index.ts
β β β βββ index.ts
β β βββ hooks/
β β β βββ index.ts
β β β βββ useCreateProfileMutation.ts
β β β βββ useGetProfileQuery.ts
β β βββ Profile.tsx
β β βββ index.ts # For cleaner imports
β β βββ profileConstants.ts (if needed)
β β βββ profileUtils.ts (if needed)
β β βββ types.ts (if needed)
- β Scalability β Features remain self-contained, making it easy to expand the app.
- β Encapsulation β Keeps related files together, reducing unnecessary dependencies.
- β Readability β Developers can quickly find what they need without deep nesting.
- β Predictability β Standardized naming and placement eliminate confusion.
A well-structured React component improves readability, maintainability, and consistency. This section defines how components should be structured, including ordering hooks, variables, functions, and the return statement.
- Always use functional components (
const MyComponent = () => {}
). - Component file names should match the folder name if applicable.
- Example:
ProfileHero.tsx
insideProfileHero/
should match the folder name.
- Example:
- Each component should be self-contained and only handle one responsibility.
- Avoid deep nesting of JSXβbreak into smaller components when necessary.
- Keep components under ~150 lines for readability.
- Early return for loading/error states to reduce indentation.
- Hooks, variables, and functions should follow a consistent order.
Components should follow this order:
- 1οΈβ£ Hooks (
useState
,useEffect
, etc.). - 2οΈβ£ Variables that are not functions (local variables, constants, etc.).
- 3οΈβ£
useEffect
hooks (side effects). - 4οΈβ£ Functions (event handlers, derived functions, etc.).
- 5οΈβ£ Return statement (JSX).
β Example: Standard Component Structure
export const Profile = () => {
const navigate = useNavigate()
const { accountHandle } = useParams()
const { hasError, isLoading, profileData } = useGetProfileQuery(accountHandle)
const [searchParams] = useSearchParams()
const { id, image } = profileData ?? {}
useEffect(() => {
// Example: Track analytics
}, [])
const getProfileAvatar = () => {}
const getProfileName = () => {}
if (isLoading || isEmpty(profileData)) return <ProfileLoading />
if (hasError) return <ProfileEmpty />
return (
<section>
<ProfileHero />
<div>
<ProfileSidebar />
<ProfileContent />
</div>
</section>
)
}
- Always add a blank line before
return
to visually separate logic from JSX. - This improves readability and scanning by making the functionβs return statement stand out.
- It helps maintain consistency across the codebase.
β Example:
export const Profile = () => {
const { hasError, isLoading, profileData } = useGetProfileQuery()
if (isLoading || isEmpty(profileData)) return <ProfileLoading />
if (hasError) return <ProfileEmpty />
return (
<section>
<ProfileHero />
<div>
<ProfileSidebar />
<ProfileContent />
</div>
</section>
)
}
β Avoid cramming return
right after logic without spacing.
export const Profile = () => {
const { hasError, isLoading, profileData } = useGetProfileQuery()
if (isLoading || isEmpty(profileData)) return <ProfileLoading />
if (hasError) return <ProfileEmpty />
return (
<section>
<ProfileHero />
<div>
<ProfileSidebar />
<ProfileContent />
</div>
</section>
)
}
export const Profile = () => {
const { hasError, isLoading, profileData } = useGetProfileQuery()
return (
<section>
<ProfileHero />
<div>
<ProfileSidebar />
<ProfileContent />
</div>
</section>
)
}
When returning JSX in functional components, maintain consistent spacing for clarity and readability.
- Use early returns for loading and error states to reduce nesting.
- Single-line early returns should not have extra space before them.
- Multiline return blocks should always be formatted for readability.
- Return statements should not have an unnecessary empty line before them unless inside a conditional block.
β Example: Correct Formatting
export const Profile = () => {
const { hasError, isLoading, profileData } = useGetProfileQuery()
if (isLoading) return <ProfileLoading />
if (hasError) return <ProfileError />
return (
<section>
<ProfileHero />
<ProfileContent />
</section>
)
}
β Example: Incorrect Formatting
export const Profile = () => {
const { hasError, isLoading, profileData } = useGetProfileQuery()
if (isLoading) {
return <ProfileLoading />
}
if (hasError) {
return <ProfileEmpty />
}
return (
<section>
<ProfileHero />
<ProfileContent />
</section>
)
}
- One-liner early returns should not have extra space.
- Multiline return blocks should always be formatted for readability.
- Use separate lines when return statements are inside a block.
To improve readability and reduce indentation, always return early in conditionals rather than nesting them inside larger blocks.
β Example: Using Early Return for Cleaner Code
export const Profile = () => {
const { hasError, isLoading, profileData } = useGetProfileQuery()
if (isLoading) return <ProfileLoading />
if (hasError) return <ProfileEmpty />
return (
<section>
<ProfileHero />
<ProfileContent />
</section>
)
}
β Example: Nested Conditionals (Harder to Read)
export const Profile = () => {
const { hasError, isLoading, profileData } = useGetProfileQuery()
if (isLoading) {
return <ProfileLoading />
} else {
if (hasError) {
return <ProfileEmpty />
} else {
return (
<section>
<ProfileHero />
<ProfileContent />
</section>
)
}
}
}
- One-line return when there is no logic.
export const Profile = () => <section>...</section>
- Use multiple lines for JSX if it improves readability.
export const Profile = () => (
<section>
<ProfileHero />
<ProfileSidebar />
</section>
)
- No extra space between hooks and variables.
const navigate = useNavigate()
const { accountHandle } = useParams()
const { hasError, isLoading, profileData } = useGetProfileQuery(accountHandle)
const [searchParams] = useSearchParams()
const { id, image } = profileData ?? {}
- Add a space between function declarations for readability.
const getProfileAvatar = () => {}
const getProfileName = () => {}
- Space out
useEffect
from other hooks.
const navigate = useNavigate()
const { accountHandle } = useParams()
useEffect(() => {
// Example: Sync data on mount
}, [])
- Use
PascalCase
for component names.
export const ProfileHero = () => <div>Profile Hero</div>
- Use
camelCase
for non-component functions.
const getProfileName = () => {}
- Loading states should mirror the component structure but with skeleton placeholders.
- A
ProfileLoading.tsx
should matchProfile.tsx
and replace dynamic content with skeletons.
export const Profile = () => (
<section className='bg-red'>
<ProfileHero />
<div>
<ProfileSidebar />
<ProfileContent />
<Button>Click me</Button>
</div>
</section>
)
export const ProfileLoading = () => (
<section className='bg-red'>
<ProfileHeroLoading />
<div>
<ProfileSidebarLoading />
<ProfileContentLoading />
<div className='h-12 w-20'>
<Skeleton variant='rounded' />
</div>
</div>
</section>
)
A component should be split into smaller components if:
- β It exceeds 150 lines.
- β It handles multiple responsibilities (e.g., UI and state logic).
- β It contains deeply nested JSX.
- β It repeats similar JSX structures that could be reused.
- If a component is reused across multiple features, move it to
common/components/
. - If a component is only used within one feature, keep it inside that feature's folder.
β Example (Feature-Specific Component)
pages/profile/common/ProfileHero.tsx
β Example (Shared Component)
common/components/ImageWithFallback.tsx
- β Keeps component logic predictable and structured.
- β Encourages clean, readable JSX formatting.
- β Prevents unnecessarily large components.
- β Standardizes naming and file placement across the codebase.
This section defines where and how utility functions should be structured to ensure readability and maintainability.
- Feature-specific utilities should be inside a featureβs folder.
- Shared utilities across multiple features should be moved to
constants/featureUtils.ts
.
β Example: Utility Function Placement
pages/profile/profileUtils.ts # Feature-specific utilities
constants/userUtils.ts # Shared utilities across features
β Example: Exporting Multiple Utilities
const getProfileAvatar = () => {}
const getProfileName = () => {}
export { getProfileAvatar, getProfileName }
- Avoid unnecessary function nesting β Functions should be flat and readable, avoiding deeply nested logic.
β Bad Example (Unnecessary Nesting)
const getUserDetails = user => {
if (user) {
return {
id: user.id,
name: user.name,
email: user.email,
}
} else {
return null
}
}
β Good Example (Flat and Readable)
const getUserDetails = user => {
if (!user) return null
return {
id: user.id,
name: user.name,
email: user.email,
}
}
Return statements inside functions follow consistent spacing rules for readability.
- If an
if
statement is at the start of the function, do not add a blank line before it. - If an
if
statement appears in the middle of the function, add a blank line before it. - If an
if
statement contains multiple lines, place thereturn
on its own line. - Single-line early returns should remain inline unless additional logic is present.
- Do not add an extra blank line before the final return in a function.
β Example: Early return at the start of the function (no blank line)
const getProfileRole = (profileData: ProfileData) => {
if (!profileData?.id) {
console.warn('Profile data is missing ID')
return 'Guest'
}
return profileData.role
}
β Example: Single-line early return (no extra space needed)
const getProfileRole = (profileData: ProfileData) => {
if (!profileData?.id) return 'Guest'
return profileData.role
}
β Example: Returning directly in a function with no logic
const getProfileName = (profileData: ProfileData) => `${profileData.firstName} ${profileData.lastName}`
β Example: if appears in the middle of the function (needs a blank line before it)
const getProfileName = (profileData: ProfileData) => {
const { firstName, lastName } = profileData ?? {}
if (!firstName || !lastName) return 'Guest'
return `${firstName} ${lastName}`
}
β Example: Missing space before if when itβs in the middle of the function
const getProfileName = (profileData: ProfileData) => {
const { firstName, lastName } = profileData ?? {}
if (!firstName || !lastName) return 'Guest'
return `${firstName} ${lastName}`
}
β Example: Extra blank line before a return when itβs the only statement
const getProfileName = (profileData: ProfileData) => {
return `${profileData.firstName} ${profileData.lastName}`
}
β Example: Extra blank line before an early return at the start of a function
const getProfileName = (profileData: ProfileData) => {
if (!firstName || !lastName) return 'Guest'
return `${firstName} ${lastName}`
}
β Example: Single-line early return should stay inline
const getProfileRole = (profileData: ProfileData) => {
if (!profileData?.id) { return 'Guest' }
}
const getProfileRole = (profileData: ProfileData) => {
if (!profileData?.id) {
return 'Guest'
}
}
Case | Blank Line Before Return? |
---|---|
Single return as the only function statement | β No |
Early return at the start of a function | β No |
if appears in the middle of the function |
β Yes |
Final return in a function | β No |
Return inside a multi-line if block |
β Yes |
Return placement follows the same logic as variables:
- If an
if
appears in the middle of a function, add a blank line before it. - If an
if
is at the start of a function, no blank line is needed. - A multi-line
if
block always places the return on its own line. - A single-line early return remains inline unless thereβs additional logic.
By keeping returns structured and predictable, code stays clean, readable, and consistent across the project. π
- β Keeps the focus on utility function placement & formatting.
- β Removes redundancy with Component Structure.
- β Ensures consistent utility function placement across the project.
A structured approach to handling GraphQL queries and mutations ensures readability, maintainability, and consistency across the application.
- Queries & Mutations should be placed in
hooks/
inside their respective feature folder
β Example:
src/pages/profile/hooks/useGetProfileQuery.ts # Feature-specific query
src/pages/profile/hooks/useCreateProfileMutation.ts # Feature-specific mutation
src/hooks/useGetPredefinedGuideTagsQuery.ts # Sitewide query (used across features)
- Use camelCase for variables inside GraphQL operations to maintain consistency with JavaScript/TypeScript naming conventions.
- Operation name should be based on the data being fetched/updated, ensuring consistency with file & function names.
β Example:
query GetProfileQueryInProfile($id: ID!) { ... }
- Sort fields alphabetically, except for
id
, which should always be listed first as the primary identifier for consistency and quick reference. - GraphQL fields should match the query name for clarity.
- For sitewide queries, the operation name should remain generic and should not include
In{featureName}
.
To differentiate feature-specific GraphQL queries/mutations from global queries, we use a structured naming convention:
- Feature-specific queries & mutations should include
In{featureName}
in the operation name to differentiate them from sitewide queries and avoid naming conflicts. - File Placement: Should be placed within the feature folder inside
pages/featureName/
.
β Example:
src/pages/profile/hooks/useGetProfileQuery.ts # Query used only in Profile
src/pages/profile/hooks/useUpdateProfileMutation.ts # Mutation used only in Profile
β Query Example:
query GetProfileQueryInProfile($id: ID!) {
node(id: $id) {
... on Profile {
id
accountHandle
displayName
image
}
}
}
- Queries that are used across multiple features should not include the feature name in their operation.
- File Placement: These should be placed in
src/hooks/
.
β Example:
src/hooks/useGetPredefinedGuideTagsQuery.ts # Sitewide query
β Query Example:
query GetPredefinedGuideTags {
predefinedGuideTags {
id
name
}
}
- Feature-Based Queries Include Feature Name
- Queries scoped to a feature include
In{featureName}
(e.g.,GetProfileQueryInProfile
) to prevent name collisions. - This ensures clarity when multiple queries exist under the same feature.
- Queries scoped to a feature include
- Sitewide Queries Should Remain Generic
- If a query is used across multiple features, it should not include the feature name.
- This prevents unnecessary feature-specific naming for shared resources.
- Why We Avoid βQueryQueryβ
- If a query is called
GetPredefinedGuideTagsQuery
, the auto-generated type would beGetPredefinedGuideTagsQueryQuery
, which is redundant. - By naming the file useGetPredefinedGuideTagsQuery.ts and using the operation name GetPredefinedGuideTags, we avoid the unnecessary duplication.
- If a query is called
π Key Takeaways:
- If a query/mutation belongs to a single feature, its operation should include the feature name (e.g.,
GetProfileQueryInProfile
,UpdateProfileMutationInProfile
). - If a query/mutation is used across multiple features, its operation name should not include the feature name
(e.g.,
GetPredefinedGuideTags
,UpdateUserSettingsMutation
). - Feature-based queries & mutations should be placed inside
pages/featureName/
. - Sitewide queries & mutations should be placed in
src/hooks/
. - Mutations should always include βMutationβ in both the GraphQL operation name and the filename (e.g.,
useUpdateProfileMutation.ts
). Feature-based mutations follow the sameIn{featureName}
rule as queries unless they are sitewide.- β
Example:
useUpdateProfileMutation.ts
- β
Example:
- Feature mutations follow the same naming rule as feature queries, including
In{featureName}
unless they are sitewide. - We avoid βQueryQueryβ in auto-generated types by keeping the operation name clean.
- We use PascalCase for hook return types, following
Use{QueryName}Result
(e.g.,UseGetProfileQueryResult
).
import { gql, useQuery } from '@apollo/client'
type UseGetProfileQueryResult = {
hasError: ApolloError
isLoading: boolean
profileData: Extract<
GetProfileQueryInProfileQuery['node'],
{
__typename?: 'Profile'
}
>
}
const profileQuery = gql(`
query GetProfileQueryInProfile($id: ID!) {
node (id: $id) {
... on Profile {
id
accountHandle
displayName
image
}
}
}
`)
export const useGetProfileQuery = (id: string): UseGetProfileQueryResult => {
const {
data,
error: hasError,
loading: isLoading,
} = useQuery(profileQuery, {
variables: {
id,
},
})
return {
hasError,
isLoading,
profileData: data?.node,
}
}
- β CamelCase is used for variables (accountHandle, displayName, etc.).
- β id is prominently placed at the top for consistency.
- β
Query follows a predictable naming structure (
GetProfileQueryInProfile
). - β
Custom hook abstracts error and loading states for better readability (
hasError
,isLoading
).
import { gql, useMutation } from '@apollo/client'
const updateProfileMutation = gql(`
mutation UpdateProfileMutationInProfile($updateProfileInput: UpdateProfileInput!) {
updateProfile(updateProfileInput: $updateProfileInput) {
id
displayName
}
}
`)
export const useUpdateProfileMutation = () => useMutation(updateProfileMutation)
export const ProfileForm = () => {
const [updateProfile, updateProfileResult] = useUpdateProfileMutation()
const onSubmit = async (id: string, displayName: string) => {
try {
await updateProfile({
variables: {
updateProfileInput: {
displayName,
id,
},
},
})
} catch (error) {
console.error('Failed to update profile', error)
}
}
return (
<form onSubmit={onSubmit}>
<button type='submit'>Update Profile</button>
</form>
)
}
- β Mutation follows the naming pattern (UpdateProfileMutationInProfile).
- β Refetching the profile query ensures UI consistency.
- β Error and loading states are aliased as hasError and isLoading for better readability.
Feature flags enable us to conditionally enable or disable features without deploying new code. This approach allows for progressive rollouts, A/B testing, and safe feature releases.
Feature flags are managed using two primary components:
-
Feature Flags Configuration (
featureFlags.ts
)- This file defines all available feature flags.
- Flags are stored as a record of boolean values.
-
Feature Flag Hook (
useFlag.ts
)- A custom hook to read feature flag values.
- Uses local storage overrides, allowing developers to toggle features locally.
src/
βββ config/
β βββ feature-flags/
β β βββ featureFlags.ts # Central feature flag configuration
βββ common/
β βββ hooks/
β β βββ useFlag.ts # Hook to check feature flag status
Feature flags are centrally defined in src/config/feature-flags/featureFlags.ts
. This ensures all available flags
are explicitly listed.
β Example: Defining Feature Flags
// src/config/feature-flags/featureFlags.ts
type FeatureFlagNames = 'profileHeroV2' | 'profileV2'
const featureFlags: Record<FeatureFlagNames, boolean> = {
profileHeroV2: false,
profileV2: false,
}
export type { FeatureFlagNames }
export { featureFlags }
The useFlag hook retrieves the current state of a feature flag, checking for local storage overrides.
β Example: Feature Flag Hook
// src/common/hooks/useFlag.ts
import { useState, useEffect } from 'react'
import type { FeatureFlagNames } from 'src/config/feature-flags/featureFlags'
import { useLocalStorageFlags } from './useLocalStorageFlags'
export const useFlag = (flagKey: FeatureFlagNames | string): boolean => {
const [isFlagEnabled, setIsFlagEnabled] = useState(false)
const [localFlags] = useLocalStorageFlags()
useEffect(() => {
if (flagKey in localFlags) {
const { [flagKey]: localStorageFlag } = localFlags
setIsFlagEnabled(String(localStorageFlag).toLowerCase() === 'true')
}
}, [flagKey, localFlags])
return isFlagEnabled
}
β Example: Conditionally Rendering Components
Feature flags allow conditional rendering of components within a section.
import { useFlag } from 'src/common/hooks/useFlag'
import { ProfileHero } from './ProfileHero'
import { ProfileHeroOld } from './ProfileHeroOld'
export const Profile = () => {
const isProfileHeroV2Enabled = useFlag('profileHeroV2')
return (
<section>
{isProfileHeroV2Enabled ? <ProfileHero /> : <ProfileHeroOld />}
</section>
)
}
For larger changes, such as enabling an entirely new Profile redesign, we rename the existing feature folder
(profile) to profile-old
and introduce a new profile/
folder.
Then, in PageRoutes.tsx
, we dynamically choose which version of Profile
to render based on the feature flag.
β Example: Routing Feature Flag Usage
import { useFlag } from 'src/common/hooks/useFlag'
import { Routes, Route } from 'react-router-dom'
import { Home } from 'src/pages/home'
import { Profile } from 'src/pages/profile'
import { ProfileOld } from 'src/pages/profile-old'
export const PageRoutes = () => {
const isProfileV2Enabled = useFlag('profileV2')
return (
<ScrollToTop>
<Routes>
<Route element={<Home />} path='/' />
<Route
element={isProfileV2Enabled ? <Profile /> : <ProfileOld />}
path='/profile/:accountHandle'
/>
</Routes>
</ScrollToTop>
)
}
- Feature flags should be short-lived
- Avoid leaving feature flags in the codebase for an extended period.
- Flags should be removed once the feature is stable.
- New feature flags must be added to featureFlags.ts
- This ensures visibility and prevents unexpected feature toggles.
- Use feature flags only for meaningful toggles
- Avoid flagging trivial UI changes.
- Flags should be used for significant features, redesigns, or experiments.
- Local storage overrides take precedence
- Developers can manually toggle flags via local storage, making testing easier.
- Feature flags are stored in
src/config/feature-flags/featureFlags.ts
. - The
useFlag
hook checks feature flag values, including local storage overrides. - Flags can be used for component toggles (
ProfileHeroV2
) or route-based toggles (ProfileV2
). - Short-lived flags should be cleaned up after rollout is complete.
A consistent approach to defining types and interfaces ensures clarity, maintainability, and flexibility across the codebase.
-
Use
interface
for functional components.- Interfaces provide better readability when defining props for components.
- Ensures a clear contract for component usage.
- Supports extending when needed.
-
Use
type
for everything else.type
provides better flexibility, particularly when defining utility types, hooks, function return values, and GraphQL queries.- Use
Extract<>
when working with GraphQL queries that return multiple types, ensuring type safety while extracting a specific expected type from a union.
-
Use
Pick<>
andOmit<>
to create subsets of types.Pick<>
is used when selecting only specific properties from a type.Omit<>
is used when removing specific properties from a type.
β Example: Functional Component Props
interface ProfileHeroProps {
onClick: () => void
title: string
}
export const ProfileHero = ({ onClick, title }: ProfileHeroProps) => (
<div onClick={onClick}>{title}</div>
)
β Example: Extending an Interface
Use interface
to extend props cleanly, while type uses &
for merging multiple types.
import { Button } from '@travelpass/design-system'
import type { GenericAddress } from 'src/__generated__/graphql'
interface ProfileAddressProps extends GenericAddress {
onClick: VoidFunction
}
export const ProfileAddress = ({
addressLine1,
city,
country,
onClick,
}: ProfileAddressProps) => (
<section>
<h2>{name}</h2>
<p>{getAddress(addressLine1, city, country)}</p>
<Button onClick={onClick}>Edit</Button>
</section>
)
Use Pick<>
when selecting only specific properties from a type, and Omit<>
when removing specific properties.
These help create lightweight, flexible types for better reusability.
β Example: Utility Type for Query Results
type UseGetProfileQueryResult = {
hasError: ApolloError
isLoading: boolean
profileData: Extract<
GetProfileQueryInProfileQuery['node'],
{
__typename?: 'Profile'
}
>
}
β Example: Extracting Only Specific Keys from an Object
type UserKeys = 'id' | 'email'
type UserInfo = Pick<User, UserKeys>
β Example: Omitting Unnecessary Fields from an Object
type User = {
id: string
email: string
password: string
}
type PublicUser = Omit<User, 'password'>
β Example: Combining Multiple Types
Use &
to merge multiple types, providing more flexibility than interface
extension.
type Base = {
createdAt: string
}
type Profile = {
id: string
name: string
}
type ProfileWithBase = Profile & Base
- Use
Extract<>
to ensure type safety when selecting a specific type from a GraphQL query result.
β Example: Extracting the Profile Type from a Query
type UseGetProfileQueryResult = {
hasError: ApolloError
isLoading: boolean
profileData: Extract<
GetProfileQueryInProfileQuery['node'],
{
__typename?: 'Profile'
}
>
}
β Bad Example: Using interface for Utility Types
interface UseGetProfileQueryResult {
hasError: ApolloError
isLoading: boolean
profileData: Profile
}
β Good Example: Using type for Flexibility
type UseGetProfileQueryResult = {
hasError: ApolloError
isLoading: boolean
profileData: Profile
}
- Use interface for defining props in functional components.
- Use type for everything else (utilities, hooks, GraphQL queries, API responses).
- Use
Extract<>
to ensure type safety when narrowing GraphQL query results. - Keep types minimal and readableβavoid unnecessary abstractions.
A minimalist approach to comments ensures code is clean, readable, and self-explanatory. Instead of excessive commenting, we prioritize descriptive function and variable names. Comments are used only when necessary, such as for complex logic, workarounds, or TODOs.
-
Favor meaningful variable and function names over comments.
- Code should explain itself rather than rely on comments.
- If logic is unclear, refactor instead of adding a comment.
-
Use JSDoc (
@see
) when the workaround requires a reference link, external documentation, or detailed explanation.- JSDoc ensures proper referencing in documentation tools like TypeDoc.
- Example:
/** * Safari requires a slight delay for smooth scrolling. * @see https://stackoverflow.com/q/xxxx */
-
Use JSDoc (
@todo
) for marking future work.- We use
@todo
in JSDoc sparingly for tracking unfinished tasks. - If a task has a corresponding Linear issue, consider referencing it using
@todo Linear-123
.- Example:
/** @todo TMNT-123 Update this when the new API version is available */
- Example:
- JSDoc comments should be above the function or logic they reference.
- Prefer compact, one-line comments whenever possible.
- We use
-
Use inline
//
comments for workarounds or technical limitations.- These should be short and placed directly above the relevant line.
-
Avoid excessive commenting.
- Only document "why" something is done, not "what" the code does.
We only use JSDoc (/** @todo */
) for tracking future work.
β Example: JSDoc TODO for Future Enhancements
/** @todo Update this when the new API version is available */
const getUserPreferences = async (userId: string) => {
try {
return await fetch(`/api/preferences/${userId}`)
} catch (error) {
console.error(error)
return null
}
}
β Avoid Unnecessary TODO Comments
This format is not compatible with JSDoc linters.
// @todo Update this when the new API version is available
const getUserPreferences = async (userId: string) => {
try {
return await fetch(`/api/preferences/${userId}`)
} catch (error) {
console.error(error)
return null
}
}
π‘ Key Difference:
- β
Use
@todo
for JSDoc-style TODOs. - β Avoid inline
// @todo
comments.
JSDoc is more structured and aligns with tools that scan TODOs.
Use inline //
comments for technical workarounds, browser quirks, or unexpected API behavior.
β Example: Workaround for Safari Quirk
const scrollToTop = () => {
window.scrollTo(0, 0)
// Safari requires a slight delay for smooth scrolling
setTimeout(() => window.scrollTo(0, 0), 10)
}
β
Example: Workaround for Safari Quirk with @see
/**
* Safari requires a slight delay for smooth scrolling.
* @see https://stackoverflow.com/q/xxxx
*/
const scrollToTop = () => {
window.scrollTo(0, 0)
setTimeout(() => window.scrollTo(0, 0), 10)
}
β Avoid Redundant Comments
const scrollToTop = () => {
// Scrolls to the top of the page
window.scrollTo(0, 0)
}
π‘ Key Difference:
- β Comments should explain βwhyβ a workaround is needed.
- β Avoid stating what the code already makes obvious.
For useEffect
, prefer extracting logic into functions instead of writing comments inline.
β Example: Extracting Logic Into a Function
useEffect(() => {
syncUserPreferences()
}, [])
const syncUserPreferences = async () => {
try {
/** @todo Remove this workaround when the API provides real-time updates */
const preferences = await getUserPreferences(user.id)
applyUserPreferences(preferences)
} catch (error) {
console.error(error)
}
}
β Example of an Overloaded useEffect with Comments
useEffect(() => {
// Fetch user preferences and apply them
fetch(`/api/preferences/${user.id}`)
.then(res => res.json())
.then(preferences => {
// Apply user preferences
applyUserPreferences(preferences)
})
}, [])
π‘ Key Takeaway:
- β
Extract logic into functions rather than writing inline comments in
useEffect
. - β Long comments inside
useEffect
add clutter.
Before writing a comment, ask:
- Is the function name clear enough?
- If no, rename the function instead of adding a comment.
- Is this logic unavoidable or non-obvious?
- If yes, add an inline comment.
- Is this a workaround for a browser quirk or API limitation?
- If yes, a comment is useful.
- Avoid unnecessary commentsβfavor meaningful variable & function names.
- Use JSDoc
@todo
for tracking future work. - Use inline
//
comments only for workarounds or unexpected behavior. - Refactor first, comment as a last resort.
- If a
useEffect
is complex, extract logic into functions instead of writing comments.
Thank you for considering contributing to this project! We appreciate your help in improving and maintaining this repository.
-
Fork the Repository
- Click the Fork button on the top right of this repository.
- This will create a copy under your GitHub account.
-
Clone Your Fork
-
Run the following command to clone the forked repository:
git clone https://github.com/YOUR-USERNAME/react-typescript-style-guide.git cd react-typescript-style-guide
-
-
Make your changes in
main
- Open the project in your preferred editor.
- Make your changes while following the projectβs coding guidelines.
-
Commit your changes
git add . git commit -m "Describe your changes"
-
Push to your fork
git push origin main
-
Create a Pull Request
- Go to the original repository on GitHub.
- Click Compare & pull request.
- Add a clear description of your changes.
- Click Create pull request.
-
Keep PRs small and focused
- If your change is large, consider breaking it into smaller PRs.
-
Follow the existing code style
- Ensure your code follows formatting and linting rules.
-
Write clear commit messages
- Use concise descriptions like
Fix button alignment issue
orAdd support for dark mode
.
- Use concise descriptions like
-
Ensure your changes do not break existing functionality
- Test your changes before submitting.
-
Be respectful and collaborative
- We appreciate all contributions and encourage constructive feedback.
β Thank you for contributing! We appreciate your support in improving this project. π
This project is licensed under the MIT License.
You are free to use, modify, distribute, and share this project with no restrictions, as long as the original license and copyright notice are included.
The full license text is available in the LICENSE.md
file.
This style guide follows widely accepted industry standards while maintaining a minimal, structured, and opinionated approach. Below are key resources that align with and support the philosophy, structure, and best practices outlined in this guide.
Each of the following references shares core principles with this style guide, such as clarity, maintainability, predictability, and reducing complexity.
Reference | Link | How It Relates |
---|---|---|
Google TypeScript Style Guide | Google TypeScript Guide | β
Readability & maintainability via consistent naming, structured function ordering, and predictable patterns. β Aligns with Component Order and Separation of Concerns principles. |
Airbnb React/JSX Style Guide | Airbnb React Guide | β
Focuses on self-contained components, logical function ordering, and clean JSX formatting. β Strongly aligns with Component Structureβespecially hooks, variables, and function organization. |
Shopify JavaScript & TypeScript Guide | Shopify JavaScript & TypeScript Guide | β
Encourages feature-based folder structure, aligning with Folder Structure. β Supports encapsulating GraphQL queries within feature folders, similar to our GraphQL Queries section. |
TS.dev TypeScript Style Guide | TS.dev Guide | β
Emphasizes clarity and minimalism, reinforcing No Unnecessary Abstraction. β Aligns with using interfaces for components and types for utilities/hooks. |
TypeScript Deep Dive Style Guide | TypeScript Deep Dive | β
Advocates predictable, structured code organization and explicit return types. β Aligns with Types & Interfaces, particularly Extract<>, Pick<>, and Omit<> usage. |
This style guide follows industry best practices while taking a minimalist approach to ensure scalability, predictability, and maintainability.
By adopting these conventions, you ensure consistency across projects while writing modern, well-structured React + TypeScript code.
π Thank you for following this guide! Your contributions help keep codebases clean, readable, and scalable.