Skip to content

React Code Conventions

Fraser McCallum edited this page Mar 7, 2022 · 11 revisions

Introduction

This page outlines a few core conventions for the React codebase. Some of these rules will be enforced in ESLint, however some may not be. Please ensure code you write and review follows the conventions listed here.

Components

Components should live in files named the same as the component. Component names and their files should be PascalCase.

Typically, only one component should live in a single file. Additional components are okay if their usage is limited to the main component of that file (i.e. prefer to export only a single component).

Only functional components should be used (no class components unless strictly needed).

A component declaration should look like this:

import React from "react"

export interface MyComponentProps {
  name: string;
}

const MyComponent: React.FC<MyComponentProps> = ({ name }) => {
  return <div>{name}</div>
}

export default MyComponent

If you use VS Code, the following can be added to your User Snippets file (Bottom left cog > user snippets):

  "Create a typescript default export functional react component": {
    "prefix": "rtse",
    "body": [
      "import React from \"react\"",
      "",
      "export interface ${1:$TM_FILENAME_BASE}Props {}",
      "",
      "const ${1:$TM_FILENAME_BASE}: React.FC<${1:$TM_FILENAME_BASE}Props> = () => {",
      "  $0",
      "  return <div></div>",
      "}",
      "",
      "export default $TM_FILENAME_BASE"
    ]
  }

This will allow you to quickly create a react component with the above style when you type "rtse".

Directory Layout

The most important directories for the project are as follows:

/src - root directory of all source code
/src/pages - contains the main pages of the application (for routing)
/src/components/{page} - contains all the components used by {page}
/src/components/common - components used by multiple pages and components
/src/hooks - custom react hooks
/src/services - code for third party things
/src/styles - home for root css code
/src/util - any other random code

/public - files to be copied directly to the root of the website

Component Patterns

Prefer to split a single unit into a controller component containing the logic and a view component purely for the presentation of the data from the controller. This allows for the separate development of the view and the logic.

When designing the view for a component, all required external data should be passed in as props. Any actions that the user takes (i.e. button clicks) should execute a callback passed to the child via props. When developing views, you can simply pass in dummy data via props to ensure your design looks as it should.

When designing the controller for a component, typically the only returned JSX would be the view component with its props passed. In the controller, handle effects, state, data fetching etc. When developing controllers, you can create a dummy view component that simply displays the data from the controller and tests callbacks to ensure things work as expected.

Once the view and controller are done, replacing the view that is being rendered by the controller to the real one, and passing down real props, should be all that is required to get a complete unit.

In summary, the view of the component should be quite "dumb". It doesn't really know anything other than what it was told in its props. The controller can have a much greater understanding of what is going on in the application, but itself should not do much in the way of changing the DOM.

Controller/View Split Example

This is a simple example of this pattern. The component is a simple text input that displays a sharing code (fetched from the API), and a button that will copy that text to your clipboard.

// ShareLinkController.tsx
import React from 'react';
import { useApi } from '../../hooks/useApi';
import { useFlatContext } from '../../hooks/useFlatContext';
import ShareLinkView from './ShareLinkView';

interface ShareLinkControllerProps {}

const ShareLinkController: React.FC<ShareLinkControllerProps> = () => {
  // Controller gets information from Context
  const { flatId } = useFlatContext();

  // Controller gets information from API
  const { data: sharingLink } = useApi('/api/flat/link', {
    method: 'GET',
    queryParams: { flatId },
  });

  // Controller defines the logic for how the copying to clipboard is done
  const onCopy = async () => {
    await navigator.clipboard.writeText(sharingLink);
  };

  // Controller passes this data to the view
  return <ShareLinkView onCopy={onCopy} sharingLink={sharingLink} />;
};

export default ShareLinkController;
// ShareLinkView.tsx

import React from 'react';
import { ClipboardCopyIcon } from '@heroicons/react/outline';

export interface ShareLinkViewProps {
  // The view declares the data it needs
  sharingLink: string;
  onCopy: () => void;
}

const ShareLinkView: React.FC<ShareLinkViewProps> = ({
  sharingLink,
  onCopy,
}) => {
  return (
    <div className="flex gap-x-2 items-center">
      <input className="rounded p-2" readOnly>
        {/* The view displays this data however it wants to */}
        {sharingLink}
      </input>
      <ClipboardCopyIcon
        className="text-white p-1 rounded w-8"
        // The view alerts the controller of actions taken, which are then handled in the controller.
        onClick={onCopy}
      />
    </div>
  );
};

export default ShareLinkView;

The Somewhat Better Way To Do This

Rather than having the View component hard coded in the controller, the controller instead declares a render function as a prop. This means the creator of the Controller can then pass in the desired view. The controller then simply calls that function with the props.

// Wherever the controller is used
return <ShareLinkController render={ShareLinkView} /> // Using ShareLinkView

return <ShareLinkController render={AnotherShareLinkView} /> // Shares the logic, but a different view
// Inside the controller

interface ShareLinkControllerProps {
  render(props: ShareLinkViewProps): void
}

const ShareLinkController: React.FC<ShareLinkControllerProps> = ({ render }) => {
  // ...
  return render({sharingLink, onCopy})
}
Clone this wiki locally