The best practice of React-hooks+TypeScript

Time:2022-11-24

React Hooks

What are Hooks

  • ReactThe use of functional components has always been advocated, but sometimes it is necessary to usestateOr for some other functions, only class components can be used, because function components have no instances and no life cycle functions, only class components have them.
  • HooksYesReact 16.8Added features that allow you to writeclassuse in the case ofstateand othersReactcharacteristic.
  • If you’re writing a functional component and realize you need to add somestate, the previous practice is that the other must be transformed intoclass. Now you can directly use in existing functional componentsHooks
  • useat the beginningReact APIare allHooks

What problems do Hooks solve?

  • Difficult to reuse state logic

    • It is difficult to reuse state logic between components, and may userender props(rendering property) orHOC(higher-order components), but whether it is a rendering attribute or a higher-order component, a layer of parent container (usually a div element) will be wrapped around the original component,lead to hierarchical redundancy
  • tends to be complex and difficult to maintain

    • Mixing irrelevant logic in the life cycle function (such as: incomponentDidMountRegister events and other logic incomponentWillUnmountUnloading events in the middle, such scattered and non-centralized writing, it is easy to writeBug )。
    • Class components are full of state access and processing, making it difficult to split components into smaller components.
  • this points to the question

    • When the parent component passes a function to the child component, it must be boundthis

Advantages of Hooks

  • Three Big Questions That Can Optimize Class Components
  • State logic can be reused without modifying the component structure (custom Hooks)
  • Ability to split interrelated parts of a component into smaller functions (such as setting a subscription or requesting data)
  • Separation of concerns for side effects

    • Side effects refer to logic that does not occur during data-to-view conversion, such asAjaxrequest, access nativeDOMElements, local persistent cache, binding/unbinding events, adding subscriptions, setting timers, recording logs, etc. In the past, these side effects were written in the class component life cycle function.

Common Hooks

useState

  1. ReactSuppose when we call multiple timesuseState, it is necessary to ensure that the order of their calls is unchanged each time they are rendered.
  2. Add some internals to the component by calling it in the function componentstateReactwill preserve this state across repeated renders
  3. useStateThe only parameter is the initialstate
  4. useStatewill return an array: astate, an updatestateThe function
  5. During initial rendering, the returned statestatewith the first parameter passed ininitialStatesame value.
    We can call update in event handler function or some other placestateThe function. it’s likeclasscomponentthis.setState, but it doesn’t put the newstateand oldstateMerge is done instead of direct replace.
Instructions
const [state, setState] = useState(initialState);

for example

import React, { useState } from 'react';

function Counter() {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>{counter}</p>
      <button onClick={() => setCounter(counter + 1)}>counter + 1</button>
    </>
  );
}

export default Counter;
Each rendering is an independent closure
  • Each render has its own Props and State
  • Each render has its own event handler
  • When you click to update the state, the function component will be called again, so each rendering is independent, and the obtained value will not be affected by subsequent operations

for example

function Counter() {
  const [counter, setCounter] = useState(0);
  function alertNumber() {
    setTimeout(() => {
      // Can only get the state when the button is clicked
      alert(counter);
    }, 3000);
  }
  return (
    <>
      <p>{counter}</p>
      <button onClick={() => setCounter(counter + 1)}>counter + 1</button>
      <button onClick={alertNumber}>alertCounter</button>
    </>
  );
}
functional update

if newstateNeed to use the previousstateCalculated, then you can pass the callback function as a parameter tosetState. This callback function will receive the previousstate, and returns an updated value.

for example

function Counter() {
  const [counter, setCounter] = useState(0);

  return (
    <>
      <p>{counter}</p>
      <button onClick={() => setCounter(counter => counter + 10)}>        counter + 10      </button>
    </>
  );
}
lazy initialization
  • initialStateThe parameter will only work in the initial rendering of the component, and will be ignored in subsequent renderings
  • if initialstateIf it needs to be obtained through complex calculations, you can pass in a function, calculate in the function and return the initialstate, this function is only called on the initial render

for example

function Counter4() {
  console.log('Counter render');

  // This function is only executed once during the initial rendering, and this function will not be called again when the component is updated to re-render the component
  function getInitState() {
    console.log('getInitState');
    // complex calculation
    return 100;
  }

  let [counter, setCounter] = useState(getInitState);

  return (
    <>
      <p>{counter}</p>
      <button onClick={() => setCounter(counter + 1)}>+1</button>
    </>
  );
}

