React

  • Always use JSX syntax (vs React.createElement) [1][2][3][4][5]

  • Always use React functional components (vs class components) [1][2]

Notes (functional components)

Exceptions: When creating custom components that need to accept a ref, use a class component (function components cannot accept refs) (see example) (in React 19, we will not need to do anything different for the "ref" prop)

  • Use function declaration syntax for components and other functions (vs function expression)

Example (syntax -- function declaration vs function expression)
// function declaration (uses 'function' keyword) (preferred)
interface Props {}

export function ComponentName({}: Props) {
  return (
    <>
      
    </>
  );
}


// function expression (uses 'const' & =>) (not preferred)
interface Props {}

export const ComponentName = ({  }: Props) => {
  return (
    <>
      
    </>
  );
};
  • Use named exports for components and other functions/variables/etc. (vs default exports) [1][2]

Notes (exports -- named vs default)

Exceptions: page.tsx , layout.tsx (use default)

Example (exports -- named vs default)
// named export (preferred) (no "default" keyword)
interface Props {}

export function ComponentName({}: Props) {
  return (
    <>
      
    </>
  );
}

// default export ("default" keyword)
interface Props {}

export default function ComponentName({}: Props) {
  return (
    <>
      
    </>
  );
}
  • Use function declaration/signature line exporting (vs bottom of file)

Example (exports -- declaration/signature line vs bottom of file)
// function declaration/signature line export (preferred)
export function ComponentName({}: Props) {
  ...
}

// bottom-of-file export (not preferred)
function ComponentName({}: Props) {
  ...
}
export { ComponentName }
  • Only include one exported component per file.

Notes (one exported component per file)
  • it is encouraged to abstract render details into their own "local components" (within same file or separate files within same "component folder") to keep the render code minimal, straightforward, and maintainable

Example (one exported component per file; local components)
// functional component (also the "new component" template we use within Pavewise)

interface Props {}

export function ComponentName({}: Props) {
  return (
    <>
      { /* Add your code here */}
    </>
  );
}
// component that has a "local component"
// still only exporting one component (the "main component")

interface Props {}

export function ComponentName({}: Props) {
  return (
    <>
      <LocalComponent />
      { /* Add your code here */}
    </>
  );
}

// local component (note that it is NOT exported -- only used in this file)
// (it is abstracted logic/UI from the main component for simplification/readability/maintainability purposes)
interface LocalComponentProps {}

function LocalComponent({}: LocalComponentProps) {
  return (
    <>
      { /* Add your code here */}
    </>
  );
}
  • Use interface Props {} for typing main component props (vs named props or type) [1]

Notes (interface vs type)
  • when combining multiple types that are objects, using interface extends Type1, Type2 catches "same property name but imcompatable types", whereas type = Type1 & Type2 does not

Example (interface Props)
// interface, generic props (preferred)
interface Props {}

// interface, named props (not preferred)
interface ComponentNameProps {}

// type (not preferred)
type ComponentNameProps = {}
  • Every component should exist within a "component file" of the same name & casing.

    • If local components, helper functions, etc. are extracted into their own files, then a "component folder" should be created and everything should be placed within it (including the component file)

Notes ("component file", "component folder")

"Component folder": When local components, helper functions, etc. are extracted into their own files, a "component folder" should be created with the same name of the component and component file, and should contain (1) the main component file itself, (2) an index.ts file that simply exports that component, and (3) "_x" folder(s) containing the extracted local components, helper functions, etc.


"_x" folders: For more complex components, it may make sense to extract logic/UI/etc. into their own local files (e.g. to prevent any single file from getting too big). In these situations, local files are to be places in local "_x" folders:

  • _components (holds components)

  • _lib (holds any non-component files/folders -- helpers, hooks, etc.)

These extract components, helper functions, types, constants, etc. that are only used by the main component are termed "local" components, helper functions, etc., and should be co-located within the component folder".


Example ("component file", "component folder")

The (1) component folder, (2) component file, and (3) function signature should all have the same name, and an index.ts file should export the component file


Example: Header component

Header "component file" (no local component/function files)

Header "component folder"

  • 📄 index.ts: export * from './Header` (only exports the main component)

  • 📁 _components: local components

  • 📁 _lib: everything else (helper functions, hooks, context, etc)


Example folder structures:

// MINIMAL
~/components/.../Component/
--Component.tsx
--index.ts (simply exports Component.tsx)

// MINIMAL (with local components)
~/components/.../MainComponent/
--Component.tsx
--index.ts (simply exports Component.tsx)
--_components/
----LocalComponent1.tsx
----LocalComponent2.tsx
----LocalComponent3.tsx
----index.ts (exports all files within _components folder)

