React hook + typescript + styled component site building

Technology selection

Comparison between Vue and React

  • Componentization
    vue's componentization is to put the UI structure (template), UI style (style), data and business logic (script) in one vue file, before running vue files will be compiled into real components;
    The Componentization of React is to implement components directly in the form of JS code
  • template engine
    Vue's view template uses HTML like writing method plus attributes and instructions. In most cases, it is clearer and more efficient than React's JSX writing method. However, in complex scenes, Vue writing method is sometimes more troublesome than React
  • Data monitoring
    Vue uses proxy / interception, which allows us to modify the data directly, but React needs to use the setState API to change the data

Project construction

directory structure

├─ mock     #Data simulation
├─ public   #static state
├─ scripts  #script
└─ src
    ├─ common      #Tool library
    ├─ components  #assembly
    ├─ hooks       #hook
    ├─ pages       #page
    ├─ styles      #style
    └─ router      #route

Technology stack

  • Development framework: React
  • Build tool: Webpack
  • Type check: TypeScript
  • Log embedding point: @ Baidu / bend SDK
  • View styles: styled components
  • State management: react hook / usereducer
  • Data request: UMI hook / userequest + Axios
  • Specification inspection: eslint + prettier + husky + lint staged + committed lint

Code normalization submission

  • husky registers git's hook function to ensure that the code scanning action is called when git executes commit
  • Lint staged ensures that only the files currently add ed to the stage area are scanned
  • prettier auto format code
  • eslint scans the code according to the configuration
  • @Commit lint / cli specification commit submission
  • @Common commit / config

Workflow

  • The code to be submitted git add is added to the staging area
  • Execute git commit
  • husky's hook function registered in Git pre commit is called to execute lint staged
  • Lint staged obtains all submitted documents and executes the written tasks in turn (ESLint and Prettier)
  • If there is an error (failing the ESlint check), stop the task, print the error message, and execute git commit after repair
  • Successfully commit and push to remote

package.json is configured as follows:

{
  ...
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged",
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  },
  "lint-staged": {
    "src/**/*.{jsx,js,tsx,ts}": [
      "prettier --write",
      "eslint --fix",
      "git add"
    ]
  }
}

.prettierrc.js

module.exports = {
    "printWidth": 100, // If the number of characters in a line exceeds, it will wrap. The default is 80
    "tabWidth": 4,
    "useTabs": false, // Note: the makefile file must use tab
    "singleQuote": true,
    "semi": true,
    "trailingComma": "es5", //Whether to use trailing comma, there are three optional values "< none|es5|all >"
    "bracketSpacing": true, //Whether there are spaces between object braces. The default is true. Effect: {foo: bar}
    "endOfLine": "auto",
    "arrowParens": "avoid"
};

.eslintrc.js

module.exports = {
  "root": true,
  "env": {
    "browser": true,
    "node": true,
    "es6": true,
    "jest": true,
    "jsx-control-statements/jsx-control-statements": true
  },
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "sourceType": 'module',
    "ecmaFeatures": {
      "jsx": true,
      "experimentalObjectRestSpread": true
    }
  },
  "globals": {
    // "wx": "readonly",
  },
  "extends": [
    "eslint:recommended",
    "plugin:react/recommended",
    "plugin:jsx-control-statements/recommended", // It needs to be used with babel plug-in
    "prettier"
  ],
  "overrides": [
    {
      "files": ["**/*.tsx"],
      "rules": {
          "react/prop-types": "off"
      }
    }
  ],
  "settings": {
    "react": {
      "version": "detect"
    }
  },
  "plugins": ["@typescript-eslint", "react", "react-hooks", "jsx-control-statements", "prettier"],
  "rules": {
    "prettier/prettier": 1,
    "no-extra-semi": 2, // Unnecessary semicolons are prohibited
    "quotes": ['error', 'single'], // Force single quotes
    "no-unused-vars": 0, // Undefined variables are not allowed
    "jsx-control-statements/jsx-use-if-tag": 0,
    "react-hooks/rules-of-hooks": "error", // Check Hook rules
    "react-hooks/exhaustive-deps": "warn" // Check the dependency of effect
  }
};

.eslintignore/.prettierignore

**/*.js
!src/**/*.js

.commitlintrc.js

module.exports = {
    extends: ['@commitlint/config-conventional']
};

The submission shall follow the format of conventional commit, namely:

type(scope?): subject

e.g. feat: teaching and training PC framework construction (cvi-3000)