useEffect

  • Effect (side effect): Refers to the logic that does not occur in the process of data to view conversion, such as ajax request, access to native dom elements, local persistent cache, binding/unbinding events, adding subscriptions, setting timers, logging, etc. .
  • Side-effect operations can be divided into two categories: those that need to be cleaned up and those that don’t.
  • It is not allowed to change the dom, send ajax requests, and perform other operations with side effects in the function component (here refers to the React rendering phase), because this may cause inexplicable bugs and destroy the consistency of the UI
  • useEffect is an Effect Hook that adds the ability to operate side effects to function components. It has the same purpose as componentDidMount, componentDidUpdate and componentWillUnmount in the class component, but it is merged into one API
  • useEffect receives a function that will be executed after the component is rendered to the screen. This function has requirements: either return a function that can clear the side effect, or return nothing
  • Unlike componentDidMount or componentDidUpdate, effects dispatched with useEffect won’t block the browser from updating the screen, which makes your app appear more responsive. In most cases, effects do not need to execute synchronously. In individual cases (such as measuring layout), there is a separate useLayoutEffect Hook for you to use, and its API is the same as useEffect.
Instructions
const App => () => {
  useEffect(()=>{})
  // or
  useEffect(()=>{},[...])
  return <></>
}
Use the class component to modify the title

In this class, we need to write duplicate code in the two lifecycle functions, because in many cases, we want to perform the same operation when the component is loaded and updated. We want it to execute after every render, but React’s class component doesn’t provide such a method. Even if we extract a method, we still have to call it in two places. Refer to the actual video explanation of React: enter learning

class Counter extends React.Component{
    state = {number:0};
    add = ()=>{
        this.setState({number:this.state.number+1});
    };
    componentDidMount(){
        this.changeTitle();
    }
    componentDidUpdate(){
        this.changeTitle();
    }
    changeTitle = ()=>{
        document.title = `You have clicked ${this.state.number} times`;
    };
    render(){
        return (
            <>
              <p>{this.state.number}</p>
              <button onClick={this.add}>+</button>
            </>
        )
    }
}
Use the useEffect component to modify the title
function Counter(){
    const [number,setNumber] = useState(0);
    // This function in useEffect will be executed after the first rendering and after the update is completed
    // Equivalent to componentDidMount and componentDidUpdate:
    useEffect(() => {
        document.title = `You clicked ${number} times`;
    });
    return (
        <>
            <p>{number}</p>
            <button onClick={()=>setNumber(number+1)}>+</button>
        </>
    )
}

What does useEffect do?By using this Hook, you can tell React components to perform certain actions after rendering. React saves the function you pass (we’ll call it “effect”) and calls it after performing the DOM update. In this effect, we set the document’s title property, but we could also perform data fetches or call other imperative APIs.

Why is useEffect called inside the component?Putting useEffect inside the component allows us to directly access the count state variable (or other props) in the effect. We don’t need a special API to read it – it’s already stored in the function scope. Hook uses JavaScript’s closure mechanism without introducing a specific React API when JavaScript already provides a solution.

Will useEffect be executed after every render?Yes, by default it executes after the first render and after every update. (We’ll talk about how to control this later) You may be more receptive to the concept of effects happening “after rendering”, without having to think about “mounting” or “updating”. React guarantees that every time an effect is run, the DOM has been updated.

clear side effects
  • The side effect function can also specify how to clear the side effect by returning a function. To prevent memory leaks, the clear function will be executed before the component is unloaded. If the component renders multiple times, the previous effect is cleared before the next one is executed.
function Counter(){
  let [number,setNumber] = useState(0);
  let [text,setText] = useState('');
  // Equivalent to componentDidMount and componentDidUpdate
  useEffect(()=>{
      console.log('Start a new timer')
      let timer = setInterval(()=>{
          setNumber(number=>number+1);
      },1000);
      // if useEffect returns a function, this function will be called when the component is unmounted and updated
      // useEffect will call the last returned function before executing the side effect function
      // If side effects are to be cleared, either return a function that clears side effects
      // return ()=>{
      //     console.log('destroy effect');
      //     clearInterval($timer);
      // }
  });
  // },[]);//Either pass in an empty dependency array here, so that it will not be repeated
  return (
      <>
        <input value={text} onChange={(event)=>setText(event.target.value)}/>        <p>{number}</p>
        <button>+</button>
      </>
  )
}
Skip Effect for performance optimization
  • The dependencies array controls the execution of useEffect
  • If some specific value doesn’t change between re-renders, you can tell React to skip calling effect by passing the array as the second optional argument to useEffect
  • If you want to execute an effect that only runs once (only when the component is mounted), you can pass an empty array ([]) as the second parameter. This tells React that your effect does not depend on any values ​​in props or state, so it never needs to re-execute
