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
InterfacesorTypesexplicitlyInstead of
EntityInterface-> doEntityInstead of
NavigationStrategyInterface-> doEntity
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
propsparameter 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.tsfor exporting files from a folderOnly 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)IntlPropswhen access toreact-intlis 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