- Code Style
- Import Organization
- Naming Conventions
- Component Structure
- TypeScript Patterns
- Functional Programming
- Pattern Matching
- State Management
- Error Handling
- GraphQL and API
- Testing
- Performance Patterns
- File Organization
-
If you find yourself using a literal value >= 3 times, define a constant for it.
-
Use arrow function notation
-
Use arrow function expressions when possible
// Compliant
const foo = () => "bar"
// Non-compliant
const foo = () => {
}- Use early returns when possible
// Compliant
const handleData = (data: Data) => {
if (!data) handleFalsyData()
if (!data.isValid()) handleInvalidData()
doSomething(data)
}
// Non-compliant
const handleData = (data: Data) => {
if (data && data.isValid()) {
doSomething(data)
} else if (!data) {
handleFalsyData()
} else if (!data.isValid()) {
handleInvalidData()
}
}- Prefer object destructuring
- Organize imports in a consistent order:
- External libraries first
- Internal packages second (@dictybase/* packages)
- Relative imports last
- Group by functionality
// External libraries
import { pipe } from "fp-ts/function"
import { match, P } from "ts-pattern"
import { useNavigate } from "react-router-dom"
// Internal packages
import { FullPageLoadingDisplay } from "@dictybase/ui-common"
import { useContentBySlugQuery } from "dicty-graphql-schema"
// Relative imports
import { ShowView } from "./ShowView"
import { NAMESPACE } from "../../common/constants/namespace"
import { useSlug } from "../../common/hooks/useSlug"- PascalCase for components and types
- camelCase for functions, variables, and hooks
- UPPER_CASE for constants and environment variables
- Props types should end with
Properties
// Components and types
type EditableViewProperties = { ... }
const EditableView = () => { ... }
// Functions and hooks
const useAuthorizedUpdate = () => { ... }
const truncateEmail = (email: string) => { ... }
// Constants
const NAMESPACE = "dicty-frontpage"
const MAX_ITEMS = 12- Use arrow function components (no class components)
- Use named exports over default exports
- Define prop types as TypeScript interfaces/types
- Separate presentation and logic concerns
// Define prop types
type MyComponentProperties = {
title: string
isActive: boolean
}
// Arrow function component with named export
const MyComponent = ({ title, isActive }: MyComponentProperties) => {
// Hooks at the top
const navigate = useNavigate()
const [state, setState] = useState(false)
// Event handlers
const handleClick = () => { ... }
// Render
return <div>...</div>
}
export { MyComponent }- Use explicit type definitions for all props, parameters, and return types
- Use type-only imports when applicable
- Use enums for constant values
- Avoid
anytype
// Type-only imports
import type { StrainCartItem, PlasmidCartItem } from "./types"
// Enums for constants
enum ErrorType {
MISSING_CONTENT_ID,
ACCESS_TOKEN_ERROR,
USER_INFO_ERROR,
}
// Explicit return types
const calculateTotal = (items: CartItem[]): number =>
items.reduce((sum, item) => sum + item.price, 0)- Use functional patterns to transform arrays of data
- Avoid using
nullorundefined. Wrap values in anOptiontype fromfp-ts - Use
pipefor function composition - Use
TaskEitherfor async operations with error handling - Use fp-ts array utilities for immutable transformations
// Use Option type instead of null/undefined
const getUserName =
(user: { firstName: string, lastName: string } | null) =>
pipe(
user,
OfromNullable, // Option<{ firstName: string, lastName: string }>
Omatch(
() => "No User",
({ firstName, lastName }) => `${firstName} ${lastName}`
)
)
// Use TaskEither for async operations
const fetchUserData = (id: string) =>
pipe(
TEDo,
TEbind("token", () => getAccessToken()),
TEbind("user", ({ token }) => fetchUser(id, token)),
TEmap(({ user }) => user),
TEmapLeft((error) => handleError(error))
)
// Use fp-ts array utilities
const uniqueItems = pipe(
items,
Aconcat(newItems),
Auniq(itemEq),
Afilter((item) => item.isActive)
)The ts-pattern library is used for conditional logic, most often for conditionally rendering a component based on the result of a data query:
import { match } from "ts-pattern"
const Show = () => {
const slug = useSlug()
const result = useContentBySlugQuery({
variables: { slug: `${NAMESPACE}-${slug}` },
errorPolicy: "all",
fetchPolicy: "cache-and-network",
})
return match(result)
.with(
{ data: { contentBySlug: P.select({ content: P.string }) } },
(content) => <ShowView data={content} />,
)
.with({ loading: true }, () => <FullPageLoadingDisplay />)
.with({ error: P.select(P.not(P.nullish)) }, (error) => (
<ErrorPageWrapper error={error} />
))
.otherwise(() => <> This message should not appear. </>)
}- Avoid ternary statements
- Use Jotai atoms for global state management
- Create derived atoms for computed state
- Use split atoms for list management
- Keep local UI state with
useState - Use Apollo Client for server state
// Base atom
const cartAtom = atom<Cart>(initialCart)
// Derived atom for computed state
const totalPriceAtom = atom(
(get) => get(cartAtom).items.reduce((sum, item) => sum + item.price, 0)
)
// Write-only atom for actions
const addItemAtom = atom(
null,
(get, set, newItem: CartItem) => {
set(cartAtom, (prev) => ({
...prev,
items: [...prev.items, newItem]
}))
}
)
// Split atom for list management
const itemAtomsAtom = splitAtom(itemsAtom)- Use typed error objects with specific error types
- Handle errors functionally with
TaskEither - Use error wrapper components for consistent UI
- Use pattern matching for error state rendering
// Typed error objects
type ContentError = {
errorType: ErrorType
message: string
}
const userInfoError: ContentError = {
errorType: ErrorType.USER_INFO_ERROR,
message: "Could not get user info",
}
// Functional error handling with pattern matching
const handleUpdate = pipe(
fetchData(),
TEmapLeft((error) =>
match(error)
.with({ errorType: ErrorType.ACCESS_TOKEN_ERROR }, () =>
handleTokenError(),
)
.with({ errorType: ErrorType.USER_INFO_ERROR }, () => handleUserError())
.otherwise(() => handleGenericError()),
),
)- Use Apollo Client with code-generated types
- Create custom hooks for queries and mutations
- Handle loading and error states with pattern matching
- Add authorization headers in context
// Custom hook for authorized mutations
const useAuthorizedUpdate = (contentId: string) => {
const { getAccessToken } = useLogto()
const [updateContent] = useUpdateContentMutation()
return async (content: string) => {
const token = await getAccessToken()
return updateContent({
variables: { input: { id: contentId, content } },
context: { headers: { Authorization: `Bearer ${token}` } }
})
}
}
// Query with pattern matching for state handling
const MyComponent = () => {
const result = useContentQuery({ variables: { id } })
return match(result)
.with({ loading: true }, () => <LoadingDisplay />)
.with({ data: P.select() }, (data) => <ContentView data={data} />)
.with({ error: P.select() }, (error) => <ErrorDisplay error={error} />)
.otherwise(() => null)
}- Use Vitest as the testing framework
- Use Testing Library for component testing
- Avoid wrapping tests in
describeblocks - Write descriptive test names
- Separate mock data into dedicated files
- Use Playwright for E2E tests
- Do not call
expectconditionally. Prefer type assertions.
// Tests at top level, no describe blocks
test("should render its title", () => {
render(<Link {...properties} />)
expect(screen.getByText(/test/i)).toBeInTheDocument()
})
test("should have the correct href", () => {
render(<Link {...properties} />)
expect(screen.getByRole("link")).toHaveAttribute("href", "google.com")
})
// Descriptive test names
test("returns seconds for recent times", () => {
const date = new Date(Date.now() - 30 * 1000)
expect(timeSince(date.toISOString())).toBe("30 seconds")
})
// Component testing with Testing Library
test("should display capacity full message when reaching 12 items", () => {
render(
<MemoryRouter>
<StrainAvailableDisplay cartData={mockCartData} />
</MemoryRouter>
)
expect(screen.getByText(/Add to Cart/)).toBeInTheDocument()
})- Use
React.memofor expensive components - Use
useMemoanduseCallbackappropriately - Implement code splitting at route level
- Use lazy loading for heavy components
// Memoized component
const ExpensiveList = React.memo(({ items }: ListProps) => {
return items.map(item => <Item key={item.id} {...item} />)
})
// Memoized calculations
const MyComponent = ({ data }: Props) => {
const processedData = useMemo(
() => expensiveCalculation(data),
[data]
)
const handleClick = useCallback(
(id: string) => {
// Handle click
},
[dependency]
)
return <div>...</div>
}
// Route-based code splitting
const LazyComponent = lazy(() => import('./HeavyComponent'))- Feature-based folder structure in apps
- Shared packages in monorepo for reusability
- Clear separation of concerns:
components/- React componentshooks/- Custom hooksutils/- Utility functionstypes/- TypeScript type definitionsconstants/- Constants and configuration__tests__/- Test files
.
├── apps/
│ ├── dicty-frontpage/
│ │ └── src/
│ │ ├── features/ # Feature-based organization
│ │ │ ├── EditablePages/
│ │ │ ├── Frontpage/
│ │ │ └── Publication/
│ │ ├── common/
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ ├── utils/
│ │ │ ├── constants/
│ │ │ └── types/
│ │ └── __tests__/
│ ├── genome-page/
│ └── stock-center/
│
└── packages/ # Shared packages
├── ui-common/
├── data-access/
├── auth/
├── editor/
├── navbar/
├── footer/
└── hook/
- To run scripts for a specific project, use the command `yarn workspace
- Don't use
npmto manage project dependencies. Useyarninstead