function Counter(){
  let [number,setNumber] = useState(0);
  let [text,setText] = useState('');
  // Equivalent to componentDidMount and componentDidUpdate
  useEffect(()=>{
      console.log('useEffect');
      let timer = setInterval(()=>{
          setNumber(number=>number+1);
      },1000);
  },[text]);// The array represents the variable that the effect depends on, and the effect function will be re-executed only when the variable changes
  return (
      <>
        <input value={text} onChange={(e)=>setText(e.target.value)}/>        <p>{number}</p>
        <button>+</button>
      </>
  )
}
Separation of Concerns Using Multiple Effects
  • One of the purposes of using Hook is to solve the problem that the life cycle function in the class often contains irrelevant logic, but separates the relevant logic into several different methods.
// class version

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
  • We can see how the logic of document.title is split intocomponentDidMountandcomponentDidUpdateIn, how is the subscription logic divided intocomponentDidMountandcomponentWillUnmountmiddle. andcomponentDidMountcontains code for two different functions at the same time. This would confuse the lifecycle functions.
  • Hooks allow us to separate them according to the purpose of the code, not like lifecycle functions.Reactwill followeffectThe order of declaration calls each of the components in turneffect
// Hooks version

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

useContext

const value = useContext(MyContext);

receive acontextobject (the return value of React.createContext) and returns thecontextthe current value of . currentcontextThe value is determined by the one closest to the current component in the upper component<MyContext.Provider>The value prop decides.

When the component upper most recent<MyContext.Provider>When updating, theHookwill trigger a re-render, and use the latest passed toMyContext providerofcontext valuevalue. Even if ancestors useReact.memoorshouldComponentUpdate, also used in the component itselfuseContextwhen re-rendering.

Don’t forget that the parameter of useContext must be the context object itself:

  • correct:useContext(MyContext)
  • mistake:useContext(MyContext.Consumer)
  • mistake:useContext(MyContext.Provider)

hint
if you are in contactHookalready rightcontext APIfamiliar, it should be understandable,useContext(MyContext)equivalent toclassin the componentstatic contextType = MyContextor<MyContext.Consumer>useContext(MyContext)just to enable you to readcontextvalue and subscriptioncontextThe change. You still need to use in the upper component tree<MyContext.Provider>to provide the underlying components withcontext。

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.light}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return (
    <button style={{ background: theme.background, color: theme.foreground }}>
      I am styled by theme context!    </button>
  );
}

Custom Hooks

  • Custom Hooks are more of a convention than a feature. If the name of the function starts with use and calls other Hooks, it is called a custom Hook
  • Sometimes we want to reuse some state logic between components, either using render props, or using high-level components, or using redux
  • Custom Hook allows you to achieve the same purpose without adding components
  • Hook is a way to reuse state logic, it does not reuse state itself
  • In fact, each invocation of Hook has a completely independent state
function useNumber(){
  let [number,setNumber] = useState(0);
  useEffect(()=>{
    setInterval(()=>{
        setNumber(number=>number+1);
    },1000);
  },[]);
  return [number,setNumber];
}
// Each component calls the same hook, just reuses the state logic of the hook, and does not share a state
function Counter1(){
    let [number,setNumber] = useNumber();
    return (
        <div><button onClick={()=>{
            setNumber(number+1)
        }}>{number}</button></div>
    )
}
function Counter2(){
    let [number,setNumber] = useNumber();
    return (
        <div><button  onClick={()=>{
            setNumber(number+1)
        }}>{number}</button></div>
    )
}

useMemo、useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

existaandbWhen the value of the variable remains unchanged,memoizedCallbackThe citations are unchanged. which is:useCallbackThe first input parameter function of will be cached, so as to achieve the purpose of rendering performance optimization.

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

existaandbWhen the value of the variable remains unchanged,memoizedValueThe value of is unchanged. which is:useMemoThe first input function of the function will not be executed, so as to save the amount of calculation.

