Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

An opinionated React + TypeScript style guide for writing scalable, maintainable, and consistent code.

License

NotificationsYou must be signed in to change notification settings

mlane/react-typescript-style-guide

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.

📖 Table of Contents


🧠 Philosophy

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.

🔹 Core Principles

  • 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.

✅ Summary

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.


📂 Folder Structure

Astructured, feature-based folder organization ensuresscalability, maintainability, and readability. Thisstructure keeps related filesencapsulated while providing clear separation betweenshared logic andfeature-specific implementations.

🔹 General Folder Structure

  • Feature-based structure (pages/featureName/)

    • Each feature has its own folder insidepages/.
      • Example:pages/profile/ contains all Profile-related logic.
    • Hooks related to a specific feature must be placed insidehooks/ within that feature’s folder.
      • Example:pages/profile/hooks/useGetProfileQuery.ts for a Profile-specific query.
      • Hooks shared across multiple features should remain incommon/hooks/.
    • Recommended depth: Whilethere's no strict limit, keepingfeatures within three levels(pages/profile/common/ProfileHero/) improves maintainability.
  • common/ for shared logic

    • Storesshared UI components, hooks, and utilities.
    • Example:common/hooks/useFlag.ts is reusable across multiple features.
  • Application-wide configurations inconfig/

    • This folder isonly for external integrations (e.g., Google Maps, Firebase, Analytics).
    • Example:config/apollo/ApolloProvider.tsx.
  • Global constants & utils (if needed) inconstants/

    • 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 inassets/fonts/ for styling purposes.
    • Images belong in a separate repository, not withinsrc/.
  • GraphQL Queries/Mutations stay in the feature root (if needed)

    • Example:useGetProfileQuery.ts,useCreateProfileMutation.ts.
    • Makesauditing API calls easier without deep nesting.
  • Standardizedindex.ts Usage

    • Wherever applicable, folders should contain anindex.ts file to simplify imports.
    • This allowscleaner imports and prevents deep import paths.
      • Example:
        import{ProfileHero}from'src/pages/profile/common'
        Instead of:
        import{ProfileHero}from'src/pages/profile/common/ProfileHero/ProfileHero'
    • Recommended for:
      • Feature directories (pages/profile/index.ts)
      • Common utilities and hooks (common/hooks/index.ts)
      • Nested components (pages/profile/common/ProfileHero/index.ts)
    • 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.

🔹 Barrel Files & Wildcard Imports

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 to Use Barrel Files

  • ✅ 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.

When to Avoid Barrel Files

  • ❌ 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.
  • ❌ Whileindex.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.

Best Practices

  • Preferexplicit imports overimport * 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)

🔹 Why This Works

  • 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.

🎭 Component Structure

A well-structured React component improvesreadability, maintainability, and consistency. This section defineshowcomponents should be structured, includingordering hooks, variables, functions, and the return statement.

🔹 General Rules for Components

  • 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.
  • 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.

🔹 Component 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>)}

🔹 Return Statement Spacing

  • Always add a blank line beforereturn 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>)}

🔹 Return Formatting in Functional Components

When returning JSX in functional components, maintainconsistent spacing for clarity and readability.

✅ General Rules:

  • 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>)}

✅ Summary

  • 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.

🔹 Early Returns for Simplicity

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>)}}}

🔹 JSX Formatting Rules

  • 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>)

🔹 Function & Hook Spacing Rules

  • 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 outuseEffect from other hooks.
constnavigate=useNavigate()const{ accountHandle}=useParams()useEffect(()=>{// Example: Sync data on mount},[])

🔹 Component Naming Conventions

  • UsePascalCase for component names.
exportconstProfileHero=()=><div>Profile Hero</div>
  • UsecamelCase for non-component functions.
constgetProfileName=()=>{}

🔹 Loading & Empty State Components

  • Loading states should mirror the component structure but with skeleton placeholders.
  • AProfileLoading.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>)

🔹 When to Split Components?

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.

🔹 When to Use acommon/ Component?

  • If a component is reused across multiple features, move it tocommon/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

🔹 Why This Works

  • Keeps component logic predictable and structured.
  • Encourages clean, readable JSX formatting.
  • Prevents unnecessarily large components.
  • Standardizes naming and file placement across the codebase.

⚡ Functions & Utilities

This section defineswhere and how utility functions should be structured to ensurereadability andmaintainability.


🔹 Utility Function Placement

  • Feature-specific utilities should be inside a feature’s folder.
  • Shared utilities across multiple features should be moved toconstants/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}

