react 注意事项

<CommentItem likeComment={() => this.likeComment(user.id)} />

这个问题会导致每次父组件render方法被调用时,一个新的函数被创建,已将其传入likeComment。这会有一个改变每个子组件props的副作用,它将会造成他们全部重新渲染,即使数据本身没有发生变化

子组件的likeComment属性将总是有相同的引用,这样就不会造成不必要的重新渲染

// 方法1 
import { useCallback } from 'react';

const List = ()=> {
  const comment = (id) => { doing something}
  const likeComment = useCallback(()=> { comment(id) }, [id]);
  // 这样自组建就不会从新渲染
  return list.map (
    v =>  <CommentItem onClick={likeComment(v.id)} >) 
}

然后再子组件中创建一个引用了传入属性的类方法:

//方法2
<CommentItem likeComment={this.likeComment} userID={user.id} />

class CommentItem extends PureComponent {
  ...
  handleLike() {
    this.props.likeComment(this.props.userID)
  }
  ...
}

不要在render方法里派生数据

每次组件重新渲染时topTen都将有一个新的引用,即使posts没有改变并且派生数据也是相同的。这将造成列表不必要的重新渲染。

render() {
  const { posts } = this.props
  const topTen = posts.sort((a, b) => b.likes - a.likes).slice(0, 9)
  return //...
}

使用派生方法解决

static getDerivedStateFromProps(nextProps, state) {
   if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    )
}

管理派生 state 本来就很复杂,而且这种复杂度是随着需要管理的属性变得越来越庞大。比如,如果我们想在组件 state 里添加第二个派生 state,那就需要写两份跟踪变化的逻辑。

import memoize from "memoize-one";

class Example extends Component {
  // state 只需要保存当前的 filter 值:
  state = { filterText: "" };

  // 在 list 或者 filter 变化时,重新运行 filter:
  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    // 计算最新的过滤后的 list。
    // 如果和上次 render 参数一样,`memoize-one` 会重复使用上一次的值。
    const filteredList = this.filter(this.props.list, this.state.filterText);

    return (
      <Fragment>
        <input onChange={this.handleChange} value={this.state.filterText} />
        <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul>
      </Fragment>
    );
  }
}

在使用 memoization 时,请记住这些约束:

  1. 大部分情况下, 每个组件内部都要引入 memoized 方法,已免实例之间相互影响。
  2. 一般情况下,我们会限制 memoization 帮助函数的缓存空间,以免内存泄漏。(上面的例子中,使用 memoize-one 只缓存最后一次的参数和结果)。
  3. 如果每次父组件都传入新的 props.list ,那本文提到的问题都不会遇到。在大多数情况下,这种方式是可取的。

如果你正在使用Redux,可以考虑使用reselect来创建”selectors”来组合和缓存派生数据。

UseInterval

所以在 class 里你要怎么用 setInterval 做到这一点呢?我会这么做:

class Counter extends React.Component {
  state = {
    count: 0,
    delay: 1000,
  };

  componentDidMount() {
    this.interval = setInterval(this.tick, this.state.delay);
  }
  componentDidUpdate(prevProps, prevState) {
    if (prevState.delay !== this.state.delay) {
      clearInterval(this.interval);
      this.interval = setInterval(this.tick, this.state.delay);
    }
  }
  componentWillUnmount() {
    clearInterval(this.interval);
  }
  tick = () => {
    this.setState({
      count: this.state.count + 1
    });
  }

  handleDelayChange = (e) => {
    this.setState({ delay: Number(e.target.value) });
  }

  render() {
    return (
      <>
        <h1>{this.state.count}</h1>
        <input value={this.state.delay} onChange={this.handleDelayChange} />
      </>
    );
  }
}

Hook 版本

因为需要许多订阅 API 可以随时顺手移除老的监听者和加个新的。但是,setInterval 和它们不一样。当我们执行 clearInterval 和 setInterval 时,它们会进入时间队列里,如果我们频繁重渲染和重执行 effects,interval 有可能没有机会被执行!

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  });

  return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");

