This document provides a comprehensive guide to the coding style, conventions, and best practices used in the Rooks project. Adhering to these guidelines ensures consistency, readability, and maintainability of the codebase.
- Clarity and Readability: Code should be easy to understand. Prioritize clear naming and a logical structure.
- Strict Type Safety: We leverage TypeScript's strongest features to catch errors at compile time.
- Consistency: Following a consistent style across the project makes the code easier to read and maintain.
- Thorough Testing: Every hook must have comprehensive tests to ensure it is robust and reliable.
Each new hook should have the following file structure:
packages/rooks/
├── src/
│ └── hooks/
│ └── useMyNewHook.ts
└── __tests__/
└── useMyNewHook.spec.ts
We enforce a high level of type safety. The TypeScript configuration has strict and noUncheckedIndexedAccess set to true.
Prefer type over interface for defining object shapes and function signatures. This helps maintain consistency across the codebase.
Example:
// Good
type CounterHandler = {
decrement: () => void;
increment: () => void;
reset: () => void;
value: number;
};
// Bad
interface CounterHandler {
decrement: () => void;
increment: () => void;
reset: () => void;
value: number;
}The use of any is strictly discouraged. When a hook needs to work with a variety of value types, use generics.
Example:
function useMyHook<T>(initialValue: T) {
const [value, setValue] = useState<T>(initialValue);
// ...
}For complex types, especially when dealing with unions, use type guards to narrow down the type.
Example:
function isKeyboardEvent(event: Event): event is KeyboardEvent {
return "key" in event;
}- Use
constfor variables that are not reassigned. - Use
letonly when a variable needs to be reassigned. - Use function declarations (
function myFunction() {}) for top-level functions. - Use arrow functions for callbacks and when preserving the
thiscontext.
- Use
async/awaitfor handling asynchronous operations. - Always wrap
awaitcalls intry...catchblocks to handle potential errors gracefully.
Example from useAsyncEffect:
try {
return await effectRef.current(shouldContinueEffect);
} catch (error) {
throw error;
}A typical hook in this project has the following structure:
- Type Definitions: Define
typealiases for the hook's return value and any complex parameter types. - JSDoc: A detailed JSDoc comment explaining the hook's purpose, parameters, return value, and a link to the documentation.
- Hook Function: The main function, starting with
use. - Export: The hook is exported using a named export.
import { useCallback, useState } from "react";
// 1. Type Definition
type CounterHandler = {
decrement: () => void;
decrementBy: (amount: number) => void;
increment: () => void;
incrementBy: (amount: number) => void;
reset: () => void;
value: number;
};
// 2. JSDoc
/**
* Counter hook
*
* @param {number} initialValue The initial value of the counter
* @returns {handler} A handler to interact with the counter
* @see https://rooks.vercel.app/docs/hooks/useCounter
*/
// 3. Hook Function
function useCounter(initialValue: number): CounterHandler {
const [counter, setCounter] = useState(initialValue);
const incrementBy = useCallback((incrAmount: number) => {
setCounter((currentCounter) => currentCounter + incrAmount);
}, []);
// ... other functions
return {
decrement,
decrementBy,
increment,
incrementBy,
reset,
value: counter,
};
}
// 4. Export
export { useCounter };useState: For managing simple state.useEffect: For side effects. Remember to provide a dependency array.useCallback: To memoize functions, especially those passed down to child components.useMemo: To memoize expensive calculations.useRef: To create mutable ref objects.
We use @testing-library/react for testing our hooks.
- Tests are located in the
packages/rooks/__tests__directory. - Test files are named
useMyHook.spec.ts. - Each test file starts with
/** @jest-environment jsdom */.
renderHook: Use therenderHookfunction from@testing-library/reactto test hooks in isolation.act: Wrap any state updates inactto ensure that React has processed the updates before you make assertions.- Assertions:
- Use
expect.hasAssertions()at the beginning of each test to ensure that at least one assertion is called. - Write clear and concise assertions.
- Use
- Mocking: Use Jest's mocking capabilities (
jest.fn(),jest.spyOn(), etc.) to mock dependencies and track function calls. - Fake Timers: Use
jest.useFakeTimers()for hooks that involve timeouts or intervals.
/**
* @jest-environment jsdom
*/
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "@/hooks/useCounter";
describe("useCounter", () => {
it("should be defined", () => {
expect.hasAssertions();
expect(useCounter).toBeDefined();
});
it("should increment the value", () => {
expect.hasAssertions();
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.value).toBe(1);
});
// ... other tests
});- Every exported hook must have a JSDoc comment.
- The comment should include:
- A brief description of the hook.
@paramfor each parameter.@returnsfor the return value.@seewith a link to the official documentation page.
Example:
/**
* useKey hook
*
* Fires a callback on keyboard events like keyDown, keyPress and keyUp
*
* @param {TrackedKeyEvents} keys List of keys to listen for. Eg: ["a", "b"]
* @param {Callback} callback Callback to fire on keyboard events
* @param {Options} options Options
* @see https://rooks.vercel.app/docs/hooks/useKey
*/By following these guidelines, we can maintain a high-quality, consistent, and easy-to-contribute-to codebase.