Skip to content

Commit

Permalink
Typescript React Recommendations (strimzi#29)
Browse files Browse the repository at this point in the history
Closes issue strimzi#19

Updates with typescript, and typescript coding practices.

Fixed review comments & merge conflicts.

Added Normal Module Replacement Plugin.

Signed-off-by: Donald Labaj <dlabaj@redhat.com>
  • Loading branch information
dlabaj authored Nov 3, 2020
1 parent 6e33da0 commit dad1d67
Show file tree
Hide file tree
Showing 13 changed files with 478 additions and 73 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Further details around how this UI is implemented can be found below:
- [Linting](./docs/Linting.md)
- [Test](./docs/Test.md)
- [Contribution](./docs/Contribution.md)
- [Coding Standards](./docs/Coding.md)

## Troubleshooting

Expand Down
21 changes: 0 additions & 21 deletions build/babelPresets.js

This file was deleted.

4 changes: 3 additions & 1 deletion build/webpack.client.dev.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { webpackDevServer, devServer } = devEnvValues;

const {
withHTMLPlugin,
withNormalModuleReplacementPlugin,
withMiniCssExtractPlugin,
withWebpackBundleAnalyzerPlugin,
withTsconfigPathsPlugin,
Expand All @@ -45,13 +46,14 @@ const devSpecificConfig = {
rules: [
withStylingModuleLoader(['style-loader']),
withTSModuleLoader('../client/tsconfig.json'),
withJSModuleLoader(),
withJSModuleLoader('../client/tsconfig.json'),
withFontModuleLoader(),
withImageModuleLoader(),
],
},
plugins: [
withHTMLPlugin(),
withNormalModuleReplacementPlugin(),
withMiniCssExtractPlugin({
hmr: true, // enable hmr as well as standard config
}),
Expand Down
4 changes: 3 additions & 1 deletion build/webpack.client.prod.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const {
} = moduleLoaders;
const {
withHTMLPlugin,
withNormalModuleReplacementPlugin,
withMiniCssExtractPlugin,
withWebpackBundleAnalyzerPlugin,
withTsconfigPathsPlugin,
Expand All @@ -49,7 +50,7 @@ const prodSpecificConfig = {
},
]),
withTSModuleLoader('../client/tsconfig.json'),
withJSModuleLoader(),
withJSModuleLoader('../client/tsconfig.json'),
withFontModuleLoader(),
withImageModuleLoader(),
],
Expand Down Expand Up @@ -89,6 +90,7 @@ const prodSpecificConfig = {
useShortDoctype: true,
},
}),
withNormalModuleReplacementPlugin(),
withMiniCssExtractPlugin(),
// gzip compress all built js output
new CompressionPlugin({
Expand Down
43 changes: 29 additions & 14 deletions build/webpack.common.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
const path = require('path');
const webpack = require('webpack');
const merge = require('lodash.merge');

const { PRODUCTION, DEVELOPMENT } = require('../utils/tooling/constants.js');
const babelPresets = require('./babelPresets.js');

// constants
const UI_TITLE = 'Strimzi UI';
Expand Down Expand Up @@ -58,6 +58,16 @@ const withWebpackBundleAnalyzerPlugin = returnPluginWithConfig(

const withTsconfigPathsPlugin = returnPluginWithConfig(TsconfigPathsPlugin, {});

const withNormalModuleReplacementPlugin = () =>
new webpack.NormalModuleReplacementPlugin(
/View.carbon/,
resource => {
if (process.env.cl === 'PATTERNFLY') {
resource.request = resource.request.replace('carbon', 'patternfly');
}
}
);

// common rules configuration
const returnModuleRuleWithConfig = (defaultConfig = {}, defaultUse = []) => (
customUse = [],
Expand Down Expand Up @@ -109,21 +119,25 @@ const withTSModuleLoader = (tsConfigFile) =>
]
)();

const withJSModuleLoader = returnModuleRuleWithConfig(
{
test: /\.js?$/,
exclude: /(node_modules)/,
},
[
const withJSModuleLoader = (tsConfigFile) =>
returnModuleRuleWithConfig(
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
presets: babelPresets,
},
test: /\.js?$/,
exclude: /(node_modules)/,
},
]
);
[
{
loader: 'ts-loader',
options: {
configFile: path.resolve(
__dirname,
tsConfigFile || 'tsconfig.common.json'
),
onlyCompileBundledFiles: true,
},
},
]
)();