// Second interval to demonstrate the issue.
// Fast updates from it cause the Counter's
// interval to constantly reset and never fire.
setInterval(() => {
  ReactDOM.render(<Counter />, rootElement);
}, 100);

当我们 只 想在 mount 时执行 effect 和 unmount 时清理它,我们可以传空 [] 的依赖数组。

function Counter() {
  let [count, setCount] = useState(0);

  useEffect(() => {
    let id = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;

但是,现在我们的计时器更新到 1 就不动了

问题在于,useEffect 在第一次渲染时获取值为 0 的 count,我们不再重执行 effect,所以 setInterval 一直引用第一次渲染时的闭包 count,以至于 count + 1 一直是 1。哎呀呀!

  • 修复它的一种方法是用像 setCount(c => c + 1) 这样的 「updater」替换 setCount(count + 1),这样可以读到新 state 变量。但这个无法帮助你获取到新的 props。
  • 另一个方法是用 useReducer()。这种方法为你提供了更大的灵活性。在 reducer 中,你可以访问到当前 state 和新的 props。dispatch 方法本身永远不会改变,所以你可以从任何闭包中将数据放入其中。useReducer() 有个约束是你不可以用它执行副作用。(但是,你可以返回新状态 —— 触发一些 effect。)
function Counter() {
  const [count, dispatch] = useReducer((state, action) => {
    if (action === 'inc') {
      return state + 1;
    }
  }, 0);

  useEffect(() => {
    let id = setInterval(() => {
      dispatch('inc');
    }, 1000);
    return () => clearInterval(id);
  }, []);

  return <h1>{count}</h1>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<Counter />, rootElement);

阻抗不匹配

我们的「阻抗匹配」不在数据库和对象之间,它在 React 编程模型和命令式 setInterval API 之间。

一个 React 组件可能在 mounted 之前流经许多不同的 state,但它的渲染结果将一次性全部描述出来。

Hooks 使我们把相同的声明方法用在 effects 上:

  // 描述每个间隔状态
  useInterval(() => {
    setCount(count + 1);
  }, isRunning ? delay : null);

我们不设置 interval,但指定它是否设置延迟或延迟多少,我们的 Hooks 做到了,用离散术语描述连续过程

相反,setInterval 没有及时地描述过程 —— 一旦设定了 interval,除了清除它,你无法对它做任何改变。

这就是 React 模型和 setInterval API 之间的不匹配。

Refs 出马

  • 我们在第一次渲染时执行带 callback1setInterval(callback1, delay)
  • 我们在下一次渲染时得到携带新的 propsstatecallbaxk2
  • 我们无法在不重置时间的情况下替换掉已经存在的 interval

那么如果我们根本不替换 interval,而是引入一个指向新 interval 回调的可变 savedCallback 会怎么样? 现在我们来看看这个方案:

  • 我们调用 setInterval(fn, delay),其中 fn 调用 savedCallback
  • 第一次渲染后将 savedCallback 设为 callback1
  • 下一次渲染后将 savedCallback 设为 callback2

这个可变的 savedCallback 需要在重新渲染时「可持续(persist)」,所以不可以是一个常规变量,我们想要一个类似实例的字段。

 const savedCallback = useRef();
  // { current: null }

useRef() 返回一个有带有 current 可变属性的普通对象在 renders 间共享,我们可以保存新的 interval 回掉给它:

  function callback() {
    // 可以读到新 props,state等。
    setCount(count + 1);
  }

  // 每次渲染后,保存新的回调到我们的 ref 里。
  useEffect(() => {
    savedCallback.current = callback;
  });

之后我们便可以从我们的 interval 中读取和调用它:

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);

感谢 [],不重执行我们的 effectinterval 就不会被重置。同时,感谢 savedCallback ref,让我们可以一直在新渲染之后读取到回调,并在 interval tick 里调用它。

提取一个 Hook

function useInterval(callback) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () => clearInterval(id);
  }, []);
}

当前,1000 delay 是写死的,我想把它变成一个参数:

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

  useInterval(() => {
    setCount(count + 1);
  }, 1000);

  return <h1>{count}</h1>;
}

function useInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect(() => {
    savedCallback.current = callback;
  });

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