performance optimization
Object.is shallow comparison
  • Hook internal useObject.isto compare old and newstateIs it equal.
  • andclassin the componentsetStateThe method is different. If you modify the state and the passed state value does not change, it will not be re-rendered.
  • andclassin the componentsetStatemethod is different,useStateUpdate objects are not merged automatically. You can use functionalsetStateCombined with the spread operator to achieve the effect of merging updated objects.
function Counter(){
    const [counter,setCounter] = useState({name:'counter',number:0});
    console.log('render Counter')
    // If the passed state value has not changed when you modify the state, it will not be re-rendered
    return (
        <>
            <p>{counter.name}:{counter.number}</p>
            <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button>
            <button onClick={()=>setCounter(counter)}>++</button>
        </>
    )
}
reduce the number of renders
  • By default, as long as the state of the parent component changes (regardless of whether the child component depends on the state), the child component will also re-render
  • General optimization:

    • Class Components: You can usepureComponent
    • Function components: useReact.memo, passing the function component tomemoAfter that, a new component will be returned, the function of the new component:Don’t re-render the function if the received property doesn’t change
  • But how to ensure that the attributes will not change? use hereuseState, each update is independent,const [number,setNumber] = useState(0)That is to say, a new value will be generated every time (even if the value has not changed), even if React.memo is used, it will still be re-rendered.
const SubCounter = React.memo(({onClick,data}) =>{
  console.log('SubCounter render');
  return (
      <button onClick={onClick}>{data.number}</button>
  )
})
const ParentCounter = () => {
  console.log('ParentCounter render');
  const [name,setName]= useState('counter');
  const [number,setNumber] = useState(0);
  const data ={number};
  const addClick = ()=>{
      setNumber(number+1);
  };
  return (
      <>
          <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>          <SubCounter data={data} onClick={addClick}/>
      </>
  )
}
  • For a deeper optimization – useuseMemo & useCallback
const SubCounter = React.memo(({onClick,data}) =>{
  console.log('SubCounter render');
  return (
      <button onClick={onClick}>{data.number}</button>
  )
})
const ParentCounter = () => {
  console.log('ParentCounter render');
  const [name,setName]= useState('counter');
  const [number, setNumber] = useState(0);
  // When the parent component is updated, the variables and functions here will be recreated every time, so the properties received by the child component will be considered new every time
  // So the subcomponents will also be updated accordingly, useMemo can be used at this time
  // Whether there is a dependency array behind is very important, otherwise it will still be re-rendered
  // If the following dependency array has no value, even if the number value of the parent component changes, the child component will not update
  //const data = useMemo(()=>({number}),[]);
  const data = useMemo(()=>({number}),[number]);
  const addClick = useCallback(()=>{
      setNumber(number+1);
  },[number]);
  return (
      <>
          <input type="text" value={name} onChange={(e)=>setName(e.target.value)}/>          <SubCounter data={data} onClick={addClick}/>
      </>
  )
}

common problem

useEffect cannot receive async as a callback function

ReactRegulationuseEffectThe receiving function either returns a function that cleans up the side effects, or returns nothing. andasyncreturn ispromise

How to Fetch Data Elegantly in Hooks
function App() {
  const [data, setData] = useState({ hits: [] });
  useEffect(() => {
    // more elegant way
    const fetchData = async () => {
      const result = await axios(
        'https://api.github.com/api/v3/search?query=redux',
      );
      setData(result.data);
    };
    fetchData();
  }, []);
  return (
    <ul>
      {data.hits.map(item => (        <li key={item.id}>
          <a href={item.url}>{item.title}</a>
        </li>
      ))}    </ul>
  );
}
Don’t rely too much on useMemo
  • useMemoIt also has its own overhead.useMemowill “remember” some values, and laterrender, the value in the dependent array will be taken out and compared with the last recorded value, and the callback function will be re-executed if they are not equal, otherwise the “remembered” value will be returned directly. This process itself consumes a certain amount of memory and computing resources. Therefore, overuseuseMemoMay affect program performance.
  • In useuseMemoBefore doing so, three questions should be considered:

    • Pass touseMemoDoes the function cost a lot?Some calculations are very expensive, we need to “remember” its return value to avoidrenderto recalculate. If you’re performing an inexpensive operation, then you don’t need to remember the return value. Otherwise, useuseMemoThe overhead itself may exceed the overhead of recalculating this value. Therefore, for some simple JS operations, we don’t need to useuseMemoto “remember” its return value.
    • Is the returned value the original value?If the calculated value is a basic type (string, boolean, null, undefined, number, symbol), then each comparison is equal, and the downstream component will not re-render; if the calculated value is a complex type ( object, array), even if the value remains unchanged, the address will change, causing downstream components to re-render. So we also need to “remember” this value.
    • When writing a customHook, the return value must maintain the consistency of the reference. Because you can’t determine how the outside world will use its return value. If the return value is used as otherHookdependencies, and every timere-renderWhen references are inconsistent (when the values ​​​​are equal), bugs may occur. So if the value exposed in the custom Hook is object, array, function, etc., you should useuseMemo. to ensure that the reference does not change when the value is the same.