🔹 Formatting Rules for Functions

  • 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 Placement in Functions

Return statements inside functions followconsistent spacing rules for readability.

✅ General Rules

  • If anif statement is at the start of the function, do not add a blank line before it.
  • If anif statement appears in the middle of the function, add a blank line before it.
  • If anif 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'}}

🔹 Summary of Return Placement Rules

CaseBlank 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

🔥 Final Thoughts

Return placement follows the same logic as variables:

  • If anif appears in the middle of a function, add a blank line before it.
  • If anif is at the start of a function, no blank line is needed.
  • A multi-lineif 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. 🚀


🔹 Why This Works

  • Keeps the focus on utility function placement & formatting.
  • Removes redundancy with Component Structure.
  • Ensures consistent utility function placement across the project.

📡 GraphQL Queries

A structured approach to handling GraphQL queries and mutations ensures readability, maintainability, and consistencyacross the application.

🔹 General Rules for GraphQL Queries & Mutations

  • Queries & Mutations should be placed inhooks/ 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 forid, 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 includeIn{featureName}.

🔹 Feature-Based Queries vs. Sitewide Queries

To differentiate feature-specific GraphQL queries/mutations from global queries, we use a structured naming convention:

Feature-Based Queries & Mutations

  • Feature-specific queries & mutations should includeIn{featureName} in the operation name to differentiate themfrom sitewide queries and avoid naming conflicts.
  • File Placement: Should be placed within the feature folder insidepages/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}}}

Sitewide Queries & Mutations

  • Queries thatare used across multiple features shouldnot include the feature name in their operation.
  • File Placement: These should be placed insrc/hooks/.

✅ Example:

src/hooks/useGetPredefinedGuideTagsQuery.ts # Sitewide query

✅ Query Example:

queryGetPredefinedGuideTags{predefinedGuideTags{idname}}

🔹 Why This Naming Works

  • Feature-Based Queries Include Feature Name
    • Queries scoped to a feature includeIn{featureName} (e.g.,GetProfileQueryInProfile) toprevent namecollisions.
    • This ensures clarity when multiple queries exist under the same feature.
  • 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 calledGetPredefinedGuideTagsQuery, the auto-generated type would beGetPredefinedGuideTagsQueryQuery, which isredundant.
    • By naming the file useGetPredefinedGuideTagsQuery.ts and using the operation name GetPredefinedGuideTags, weavoidthe unnecessary duplication.

📌 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 insidepages/featureName/.
  • Sitewide queries & mutations should be placed insrc/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
  • Feature mutations follow the same naming rule as feature queries, includingIn{featureName} unless they aresitewide.
  • We avoid “QueryQuery” in auto-generated types by keeping the operation name clean.
  • We use PascalCase for hook return types, followingUse{QueryName}Return (e.g.,UseGetProfileQueryReturn).

🔹 Example: Query for Fetching Profile Data

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,}}

🔹 Why This Works

  • 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).

🔹 Example: Mutation for Updating Profile

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>)}

🔹 Why This Works

  • 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

Feature flags enable us toconditionally enable or disable features without deploying new code. This approach allowsforprogressive rollouts,A/B testing, andsafe feature releases.

🔹 General Structure

Feature flags are managed usingtwo primary components:

  1. Feature Flags Configuration (featureFlags.ts)

    • This file defines allavailable feature flags.
    • Flags are stored as arecord of boolean values.
  2. Feature Flag Hook (useFlag.ts)

    • Acustom hook to read feature flag values.
    • Useslocal storage overrides, allowing developers to toggle features locally.

📂 File Structure

src/├── config/│   ├── feature-flags/│   │   ├── featureFlags.ts    # Central feature flag configuration├── common/│   ├── hooks/│   │   ├── useFlag.ts         # Hook to check feature flag status

🔹 Feature Flags Configuration

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}

🔹 Accessing Feature Flags with useFlag

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}

🔹 Using Feature Flags in Components

✅ 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>)}