等等,我们不是要避免重置 interval effect,并专门通过 [] 来避免它吗?不完全是,我们只想在回调改变时避免重置它,但当 delay 改变时,我们想要重启 timer

有效!我们现在可以不用想太多 useInterval() 的实现过程,在任意组件中使用它。

暂停 Interval

假设我们希望能够通过传递 null 作为 delay 来暂停我们的 interval:

 const [delay, setDelay] = useState(1000);
  const [isRunning, setIsRunning] = useState(true);

  useInterval(() => {
    setCount(count + 1);
  }, isRunning ? delay : null);

如何实现这个?答案时:不创建 interval

  useEffect(() => {
    function tick() {
      savedCallback.current();
    }

    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);

vue watch vs getDerivedStateFromProps + componentDidUpdate

vue 中的watch 监听 props, data, computed 的变化, 执行异步或者开销较大的操作

getDerivedStateFromProps

getDerivedStateFromProps 会在render 前调用,并且在初始挂载和后续更新中都会调用, 返回一个对象更新state, 返回null 不更新任何内容,

当state 的值 在任何时候都需要props 的时候 适用该方法

static getDerivedStateFromProps(nextprops, preState){
  if(nextProps.info !== preState.info){
   return {info: nextProps.info}

  }
}

componentDidUpdate(prevProps, prevState, snapshot)

componentDidUpdate()方法在组件更新后被调用。首次渲染不会执行此方法。当组件更新后,可以在此处操作DOM、执行setState或者执行异步请求操作。

  • 1)componentDidUpdate()的第三个参数snapshot参数来源于getSnapshotBeforeUpdate()生命周期的返回值。若没有实现getSnapshotBeforeUpdate(),此参数值为undefined。
  • 2)可以在componentDidUpdate()中直接调用setState(),但是它必需被包裹在一个条件语句里,否则会导致死循环。
  • 3)可以在componentDidUpdate()对更新前后的props进行比较,执行异步操作。
  • 4)如果shouldComponentUpdate()返回值为false,则不会调用componentDidUpdate()。

useMemo useCallback useRef

useMemo和useCallback都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行;并且这两个hooks都返回缓存的值,useMemo返回缓存的变量,useCallback返回缓存的函数。

这里创建了两个state,然后通过expensive函数,执行一次昂贵的计算,拿到count对应的某个值。我们可以看到:无论是修改count还是val,由于组件的重新渲染,都会触发expensive的执行(能够在控制台看到,即使修改val,也会打印);但是这里的昂贵计算只依赖于count的值,在val修改的时候,是没有必要再次计算的。在这种情况下,我们就可以使用useMemo,只在count的值修改时,执行expensive计算:

export default function WithMemo() {
    const [count, setCount] = useState(1);
    const [val, setValue] = useState('');
    const expensive = useMemo(() => {
        console.log('compute');
        let sum = 0;
        for (let i = 0; i < count * 100; i++) {
            sum += i;
        }
        return sum;
    }, [count]);
 
    return (
      <div>
        <h4>{count}-{expensive}</h4>
          {val}
        <div>
          <button onClick={() => setCount(count + 
              1)}> +c1 </button>
            <input value={val} onChange={event => 
                setValue(event.target.value)}/>
        </div>
    </div>);
}

useCallback

import React, { useState, useCallback } from 'react';

const set = new Set();
export default function Callback() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');

    const callback = useCallback(() => {
        console.log(count);
    }, [count]);

    set.add(callback);
 
    return (
      <div>
        <h4>{count}</h4>
        <h4>{set.size}</h4>
        <div>
           <button onClick={() => setCount(count + 1)}>+ 
           </button>
           <input value={val} 
            onChange={event => setVal(event.target.value)}/>
        </div>
      </div>);
}

我们可以看到,每次修改count,set.size就会+1,这说明useCallback依赖变量count,count变更时会返回新的函数;而val变更时,set.size不会变,说明返回的是缓存的旧版本函数。

知道useCallback有什么样的特点,那有什么作用呢?

使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

