Last year, the company had cross-end requirements for small programs, so I gradually did some tasks based on Taro’s development of small programs. Later, I thought this experience could be summarized into notes and shared with new employees for reference.

Notes are mainly for newcomers without React/Taro experience. Please point out any inaccuracies in the content and correct them in time.

Environment set up

The official website document is directly lost here.

2.x official documentation

Since the company’s small program based on Taro2 development, only Taro2 related functions are introduced here.

Front knowledge

  1. Native small program development experience;
  2. The React;
  3. TypeScript basics (if the project is developed on TS).

Pre-knowledge is the need to master in advance, otherwise it may be in the development of a face meng, half the effort.

Taro compares native applets

Some common usage scenarios

Life cycle comparison

Native applet

Page({
  data: {
    text: "This is page data."
  },
  onLoad: function(options) {
    // execute when the page is created
  },
  onShow: function() {
    // execute when the page appears in the foreground
  },
  onReady: function() {
    // execute when the page is first rendered
  },
  onHide: function() {
    // execute when the page changes from foreground to background
  },
  onUnload: function() {
    // execute when the page is destroyed
  },
  onPullDownRefresh: function() {
    // Execute when a pull-down refresh is triggered
  },
  onReachBottom: function() {
    // execute when the page hits the bottom
  },
  onShareAppMessage: function () {
    // execute when the page is shared by the user
  },
  onPageScroll: function() {
    // execute while the page is scrolling
  },
  onResize: function() {
    // execute when the page size changes}})Copy the code

Taro class components

export default class Example extends Component {
  constructor(props) {
    super(props);

    this.state = {
      text: "This is page data."}; }// Replace the onLoad method with constructor or componentDidMount.
  // This hook is deprecated in React
  componentWillMount() {}
  // corresponds to the onShow method
  componentDidShow() {}
  // corresponds to the onReady method
  componentDidMount() {}
  // corresponds to the onHide method
  componentDidHide() {}
  // Corresponds to the onUnload method
  componentWillUnmount() {}
  // Execute before component update (when new props or state is received), first render does not fire,
  // can't setState here, the hook is deprecated in React
  componentWillUpdate(prevProps, prevState) {}
  // Execute when new props are received. It is recommended to use componentDidUpdate instead
  componentWillReceiveProps(nextProps) {}
  // Execute after component update
  componentDidUpdate(prevProps, prevState) {
    // It is common to do some operations here by determining the changes between old and new data, for example
    // if (prevProps.sessionId ! == this.props.sessionId) {
    // Already logged in
    // }
  }
  // Whether the child component needs to reproduce the rendering
  shouldComponentUpdate(nextProps, nextState) {}
  // onPullDownRefresh, onReachBottom, onShareAppMessage... Consistent with native applets
}
Copy the code

Have been abandoned in the React life cycle componentWillMount, componentWillUpdate, componentWillReceiveProps is not recommended.

Taro function components

function Example() {
  /** * useEffect => componentDidMount componentDidUpdate componentWillUnmount */

  // This parameter is used by default, mounted, unmounted, and update
  useEffect(() = > {
    // do not setState, otherwise it will cause an infinite loop
  });
  
  // Upload [], mounted and unmounted
  useEffect(() = > {
    // ...} []);// Pass state, which is executed only when the state changes
  const [count, setCount] = useState(0);
  useEffect(() = > {
    SetState => count; otherwise, an infinite loop will be created
  }, [count]);
  

  // The unmounted stage is executed automatically when the function is returned, and can be used to destroy obsolete objects
  useEffect(() = > {
    const id = setInterval(() = > {
      setCount(count= > count + 1);
    }, 1000);
    return () = > clearInterval(id); } []);/** * other useDidShow, useDidHide, useReachBottom, useResize... * reference: https://taro-docs.jd.com/taro/docs/2.x/hooks * /

  return (<View></View>);
}
Copy the code

Simple example

If you are not familiar with React Hooks, it is recommended to start with the Class component.

The class components

A component of a class component

  1. Define defaultProps
  2. Define the state
  3. Component life cycle
  4. Render function

The parent component

import Taro, { Component } from "@tarojs/taro";
import { View } from "@tarojs/components";
// Public components
// import Empty from './empty';
// import Error from './error';
/ / child component
import Item from "./item";
export default class ExampleList extends Component {
  // constructor... Super is a fixed form and can be ignored
  /*constructor(props) { super(props); } * /

  / / define the state
  state = {
    list: [].nothing: false.error: false};// ref access components (if needed)
  itemRefComp = Taro.createRef();

  // The equivalent of a native applet, page.json
  config = {
    navigationBarTitleText: "Sample List".enablePullDownRefresh: true};// Request background data in the lifecycle
  componentDidMount() {
    this.fetchData();
  }

  // Custom methods
  fetchData() {
    const list = [
      {
        id: 1.value: "Test data"}, {id: 2.value: "Test data",},];this.setState({
      list,
    });
  }

  // Custom methods passed to child components via props
  handleJumpToDetail = () = > {
    // Call child component methods through ref
    this.itemRefComp.current.testRef();

    console.log("detail");
  };

  // render function, returns JSX
  render() {
    const { list, nothing, error } = this.state;

    return (
      <View className="container">{!!!!! list && !! list.length && (<View className="list">
            {list.map((item) => {
              return (
                <Item
                  ref={this.itemRefComp}
                  item={item}
                  key={item.id}
                  onItemClick={this.handleJumpToDetail}
                />
              );
            })}
          </View>
        )}
        {/* {nothing && <Empty message='No data at present' />}
        {error && <Error />* /}}</View>); }}Copy the code

Child components

import Taro, { Component } from "@tarojs/taro";
import { Button } from "@tarojs/components";
export default class Item extends Component {
  / / define the props
  static defaultProps = {
    item: {},
    onItemClick: null};// Custom methods
  handleClickItem = (item, e) = > {
    const { onItemClick } = this.props;
    // trigger the method passed by the parent component through props
    onItemClick && onItemClick(item);
  };

  // Custom method (called by ref from parent component)
  testRef() {
    console.log("testRef click".this.props.item);
  }

  // render function, returns JSX
  render() {
    const { item } = this.props;

    return (
      <Button className="item" onClick={this.handleClickItem.bind(this, item)} >
        {item.value}
      </Button>); }}Copy the code

It should be noted that setState is not synchronous like the native applet setData. If there is value operation after it, it should be obtained in the callback function or using async/await, for example:

logList() {
  const { list } = this.state;

  console.log(list);
}
// Incorrect usage
fetchData() {
  const list = [1.2];

  this.setState({
    list,
  });
  // Failed to get list correctly in logList
  this.logList();
}
// This is a callback function
fetchData() {
  const list = [1.2];

  this.setState(
    {
      list,
    },
    () = > {
      this.logList(); }); }// async/await
async fetchData() {
  const list = [1.2];

  await this.setState({
    list,
  });

  this.logList();
}
Copy the code

The function components

Function Component components

  1. Define defaultProps
  2. Define the state
  3. UseEffect (similar to life cycle)
  4. Returns the JSX

The parent component

import { useState, useEffect, useRef } from "@tarojs/taro";
import { View } from "@tarojs/components";
// Public components
// import Empty from './empty';
// import Error from './error';
/ / child component
import Item from "./item";
export default function ExampleList(props) {
  / / define the state
  const [list, setList] = useState([]);
  const [nothing, setNothing] = useState(false);
  const [error, setError] = useState(false);

  // ref access components (if needed)
  const itemRefComp = useRef();

  // useEffect to request background data
  useEffect(() = >{ fetchData(); } []);// Custom methods
  const fetchData = () = > {
    const list = [
      {
        id: 1.value: "Test data"}, {id: 2.value: "Test data",},]; setList(list); };// Custom methods passed to child components via props
  const handleJumpToDetail = () = > {
    // Call child component methods through ref
    itemRefComp.current.testRef();

    console.log("detail");
  };

  / / return JSX
  return (
    <View className="container">{!!!!! list && !! list.length && (<View className="list">
          {list.map((item) => {
            return (
              <Item
                childRef={itemRefComp}
                key={item.id}
                item={item}
                onItemClick={handleJumpToDetail}
              />
            );
          })}
        </View>
      )}

      {/* {nothing && <Empty message="No data at present" />}
      {error && <Error />* /}}</View>
  );
}

// The equivalent of a native applet, page.json
ExampleList.config = {
  navigationBarTitleText: "Sample List".enablePullDownRefresh: true};Copy the code

Child components

import { useImperativeHandle } from "@tarojs/taro";
import { Button } from "@tarojs/components";

const Item = (props) = > {
  const { item, onItemClick, childRef } = props;
  // expose the method called in ref mode
  useImperativeHandle(childRef, () = > {
    return {
      testRef,
    };
  });

  // Custom methods
  const handleClickItem = (item) = > {
    return () = > {
      // trigger the method passed by the parent component through props
      onItemClick && onItemClick(item);
    };
  };

  // Custom method (called by ref from parent component)
  function testRef() {
    console.log("testRef click", item);
  }

  / / return JSX
  return (
    <Button className="item" onClick={handleClickItem(item)}>
      {item.value}
    </Button>
  );
};
/ / define defaultProps
Item.defaultProps = {
  item: {},
  onItemClick: null};export default Item;
Copy the code

The above is mainly to show the basic usage, not to do a detailed introduction to each detail.

Performance optimization correlation

For debugging purposes, some of the sample code below is based on React rather than Taro, and in fact they are used basically the same way.

Optimize the re-rendering problem

React does not make dependency and equality judgments when state changes, as other frameworks do, to avoid unnecessary rendering. In fact, every time after setState, regardless of whether the state is referenced in JSX, or even whether the value of state changes, it will cause the current component and its children to re-render, which is undoubtedly an unnecessary overhead.

React does diff comparisons using the Virtual DOM (H5 side) to minimize changes rather than completely discard rerendering, but reducing render function execution, virtual DOM generation, and comparison can still improve performance, especially if the DOM is complex and has many subcomponents.

Some basic solutions

  1. Move undependent data in JSX out of state that is dependent but does not change twice;
  2. Functions passed to child components do not use bind and do not use anonymous function wrapping (which invalidates PureComponent optimizations).

Such as:

export default class Example extends Component {
  / / define the state
  state = {
    list: [].error: false.nothing: false};// Render function does not rely on, or does not change the data twice
  pageNo = 1;
  hasMore = true;
  noChange = 1;

  // Use bind in render to ensure this is correct, not recommended
  // fun() {}

  Bind (this) is not required in render when using arrow function
  fun = () = > {
    console.log(this);
  };

  render() {
    const { list, nothing, error } = this.state;
    const { noChange } = this;

    return (
      <View className="container">{list} {nothing} {error} {noChange} {/* Common use of bind and anonymous functions causes a new function to be generated for each rendering, triggering the re-rendering of child components */} {/*<Child onFun={this.fun.bind(this)}></Child>* /} {/ *<Child onFun={()= > this.fun()}></Child>* /}<Child onFun={this.fun}></Child>
      </View>); }}Copy the code

ShouldComponentUpdate, PureComponent, Memo and other official API to do some old and new data judgment, to decide whether to re-render, and use useMemo, useCallback to cache functions.

shouldComponentUpdate

ShouldComponentUpdate lets us decide whether to render child components or not, but when there is too much data and reference types (objects, arrays, etc.), the need for traversal, recursive judgment (deep comparisons) may not be worth the performance penalty.

shouldComponentUpdate(nextProps, nextState) {
   // Render is allowed only when state data is not identical
   returnnextState.someData ! = =this.state.someData;
}
Copy the code

PureComponent

PureComponent will automatically shallow compare props to state to render the view, but if the memory reference of the data does not change, rendering cannot be triggered. ForceUpdate can be used to force the update (not recommended), and it is recommended to return a new object each time.

import React, { PureComponent, Component } from "react";
class Child extends PureComponent {
  updateChild() {
    this.forceUpdate();
  }

  render() {
    console.log("Child Component render");
    const { name } = this.props.userInfo;
    return (
      <div>Here is the child component:<p>{name}</p>
      </div>); }}class Parent extends PureComponent {
  state = {
    userInfo: { name: "Zhang".age: 18}}; childRef = React.createRef(); changeName =() = > {
    const { userInfo } = this.state;
    // Parent component updates can be triggered (for reference type data, a new object needs to be returned each time)
    /*this.setState({ userInfo: { ... UserInfo, name: "userInfo ",},}); * /

    // The data memory address has not changed and cannot trigger parent-child component updates
    userInfo.name = "Bill";
    this.setState({
      userInfo,
    });

    // Force child components to update. Not recommended
    this.childRef.current.updateChild();
  };

  render() {
    console.log("Parent Component render");
    const { userInfo } = this.state;

    return (
      <div>
        <p>{userInfo.name}</p>
        <button onClick={this.changeName}>Change the parent component state</button>
        <br />
        <Child ref={this.childRef} userInfo={userInfo}></Child>
      </div>); }}export default Parent;
Copy the code

memo

The memo is a higher-order component, which is more like a shouldComponentUpdate/PureComponent combination for function.

By default, the memo does a shallow comparison for props. If the props are equal, the update will not be done. However, like PureComponent, it can be unreliable to determine the reference type.

import React, { useState, memo } from "react";
// If the function is not optimized, the update of the parent component will cause the update of the child component
const Child = (props = {}) = > {
  console.log(`--- re-render ---`);
  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
};
// Using the Memo optimized function, like PureComponent, changes in step and count of the parent component do not cause the component to update
/*const ChildMemo = memo((props = {}) => { console.log(`--- memo re-render ---`); return ( 
      

number is : {props.number}

); }); * /
/** */ shouldComponentUpdate (); /** * const isEqual = (prevProps, nextProps) = > { if(prevProps.number ! == nextProps.number) {return false; } return true; }; const ChildMemo = memo((props = {}) = > { console.log(`--- memo re-render ---`); return ( <div> <p>number is : {props.number}</p> </div> ); }, isEqual); export default (props = {}) => { const [step, setStep] = useState(0); const [count, setCount] = useState(0); const [number, setNumber] = useState(0); const handleSetStep = () = > { setStep(step + 1); }; const handleSetCount = () = > { setCount(count + 1); }; const handleCalNumber = () = > { setNumber(count + step); }; return ( <div> <button onClick={handleSetStep}>step is : {step} </button> <button onClick={handleSetCount}>count is : {count} </button> <button onClick={handleCalNumber}>number is : {number} </button> <hr /> <Child number={number} /> <hr /> <ChildMemo number={number} /> </div> ); }; Copy the code

useMemo

UseMemo is very similar to Vue’s computed, which has a caching function and different results depending on the passing of the second parameter, as follows:

  1. Without passing parameters, each subcomponent update is executed (equivalent to no optimization);
  2. Pass [], which is executed only once;
  3. Pass [state/props], so that the function is reexecuted when the dependent parameters change.

Because the function component is recreated each time it is rendered, in addition to implementing computed with useMemo, it can also be used to cache complex functions to reduce the cost of recreating.

import React, { useState, useMemo } from "react";
export default (props = {}) => {
  console.log("---function---render---");
  const [step, setStep] = useState(5);
  const [count, setCount] = useState(0);

  // Computed, like VUE, has a caching effect and is re-executed only when a dependency (step) changes
  const { sum } = useMemo(() = > {
    console.log("---useMemo---render---");
    let sum = 10;

    sum += step;

    return {
      sum,
    };
  }, [step]);

  // Use it as a normal function for caching
  // const { sum } = useMemo(() => {
  // console.log("---useMemo---render---");
  // let sum = 0;
  // // hypothesis is a very complicated calculation process
  // for (let i = 0; i < 10000; i++) {
  // sum += 5;
  / /}

  // return {
  // sum,
  / /};
  / /} []);

  const handleSetCount = () = > {
    setCount(count + 1);
  };

  const handleSetStep = () = > {
    setStep(step + 1);
  };

  return (
    <div>
      <button onClick={handleSetCount}>count is : {count} </button>
      <p onClick={handleSetStep}>
        step is: {step} sum is: {sum}
      </p>
    </div>
  );
};
Copy the code

useCallback

Going back to the memo above, using the Memo allows the props data to remain unchanged without triggering a re-rendering of the child components.

However, this is still not a silver bullet. We know that the function component is recreated every time. When passing functions to sub-components through props, because the function is recreated, the memory address of the function changes every time.

UseCallback is designed to solve this problem. It allows us to cache functions so that they are not recreated every time we re-render them.

import React, { useState, memo, useCallback } from "react";
const ChildMemo = memo((props = {}) = > {
  console.log("--- memo re-render ---");
  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
});
const ChildMemo2 = memo((props = {}) = > {
  console.log("--- memo re-render2 ---");

  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
});
export default (props = {}) => {
  const [step, setStep] = useState(0);
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(0);

  const handleSetStep = () = > {
    setStep(step + 1);
  };

  const handleSetCount = () = > {
    setCount(count + 1);
  };

  const handleCalNumber = () = > {
    setNumber(count + step);
  };

  // A normal function
  const onClickA = () = > {};

  // useCallback
  // No parameter is passed, no caching effect
  // const onClickB = useCallback(() => {});

  // Pass an empty array, no more updates
  const onClickB = useCallback(() = >{} []);// pass [state/props], which will be updated when the dependency changes
  // const onClickB = useCallback(() => {
  // console.log(step);
  // }, [step]);

  return (
    <div>
      <button onClick={handleSetStep}>step is : {step} </button>
      <button onClick={handleSetCount}>count is : {count} </button>
      <button onClick={handleCalNumber}>number is : {number} </button>
      <hr />
      <ChildMemo number={number} onClick={onClickA} /> <hr />
      <ChildMemo2 number={number} clickFun={onClickB} />
    </div>
  );
};
Copy the code

Here are some ways to optimize performance, but there are many more that I haven’t written down yet.

“We should ignore small performance optimizations, which 97 percent of the time are the root of all evil, and focus on the other 3 percent of the code that matters most.” – gartner,

Some conclusions should be drawn from this, for example:

  1. We should focus more on performance bottlenecks;
  2. Without good rules and a promise of improved performance, premature optimization can lead to code that is too complex and difficult to maintain.

Q&A

Why is the state or props sometimes used in a function component (capture value)?

When we click on a button, the text inside the button will say “count is: 5”. This seems fine, but the log inside the handleClick tells us that count is 0.

import React, { useState } from "react";

export default (props = {}) => {
  console.log('render')
  const [count, setCount] = useState(0);

  const handleClick = () = > {
    setCount(5);
    setTimeout(() = > {
      console.log('count' has been set to 5, so now =>${count}`);/ / 0
    }, 3e3);
  };

  return <button onClick={handleClick}>count is: {count}</button>;
};
Copy the code

This problem still belongs to the function component mentioned above, which is recreated each time it is rendered. A simple comparison can be made with the following example.

const funComponent = (isAlert) = > {
  const { count } = state;
  console.log(`render----count is ${count}`);

  if (isAlert) {
    setTimeout(() = > {
      console.log(`count is ${count}`);/ / 2, 4
    }, 3e3); }};const state = new Proxy({count: 0}, {set(target, property, value){ target[property] = value; funComponent(! (value %2));
      returnvalue; }}); state.count =1;
state.count = 2;
state.count = 3;
state.count = 4;
state.count = 5;
Copy the code

Since the function component is recreated and executed each time, and state is evaluated through a destructor, there is no reference to the actual state.

After the click event is triggered, setTimeout is in the context of the last function, not the current one, and cannot get the latest value from the last state.

${state. Count} = ${state. Count} = ${state.

React provides a similar workaround, useRef:

import React, { useState, useRef } from "react";

export default (props = {}) => {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  const handleClick = () = > {
    setCount(latestCount.current = 5);
    setTimeout(() = > {
      console.log('count' has been set to 5, so now =>${latestCount.current}`); / / 5
    }, 3e3);
  };

  return <button onClick={handleClick}>count is: {count}</button>;
};
Copy the code

Using a looping index variable as the key is an anti-optimization, right?

The main reason for this is that we do not know the disadvantages of using an index as a key. The main reason for this is that we do not know the disadvantages of using an index as a key.

So again, why is using the index variable as the key in a loop an anti-optimization?

To put it simply, diff algorithm can correctly identify nodes and find the correct position to update the array when adding, deleting, or modifying an array with a unique key. When index is used as key, it will cause batch re-rendering of nodes, or even incorrect update of nodes, because the unique identification key is changed.

To learn more, check out the Vue/React dom Diff algorithm.

In general, the back end will give us a unique ID for the data, for example:

<ul>
    {list.map((item) = > {
      return <li key={item.id}>{item.name}</li>;
    })}
</ul>
Copy the code

If not, we can generate unique keys by following certain rules, or using some plug-in such as a UUID.

In practice, though, most DOM for loops are for data presentation only and do not require a key binding. But putting a unique key attribute on the for loop whenever possible is a good practice for code rigor, eliminating hidden bugs, and avoiding compiler errors.