// MINIMAL (with local non-component folders/files) (e.g. helper functions, etc.)
~/components/.../Component/
--Component.tsx
--index.ts (simply exports Component.tsx)
--_lib/  (containing local types, helpers, hooks, etc.)
----_hooks/
------useLocalHook.tsx
------index.ts (exports all files within _hooks folder)
----_helpers/
------customLocalHelperFunction.tsx
------index.ts (exports all files within _helpers folder)
----index.ts (exports all folders/files within _lib folder)

// COMPLEX COMPONENT (with local components, helper functions, Storybook (testing) files, etc.)
~/components/.../Component/
--Component.tsx
--Component.stories.tsx  (StorybookJS)
--index.ts (simply exports Component.tsx)
--_components/
----...
----index.ts (exports all folders/files within _components folder)
--_lib/
----_hooks/
----_helpers/
----...
----index.ts (exports all folders/files within _lib folder)

  • For conditional rendering, prefer multiple returns (early returns) (vs single return with internal conditional rendering of JSX) [1]

    • Multiple returns (early returns)

      • if conditions

    • Single return

      • && (logical AND operator)

      • ? : (ternary operator)

Example (conditional rendering -- multiple returns vs single return)
// Example: multiple returns (early returns) (preferred)
function Component({ isLoading, data }) {
  if (isLoading) return <Loading />;
  if (data.length === 0) return <NoData />;

  return <SuccessReturn />;
}

// Example: single return (conditional rendering, using logical AND operator)
function Component({ isLoading, data }) {
  return (
    <>
      {isLoading && <Loading />}
      {data.length === 0 && <NoData />}
      {data.length > 0 && <SuccessReturn />}
    </>
  );
}

// Example: single return (conditional rendering, using ternary operators)
function Component({ isLoading, data }) {
  return isLoading ? (
    <Loading />
  ) : data.length === 0 ? (
    <NoData />
  ) : (
    <SuccessReturn />
  );
}
  • For event handlers:

    • use inline anonymous functions (arrow syntax) when the handling logic is minimal and straightforward (vs creating an extracted/named event handler function) [1]

    • use handleX syntax for extracted/named event handlers functions [1]

      • X should be consistent with the prop name when possible (onClick prop = handleClick event handler)

    • use onX syntax for event handler props (component prop names that receive event handler functions) [1]

    • use e when referring to event objects in event handler functions (vs event, etc.) [1]

Notes (event handlers)
  • inline anonymous functions

    • Goal: keep component body logic minimal (easier code navigation; less mental overhead)

  • X should be consistent with the prop name when possible

    • If there is only a single instance of a prop name in a given component (which can often happen, with modular, SRP components), use it for the function name

      • prop name: onClick

      • function name: handleClick

    • If there are multiple instances of a prop name, then follow the "NounAdjective" convention (append further specificity to the end of the name), or use more specific/descriptive

      • handleClickX, handleClickY, ...

      • handleCreate, handleDelete, ...

Example (event handlers -- inline anonymous vs extracted named)
// event handler: inline anonymous arrow function (preferred)
function Component() {
  const [state, setState] = useState();

  return <button onClick={() => setState('clicked')}>Click me</button>;
}

// event handler: extracted named function
// - only use when logic starts to become complex
function Component() {
  const [state, setState] = useState();

  function handleClick() {
    setState('clicked');
  }

  return <button onClick={handleClick}>Click me</button>;
}

  • For (state) updater functions, use prev or first letters of corresponding state variable as the argument name [1]

Example (updater functions -- prev or first letters of state variable)
// prev
setEnabled(prev => !prev);
setLastName(prev => prev.reverse());
setFriendCount(prev => prev & 2);

// first letters of corresponding state variable
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
  • If using reducers: [1]

    • use a switch statement (vs if/else)

      • use { and } for each case when needing to declare local variables

    • use more generic action strings (vs state-specific names)

Example (reducers)
// REDUCER LOGIC
// switch statement (preferred) (with {...} around each case)
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [
        ...tasks,
        {
          id: action.id,
          text: action.text,
          done: false,
        },
      ];
    }
    case 'changed': {
      return tasks.map((t) => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

// if/else statement
function tasksReducer(tasks, action) {
  if (action.type === 'added') {
    return [
      ...tasks,
      {
        id: action.id,
        text: action.text,
        done: false,
      },
    ];
  } else if (action.type === 'changed') {
    return tasks.map((t) => {
      if (t.id === action.task.id) {
        return action.task;
      } else {
        return t;
      }
    });
  } else if (action.type === 'deleted') {
    return tasks.filter((t) => t.id !== action.id);
  } else {
    throw Error('Unknown action: ' + action.type);
  }
}

// ACTION STRINGS
// generic (preferred)
// "added", "changed"/"updated", "deleted"

// state-specific
// "added_task", "changed_task"/"updated_task", "deleted_task"

Last updated