import React, { useState, useCallback, useEffect } from 'react';
function Parent() {
    const [count, setCount] = useState(1);
    const [val, setVal] = useState('');
 
    const callback = useCallback(() => {
        return count;
    }, [count]);

    return (
      <div>
        <h4>{count}</h4>
        <Child callback={callback}/>
        <div>
           <button onClick={() => setCount(count + 1)}>+ 
           </button>
           <input value={val} onChange={event => 
                setVal(event.target.value)}/>
        </div>
     </div>);
}
 
function Child({ callback }) {
    const [count, setCount] = useState(() => callback());
    useEffect(() => {
        setCount(callback());
    }, [callback]);
    return (<div>  {count} </div>)
}

不仅是上面的例子,所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。

useEffect、useMemo、useCallback都是自带闭包的。也就是说,每一次组件的渲染,其都会捕获当前组件函数上下文中的状态(state, props),所以每一次这三种hooks的执行,反映的也都是当前的状态,你无法使用它们来捕获上一次的状态。对于这种情况,我们应该使用ref来访问。

useRef

useRef hooks函数,除了传统的用法之外,它还可以“跨渲染周期”保存数据

它保存数据的用法

在一个组件中有什么东西可以跨渲染周期,也就是在组件被多次渲染之后依旧不变的属性?第一个想到的应该是state。没错,一个组件的state在多次渲染之后依旧不变。但是的,state问题在于一旦修改了它就会造成组件的重新渲染。

那么这个时候就可以使useRef用来跨越渲染周期存储数据,而且对它修改也不会引起组件渲染。

import React, { useState, useEffect, useMemo, useRef } from 'react';

export default function App(props){
  const [count, setCount] = useState(0);

  const doubleCount = useMemo(() => {
    return 2 * count;
  }, [count]);

  const timerID = useRef();
  
  useEffect(() => {
    timerID.current = setInterval(()=>{
        setCount(count => count + 1);
    }, 1000); 
  }, []);
  
  useEffect(()=>{
      if(count > 10){
          clearInterval(timerID.current);
      }
  });
  
  return (
    <>
      <button ref={couterRef} onClick={() => {setCount(count 
        + 1)}}>
         Count: {count}, double: {doubleCount}
     </button>
    </>
  );
}
//  useRef 穿透闭包
function Form() {
  const [text, updateText] = useState('');
  const textRef = useRef();

 // useLayoutEffect: DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
  useLayoutEffect(() => {
    textRef.current = text; // 将 text 写入到 ref
  });

  const handleSubmit = useCallback(() => {
    const currentText = textRef.current; // 从 ref 中读取 text
    alert(currentText);
  }, [textRef]); // handleSubmit 只会依赖 textRef 的变化。不会在 text 改变时更新

  return (
    <>
      <input value={text} onChange={e => 
        updateText(e.target.value)} 
      />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

上面 input 值改变了, handleSubmit 也获取到的值也会变, 但 handleSubmit 不会变,不会触发ExpensiveTree 更新,

React 不可变数据

  • 如果直接修改数据 可能 检测不到 数据变化, 导致 组件不会更新
  • immutable.js 生成新数据, 并共享没有改变的数据, 新的props 或state 新的部分触发组件更新, 老的共享结构自然不更新

pure components 会等 props 和 state 浅比较, components 则 父组件更新 它就更新

PureComponent (React.momo)

在使用PureComponent的时候,只能把react组件写成是class的形式,不能使用函数的形式;react v16.6.0之后,可以使用React.memo来实现函数式的组件,也有了PureComponent的功能。

const ListComponent = React.momo(() => (
    <div>{this.props.data || 'loading'}</div>
))

所谓浅比较(shallowEqual),即react源码中的一个函数,然后根据下面的方法进行是不是PureComponent的判断,帮我们做了本来应该我们在shouldComponentUpdate中做的事情。

  1. props 或 state 
if (this._compositeType === CompositeTypes.PureClass) {
  shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState);
}

可能用不到派生 state ,可控 组件,非可控组件

什么时候用到派生

  • 1,直接复制 props 到 state 上;
  • 2,如果 props 和 state 不一致就更新 state。

如果你只是为了缓存(memoize)基于当前 props 计算后的结果的话, 请看 https://www.xmetal.cc/?p=1315

问题 :针对输入 组件

  1. 父组件更新, props 改变, 会重置 state ,我们输入的所有东西都会丢失
  2. if (nextProps.email !== this.props.email) {}只要 props.email 改变,就改变 state, 这样不会丢掉
  3. 上面会导致父组件格式如果是[{email:xxx, email:xxx}] , 子组件的input 就不能更新

设计组件时,重要的是确定组件是受控组件还是非受控组件

不要直接复制(mirror) props 的值到 state 中,而是去实现一个受控的组件,然后在父组件里合并两个值。比如,不要在子组件里被动的接受 props.value 并跟踪一个临时的 state.value,而要在父组件里管理 state.draftValue 和 state.committedValue,直接控制子组件里的值。这样数据才更加明确可预测

建议:完全可控的组件

function EmailInput(props) {
  return <input onChange={props.onChange} value={props.email} />;
}

建议:有 key 的非可控组件

在这密码管理器的例子中,为了在不同的页面切换不同的值,我们可以使用 key 这个特殊的 React 属性。当 key 变化时, React 会创建一个新的而不是更新一个既有的组件。 Keys 一般用来渲染动态列表,但是这里也可以使用

class EmailInput extends Component {
  state = { email: this.props.defaultEmail };

  handleChange = event => {
    this.setState({ email: event.target.value });
  };

  render() {
    return <input onChange={this.handleChange} value= 
     {this.state.email} />;
  }
}
//每次 ID 更改,都会重新创建 EmailInput ,并将其状态重置为最新的 defaultEmail 值。
<EmailInput
  defaultEmail={this.props.user.email}
  key={this.props.user.id}
/>

选项一:用 prop 的 ID 重置非受控组件

如果某些情况下 key 不起作用(可能是组件初始化的开销太大),一个麻烦但是可行的方案是在 getDerivedStateFromProps 观察 userID 的变化:

class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail,
    prevPropsUserID: this.props.userID
  };