The type can be:

  • feat: new feature s
  • upd: update a function
  • Fix: fix bug s
  • docs: documentation
  • style: format (changes that do not affect code operation)
  • refactor: refactoring (that is, it is not a new function or a code change to modify a bug)
  • Test: add test
  • chore: changes in the construction process or auxiliary tools

Style scheme

styled-components

Official document of styled comonts

Is a class library of CSS in JS, that is, you can write CSS syntax in JS
Using Sass/Less and other preprocessing languages requires configuration of various loader s in Webpack
Styled components only need to be referenced directly

import styled from 'styled-components';
  • Stylized components are mainly used to write actual CSS code to design component styles without mapping between components and styles. After creation, they are actually a React component
  • After use, you no longer need to use className to control the style, but write it as a more semantic component
  • The compiled nodes will randomly generate class es, which can avoid global pollution, but will increase the difficulty of maintenance

Solution: add the styled components plug-in to the babel configuration

babel-plugin-styled-components

use: [
 {
    loader: 'babel-loader',
    options: {
      ...
      plugins: [
        'babel-plugin-styled-components',
        ...
      ]
    }
  },
  ...
]

After compilation:

Basic use

// src/components/styles/index.ts
...
interface ICardProps {
    type?: string;
}
...
const typeMap = (type: string | undefined) => {
    switch (type) {
        case 'b':
            return 'block';
        case 'i':
            return 'inline-block';
        case 'f':
            return 'flex';
        case 'n':
            return 'none';
        default:
            return 'block';
    }
};
/**
 * Card container
 */
const Card = styled.div`
    display: ${(props: ICardProps) => typeMap(props.type)};
    padding: 24px 24px 15px;
    background-color: #fff;
`;
Card.defaultProps = {
    type: 'f',
};
...
// src/pages/shop/index.tsx
import styled from 'styled-components';
...
const Shop = () => {
	...
	return (
		<Card type="b">
			...
		</Card>
	);
}

Global style

// src/style.ts
import { createGlobalStyle } from 'styled-components';

export const GlobalStyle = createGlobalStyle`
    html,body,div,span,applet,object,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video,button {
   margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    font-weight: normal;
    vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section {
    display: block;
}
ol,ul,li {
    list-style:none;
}
...
`;
...
import { GlobalStyle } from '@/style';
...

const App: React.FC = () => {
	return (
        <Router>
            <GlobalStyle />
            ...
        </Router>
    );
};

export default App;

code snippet

// src/components/styles/snippet.ts
import { css } from 'styled-components';

const mask = css`
    &::after {
        content: '';
        position: absolute;
        left: 0;
        right: 0;
        top: 0;
        bottom: 0;
        background-image: radial-gradient(50% 129%, rgba(0, 0, 0, 0) 50%, rgba(0, 0, 0, 0.1) 100%);
    }
`;
...
export default {
    mask,
    ...
};
// src/components/styles/index.ts
import styled from 'styled-components';
import s from './snippet';
...
const Img = styled.div`
	...
	${(props: ICardImgProps) => (props.mask ? s.mask : '')}
	...
`

Type system

TypeScript is an open source programming language developed by Microsoft

TypeScript official documentation

Is a superset of JS. It mainly provides type system and support for the new features of ECMAScript that have not been officially released. It will eventually be compiled into pure JavaScript code

advantage:

  • Most errors can be found during the compilation phase
  • Increase the readability and maintainability of the code. Most functions know how to use it by looking at the type definition
  • VSCode provides TS with code completion, interface prompt, jump definition and other functions

Disadvantages:

  • Many type definitions need to be written during development, which will increase the development cost in the short term, but for long-term maintenance projects, it can reduce the maintenance cost
  • Integration into the build process requires a certain amount of work
  • The combination with some third-party libraries may not be perfect (such as styled components.)
// tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "lib": [
            "dom",
            "dom.iterable",
            "esnext" 
        ],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "experimentalDecorators": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "noEmit": true,
        "jsx": "react",
        "downlevelIteration": true,
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        },
        "plugins": [
            {
                "transform": "typescript-plugin-styled-components",
                "type": "config"
            }
        ]
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}

React Hook

Hook is a new feature of React 16.8. It allows you to use state and other React features without writing class es.

  • Hook strengthens the ability of React function components, so that function components can achieve the state and life cycle in class components. Hook allows class components to be implemented in function components
  • The syntax is more concise, which solves the problems of using and understanding high-order components
  • Backward compatibility, class components will not be discarded

Class components and function components

Class component

import React, { Component } from 'react';