TypeScript

What is TypeScript

TypeScriptYesJavaScriptA superset of , which mainly providestype systemandrightES6support
The best practice of React-hooks+TypeScript

Why choose TypeScript

  • TypeScript increases code readability and maintainability

    • The type system is actually the best documentation, most functions can know how to use by looking at the type definition
    • Most errors can be found at compile time, which is better than errors at runtime
    • Enhancements to the editor and IDE, including code completion, interface hints, jump to definition, refactoring, and more
  • TypeScript is very inclusive

    • TypeScript is a superset of JavaScript, and .js files can be renamed directly to .ts
    • Automatic type inference even without explicitly defining the type
    • Can define almost any type from simple to complex
    • JavaScript files can be generated even if TypeScript compiles an error
    • Compatible with third-party libraries, even if the third-party library is not written in TypeScript, you can also write a separate type file for TypeScript to read
  • TypeScript has an active community

    • Most third-party libraries have type definition files provided to TypeScript
    • TypeScript embraces the ES6 specification and also supports some ESNext draft specifications

After understanding React Hooks and TypeScript, let’s take a look at the combination of the two!

practice

This practice comes from the open source component library project Azir Design that I am developingGridGrid layout component.

Target

The best practice of React-hooks+TypeScript

API

Row

Attributes illustrate type Defaults
className class name string
style Row component style object:CSSProperties
align vertical alignment top|middle|bottom top
justify horizontal arrangement start|end|center|space-around|space-between start
gutter Grid spacing, which can be written as pixel values ​​to set horizontal and vertical spacing or set [horizontal spacing, vertical spacing] at the same time in the form of an array number|[number,number] 0

Col

Attributes illustrate type Defaults
className class name string
style Col component style object:CSSProperties
flex flex layout properties string|number
offset The number of intervals on the left side of the grid, there cannot be a grid in the interval number 0
order grid order number 0
pull Move the grid to the left by the number of cells number 0
push Move the grid to the right by the number of cells number 0
span The number of grid spacers, when it is 0, it is equivalent to display: none number
xs <576px reactive raster, which can be a raster number or an object containing other properties number|object
sm ≥576px responsive grid, which can be a grid number or an object containing other properties number|object
md ≥768px responsive grid, which can be a grid number or an object containing other properties number|object
lg ≥992px responsive grid, which can be a grid number or an object containing other properties number|object
xl ≥1200px responsive grid, which can be a grid number or an object containing other properties number|object
xxl ≥1600px responsive grid, can be a grid number or an object containing other properties number|object

Show your skills

This practice mainly introduces the practice of React Hooks + TypeScript, without going into too much detail on CSS.

Step-1 Define the type of Prop for the Row component according to the API

// Row.tsx

+ import React, { CSSProperties, ReactNode } from 'react';
+ import import ClassNames from 'classnames';
+
+ type gutter = number | [number, number];
+ type align = 'top' | 'middle' | 'bottom';
+ type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';
+
+ interface RowProps {
+   className?: string;
+   align?: align;
+   justify?: justify;
+   gutter?: gutter;
+   style?: CSSProperties;
+   children?: ReactNode;
+ }

Here we use the basic data types, joint types, and interfaces provided by TypeScript.

basic data typeThere are two types of JavaScript:Primitive data types)andObject types

Primitive data types include:Boolean valuevaluestringnullundefinedand the new type in ES6Symbol. We mainly introduce the application of the first five primitive data types in TypeScript.

union typeUnion types (Union Types) indicate that the value can be one of many types.

type aliasType aliases are used to give a type a new name.

interfaceInterface is a very flexible concept in TypeScript. In addition to being used to abstract part of the behavior of a class, it is also often used toThe shape of the object (Shape)to describe. We describe RowProps here using an interface.

Step-2 Write the basic skeleton of the Row component

// Row.tsx