//使用了 getDerivedStateFromProps,用 componentWillReceiveProps 也一样。
  static getDerivedStateFromProps(props, state) {
    // 只要当前 user 变化,
    // 重置所有跟 user 相关的状态。
    // 这个例子中,只有 email 和 user 相关。
    if (props.userID !== state.prevPropsUserID) {
      return {
        prevPropsUserID: props.userID,
        email: props.defaultEmail
      };
    }
    return null;
  }

  // ...
}

选项二:使用实例方法重置非受控组件

更少见的情况是,即使没有合适的 key,我们也想重新创建组件。一种解决方案是给一个随机值或者递增的值当作 key,另外一种是用实例方法强制重置内部状态:

refs 在某些情况下很有用,比如这个。但通常我们建议谨慎使用。即使是做一个演示,这个命令式的方法也是非理想的,因为这会导致两次而不是一次渲染。

export default class AccountsList extends Component {
  inputRef = React.createRef();

  state = {
    selectedIndex: 0
  };

  handleChange = index => {
    this.setState({ selectedIndex: index }, () => {
      const selectedAccount = this.props.accounts[index];
    this.inputRef.current.resetEmailForNewUser(selectedAccount.email);
    });
  };

  render() {
    const { accounts } = this.props;
    const { selectedIndex } = this.state;
    const selectedAccount = accounts[selectedIndex];
    return (
      <Fragment>
        <UncontrolledEmailInput
          defaultEmail={selectedAccount.email}
          ref={this.inputRef}
        />
        <p>
          Accounts:
          {this.props.accounts.map((account, index) => (
            <label key={account.id}>
              <input
                type="radio"
                name="account"
                checked={selectedIndex === index}
                onChange={() => this.handleChange(index)}
              />{" "}
              {account.name}
            </label>
          ))}
        </p>
      
      </Fragment>
    );
  }
}
//子组件
class EmailInput extends Component {
  state = {
    email: this.props.defaultEmail
  };

  resetEmailForNewUser(newEmail) {
    this.setState({ email: newEmail });
  }

  // ...
}