const withFontModuleLoader = returnModuleRuleWithConfig(
{
Expand Down Expand Up @@ -186,6 +200,7 @@ module.exports = {
returnBasicConfigMergedWith,
plugins: {
withHTMLPlugin,
withNormalModuleReplacementPlugin,
withMiniCssExtractPlugin,
withWebpackBundleAnalyzerPlugin,
withTsconfigPathsPlugin,
Expand Down
11 changes: 9 additions & 2 deletions client/Images/Images.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,12 @@
* Copyright Strimzi authors.
* License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
*/
declare module '*.png';
declare module '*.svg';
declare module '*.png' {
const content: string;
export default content;
}

declare module '*.svg' {
const content: string;
export default content;
}
4 changes: 2 additions & 2 deletions docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ This UI makes use of:
- React Hooks: to enable shareable common logic across components
- React Lazy and Suspense: to enable asynchronous loading of component code where required
- [Sass](https://sass-lang.com/) for styling. This adds a number of helpful features on top of the core css language to commonise and speed up style implementation
- Webpack and Babel for building, bundling, treeshaking and transpiling the UI, and enabling day to day development. See [the build documentation](./Build.md#ui-build) for further details on build choices and setup.
- Webpack and Typescript for building, bundling, treeshaking and transpiling the UI, and enabling day to day development. See [the build documentation](./Build.md#ui-build) for further details on build choices and setup.
- [`Barrel files`](https://basarat.gitbook.io/typescript/main-1/barrel) and named exports. Barrel files (when combined with [Webpack aliases](./Build.md#webpack-aliases)) allow for many components/modules/functions/constants which are related to be imported and used via one import statement. This relies on exported components/modules/functions/constants to be individually named so they can be imported directly.
- [`React Router`](https://github.com/ReactTraining/react-router) for it's declarative routing capabilities.

Expand Down Expand Up @@ -421,7 +421,7 @@ Tooling, such as [`npm audit`](https://docs.npmjs.com/cli/audit) make use of/uti

The Strimzi-ui will not be published. Instead, it will consume a number of packages, and be built to provide a UI. Some of these packages will be used and shipped directly, but some will also generate and polyfill code in this repository to produce the the built UI. Thus, in the event of a security issue, we need to understand if the issue relates in any way to what we ship. We therefore categorise our dependencies as follows:

- `dependencies` are any dependency which are either shipped bundled in the built output via direct usage (E.g. React), included in the dockerfile which will run the UI (E.g. Express) or generate output that is then built and shipped (I.e. babel transforms/plugins, Webpack plugins etc)
- `dependencies` are any dependency which are either shipped bundled in the built output via direct usage (E.g. React), included in the dockerfile which will run the UI (E.g. Express) or generate output that is then built and shipped (I.e. typescript transforms/plugins, Webpack plugins etc)
- `devDependencies` are used to support the development and test of the UI, but that are not shipped. E.g. Webpack, Jest, eslint

The correct usage/catagorisation of dependencies will be checked on PR, alongside other checks, such as the licence that dependency comes with.
Expand Down
4 changes: 2 additions & 2 deletions docs/Build.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This documentation will cover both the [building of the UI client and server cod

## UI client and server build

This UI codebase (client and server) is built using two tools, [Babel](https://babeljs.io/) and [Webpack](https://webpack.js.org/). Webpack acts as our main build and bundling tool, while Babel acts as a transpiler - meaning the UI codebase can make use of the latest and greatest ECMAscript syntax, and Babel will polyfill where appropriate to provide cross browser support. The below will detail choices we have made regarding how the build works, configuration and considerations to be aware of, and the end output. The aim of this stack is to have a fast and efficient build for day to day development, but also the smallest possible built bundles so users do not need to wait a long time for all required assets to be retrieved by their browser.
This UI codebase (client and server) is built using two tools, [TypeScript](https://www.typescriptlang.org/) and [Webpack](https://webpack.js.org/). Webpack acts as our main build and bundling tool, while TypeScript acts as a transpiler - meaning the UI codebase can make use of the latest and greatest ECMAscript syntax, and TypeScript will polyfill where appropriate to provide cross browser support. The below will detail choices we have made regarding how the build works, configuration and considerations to be aware of, and the end output. The aim of this stack is to have a fast and efficient build for day to day development, but also the smallest possible built bundles so users do not need to wait a long time for all required assets to be retrieved by their browser.

### Treeshaking

Expand Down Expand Up @@ -82,7 +82,7 @@ In addition to aliasing, the webpack configuration will be as follows:
| mode | `production` or `development` (provided via config file used) | Build mode. If production, code will be minified and have developer helpers (warnings etc) removed from the built output |
| target | `web` or `node` (provided via config file used) | The build target. The client side code will be built for `web`, and server `node`, reflecting where the code runs (and thus what Webpack will/will not include in it's output). |
| output.path | `dist/client` or `dist/server` (via constant) | All output to be placed in the `dist` directory, with client code going into the `client` directory, and all server code into `server` respectively. |
| output.publicPath | `` (empty string) | The public path of static/public content included by Webpack. Is relative to the URL. |
| output.publicPath | `` (empty string) | The public path of static/public content included by Webpack. Is relative to the URL. |
| output.filename | `[name].bundle.js` | Name of the built entry bundle. Will be `main.bundle.js` once built as we have one entry point |
| module.rules | Array of rules - [see here for details](#module-rules) | Rules/tests to run on modules being built. Depending on the file being parsed, different transformations should be run |
| plugins | Array of plugins - [see here for details](#webpack-plugins) | Additional tools to customise the build to produce the required output |
Expand Down
192 changes: 192 additions & 0 deletions docs/Coding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#Coding Standards

The Strimzi UI projects uses best practices based off the official [React TypeScript Cheat sheet](https://react-typescript-cheatsheet.netlify.app/), with modifications for this project. The React TypeScript Cheat sheet is maintained and used by developers through out the world, and is a place where developers can bring together lessons learned using TypeScript and React.

## Imports

Since we are using TypeScript 4.x + for this project, default imports should conform to the new standard set forth in TypeScript 2.7:

```typescript
import React from 'react';
import ReactDOM from 'react-dom';
```

For imports that are not the default import use the following syntax:

```typescript
import { X1, X2, ... Xn } from 'package-x';
```

## View Props

Since Strimzi UI is made up of 2 different views (one for PatternFly, and one for Carbon) props for a component should be declared in separate file instead of within the components source code. For example Carbon views are written in **View.carbon.tsx**, while PatternFly views are written in **View.patternfly.tsx**. In order to allow the views to share the same props, the code for the props should live in **View.props.tsx**.

For props we are using **type** instead of **interfaces**. The reason to use types instead of interfaces is for consistency between the views and because it's more constrained (See [Types or Interfaces](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/types_or_interfaces) for more clarification). By using types we are ensuring that both views will not deviate from the agreed upon [contract](https://dev.to/reyronald/typescript-types-or-interfaces-for-react-component-props-1408).

The following is an example of using a type for props:

```typescript
export type ExampleComponentProps = {
message: string;
};
```

Since we are using typescript we no longer need to use prop-types as typescript provides the functionality we received with prop-types via types. To set the default value of the props you can do so in by specifying a value in the argument's for the function component. The following is an example on how to do that:

```typescript
type GreetProps = { age?: number };

const Greet: FunctionComponent<GreetingProps> = ({ age = 21 }: GreetProps) => // etc
```
## State objects should be types
When maintaining state for a component that requires it's state to be defined by an object, it is recommended that you use a [type instead of an interface](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/types_or_interfaces). For example if you need to maintain the currentApiId and isExpanded in a single object you can do the following:
```typescript
type ApiDrawerState = {
currentApiId: string;
isExpanded: boolean;
};
```

## Interfaces

Interfaces should be used for all public facing API definitions, as well as models. A table describing when to use interfaces vs. types can be found [here](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/types_or_interfaces).

```typescript
// The following is an example of memory information model

export interface MemoryInfoRepresentation {
total: number;
totalFormatted: string;
used: number;
usedFormatted: string;
free: number;
freePercentage: number;
freeFormatted: string;
}
```

## Function Components

This project uses function components and hooks over class components. When coding function components in typescript a developer should include any specific props from the **View.props.tsx**

```typescript
export const ExampleComponent: FunctionComponent<ExampleComponentProps> = ({
message,
children,
}: ExampleComponentProps) => (
<>
<div>{message}</div>
<div>{children}</div>
</>
);
```

For components that do not have any additional props an empty object should be used instead:

```typescript
export const ExampleNoPropsComponent: FunctionComponent<{}> = () => (
<div>Example Component with no props</div>
);
```

Additional details around function components can be found [here](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/function_components).

## Hooks

When using hooks with Typescript there are few recommendations that we follow below. Additional recommendations besides the ones mention in this document can be found [here](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks).

### Inference vs Types for useState

Currently we recommend using inference for the primitive types booleans, numbers, and string when using useState. Anything other then these 3 types should use a declarative syntax to specify what is expected. For example the following is an example of how to use inference:

```typescript
const [isEnabled, setIsEnabled] = React.useState(false);
```

Here is an example how to use a declarative syntax. When using a declarative syntax if the value can be null that will also need to be specified:

```typescript
const [user, setUser] = useState<IUser | null>(null);

setUser(newUser);
```

### useReducers

When using reducers make sure you specify the [return type and do not use inference](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks#usereducer).

```typescript
const initialState = { count: 0 };

type ACTIONTYPE =
| { type: 'increment'; payload: number }
| { type: 'decrement'; payload: string };

function reducer(state: typeof initialState, action: ACTIONTYPE) {
switch (action.type) {
case 'increment':
return { count: state.count + action.payload };
case 'decrement':
return { count: state.count - Number(action.payload) };
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = React.useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>
-
</button>
<button onClick={() => dispatch({ type: 'increment', payload: 5 })}>
+
</button>
</>
);
}
```

### useEffect

For useEffect only [return the function or undefined](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks#useeffect).

```typescript
function DelayedEffect(props: { timerMs: number }) {
const { timerMs } = props;
// bad! setTimeout implicitly returns a number because the arrow function body isn't wrapped in curly braces
useEffect(
() =>
setTimeout(() => {
/* do stuff */
}, timerMs),
[timerMs]
);
return null;
}
```

### useRef

When using useRef there are two options with Typescript. The first one is when creating a [read-only ref](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks#useref).

```typescript
const refExample = useRef<HTMLElement>(null!);
```

By passing in null! it will prevent Typescript from returning an error saying refExample maybe null.

The second option is for creating [mutable refs](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/hooks#useref) that you will manage.

```typescript
const refExampleMutable = (useRef < HTMLElement) | (null > null);
```

## Additional Typescript Pointers

Besides the details outlined above a list of recommendations for Typescript is maintained by several Typescript React developers [here](https://react-typescript-cheatsheet.netlify.app/). This is a great reference to use for any additional questions that are not outlined within the coding standards.
Loading

0 comments on commit dad1d67

Please sign in to comment.