- import React, { CSSProperties, ReactNode } from 'react';
+ import React, { CSSProperties, ReactNode, FC } from 'react';

import ClassNames from 'classnames';

type gutter = number | [number, number];
type align = 'top' | 'middle' | 'bottom';
type justify = 'start' | 'end' | 'center' | 'space-around' | 'space-between';

interface RowProps {
  // ...
}
+ const Row: FC<RowProps> = props => {
+   const { className, align, justify, children, style = {} } = props;
+   const classes = ClassNames('azir-row', className, {
+     [`azir-row-${align}`]: align,
+     [`azir-row-${justify}`]: justify
+   });
+
+   return (
+     <div className={classes} style={style}>
+       {children}+     </div>
+   );
+ };

+ Row.defaultProps = {
+   align: 'top',
+   justify: 'start',
+   gutter: 0
+ };

+ export default Row;

Here we use thegeneric, so what is generic?

genericGenerics (Generics) refers to a feature that does not specify a specific type in advance when defining a function, interface, or class, but specifies the type when it is used.

function loggingIdentity<T>(arg: T): T {
    return arg;
}

Step-3 Define the Prop type for the Col component according to the API

// Col.tsx

+ import React, {ReactNode, CSSProperties } from 'react';
+ import ClassNames from 'classnames';
+
+ interface ColCSSProps {
+   offset?: number;
+   order?: number;
+   pull?: number;
+   push?: number;
+   span?: number;
+ }
+
+ export interface ColProps {
+   className?: string;
+   style?: CSSProperties;
+   children?: ReactNode;
+   flex?: string | number;
+   offset?: number;
+   order?: number;
+   pull?: number;
+   push?: number;
+   span?: number;
+   xs?: ColCSSProps;
+   sm?: ColCSSProps;
+   md?: ColCSSProps;
+   lg?: ColCSSProps;
+   xl?: ColCSSProps;
+   xxl?: ColCSSProps;
+ }

Step-4 Write the basic skeleton of the Col component

// Col.tsx

import React, {ReactNode, CSSProperties } from 'react';
import ClassNames from 'classnames';
interface ColCSSProps {
  // ...
}
export interface ColProps {
  // ...
}

+ type mediaScreen = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';

+ function sc(size: mediaScreen, value: ColCSSProps): Array<string> {
+   const t: Array<string> = [];
+   Object.keys(value).forEach(key => {
+     t.push(`azir-col-${size}-${key}-${value[key]}`);
+   });
+   return t;
+ }

+ const Col: FC<ColProps> = props => {
+   const {
+    className,
+    style = {},
+    span,
+    offset,
+    children,
+    pull,
+    push,
+    order,
+    xs,
+    sm,
+    md,
+    lg,
+    xl,
+    xxl
+   } = props;
+
+   const [classes, setClasses] = useState<string>(
+    ClassNames('azir-col', className, {
+      [`azir-col-span-${span}`]: span,
+      [`azir-col-offset-${offset}`]: offset,
+      [`azir-col-pull-${pull}`]: pull,
+      [`azir-col-push-${push}`]: push,
+      [`azir-col-order-${order}`]: order
+    })
+   );
+
+ // responsive xs,sm,md,lg,xl,xxl
+   useEffect(() => {
+     xs && setClasses(classes => ClassNames(classes, sc('xs', xs)));
+     sm && setClasses(classes => ClassNames(classes, sc('sm', sm)));
+     md && setClasses(classes => ClassNames(classes, sc('md', md)));
+     lg && setClasses(classes => ClassNames(classes, sc('lg', lg)));
+     xl && setClasses(classes => ClassNames(classes, sc('xl', xl)));
+     xxl && setClasses(classes => ClassNames(classes, sc('xxl', xxl)));
+   }, [xs, sm, md, lg, xl, xxl]);
+
+   return (
+     <div className={classes} style={style}>
+       {children}+     </div>
+   );
+ };
+ Col.defaultProps = {
+   offset: 0,
+   pull: 0,
+   push: 0,
+   span: 24
+ };
+ Col.displayName = 'Col';
+
+ export default Col;

it’s hereTypeScriptThe compiler throws a warning.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'ColCSSProps'.
  No index signature with a parameter of type 'string' was found on type 'ColCSSProps'.  TS7053

    71 |       const t: Array<string> = [];
    72 |       Object.keys(value).forEach(key => {
  > 73 |         t.push(`azir-col-${size}-${key}-${value[key]}`);
       |                                           ^
    74 |       });
    75 |       return t;
    76 |     }