🔹 Using Feature Flags for Route-Based Feature Toggles

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 Flag Guidelines

  • 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.

✅ Summary

  • Feature flags are stored insrc/config/feature-flags/featureFlags.ts.
  • TheuseFlag 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.

🔠 Types & Interfaces

Aconsistent approach to defining types and interfaces ensuresclarity, maintainability, and flexibility acrossthe codebase.


🔹 General Rules

  • Useinterface for functional components.

    • Interfaces provide better readability when definingprops for components.
    • Ensures aclear contract for component usage.
    • Supportsextending when needed.
  • Usetype for everything else.

    • type provides better flexibility, particularly when definingutility types, hooks, function return values, andGraphQL queries.
    • UseExtract<> when working withGraphQL queries that return multiple types, ensuring type safety whileextracting aspecific expected type from a union.
  • UsePick<> 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.

🔹 Component Props: Useinterface

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>)

🔹 Utility Types: Usetype

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

🔹 When to UseExtract<> in GraphQL

  • UseExtract<> 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'}>}

🔹 Avoid Unnecessary interface Usage

❌ Bad Example: Using interface for Utility Types

interfaceUseGetProfileQueryReturn{hasError:ApolloErrorisLoading:booleanprofileData:Profile}

✅ Good Example: Using type for Flexibility

typeUseGetProfileQueryReturn={hasError:ApolloErrorisLoading:booleanprofileData:Profile}

✅ Summary

  • Use interface for defining props in functional components.
  • Use type for everything else (utilities, hooks, GraphQL queries, API responses).
  • UseExtract<> to ensure type safety when narrowing GraphQL query results.
  • Keep types minimal and readable—avoid unnecessary abstractions.

📝 Comments & Documentation

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.


🔹 General Rules

  • 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 */
    • JSDoc comments should beabove the function or logic they reference.
    • Prefercompact, one-line comments whenever possible.
  • 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.

🔹 Using JSDoc for TODOs

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.


🔹 Inline Comments for Workarounds

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.

🔹 Handling Complex useEffect Hooks

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 inuseEffect.
  • ❌ Long comments insideuseEffect add clutter.

🔹 When to Write a Comment vs. Refactor?

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.

✅ Summary

  • 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 auseEffect is complex, extract logic into functions instead of writing comments.

🤝 Contributing

Thank you for considering contributing to this project! We appreciate your help in improving and maintaining thisrepository.


🔹 How to Contribute

  1. Fork the Repository

    • Click theFork button on the top right of this repository.
    • This will create a copy under your GitHub account.
  2. 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
  3. Make your changes inmain

    • Open the project in your preferred editor.
    • Make your changes while following the project’s coding guidelines.
  4. Commit your changes

    git add.git commit -m"Describe your changes"
  5. Push to your fork

    git push origin main
  6. Create a Pull Request

    • Go to theoriginal repository on GitHub.
    • ClickCompare & pull request.
    • Add aclear description of your changes.
    • ClickCreate pull request.

🔹 Contribution Guidelines

  • 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 likeFix button alignment issue orAdd support for dark mode.
  • 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. 🚀


📜 License

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.

📄 Full License

The full license text is available in theLICENSE.md file.


📚 References & Inspirations

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.

📌 Key Influences on This Guide

Each of the following referencesshares core principles with this style guide, such asclarity, maintainability,predictability, and reducing complexity.

ReferenceLinkHow It Relates
Google TypeScript Style GuideGoogle TypeScript GuideReadability & maintainability viaconsistent naming, structured function ordering, and predictable patterns.
✅ Aligns withComponent Order andSeparation of Concerns principles.
Airbnb React/JSX Style GuideAirbnb 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 GuideShopify 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 GuideTS.dev Guide✅ Emphasizesclarity and minimalism, reinforcingNo Unnecessary Abstraction.
✅ Aligns withusing interfaces for components and types for utilities/hooks.
TypeScript Deep Dive Style GuideTypeScript Deep Dive✅ Advocatespredictable, structured code organization andexplicit return types.
✅ Aligns withTypes & Interfaces, particularlyExtract<>, Pick<>, and Omit<> usage.

💡 Final Thoughts

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.

Releases

No releases published

Packages

No packages published

[8]ページ先頭

©2009-2025 Movatter.jp