- Notifications
You must be signed in to change notification settings - Fork0
An opinionated React + TypeScript style guide for writing scalable, maintainable, and consistent code.
License
mlane/react-typescript-style-guide
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
"Any fool can write code that a computer can understand. Good programmers write code that humans canunderstand."
—Martin Fowler
Astructured, scalable, and opinionated style guide for buildingmaintainable React applications withTypeScript. This guide ensuresconsistency, 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 ensureconsistency, readability, and maintainability in React + TypeScript projects.By following a structured approach, we aim to reduce cognitive load, improve collaboration, and make codebases easier toscale.
Minimal Mental Overhead
Code should beeasy to scan and understand without requiring excessive comments or context switching. Developersshould be able to predict where things are located and how they are structured.Predictability
Every file and component follows aconsistent structure, minimizing 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 writingcode but to ensure that code isuniform and easy to maintain.Encapsulation
Each feature should beself-contained, meaning components, hooks, and utilities related to a feature should livein the same folder. This improves modularity and reduces cross-dependencies.No Unnecessary Abstraction
Over-engineering leads toharder-to-read code. We avoid unnecessary wrapper functions, excessive prop drilling,and premature optimizations unless there is aclear need for them.Early Returns for Simplicity
When dealing with conditionals, wereturn early toreduce nesting and improve readability.Separation of Concerns
Logic, UI, and state management should beproperly separated to improve maintainability. Business logic shouldlive in hooks or utility functions rather than in the UI layer.
By following this guide, teams can writecleaner, more scalable, and easier-to-maintain code. The focus is onconsistency, clarity, and minimal cognitive load while following modern React + TypeScript best practices.
Astructured, feature-based folder organization ensuresscalability, maintainability, and readability. Thisstructure keeps related filesencapsulated while providing clear separation betweenshared logic andfeature-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: Whilethere's no strict limit, keepingfeatures within three levels(
pages/profile/common/ProfileHero/
) improves maintainability.
- Each feature has its own folder inside
common/
for shared logic- Storesshared UI components, hooks, and utilities.
- Example:
common/hooks/useFlag.ts
is reusable across multiple features.
Application-wide configurations in
config/
- This folder isonly for external integrations (e.g., Google Maps, Firebase, Analytics).
- Example:
config/apollo/ApolloProvider.tsx
.
Global constants & utils (if needed) in
constants/
- This folder is forapp-wide constants and utilities that are used inmultiple features.
- If a constant or utilityis 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 utilities should remain inside therespective 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
. - Makesauditing API calls easier without deep nesting.
- Example:
Standardized
index.ts
Usage- Wherever applicable, folders should contain an
index.ts
file to simplify imports. - This allowscleaner 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
Usingbarrel 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 alarge number of exports, making tree-shaking less effective.
- ❌ If the file isfrequently updated, causing unnecessary rebuilds.
- ❌ When usingRemix 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.
- Preferexplicit imports over
import * as X
to avoid unintended re-exports. - Keep barrel filesfeature-scoped rather than app-wide.
- Be mindful of howtree-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.
- Wildcard imports (
import *
) force the bundler to include unused code, increasing bundle size and reducing tree-shaking efficiency.
import*asutilsfrom‘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 remainself-contained, making it easy to expand the app.
- ✅Encapsulation → Keeps related filestogether, reducing unnecessary dependencies.
- ✅Readability → Developers can quickly find what they needwithout deep nesting.
- ✅Predictability → Standardized naming and placementeliminate confusion.
A well-structured React component improvesreadability, maintainability, and consistency. This section defineshowcomponents should be structured, includingordering 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 handleone 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
exportconstProfile=()=>{constnavigate=useNavigate()const{ accountHandle}=useParams()const{ hasError, isLoading, profileData}=useGetProfileQuery(accountHandle)const[searchParams]=useSearchParams()const{ id, image}=profileData??{}useEffect(()=>{// Example: Track analytics},[])constgetProfileAvatar=()=>{}constgetProfileName=()=>{}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 improvesreadability and scanning by making the function’s return statement stand out.
- It helps maintain consistency across the codebase.
✅ Example:
exportconstProfile=()=>{const{ hasError, isLoading, profileData}=useGetProfileQuery()if(isLoading||isEmpty(profileData))return<ProfileLoading/>if(hasError)return<ProfileEmpty/>return(<section><ProfileHero/><div><ProfileSidebar/><ProfileContent/></div></section>)}
❌ Avoid crammingreturn
right after logic without spacing.
exportconstProfile=()=>{const{ hasError, isLoading, profileData}=useGetProfileQuery()if(isLoading||isEmpty(profileData))return<ProfileLoading/>if(hasError)return<ProfileEmpty/>return(<section><ProfileHero/><div><ProfileSidebar/><ProfileContent/></div></section>)}
exportconstProfile=()=>{const{ hasError, isLoading, profileData}=useGetProfileQuery()return(<section><ProfileHero/><div><ProfileSidebar/><ProfileContent/></div></section>)}
When returning JSX in functional components, maintainconsistent 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
exportconstProfile=()=>{const{ hasError, isLoading, profileData}=useGetProfileQuery()if(isLoading)return<ProfileLoading/>if(hasError)return<ProfileError/>return(<section><ProfileHero/><ProfileContent/></section>)}
❌ Example: Incorrect Formatting
exportconstProfile=()=>{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, alwaysreturn early in conditionals rather than nesting them inside larger blocks.
✅Example: Using Early Return for Cleaner Code
exportconstProfile=()=>{const{ hasError, isLoading, profileData}=useGetProfileQuery()if(isLoading)return<ProfileLoading/>if(hasError)return<ProfileEmpty/>return(<section><ProfileHero/><ProfileContent/></section>)}
❌ Example: Nested Conditionals (Harder to Read)
exportconstProfile=()=>{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.
exportconstProfile=()=><section>...</section>
- Use multiple lines for JSX if it improves readability.
exportconstProfile=()=>(<section><ProfileHero/><ProfileSidebar/></section>)
- No extra space between hooks and variables.
constnavigate=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.
constgetProfileAvatar=()=>{}constgetProfileName=()=>{}
- Space out
useEffect
from other hooks.
constnavigate=useNavigate()const{ accountHandle}=useParams()useEffect(()=>{// Example: Sync data on mount},[])
- Use
PascalCase
for component names.
exportconstProfileHero=()=><div>Profile Hero</div>
- Use
camelCase
for non-component functions.
constgetProfileName=()=>{}
- Loading states should mirror the component structure but with skeleton placeholders.
- A
ProfileLoading.tsx
should matchProfile.tsx
and replace dynamic content with skeletons.
exportconstProfile=()=>(<sectionclassName='bg-red'><ProfileHero/><div><ProfileSidebar/><ProfileContent/><Button>Click me</Button></div></section>)
exportconstProfileLoading=()=>(<sectionclassName='bg-red'><ProfileHeroLoading/><div><ProfileSidebarLoading/><ProfileContentLoading/><divclassName='h-12 w-20'><Skeletonvariant='rounded'/></div></div></section>)
A component should besplit into smaller components if:
- ✅ It exceeds150 lines.
- ✅ Ithandles multiple responsibilities (e.g., UI and state logic).
- ✅ Itcontains deeply nested JSX.
- ✅ Itrepeats 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 defineswhere and how utility functions should be structured to ensurereadability andmaintainability.
- 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 utilitiesconstants/userUtils.ts # Shared utilities across features
✅Example: Exporting Multiple Utilities
constgetProfileAvatar=()=>{}constgetProfileName=()=>{}export{getProfileAvatar,getProfileName}
- Avoid unnecessary function nesting → Functions should beflat and readable, avoiding deeply nested logic.
❌Bad Example (Unnecessary Nesting)
constgetUserDetails=user=>{if(user){return{id:user.id,name:user.name,email:user.email,}}else{returnnull}}
✅Good Example (Flat and Readable)
constgetUserDetails=user=>{if(!user)returnnullreturn{id:user.id,name:user.name,email:user.email,}}
Return statements inside functions followconsistent 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)
constgetProfileRole=(profileData:ProfileData)=>{if(!profileData?.id){console.warn('Profile data is missing ID')return'Guest'}returnprofileData.role}
✅ Example: Single-line early return (no extra space needed)
constgetProfileRole=(profileData:ProfileData)=>{if(!profileData?.id)return'Guest'returnprofileData.role}
✅ Example: Returning directly in a function with no logic
constgetProfileName=(profileData:ProfileData)=>`${profileData.firstName}${profileData.lastName}`
✅ Example: if appears in the middle of the function (needs a blank line before it)
constgetProfileName=(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
constgetProfileName=(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
constgetProfileName=(profileData:ProfileData)=>{return`${profileData.firstName}${profileData.lastName}`}
❌ Example: Extra blank line before an early return at the start of a function
constgetProfileName=(profileData:ProfileData)=>{if(!firstName||!lastName)return'Guest'return`${firstName}${lastName}`}
❌ Example: Single-line early return should stay inline
constgetProfileRole=(profileData:ProfileData)=>{if(!profileData?.id){return'Guest'}}
constgetProfileRole=(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-lineif 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 staysclean, 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 consistencyacross the application.
- Queries & Mutations should be placed in
hooks/
inside their respective feature folder
✅ Example:
src/pages/profile/hooks/useGetProfileQuery.ts # Feature-specific querysrc/pages/profile/hooks/useCreateProfileMutation.ts # Feature-specific mutationsrc/hooks/useGetPredefinedGuideTagsQuery.ts # Sitewide query (used across features)
- Use camelCase for variables inside GraphQL operations to maintain consistency with JavaScript/TypeScript namingconventions.
- Operation name should be based on the data being fetched/updated, ensuring consistency with file & function names.
✅ Example:
queryGetProfileQueryInProfile($id:ID!){ ...}
- Sort fields alphabetically, except for
id
, which should always be listed first as the primary identifier forconsistency 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 themfrom 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 Profilesrc/pages/profile/hooks/useUpdateProfileMutation.ts # Mutation used only in Profile
✅ Query Example:
queryGetProfileQueryInProfile($id:ID!){node(id:$id){ ...onProfile{idaccountHandledisplayNameimage}}}
- Queries thatare used across multiple features shouldnot 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:
queryGetPredefinedGuideTags{predefinedGuideTags{idname}}
- Feature-Based Queries Include Feature Name
- Queries scoped to a feature include
In{featureName}
(e.g.,GetProfileQueryInProfile
) toprevent namecollisions. - 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 isredundant. - By naming the file useGetPredefinedGuideTagsQuery.ts and using the operation name GetPredefinedGuideTags, weavoidthe 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 theyare sitewide.- ✅ Example:
useUpdateProfileMutation.ts
- ✅ Example:
- Feature mutations follow the same naming rule as feature queries, including
In{featureName}
unless they aresitewide. - We avoid “QueryQuery” in auto-generated types by keeping the operation name clean.
- We use PascalCase for hook return types, following
Use{QueryName}Return
(e.g.,UseGetProfileQueryReturn
).
import{gql,useQuery}from'@apollo/client'typeUseGetProfileQueryReturn={hasError:ApolloErrorisLoading:booleanprofileData:Extract<GetProfileQueryInProfileQuery['node'],{__typename?:'Profile'}>}constprofileQuery=gql(` query GetProfileQueryInProfile($id: ID!) { node (id: $id) { ... on Profile { id accountHandle displayName image } } }`)exportconstuseGetProfileQuery=(id:string):UseGetProfileQueryReturn=>{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'constupdateProfileMutation=gql(` mutation UpdateProfileMutationInProfile($updateProfileInput: UpdateProfileInput!) { updateProfile(updateProfileInput: $updateProfileInput) { id displayName } }`)exportconstuseUpdateProfileMutation=()=>useMutation(updateProfileMutation)
exportconstProfileForm=()=>{const[updateProfile,updateProfileResult]=useUpdateProfileMutation()constonSubmit=async(id:string,displayName:string)=>{try{awaitupdateProfile({variables:{updateProfileInput:{ displayName, id,},},})}catch(error){console.error('Failed to update profile',error)}}return(<formonSubmit={onSubmit}><buttontype='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 toconditionally enable or disable features without deploying new code. This approach allowsforprogressive rollouts,A/B testing, andsafe feature releases.
Feature flags are managed usingtwo primary components:
Feature Flags Configuration (
featureFlags.ts
)- This file defines allavailable feature flags.
- Flags are stored as arecord of boolean values.
Feature Flag Hook (
useFlag.ts
)- Acustom hook to read feature flag values.
- Useslocal 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 arecentrally defined insrc/config/feature-flags/featureFlags.ts
. This ensures all available flagsare explicitly listed.
✅ Example: Defining Feature Flags
// src/config/feature-flags/featureFlags.tstypeFeatureFlagNames='profileHeroV2'|'profileV2'constfeatureFlags:Record<FeatureFlagNames,boolean>={profileHeroV2:false,profileV2:false,}exporttype{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.tsimport{useState,useEffect}from'react'importtype{FeatureFlagNames}from'src/config/feature-flags/featureFlags'import{useLocalStorageFlags}from'./useLocalStorageFlags'exportconstuseFlag=(flagKey:FeatureFlagNames|string):boolean=>{const[isFlagEnabled,setIsFlagEnabled]=useState(false)const[localFlags]=useLocalStorageFlags()useEffect(()=>{if(flagKeyinlocalFlags){const{[flagKey]:localStorageFlag}=localFlagssetIsFlagEnabled(String(localStorageFlag).toLowerCase()==='true')}},[flagKey,localFlags])returnisFlagEnabled}
✅ 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'exportconstProfile=()=>{constisProfileHeroV2Enabled=useFlag('profileHeroV2')return(<section>{isProfileHeroV2Enabled ?<ProfileHero/> :<ProfileHeroOld/>}</section>)}
Forlarger changes, such as enabling an entirely new Profile redesign, we rename the existing feature folder(profile) toprofile-old
and introduce a newprofile/
folder.
Then, inPageRoutes.tsx
, we dynamically choose which version ofProfile
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'exportconstPageRoutes=()=>{constisProfileV2Enabled=useFlag('profileV2')return(<ScrollToTop><Routes><Routeelement={<Home/>}path='/'/><Routeelement={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 beremoved once the feature is stable.
- New feature flags must be added to featureFlags.ts
- This ensuresvisibility and prevents unexpected feature toggles.
- Use feature flags only for meaningful toggles
- Feature flags should be reserved forsubstantial feature rollouts, experiments, or redesigns—not minor UI tweaks.
- Local storage overrides take precedence
- Developers canmanually 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.
Aconsistent approach to defining types and interfaces ensuresclarity, maintainability, and flexibility acrossthe codebase.
Use
interface
for functional components.- Interfaces provide better readability when definingprops for components.
- Ensures aclear contract for component usage.
- Supportsextending when needed.
Use
type
for everything else.type
provides better flexibility, particularly when definingutility types, hooks, function return values, andGraphQL queries.- Use
Extract<>
when working withGraphQL queries that return multiple types, ensuring type safety whileextracting aspecific expected type from a union.
Use
Pick<>
andOmit<>
to create subsets of types.Pick<>
is used when selectingonly specific properties from a type.Omit<>
is used when removingspecific properties from a type.
✅Example: Functional Component Props
interfaceProfileHeroProps{onClick:()=>voidtitle:string}exportconstProfileHero=({ onClick, title}:ProfileHeroProps)=>(<divonClick={onClick}>{title}</div>)
✅ Example: Extending an Interface
Useinterface
to extend props cleanly, while type uses&
for merging multiple types.
import{Button}from'@travelpass/design-system'importtype{GenericAddress}from'src/__generated__/graphql'interfaceProfileAddressPropsextendsGenericAddress{onClick:VoidFunction}exportconstProfileAddress=({ addressLine1, city, country, onClick,}:ProfileAddressProps)=>(<section><h2>{name}</h2><p>{getAddress(addressLine1,city,country)}</p><ButtononClick={onClick}>Edit</Button></section>)
UsePick<>
when selecting only specific properties from a type, andOmit<>
when removing specific properties.
These help create lightweight, flexible types for better reusability.
✅ Example: Utility Type for Query Results
typeUseGetProfileQueryReturn={hasError:ApolloErrorisLoading:booleanprofileData:Extract<GetProfileQueryInProfileQuery['node'],{__typename?:'Profile'}>}
✅ Example: Extracting Only Specific Keys from an Object
typeUserKeys='id'|'email'typeUserInfo=Pick<User,UserKeys>
✅ Example: Omitting Unnecessary Fields from an Object
typeUser={id:stringemail:stringpassword:string}typePublicUser=Omit<User,'password'>
✅ Example: Combining Multiple Types
Use&
to merge multiple types, providing more flexibility thaninterface
extension.
typeBase={createdAt:string}typeProfile={id:stringname:string}typeProfileWithBase=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
typeUseGetProfileQueryReturn={hasError:ApolloErrorisLoading:booleanprofileData:Extract<GetProfileQueryInProfileQuery['node'],{__typename?:'Profile'}>}
❌ Bad Example: Using interface for Utility Types
interfaceUseGetProfileQueryReturn{hasError:ApolloErrorisLoading:booleanprofileData:Profile}
✅ Good Example: Using type for Flexibility
typeUseGetProfileQueryReturn={hasError:ApolloErrorisLoading:booleanprofileData: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.
Aminimalist approach to comments ensures code isclean, readable, and self-explanatory. Instead of excessivecommenting, we prioritizedescriptive function and variable names. Comments are usedonly when necessary, suchas forcomplex logic, workarounds, or TODOs.
Favor meaningful variable and function names over comments.
- Code shouldexplain 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 detailedexplanation.- 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 JSDocsparingly 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 beabove the function or logic they reference.
- Prefercompact, one-line comments whenever possible.
- We use
Use inline
//
comments for workarounds or technical limitations.- These should beshort and placeddirectly above the relevant line.
Avoid excessive commenting.
- Only document "why" something is done, not"what" the code does.
Weonly use JSDoc (/** @todo */
) for tracking future work.
✅ Example: JSDoc TODO for Future Enhancements
/**@todo Update this when the new API version is available */constgetUserPreferences=async(userId:string)=>{try{returnawaitfetch(`/api/preferences/${userId}`)}catch(error){console.error(error)returnnull}}
❌ Avoid Unnecessary TODO Comments
This format is not compatible with JSDoc linters.
//@todo Update this when the new API version is availableconstgetUserPreferences=async(userId:string)=>{try{returnawaitfetch(`/api/preferences/${userId}`)}catch(error){console.error(error)returnnull}}
💡 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
constscrollToTop=()=>{window.scrollTo(0,0)// Safari requires a slight delay for smooth scrollingsetTimeout(()=>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 */constscrollToTop=()=>{window.scrollTo(0,0)setTimeout(()=>window.scrollTo(0,0),10)}
❌ Avoid Redundant Comments
constscrollToTop=()=>{// Scrolls to the top of the pagewindow.scrollTo(0,0)}
💡 Key Difference:
- ✅ Comments should explain “why” a workaround is needed.
- ❌ Avoid stating what the code already makes obvious.
ForuseEffect
, prefer extracting logic into functions instead of writing comments inline.
✅ Example: Extracting Logic Into a Function
useEffect(()=>{syncUserPreferences()},[])constsyncUserPreferences=async()=>{try{/**@todo Remove this workaround when the API provides real-time updates */constpreferences=awaitgetUserPreferences(user.id)applyUserPreferences(preferences)}catch(error){console.error(error)}}
❌ Example of an Overloaded useEffect with Comments
useEffect(()=>{// Fetch user preferences and apply themfetch(`/api/preferences/${user.id}`).then(res=>res.json()).then(preferences=>{// Apply user preferencesapplyUserPreferences(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?
- Ifno, rename the function instead of adding a comment.
- Is this logic unavoidable or non-obvious?
- Ifyes, add an inline comment.
- Is this a workaround for a browser quirk or API limitation?
- Ifyes, 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 thisrepository.
Fork the Repository
- Click theFork 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.gitcd 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 theoriginal repository on GitHub.
- ClickCompare & pull request.
- Add aclear description of your changes.
- ClickCreate 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 theMIT License.
You arefree to use, modify, distribute, and share this project with no restrictions, as long as the originallicense and copyright notice are included.
The full license text is available in theLICENSE.md
file.
This style guide followswidely accepted industry standards while maintaining aminimal, structured, andopinionated approach. Below are key resources thatalign with and support the philosophy, structure, and bestpractices outlined in this guide.
Each of the following referencesshares core principles with this style guide, such asclarity, maintainability,predictability, and reducing complexity.
Reference | Link | How It Relates |
---|---|---|
Google TypeScript Style Guide | Google TypeScript Guide | ✅Readability & maintainability viaconsistent naming, structured function ordering, and predictable patterns. ✅ Aligns withComponent Order andSeparation of Concerns principles. |
Airbnb React/JSX Style Guide | Airbnb React Guide | ✅ Focuses onself-contained components, logical function ordering, and clean JSX formatting. ✅ Strongly aligns withComponent Structure—especiallyhooks, variables, and function organization. |
Shopify JavaScript & TypeScript Guide | Shopify JavaScript & TypeScript Guide | ✅ Encouragesfeature-based folder structure, aligning withFolder Structure. ✅ Supportsencapsulating GraphQL queries within feature folders, similar to ourGraphQL Queries section. |
TS.dev TypeScript Style Guide | TS.dev Guide | ✅ Emphasizesclarity and minimalism, reinforcingNo Unnecessary Abstraction. ✅ Aligns withusing interfaces for components and types for utilities/hooks. |
TypeScript Deep Dive Style Guide | TypeScript Deep Dive | ✅ Advocatespredictable, structured code organization andexplicit return types. ✅ Aligns withTypes & Interfaces, particularlyExtract<>, Pick<>, and Omit<> usage. |
This style guide followsindustry best practices while taking aminimalist approach to ensurescalability,predictability, and maintainability.
By adopting these conventions, youensure consistency across projects while writingmodern, well-structuredReact + TypeScript code.
🚀Thank you for following this guide! Your contributions help keep codebases clean, readable, and scalable.
About
An opinionated React + TypeScript style guide for writing scalable, maintainable, and consistent code.
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.