The translation is: the element implicitly hasanytype, typestringcannot be used forColCSSPropsThe index type. So how should this problem end?

interface ColCSSProps {
  offset?: number;
  order?: number;
  pull?: number;
  push?: number;
  span?: number;
+  [key: string]: number | undefined;
}

we just need to tellTypeScript ColCSSPropsThe key type for isstringValue type isnumber | undefinedThat’s it.

test

Now that I’ve written it, it’s time to test the code.

// example.tsx

import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
  return (
    <div data-test="row-test" style={{ padding: '20px' }}>
      <Row className="jd-share">
        <Col style={{ background: 'red' }} span={2}>
          123        </Col>
        <Col style={{ background: 'yellow' }} offset={2} span={4}>
          123        </Col>
        <Col style={{ background: 'blue' }} span={6}>
          123        </Col>
      </Row>
      <Row>
        <Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}>
          <div style={{ height: '100px', backgroundColor: '#3170bb' }}>
            Col1          </div>
        </Col>
        <Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}>
          <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div>
        </Col>
      </Row>
    </div>
  );
};

xs size screen

The best practice of React-hooks+TypeScript

under lg size screen

The best practice of React-hooks+TypeScript

So far, the effect is not bad.

Step-5 Limit the Children of the Row component

Although the effect is not bad, butRowcomponentChildrenAny element can be passed

// row.tsx

const Row: FC<RowProps> = props => {

  // ...

  return (
    <div className={classes} style={style}>
      {children}    </div>
  );
};

This is too random! ifChildrencontains noColThere will definitely be problems with the layout of the node of the component, so I decided to limit it hereRowcomponentChildrentype.

So how to limit it? Some people think that directlychildren.map, Isn’t it all right to judge based on the structure? It is not advisable to do so,ReactThe official also pointed out that inchildrencall directly onmapis very dangerous because we cannot be surechildrentype. So what to do?ReactThe official is very considerate and also provides us with an APIReact.Children

Before that we giveColThe component sets a built-in propertydisplayNameAttributes to help us determine the type.

// col.tsx

const Col: FC<ColProps> = props => {
  // ...
};
// ...
+ Col.displayName = 'Col';

Then we ask because big brotherReact.ChildrenAPIs. thisAPIcan be specially used to deal withChildren. We write a Row componentrenderChildrenfunction

// row.tsx
const Row: FC<RowProps> = props => {
  const { className, align, justify, children, style = {} } = props;
  const classes = ClassNames('azir-row', className, {
    [`azir-row-${align}`]: align,
    [`azir-row-${justify}`]: justify
  });

+  const renderChildren = useCallback(() => {
+     return React.Children.map(children, (child, index) => {
+       try {
+ // child is ReactNode type, there are many subtypes under this type, we need to assert
+         const childElement = child as React.FunctionComponentElement<ColProps>;
+         const { displayName } = childElement.type;
+         if (displayName === 'Col') {
+           return child;
+         } else {
+           console.error(
+             'Warning: Row has a child which is not a Col component'
+           );
+         }
+       } catch (e) {
+         console.error('Warning: Row has a child which is not a Col component');
+       }
+     });
+   }, [children]);

  return (
    <div className={classes} style={style}>
-     {children}+     {renderChildren()}    </div>
  );
};

We’re 80% done at this point, are we forgetting something? ? ?

Step-6 icing on the cake-gutter

we passouter margin + Inner paddingmode to match the setting of horizontal and vertical spacing.

// row.tsx

import React, {
  CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';

// ...

const Row: FC<RowProps> = props => {
-  const { className, align, justify, children, style = {} } = props;
+  const { className, align, justify, children, gutter, style = {} } = props;

+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);

  // ...

  return (
-   <div className={classes} style={style}>
+   <div className={classes} style={rowStyle}>

      {renderChildren()}    </div>
  );};// ...export default Row;

RowcomponentmarginAlready set this up, thenColcomponentpaddingwhat can we do about it? There are two ways, one is to passprops, the second is to usecontext, I decided to use context for component communication, because I don’t want the props of the Col component to be too messy (it’s messy enough…).

// row.tsx

import React, {
  CSSProperties, ReactNode, FC, FunctionComponentElement, useCallback, useEffect, useState
} from 'react';

// ...


export interface RowContext {
  gutter?: gutter;
}
export const RowContext = createContext<RowContext>({});

