This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

React Client Guidelines

Note

We are in the process of migrating our codebase to TypeScript. It is generally possible to write TypeScript from version 3.14 onwards, and is required for new code from version 3.15 onwards. Changes to existing code may still be done without rewriting everything in TypeScript.

Version

TypeScript Version

Nice Versions

5

3.14 -

General

  • Always include curly brackets, even for one-line if statements or for loops.

  • One statement per line. Avoid too long line (make a line break after ~ 120 characters).

Naming

  • Do not mark Interfaces or Types explicitly

    • Instead of EntityInterface -> do Entity

    • Instead of NavigationStrategyInterface -> do Entity

  • Methods and field names are declared in camel case

  • Components are declared in pascal case

  • Constants are declared in uppercase snake case

Naming conventions for folders

What

Case

Example

Packages

Kebab

entity-browser

Components

Pascal

SearchField

Module

Camel

searchForm

Any other

Kebab

test-data

Tocco variable naming

To be consistent trough all apps following variable names should be chosen when dealing with the nice backend:

Name

Alternative name

Description

entityName

name

Technical entity name e.g. User or Education_schedule

entityLabel

label

Localized entity label e.g. Person or Lehrplan

entityModel

model

Object containing the whole model including the name, label and fields

entityKey

key

Primary key of entity. Avoid “id” or “pk” as substitute

entityId

id

Object containing entityName and entityKey eg. {entityName: ‘User, entityKey: “33”}

formName

Form name including the scope e.g. User_list

formBase

Only the form base without the scope e.g. UserSearch

form

Form object containing name and fields

React components

  • Use arrow functions

  • Use functional components whenever possible

  • Destruct props

    • Use props parameter for spreading props on child components but destruct needed values

// Good
const Comp({a, b}) => {}

// Good
const Comp(props) => {
  const {a, b} = props

  const foo = a + b

  return <Child {...props} />
}

// Not so good
const Comp(props) => {
  const foo = props.a + props.b

  return <Child {...props} />
}
  • Use () for better JSX-alignment

// Good
return (
  <Comp>
    {children}
  </Comp>
)

// Not so good
return <Comp>
  {children}
</Comp>

Functions

  • Use arrow functions

// Good
const calculateAnything = () => {}

// Not so good
function calculateAnything () {}

Variables

  • Use variables for better reading/understanding

// Good
const hasFilterApplied = Boolean(filter) && filter.length > 2
return hasFilterApplied ? searchResults : []

// Not so good
return Boolean(filter) && filter.length > 2 ? searchResults : []

// Good
const entityBaseUrl = getBaseUrl(entity.id)
navigateTo(`${entityBaseUrl}/${relationName}`)

// Not so good
navigateTo(`${getBaseUrl(entity.id)}/${relationName}`)
  • Omit unnecessary variables

// Good
doSomething(history.location)

// Not so good
const location = history.location
doSomething(location)

Actions

  • Wrap arguments in payload attribute

  • Use arrow functions

  • Returning object literals (no return statement used)

// Good
export const setPending = (pending = false) => ({
  type: SET_PENDING,
  payload: {
    pending
  }
})

// Not so good
export function setPending(pending = false) {
  return {
    type: SET_PENDING,
    pending: pending
   }
}

Reducers

  • Use arrow functions

  • Use destructuring assignment

// Good
const updateOldPassword = (state, {payload}) => ({
  ...state,
  oldPassword: payload.oldPassword
})

// Not so good
function updateOldPassword(state, args) {
  return Object.assign({}, state, {
    oldPassword: args.payload.oldPassword
  })
}

Tests

  • Group tests hierarchically according to directory structure starting with the package-name

  • test description should always start with should