export default class Button extends Component {
  constructor() {
    super();
    this.state = { buttonText: 'Click' };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState(() => {
      return { buttonText: 'Click Done' };
    });
  }
  render() {
    const { buttonText } = this.state;
    return <button onClick={this.handleClick}>{buttonText}</button>;
  }
}

Function component

import React, { useState } from 'react';

export default function Button() {
  const [buttonText, setButtonText] = useState('Click');
  function handleClick() {
    return setButtonText('Click Done');
  }
  return <button onClick={handleClick}>{buttonText}</button>;
}

Disadvantages of class components:

  • this pointer needs to be bound manually
  • Large components are difficult to split and refactor
  • Business logic is scattered in various life cycle functions, which will lead to logic duplication (solved by function component useEffect)
  • Complex programming modes are introduced, such as Render Props and HOC (custom Hook solution for function components)

Render Props

Use a prop whose value is a function to pass the components that need dynamic rendering

import UIDemo from 'components/demo';
class DataProvider extends React.Component {
	constructor(props) {
		super(props);
		this.state = {target: 'Payen'};
	}
	render() {
		return (
			<div>
				{this.props.render(this.state)}
			</div>
		)
	}
}

<DataProvider render={data => (
	<UIDemo target={data.target} />
)}/>

<DataProvider>
	{data => (
		<UIDemo target={data.target}/>
	)}
</DataProvider>

HOC

The function receives a component as a parameter and returns a new component after a series of processing

const withUser = WrappedComponent => {
	const user = sessionStorage.getItem('user');
	return props => <WrappedComponent user={user} {...props}/>;
}
const UserPage = props => (
	<div>
		<p>name: {props.user}</p>
	</div>
);

Built in Hook API

Hook API name starts with use

Basic Hook:

  • useState
  • useEffect
  • useContext

Additional Hook:

  • useReducer
  • useCallback
  • useMemo
  • useRef
  • useImperativeHandle
  • useLayoutEffect
  • useDebugValue

Basic Hook

useState

// Pass in the initial state value
const [state, setState] = useState(initState);

// Pass in the function. The return value of the function is the initial state. The function will only be called during the initial rendering
const [state, setState] = useState(() => {
	const initState = Func();
	return initState;
});

The only parameter of useState is the initial state value, which is only used in the first rendering
Returns the current state value (state) and the function used to change the state (setState)
Similar to this. In class components setState
Deconstruction assignment syntax allows us to assign different names to the declared state

useEffect

useEffect(() => {
	Func(state);
	return () => {
		// It is executed when the component is unloaded and before subsequent rendering reruns the effect
	};
}, [state]);

useEffect receives a function that contains imperative code that may have side effects
In the React rendering stage, operations including side effects such as changing DOM and setting timers in the main body of function components are not allowed, because they will affect other components
useEffect has the same purpose as componentDidMount, componentDidUpdate and componentWillUnmount in class components

import { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

Usage rules

  • Hooks can only be called at the top level. You cannot use the Hook API in loops, conditions, and nested functions
  • Use the Hooks API only in the React function component and in custom Hooks functions

React will store the values in the array according to the order in which hook is called
If there is condition judgment, the corresponding value may not be obtained during update, resulting in confusion of values

Customize Hook

To distinguish from ordinary functions, custom Hook names start with use
Used to solve the problem of logical reuse

// src/hooks/index.tsx
import React, { useState, useEffect, ... } from 'react';
...
// Drag and drop hook
function useDraggable(ref: React.RefObject<HTMLElement>) {
    const [{ dx, dy }, setOffset] = useState({ dx: 0, dy: 0 });

    useEffect(() => {
        if (ref.current == null) {
            throw new Error('[useDraggable] ref Not registered in component');
        }
        const el = ref.current;

        const handleMouseDown = (event: MouseEvent) => {
            const startX = event.pageX - dx;
            const startY = event.pageY - dy;

            const handleMouseMove = (event: MouseEvent) => {
                const newDx = event.pageX - startX;
                const newDy = event.pageY - startY;
                setOffset({ dx: newDx, dy: newDy });
            };

            document.addEventListener('mousemove', handleMouseMove);
            document.addEventListener(
                'mouseup',
                () => {
                    document.removeEventListener('mousemove', handleMouseMove);
                },
                { once: true }
            );
        };

        el.addEventListener('mousedown', handleMouseDown);

        return () => {
            el.removeEventListener('mousedown', handleMouseDown);
        };
    }, [dx, dy, ref]);

    useEffect(() => {
        if (ref.current) {
            ref.current.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
        }
    }, [dx, dy, ref]);
}
// src/components/Usual/ButtonGroup.tsx
import { useDraggable, ... } from '@/hooks';
...
const ButtonGroup = (props: IButtonGroupProps) => {
	...
	// The appointment pop-up window can be dragged
	const el = useRef<HTMLDivElement>(null);
    useDraggable(el);
    return (
	    <Space gap="b" nowrap>
		    ...
		    <PopupWrapper ref={el}>
                <AppointPopup
                    show={showAppointBox}
                    switchAppointBox={switchAppointBox}
                    formid={formid}
                />
            </PopupWrapper>
		</Space>
    )
};

Hook vs. HOC

import React from 'react';

function hocMatch(Component) {
  return class Match React.Component {
    componentDidMount() {
      this.getMatchInfo(this.props.matchId)
    }
    componentDidUpdate(prevProps) {
      if (prevProps.matchId !== this.props.matchId) {
        this.getMatchInfo(this.props.matchId)
      }
    }
    getMatchInfo = (matchId) => {
      // Request the background interface to obtain the event information
    }
    render () {
      return (
        <Component {...this.props} />
      )
    }
  }
}

const MatchDiv=hocMatch(DivUIComponent)
const MatchSpan=hocMatch(SpanUIComponent)

<MatchDiv matchId={1} matchInfo={matchInfo} />
<MatchSpan matchId={1} matchInfo={matchInfo} />
function useMatch(matchId) {
  const [ matchInfo, setMatchInfo ] = useState('');
  useEffect(() => {
    // Request the background interface to obtain the event information
    // ...
    setMatchInfo(serverResult) // serverResult back end return data
  }, [matchId]);
  return [matchInfo];
}
...
export default function Match({matchId}) {
  const [matchInfo] = useMatch(matchId);
  return <div>{matchInfo}</div>;
}

Other issues

Map jump

const jumpMap = (e: React.MouseEvent<HTMLParagraphElement, MouseEvent>) => {
    // coord_type coordinate type: select the coordinates of the National Survey Bureau (Mars coordinate system)
    window.open(
        `http://api.map.baidu.com/marker?location=${pos?.lat},${pos?.lng}&title=${name ||
            'My position'}&content=${children}&output=html&coord_type=gcj02`
    );
    e.stopPropagation();
};

Baidu map tune up interface

QR code

import QRCode from 'qrcode.react';
...
<QRCode
    value={qrCodeUrl}
    size={300}
    fgColor="#000"
    imageSettings={{
        src: logo,
        height: 60,
        width: 60,
        excavate: false,
    }}
/>

Screen adaptation

CSS adaptation

Directly use CSS media query to set styles separately under the narrow screen

const PageContent = styled.div`
    width: 1200px;
    padding-top: 16px;
    margin: 0 auto;
    @media (max-width: 900px) {
        width: 740px;
    }
`;

JS adaptation

Implement a custom Hook

// src/hooks/index.ts
...
function useNarrowScreen() {
    const isNarrow = () => window.innerWidth <= 900;
    const [narrow, setNarrow] = useState(isNarrow);
    useEffect(() => {
        const resizeHandler = () => setNarrow(isNarrow());
        window.addEventListener('resize', resizeHandler);
        return () => window.removeEventListener('resize', resizeHandler);
    });
    return narrow;
}
// src/components/Base/PhotoAlbum.tsx
...
import { useNarrowScreen } from '@/hooks';
...
const PhotoAlbum = (props: IPhotoAlbum) => {
	...
	const [baseLen, setBaseLen] = useState(0);
	const isNarrow = useNarrowScreen();
	useEffect(() => {
		setBaseLen(isNarrow ? 3 : 5);
		...
	}, [isNarrow, ...]);
};

Log correlation

click log

import React, { useEffect } from 'react';
import { sendLog } from '@/common/log';
...
useEffect(() => {
    const listenedEles = document.querySelectorAll('[data-mod]') || [];
    listenedEles.forEach((ele: HTMLElement) => {
        ele.onclick = function() {
            const mod = ele.getAttribute('data-mod');
            ...
        };
    });
}, []);
// src/hooks/useLog.ts
function useClickLog(ref: React.RefObject<HTMLElement>) {
    useEffect(() => {
        const el: any = ref.current;

        el.onclick = function() {
            sendLog(el);
        };
    }, [ref]);
}
const jumpMapRef = useRef(null);
useClickLog(jumpMapRef);
...
<MapHref
	ref={jumpMapRef}
	onClick={jumpMap}
	data-mod="map_click"
>
   see
</MapHref>

show log

Page entry buried point

Add in the route component rendering callback function

// src/router/RouterWithSubRoutes.tsx
import React from 'react';
import { Route, Redirect, RouteComponentProps } from 'react-router-dom';
import { RouteInterface } from '@/types/route';
import { sendLog } from '@/common/log';
import { routers } from '@/common/router-config';

export const RouteWithSubRoutes = (
    route: RouteInterface,
    i: number,
    authed: boolean,
    authPath: string
) => {
    return (
        <Route
            key={i}
            path={route.path}
            exact={route.exact}
            render={(props: RouteComponentProps) => {
                const { match } = props;
                const location = props.location;
                if (routers[location.pathname]) {
                    sendLog({
                        mod: 'detail_show',
                        s_type: 'show',
                    });
                }
                if (!route.auth || authed || route.path === authPath) {
                    return <route.component {...props} routes={route.routes} />;
                }
                return <Redirect to={{ pathname: authPath, state: { from: props.location } }} />;
            }}
        />
    );
};

Page leaving buried point

Using the departure confirmation component Prompt provided by react router

// src/App.tsx
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { routes } from '@/router/router';
import { RenderRoutes } from '@/router/RenderRoutes';
import { GlobalStyle } from '@/style';
import { Prompt } from 'react-router';
import { sendLog } from '@/common/log';
import { routers } from '@/common/router-config';
...

const App: React.FC = () => {
	...
    return (
        <Router>
            <GlobalStyle />
            {RenderRoutes(routes, authed, authPath)}
            <Prompt
                message={location => {
                    // Logic before exiting the page
                    window.scrollTo(0, 0);

                    const { pathname, search } = window.location;
                    if (routers[pathname]) {
                        // sendLog
                    }
                    return true;
                }}
            />
        </Router>
    );
};

export default App;

Page state cache

Vue has a keep alive component function, but React does not provide official support
When < route > is used, the components corresponding to the route cannot be cached in forward and backward, and data and behavior will be lost

For example: after the list page scrolls to the bottom, click to jump to the details page. After returning, it will return to the top of the list, and the data and scrolling position will be reset

The components configured in < route > will be unloaded when the paths do not match, and the corresponding real nodes will also be removed from the DOM tree

Three solutions:

  • Manually implement the keep alive function similar to Vue
  • Migrate Route libraries that other third parties can implement state caching
  • Save page status to sessionStorage

By implementing user-defined hook useStorage, you can proxy data to other data sources
LocalStorage / SessionStorage

// src/pages/search/index.tsx
...
import { useStorage } from '@/hooks';
...
const Search = () => {
	const location = useLocation();
    const params = new URLSearchParams(location.search);
    const tabSearch = params.get('tab') || 'shop';
	...
	// const [currentTab, changeCurrentTabState] = useState(tabSearch);
	const [currentTab, changeCurrentTabState] = useStorage('zlhx_home_tab', tabSearch);
	...
}
// src/hooks/index.ts
import React, { useState, useEffect, useCallback, Dispatch, SetStateAction } from 'react';
...
function useStorage<T>(
    key: string,
    defaultValue?: T | (() => T), // Default value
    keepOnWindowClosed: boolean = false // Do you want to keep the data after the window is closed
): [T | undefined, Dispatch<SetStateAction<T>>, () => void] {
    const storage = keepOnWindowClosed ? localStorage : sessionStorage;

    // Attempting to recover values from Storage
    const getStorageValue = () => {
        try {
            const storageValue = storage.getItem(key);
            if (storageValue != null) {
                return JSON.parse(storageValue);
            } else if (defaultValue != null) {
                // Set default values
                const value =
                    typeof defaultValue === 'function' ? (defaultValue as () => T)() : defaultValue;
                storage.setItem(key, JSON.stringify(value));
                return value;
            }
        } catch (err) {
            console.warn(`useStorage Unable to get ${key}: `, err);
        }

        return undefined;
    };

    const [value, setValue] = useState<T | undefined>(getStorageValue);

    // Update component status and save to Storage
    const save = useCallback<Dispatch<SetStateAction<T>>>(
        value => {
            setValue(prev => {
                const finalValue =
                    typeof value === 'function'
                        ? (value as (prev: T | undefined) => T)(prev)
                        : value;
                storage.setItem(key, JSON.stringify(finalValue));
                return finalValue;
            });
        },
        [storage, key]
    );

    // Remove status
    const clear = useCallback(() => {
        storage.removeItem(key);
        setValue(undefined);
    }, [storage, key]);

    return [value, save, clear];
}

Keywords: Javascript Front-end React TypeScript

Added by ganeshasri on Wed, 02 Feb 2022 16:01:20 +0200