const Row: FC<RowProps> = props => {
-  const { className, align, justify, children, style = {} } = props;
+  const { className, align, justify, children, gutter, style = {} } = props;

+ const [rowStyle, setRowStyle] = useState<CSSProperties>(style);

+ const passedContext: RowContext = {
+   gutter
+ };

  // ...

  return (
    <div className={classes} style={rowStyle}>
+     <RowContext.Provider value={passedContext}>
        {renderChildren()}+     </RowContext.Provider>
    </div>
  );
};

// ...

export default Row;

we are atRowcomponent creates acontext, next will be inColused in the component, and calculate theColcomponentsguttercorrespondingpaddingvalue.

// col.tsx
import React, {
  ReactNode,
  CSSProperties,
  FC,
  useState,
  useEffect,
+  useContext
} from 'react';
import ClassNames from 'classnames';
+ import { RowContext } from './row';

  // ...
const Col: FC<ColProps> = props => {
  // ...

+ const [colStyle, setColStyle] = useState<CSSProperties>(style);
+ const { gutter } = useContext(RowContext);
+ // horizontal and vertical spacing
+ useEffect(() => {
+   if (Object.prototype.toString.call(gutter) === '[object Number]') {
+     const padding = gutter as number;
+     if (padding >= 0) {
+       setColStyle(style => ({
+         padding: `${padding / 2}px`,
+         ...style
+       }));
+     }
+   }
+   if (Object.prototype.toString.call(gutter) === '[object Array]') {
+     const [paddingX, paddingY] = gutter as [number, number];
+     if (paddingX >= 0 && paddingY >= 0) {
+       setColStyle(style => ({
+         padding: `${paddingY / 2}px ${paddingX / 2}px`,
+         ...style
+       }));
+     }
+   }
+ }, [gutter]);
  // ...

  return (
-   <div className={classes} style={style}>
+   <div className={classes} style={colStyle}>
      {children}    </div>
  );};// ...export default Col;

At this point, our grid component is done! Let’s test it out!

test

import React from 'react';
import Row from './row';
import Col from './col';
export default () => {
  return (
    <div data-test="row-test" style={{ padding: '20px' }}>
      <Row>
        <Col span={24}>
          <div style={{ height: '100px', backgroundColor: '#3170bb' }}>
            Col1          </div>
        </Col>
      </Row>
      <Row gutter={10}>
        <Col order={1} span={8} xs={{ span: 20 }} lg={{ span: 11, offset: 1 }}>
          <div style={{ height: '100px', backgroundColor: '#3170bb' }}>
            Col1          </div>
        </Col>
        <Col span={4} xs={{ span: 4 }} lg={{ span: 12 }}>
          <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col2</div>
        </Col>
      </Row>
      <Row gutter={10} align="middle">
        <Col span={8}>
          <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
        </Col>
        <Col offset={8} span={8}>
          <div style={{ height: '100px', backgroundColor: '#3170bb' }}>
            Col2          </div>
        </Col>
      </Row>
      <Row gutter={10} align="bottom">
        <Col span={4}>
          <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
        </Col>
        <Col span={8}>
          <div style={{ height: '100px', backgroundColor: '#3170bb' }}>
            Col2          </div>
        </Col>
        <Col push={3} span={9}>
          <div style={{ height: '130px', backgroundColor: '#2170bb' }}>
            Col3          </div>
        </Col>
        <Col span={4}>
          <div style={{ height: '80px', backgroundColor: '#2170bb' }}>Col1</div>
        </Col>
        <Col span={8}>
          <div style={{ height: '100px', backgroundColor: '#3170bb' }}>
            Col2          </div>
        </Col>
        <Col span={8}>
          <div style={{ height: '130px', backgroundColor: '#2170bb' }}>
            Col3          </div>
        </Col>
        <Col pull={1} span={3}>
          <div style={{ height: '100px', backgroundColor: '#3170bb' }}>
            Col2          </div>
        </Col>
      </Row>
    </div>
  );
};

Summarize

so farReact Hooks + TypeScriptThe practical sharing is over, I only list the more commonly usedHooks APIandTypeScriptAlthough the sparrow is small, it has all the internal organs, we can already experienceReact Hooks + TypeScriptThe benefits brought, the cooperation of the two will definitely make our code both lightweight and robust. aboutHooksandTypeScriptFor the content, readers are expected to go to the official website for more in-depth study.