// Good
describe('package-name', () => {
  describe('components', () => {
    describe('Image component', () => {
      test('should render an image', () => {
        //...

// Bad
describe('Image component', () => {
   test('renders an image', () => {
      //...
  • Use Chai to.be.true instead of equal(true)

// Good
expect(withTitle.find(LoginFormContainer).prop('showTitle')).to.be.true

// Not so good
expect(withTitle.find(LoginFormContainer).prop('showTitle')).to.equal(true)

Export / Import

  • Use index.ts for exporting files from a folder

  • Only import files in app via main.ts

// Good
import {SearchBox} from 'tocco-ui'
import {chooseDocument} from 'tocco-docs-browser/src/main'

// Not so good
import SearchBox from 'tocco-ui/src/SearchBox'
import chooseDocument from 'tocco-docs-browser/src/modules/chooseDocument'

Unit Tests

Chai Assertions

We use BDD (Behaviour Driven Development) assertions with the base of expect.

See: Chai Assertions for the API

React Testing Library

The React Testing Library (RTL) only provides a full render function. In addition the RTL operates on the outputted html and React components cannot be used for any assertion or prop updates.

Render without redux

import {screen} from '@testing-library/react'
import {testingLibrary} from 'tocco-test-util'

it(() => {
  testingLibrary.renderWithIntl(<MyComp />)
  expect(screen.getAllByText('client.comp.textid')).to.exist
})

Render with redux

import {testingLibrary} from 'tocco-test-util'

import reducers, {sagas} from '../../modules/reducers' // import reducers and sagas from package

it(() => {
  const input = {}
  const store = appFactory.createStore(reducers, sagas, input)
  store.dispatch(...) // dispatch actions to prefill state
  testingLibrary.renderWithStore(<MyComp />, {store})
})

Prevent render with mocking

When a child component should not render itself it can be mocked. This can be useful if only the existens of a component is important but not its behaviour itself.

import {screen} from '@testing-library/react'
import {testingLibrary} from 'tocco-test-util'

jest.mock('tocco-entity-list/src/main', () => () => <div data-testid="entity-list" />)

it(() => {
  testingLibrary.renderWithIntl(<MyComp />)
  expect(screen.getByTestId('entity-list')).to.exist
})

Typing

Official Typescript Cheat-Sheets

Explicit vs. Implicit Types

Implicit types may be used where they do not hinder the ability to read the code without an IDE. Function arguments are usually explicitly typed, while trivial return types are often omitted. An implicit any type is never allowed, and must be explicitly set.

Interfaces vs. Types

Use Interfaces over Types (see this example)

Global Types

Use and create global Tocco types in tocco-types package. Make sure these are actually relevant to most packages, so we do not pollute the namespace with unnecessary types. If you want to reuse some specialized types in multiple packages, see Export & Import Types.

REST Types

Create types for your REST request and responses. It allows changes to the backend resources to be concentrated in a single location. Optimally, these could be generated out of the backend code in the future.

Export & Import Types

Exporting and importing types is done through the export type and import type syntax. The general import and export guidelines still apply, except for packages without a main.tsx file. You will have to use the entire path to the file to import such types:

import type {ActionType, EntitySelection, ActionDefinition} from 'tocco-app-extensions/src/actions/modules/actions'

You can mix regular exports and type exports like this

export {
  Some,
  Components,
  andOtherThings,
  type OurExportedType
}

Organizing Package Types

We have some conventions on how to organize types in app packages, i.e. packages that export some runnable code.

App Inputs

The main.tsx file usually defines a type that holds all external inputs to the app. For example, these may be just some properties, widget configurations, action environment data, or similar.

// main.tsx
const PackageNameApp = (props: PackageNameAppProps) => {
  // app code here
}

export interface PackageNameAppProps {
  formBase: string
  limit: number | string
  backendUrl: string
}

State Types

Each individual reducer defines their own types for everything they hold in their state. These types then get combined together with the app input types, to build a type that describes the entire available state of an app.

// src/modules/someModule/reducer.ts
const initialState: SomeModuleStateTypes = {
  data: [],
  value: null
}

export default function reducer(state: SomeModuleStateTypes = initialState, action) {
  const handler = ACTION_HANDLERS[action.type]
  return handler ? handler(state, action) : state
}

export interface SomeModuleStateTypes {
  data: string[],
  value: number | null
}

// src/modules/someModule/index.ts
import {SomeModuleStateTypes} from './reducer'

export type {SomeModuleStateTypes}

// src/modules/reducers.ts
import {PackageNameAppProps} from '../main'

import {SomeModuleStateTypes} from './someModule/reducer'

export interface AppStateTypes {
  someModule: SomeModuleStateTypes
  input: PackageNameAppProps
}

Action Types

Action creators always define the types of their parameters. If the payload of an action is used in a saga, their payload types will need to be extracted and exported, so the saga can reuse the types. See Saga Types. Generally, we can use PayloadAction with our own payload type.

// src/modules/someModule/actions.ts
export interface DoActionPayload {
  generatePdf: boolean
}

export const doAction = (generatePdf: boolean): PayloadAction<DoActionPayload> => ({
  type: DO_ACTION,
  payload: {
    generatePdf
  }
})

Saga Types

Sagas usually do not need to export any types (except for their own tests when useful). When using the payload of an action in a saga, we can import the payload type from the action. Similarly, when accessing the state, importing the state types from the reducers may be useful.

// src/modules/someModule/sagas.ts
import * as actions from './actions'
import {SomeModuleStateTypes} from './reducer'

export const someModuleSelector = (state: AppStateTypes) => state.someModule

export default function* mainSagas() {
  yield all([
    takeLatest(actions.DO_ACTION, actionSaga)
  ])
}

export function* actionSaga({payload: {generatePdf}}: PayloadAction<actions.DoActionPayload>) {
  const {data}: SomeModuleStateTypes = yield select(someModuleSelector)
  // saga code here
}

Components (without Redux)

Components that are not connected to the Redux store simply define all their inputs in a single type.

// src/components/ComponentWithoutRedux/SomeComponent.tsx
interface Props {
  someName: string,
  someData: number
}

const SomeComponent = ({someName, someData}: Props) => {
  // component code here
}

Components (with Redux)

Components that are connected to the Redux store, and therefore also have a parent container component, need a more involved type setup.

Components still define their inputs, but split up into multiple types. These are:

  • inputs loaded from the Redux state (StateProps)

  • action creators (ActionProps)

  • properties defined by the calling code (InputProps)

  • IntlProps when access to react-intl is required

// src/components/ComponentWithRedux/ComponentWithRedux.tsx
import {IntlShape} from 'react-intl'

interface InputProps {
  menuItemName: string
}

interface IntlProps {
  intl: IntlShape
}

export interface StateProps {
  isFavorite: boolean
}

export interface ActionProps {
  deleteUserPreferences: (path: string) => void
}

const ComponentWithRedux = ({
  menuItemName,
  isFavorite,
  deleteUserPreferences,
  intl
}: InputProps & StateProps & ActionProps & IntlProps) => {
  // component code here
}

The container component then uses these to connect them.

// src/components/ComponentWithRedux/ComponentWithReduxContainer.tsx
import {deleteUserPreferences} from '../../modules/preferences/actions'

const mapActionCreators: ActionProps = {
  deleteUserPreferences
}

const mapStateToProps = (state: AppStateTypes): StateProps => ({
  isFavorite: state.isFavorite
})

Utility Types

Types use in multiple locations of a package, but which are not exported to other packages, can simply be defined in a util file and then used anywhere in the package, without exporting and importing.

// src/util/types.ts
interface SomeData {
  data: number
}

FAQ & Common errors

Combination of reduxForm and connect / injectIntl

Components that have been wrapped in reduxForm may require being casted to any when they are passed to connect or injectIntl. The error you will see is generally something along the lines of Type XYZ is not assignable to type 'ReactNode' from the render() function.

// ComponentWithReduxForm.tsx
import {reduxForm} from 'redux-form'
const ComponentWithReduxForm = (props: Props) => {
  // component code here
}
export default reduxForm<{}, Props>({form: reduxFormName, destroyOnUnmount: false})(
  ComponentWithReduxForm
)

// ComponentWithReduxFormContainer.ts
export default connect(mapStateToProps, mapActionCreators)(injectIntl(ComponentWithReduxForm)) // fails
export default connect(mapStateToProps, mapActionCreators)(injectIntl(ComponentWithReduxForm as any)) // succeeds