关于react.js:ReactHook最佳实践

53次阅读

共计 17005 个字符,预计需要花费 43 分钟才能阅读完成。

React Hook 新呈现背景

类组件的问题

  • 复用组件状态难,高阶组件 + 渲染属性 providers customers,等一堆工具都是为了解决这个问题,然而造成了很重大的了解老本和组件嵌套天堂
  • 生命周期带来的负面影响,逻辑拆分重大
  • This 的指向问题

函数组件的局限

  • 之前函数组件没有 state 和 生命周期,导致应用场景无限

React Hook

HooksReact 16.8 新增的个性,它能够让你在不编写 class 的状况下应用 state 以及其余的 React 个性,无需转化成类组件

Hook 的应用和实际

useStateHook 的闭包机制

// hook 组件
function Counter() {const [count, setCount] = useState(0);
  const log = () => {setCount(count + 1);
    setTimeout(() => {console.log(count);
    }, 3000);
  };
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={log}>Click me</button>       </div>
  );
}

// 等效的类组件
class Counter extends Component {state = { count: 0};
  log = () => {
    this.setState({count: this.state.count + 1,});
    setTimeout(() => {console.log(this.state.count);
    }, 3000);
  };
  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.log}>Click me</button>            </div>
    );
  }
}

疾速点击下的状况下,想想 Hook 组件和函数式组件控制台打印进去的是什么?

  • 类组件打印进去的是 3 3 3
    Class 组件的 state 是不可变的,通过 setState 返回一个新的援用,this.state 指向一个新的援用
    setTimeout 执行的时候,通过 this 获取最新的 state 援用,所以这个输入都是 3
  • 函数组件打印的后果是 0 1 2
    函数组件闭包机制,函数组件每一次渲染都有独立的 propsstate
    每一次渲染都有独立的事件处理函数
    每一次渲染的状态不会受到前面事件处理的影响

函数组件渲染拆解

既然每次渲染都是一个独立的闭包,能够尝试代码拆解函数式组件的渲染过程

// 第一次点击
function Counter() {const [0, setCount] = useState(0);  
  const log = () => {setCount(0 + 1);
    // 只能获取这次点击按钮的 state
    setTimeout(() => {console.log(0);
    }, 3000);
  };
}
// 第二次点击
function Counter() {const [1, setCount] = useState(0);  
  const log = () => {setCount(1 + 1);
    setTimeout(() => {console.log(1);
    }, 3000);
  };
}
// 第三次点击
function Counter() {const [2, setCount] = useState(0); 
  const log = () => {setCount(2 + 1);
    setTimeout(() => {console.log(2);
    }, 3000);
  };
}
  • 三次点击,共 4 次渲染,count0 变为 3
  • 页面第一次渲染,页面看到的 count = 0
  • 第一次点击,事件处理器获取的 count = 0count 变成 1,第二次渲染,渲染后页面看到 count = 1,对应上述代码第一次点击
  • 第二次点击,事件处理器获取的 count = 1count 变成 2,第三次渲染,渲染后页面看到 count = 2,对应上述代码第二次点击
  • 第三次点击,事件处理器获取的 count = 2count 变成 3,第四次渲染,渲染后页面看到 count = 3,对应上述代码第三次点击

让函数式组件也能够输入 3 3 3

有种比较简单并且能解决问题的计划,借用 useRef

  • useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)
  • useRef 返回的 ref 对象在组件的整个生命周期内放弃不变,也就是说每次从新渲染函数组件时,返回的 ref 对象都是同一个
  • useRef 能够类比成类组件实例化后的 this,在组件没有销毁的返回的援用都是同一个
function Counter() {const count = useRef(0);
  const log = () => {
    count.current++;
    setTimeout(() => {console.log(count.current);
    }, 3000);
  };
  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button onClick={log}>Click me</button>        </div>
  );
}
  • 这样批改一下,控制台输入的的确是 3 3 3
  • ?既然 Ref 对象整个生命周期都不变,批改 current 属性也只是批改属性,那除了打印,这里的 You clicked 0 times,点击三次,会变成 3 么?
  • 显然不能,这个组件没有任何的属性和状态扭转,会从新渲染才怪,所以这里尽管点击了 3 次,然而不会像 useState 一样,渲染 4 次,这里只会渲染 1 次,而后看到的都是 You clicked 0 times
  • 修复一个问题把另外一个更大的问题引进来,这很程序员。。。

useEffect

通过 useRef 尽管能解决打印的问题,然而页面渲染是不对的,这里还是应用 useState 的计划,配合 useEffect 能够实现咱们想要的成果

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
  • 看下 useEffect 的签名,effect 是函数类型,并且必填,还有第二个可选参数,类型是只读数组
  • useEffect 是解决副作用的,其执行机会在 每次 Render 渲染结束后,换句话说就是每次渲染都会执行,在实在 DOM 操作结束后。

配合这个 hook,如果每次 state 扭转后渲染完之后,把 ref 外面的值更新,而后控制台打印 ref 的值,参考 React 实战视频解说:进入学习

function Counter() {const [count, setCount] = useState(0);
  const currentCount = useRef(count);
  useEffect(() => {currentCount.current = count;});
  const log = () => {setCount(count + 1);
    setTimeout(() => {console.log(currentCount.current);
    }, 3000);
  };
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={log}>Click me</button>         </div>
  );
}

这样子写能够合乎咱们的预期成果,页面展现从 0 1 2 3,而后控制台输入 3 3 3,而后咱们拆解下渲染过程。

  • 三次点击,共 4 次渲染,count0 变为 3
  • 页面初始化渲染,count = 0currentCount.current = 0,页面显示 0,渲染实现,触发 useEffectcurrentCount.current = 0
  • 第一次点击,count = 0,渲染实现后,count = 1,页面显示 1,触发 useEffectcurrentCount.current = 1
  • 第二次点击,count = 1,渲染实现后,count = 2,页面显示 2,触发 useEffectcurrentCount.current = 2
  • 第三次点击,count = 2,渲染实现后,count = 3,页面显示 3,触发 useEffectcurrentCount.current = 3
  • 三次点击实现,currentCount.current = 3,第四次渲染,页面看到 count = 3setTimeout 中调用的是 currentCount 这个对象,输入都是 3

useEffect 的函数返回值

type EffectCallback = () => void | (() => void | undefined);

useEffect 的回调函数能够返回空,也能够返回一个函数,如果返回一个函数的话,在 effect 执行回调函数的时候,会先执行上一次 effect 回调函数返回的函数

useEffect(() => {console.log('after render');
  return () => {console.log('last time effect return');
  };
});

这个 useEffect,每次渲染完之后,控制台会先输入 last time effect return,而后再输入 after render

useEffect 和 类组件生命周期

之前提到,useEffct 有两个参数,第二参数是个可选参数,是 effect 的依赖列表,React 依据这些列表的值是否有扭转,决定渲染完之后,是否执行这个副作用的回调

如果不传这个参数,React 会认为这个 effect 每次渲染然之后都要执行,等同于 componentDidUpdate 这个生命周期无约束执行

useEffect(() => {currentCount.current = count;});
componentDidUpdate() {currentCount.current = this.state.count;}

如果这个参数是空数组,React 会认为组件内任何状态和属性扭转,都不会触发这个 effect,相当于这个 effect 是仅仅在组件渲染完之后,执行一次,前面组件任何更新都不会触发这个 effect,等同 componentDidMount

useEffect(() => {currentCount.current = count;}, []);
componentDidMount() {currentCount.current = this.state.count;}

如果配合 useEffect 回调函数的返回函数,能够实现相似 componentWillUnmount 的成果,因为如果是空数组的话,组件任何更新都不会触发 effect,所以回调函数的返回函数只能在组件销毁的时候执行

useEffect(() => {return () => {console.log('will trigger ar willUnmount')
  }
}, []);
componentWillUnmount() {console.log('will trigger ar willUnmount')
}

如果依赖列表外面有值,则相似 componentDidMount 有条件束缚更新,只有当上一次的状态和这次的不一样,才执行

useEffect(() => {currentCount.current = count;}, [count]);
componentDidUpdate(prevProps, prevState) {if (prevState.count !== this.state.count) {currentCount.current = this.state.count;}
}

useEffect 和 闭包问题

假如组件须要在初始化的时候,定义一个定时器,让 count 自增,自然而然的能够写出以下的代码

// 初始化的 count = 0
useEffect(() => {const id = setInterval(() => {setCount(count + 1);
  }, 1000);
}, []);
componentDidMount() {setInterval(() => {this.setState({ count: this.state.count + 1});
  }, 1000);
}

然而理论运行的时候,类组件展现是对的,函数组件从 0 递增到 1 之后,页面渲染就再也不变了

  • 之前有提过,类组件因为有 this 这个援用,很容易通过 state 拿到最新的值
  • 函数组件每次渲染都是独立的闭包,这里因为写的依赖值是 [],所以只有首次渲染后,才会这行这个 effect,首次渲染后,count 就是 0,所以 setCount(count + 1) 每次都是执行 setCount(0 + 1),所以定时器工作是失常的,不过取的值有问题。

闭包问题的切入点和产生场景

闭包问题,大多产生在,有些回调函数执行,依赖到组件的某些状态,然而这些状态并没有写到 useEffect 的依赖列表外面。导致执行回调函数的时候,拿到组件的状态不是最新的。
次要的场景有:

  • 定时器
  • 事件监听的回调
  • 各种 Observer 的回调

这些场景,通常只有在组件初始化渲染完之后,定义一次回调函数就好,然而如果回调函数依赖到组件的转态或者属性,这时候就要小心,闭包问题

function Router() {const [state, setState] = useState<string>('');
  useEffect(() => {
    window.addEventListener<'hashchange'>(
      'hashchange',
      () => {// 监听 hash 变动,这里依赖到 state},
      false
    );
  }, []);
}

例如这里的写法,在组件渲染完监听 hashchange,回调函数是拿不到后续更新的 state 的,只能能到初始化时候的空字符串。

尝试解决闭包问题 - 监听 state 变动

既然回调函数要每次都拿到最新的 state,能够监听 state 的变动,state 变动的时候,从新定义事件监听器,改写一下

function Router() {const [state, setState] = useState<string>('');
  useEffect(() => {
    window.addEventListener(
      'hashchange',
      () => {// 监听 hash 变动,这里依赖到 state},
      false
    );
  }, [state]);
}

以上代码能用,然而 state 每次扭转,就会从新定义一个 hashchange 回调函数,然而上一次的 hashchange 的事件监听器并没有革除,代码能跑,然而内存透露也太重大了,能够配合 useEffect 回调函数返回的函数配合清掉上一次的事件监听器

function Router() {const [state, setState] = useState<string>('');
  useEffect(() => {const callback = () => {};
    window.addEventListener('hashchange', callback, false);
    return () => window.removeEventListener('hashchange', callback, false);
  }, [state]);
}

这样内存透露的问题被解决了,然而这种事件监听,失常来说设置一次就好,没必要从新定义,还有别的更好的办法么?

尝试解决闭包问题 – setState 另外一种更新组件状态的形式

useState 返回的更新状态的函数,除了能够传一个值,还能够传一个回调函数,回调函数带一个参数,这个参数是最新的 state,像这样的话,之前那个定时器的例子,能够批改成这样。

function Counter() {const [count, setCount] = useState(0);
  useEffect(() => {const id = setInterval(() => {// setCount(count + 1)
      setCount((c) => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>{count}</h1>;
}

这里咱们改了一行代码,setCount(count + 1) 改成了 setCount((c) => c + 1),这样批改之后,其实定时器回调曾经没有依赖到 count 这个值了,由 setCount 外面的回调函数,返回最新的 count 的值,就是setCount((c) => c + 1),外面的 c.

同样的,对于事件监听器外面,咱们也能够通过这个形式去获取最新的 state,然而这里有几个问题

  • 这个回调函数,其实也只有获取最新的 state,所以在调用 setState 的时候,拿到最新的值的同时,记得把 setState 的值,设置成和以后同一个,如果没有返回,那调用 setState 之后,state 的值会变成 undefined
  • setState 返回一个同样的值,会不会导致组件和它的子组件从新渲染?找了下文档阐明是这样的:调用 State Hook 的更新函数并传入以后的 state 时,React 将跳过子组件的渲染及 effect 的执行。须要留神的是,React 可能仍须要在跳过渲染前渲染该组件。不过因为 React 不会对组件树的“深层”节点进行不必要的渲染,所以大可不必放心
  • 看起来可行的,做一下简略的批改其实能够改成这样
function Router() {const [state, setState] = useState<string>('');
  useEffect(() => {const callback = () => {
      let latestState =‘’;
      setState((stateCallback) => {
        // stateCallback 是最新的 state
        latestState = stateCallback;
        // 记得把 stateCallback 返回去,不然 state 会被改成 undefined
        return stateCallback;
      });
      // latestState 曾经被赋值为最新的 state 了
    };window.addEventListener<'hashchange'>('hashchange', callback, false);
  }, [])
}

这样根本就没问题了,做到了只定义了一次回调,而后也能够获取最新的 state,两全其美,然而还是有问题的

  • setState 回调函数如果不写 return stateCallback; 这段代码,会导致 state 莫名其妙被设置成 undefined,而且十分不好发现,维护性太差
  • setState 是用来扭转组件的 state 的,不是让你这样用的的,尽管这样用齐全没问题。然而可维护性太差了,如果你的代码被接手,他人就会纳闷这里为什么要这么写,无正文和变量命名太蹩脚的状况下,代码能够维护性根本为 0
  • 设置一个同样的 state,尽管不会导致子组件从新渲染,然而本组件还是有可能从新渲染的,按官网的说法

这个计划不完满。思路再发散一下?执行回调函数的时候,须要获取到最新的 state,能不能用一个不变的值缓存 state?等等??不变的值???

解决闭包问题最佳实际 -useStateuseRef

useRef的返回是在整个组件生命周期都是不变的一个对象,能够借助 useRef 来取得最新的 state。例如这个例子能够改成这样:

function Router() {const [state, setState] = useState<string>('');
  const stateRef = useRef<string>(state);
  // 这样,能够把 stateRef 和最新的 state 绑定起来
  stateRef.current = state;
  // 或者这样,能够把 stateRef 和最新的 state 绑定起来
  useEffect(() => {stateRef.current = state;}, [state]);
  useEffect(() => {const callback = () => {const latestState = stateRef.current;};
    window.addEventListener<'hashchange'>('hashchange', callback, false);
  }, []);
}

stateRef.current 下面两种写法,都能够取得最新的 count,回调函数外面外面间接读取 stateRef.current 的值,能够拿到最新的 state 闭包问题的最优解,节本就是这样了。

useRefuseState 的最佳实际

  • useStateuseRef 认真想想和和类组件的什么属相很类似?是不是和 this.statethis 的属性很像
  • 在类组件中,如果是不参渲染的属性,间接挂 this 上就好了,如果须要参加渲染的属性,挂在 this.state
  • 同样的,在 Hook 中,useRefuseState 能够实现相似成果

例如以下的例子

// 函数组件
const Child = React.memo(() => {
  // count 参加页面渲染
  const [count, setCount] = useState(0);
  // userInfo 不参加渲染
  const userInfo = useRef(null);
});
// 类组件
class Child extends React.PureComponent {constructor(props) {super(props);
    // 不参加渲染
    this.userInfo = null;
    // 参加渲染的属性
    this.state = {count: 0,};
  }
}

再看看 useEffect 回调函数的返回值

type EffectCallback = () => (void | (() => void | undefined));
return void | (() => void | undefined)

确定是没有返回或者返回一个函数,所以上面这种写法是有问题的,尽管也没有显著表明返回体,就是没有返回一样,然而这个回调函数是异步函数,异步返回默认返回一个 Promise 对象,所以这种写法是不提倡的

const [data, setData] = useState({hits: [] });
  useEffect(async () => {
    const result = await axios('url‘,);    setData(result.data);}, []);

为了躲避这个问题,能够批改一下写法

useEffect(() => {const fetchData = async () => {const result = await axios('url');
    setData(result.data);
  };
  fetchData();}, []);

useCallback

把函数写进外面没什么问题,官网也举荐,然而万一我的副作用外面须要解决多个函数或者一个超长的函数的话,一个是不美观,一个是太难保护
这个实用能够利用 useCallback 把函数抽离进来,useCallback 返回一个记忆化的函数,当且仅当依赖列表有任何属性扭转的时候,它才会返回一个新的函数,所以这个个性比拟适宜传给子组件的回调函数

function Counter() {const [count, setCount] = useState(0);
  const getFetchUrl = useCallback(() => {return 'https://v?query=' + count;}, [count]);
  useEffect(() => {getFetchUrl();
  }, [getFetchUrl]);
  return <h1>{count}</h1>;
}

这里如果 count 扭转的时候,getFetchUrl的值也会扭转,从而导致 useEffect 也触发

React.memo

React.memo() 返回一个记忆化的值,如果 React 外部会断定,如果从新渲染 props` 不相等,就会从新渲染,如果没有扭转,就不会触发组件渲染
这个个性比拟有用,因为如果父组件从新渲染的时候,子组件就会从新渲染,应用这个个性能够缩小不必要的子组件从新渲染

const Child = memo((props) => {useEffect(() => {}, [])
  return (// ...)
}, (prevProps, nextProps) => {
  // 断定相等的逻辑
  // 如果某些属性的扭转不须要从新渲染
  // 能够编写这个函数
})

React.useCallbackReact.memo

  • 为什么讲 useCallback 要把 memo 拎进去讲,想一下 useCallback 的作用,返回一个缓存的函数,在函数组件外面,每次渲染都会执行一次组件函数,组件函数每次执行,在组件外部的函数都会从新定义,这样的话,父组件传给子组件的回调函数每次渲染都会变
  • 再从 memo 的角度去看,父组件每次渲染,子函数组件如果不加 memo 的话,就算是子组件无任何依赖,属性都不变的状况下,子组件也会从新渲染
  • 如果在父组件独自加为子组件的回调函数增加 useCallback,这样能够防止回调函数从新定义,然而子组件如果不必 memo 包裹,就算任何子组件属性没扭转,还是会导致子组件从新渲染;
  • 同样的,如果子组件独自用 memo 包裹,父组件每次渲染,从新定义回调函数,还是会导致从新
  • 所以,memouseCallback 必须都用上,不然是没用的,不仅达不到优化的成果,而且会减轻 React 比拟的累赘。要不就别用,要不就都用上。

React.useCallbackReact.memo 最佳实际

父组件用 useCallback 包裹函数,子组件用 memo 包裹组件,要不就都不必

// 子组件
// callback 为父组件传过来的回调函数
const Child = ({callback}) => {}
// 子组件用 React.memo 包裹
export default React.memo(Child);

// 父组件 const Parent = () => {
  // 子组件的回调函数用 useCallback 包裹
  const callback = React.useCallback(() => {}, []);
 return <Child callback={callback} />
};

Raect.memo 的局限

  • React.memo 包裹在组件上,能够对传给组件的属性进行断定,父组件导致子组件从新渲染的时候,memo 包裹的组件,会断定属性是否和上次渲染时候否扭转,如果有扭转,子组件从新渲染,否则不会从新渲染。
  • React.memo 有个局限,只能避免来源于内部的属性,如果是来源于外部的属性,React.memo 是无作用的,例如通过 useContext 间接注入组件外部的属性,它没法避免,能够看下上面这个简略的例子
export const Store = React.createContext(null);
export default function Parent() {const [state, dispatch] = useReducer(reducer, { count: 0, step: 0});

  return (<Store.Provider value={{ dispatch, state}}>
      <Step />
      <Count />
    </Store.Provider>
  );
}

export const Count = memo(() => {const { state, dispatch} = useContext(Store);
  const setCount = () => dispatch({ type: 'changeCount'});
  return (
    <>
      <span>count: {state.count}</span>
      <button onClick={setCount}>change count</button>
    </>
  );
});

export const Step = memo(() => {const { state, dispatch} = useContext(Store);
  const setCount = () => dispatch({ type: 'changeStep'});
  return (
    <>
      <span>count: {state.step}</span>
      <button onClick={setStep}>change step</button>
    </>
  );
});
  • 下面的组件,count 或者 step 任意这个属性扭转,都会导致两个子组件从新渲染,这显然是不对的。

React.useMemo 代替 React.momo

useMemomemo 一样,返回一个记忆化的值,如果依赖项没有扭转,会返回上一次渲染的后果,它和 useCallback 的差异就在一个是返回记忆化的函数,一个是返回记忆化的值,如果 useMemo 的回调函数执行返回一个函数,那它的成果和 useCallback 是一样的。
因此下面的组件能够改一下,上面这种写法就能够避免任意一个属性扭转会导致两个子组件从新渲染的问题

export const Count = () => {const { state, dispatch} = useContext(Store);
  const setCount = () => dispatch({ type: 'changeCount'});
  return useMemo(() => (
      <>
        <span>count: {state.count}</span>
        <button onClick={setCount}>change count</button>
      </>
    ),
    [state.count]
  );
};
export const Step = () => {const { state, dispatch} = useContext(Store);
  const setStep = () => dispatch({ type: 'changeStep'});
  return useMemo(() => (
      <>
        <span>step: {state.step}</span>
        <button onClick={setStep}>change step</button>
      </>
    ),
    [state.step]
  );
};

React.momoReact.useMemo

  • React.momo 在避免子组件从新渲染方面,是最简略的,在类组件外面有个 React.PureComponent,其作用也是。然而它无奈检测函数外部的状态变动,并且避免从新渲染,例如 useContext 注入的状态。不过它主动比拟全副属性,应用起来方面。
  • React.memo 依照依赖列表是否有属性扭转,决定是否返回新的值,肯定水平上和 Vue 的计算属性相似,然而须要说动申明依赖的属性。相比 React.momo,它的管制的粒度更细,然而个别的内部属性变动,用这个显著没有 React.memo 不便

useReducer useContext

  • useReduceruseState 的一种代替计划,useState 的外部实现就是 useReducer
  • 它接管两个参数,和 redux 一样,一个是 reducer,一个是初始值,有两个返回,始终是以后的 state,一个是 dispatch
  • 通过 dispatch 调用 action 就能够批改 state 外面的数据
  • 实质的作用是,让数据和函数组件解耦,让函数组件只有收回 Action,就能够批改数据,因为数据不在组件外部,也不必解决外部 state 变动带来的 effect
  • useContextuseReducer 联合,肯定水平上能够实现一个 React Redux

其余 Hook

  • useImperativeHandle,搭配 useRefforwardRefs 能够实现定制父组件能够援用子组件的属性和办法,而不是间接援用整个子组件的实例,在父组件须要调用子组件属性和办法,然而又不想全副属性和办法都给父组件调用的时候应用
  • useLayoutEffect 应用的不多,作用和 useEffect 一样,然而这个 hook 是在组件变动后,DOM 节点生成后,渲染之前调用,区别于 useEffect 是渲染之后调用,不太举荐应用,会阻塞渲染
  • useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。相似 Vue 组件用的 name 或者 React 组件中的 displayName,不影响代码运行

组件复用

React Hook 有自定义 HookReact 类组件有高阶组件或者渲染属性
有个比拟常见的场景,进入页面须要调用后端接口的问题,如果每个组件都写一次,很繁琐,假如解决数据的接口长这样子

interface Response<T> {
  /** 是否有谬误 */
  hasError: boolean;
  /** 是否有数据 */
  hasData: boolean;
  /** 是否申请中 */
  Loading: boolean;
  /** 申请回来的数据 */
  data: T;
}

高阶组件

高阶组件 (HOC)React 中用于复用组件逻辑的一种高级技巧。HOC 本身不是 React API 的一部分,它是一种基于 React 的组合个性而造成的设计模式。这种组件复用还挺常见的,比方 React-redux 外面的 connectReact RouterwithRouter

它能够做到:

  • 属性代理,比方多个组件都应用到的公共属性,注入属性
  • 包裹组件,比方将组件包裹在写好的容器外面
  • 渲染挟持,比方权限管制

用途

  • 代码复用
  • 性能监测 打点
  • 权限管制,依照不懂的权限等级,渲染不同的页面

高阶组件编写和应用

按下面申请的需要,做一个组件渲染完之后,就立刻开始申请初始数据

function requestWrapper(options) {return (Component) => {
    return class WapperComponent extends React.Component {constructor(props) {super(props);
        this.state = {
          loading: false,
          hasError: false,
          hasData: false,
          data: null,
        };
      }
      httpRequest = async () => {this.setState({ loading: true});
        // 这里做一些申请工作,我这里就不写了,没必要
        this.setState({hasData: true});
        this.setState({hasError: false});
        this.setState({
          data: {/** 这里是申请回来的数据 */},
        });
        this.setState({loading: false});
      };
      componentDidMount() {this.httpRequest();
      }
      render() {
        // 透传内部传过来的属性,而后合并 this.state,传给包裹组件
        const combineProps = {...this.props, ...this.state};
        return this.state.loading ? (<Loading />) : (<Component {...combineProps} />
        );
      }
    };
  };
}

应用方面,高阶组件能够润饰类组件或者函数组件

function Page(props) {
// props 蕴含 loading hasError hasData data prop1
// 其中 prop1 来源于内部属性,其余的属性来源于包裹的组件
}
// 类组件应用,应用装璜器
@requestWrapper({url: '', param: {} })
class ClassComponent extends React.PureComponent {
}
// 类组件应用,不实用装璜器
export default requestWrapper({url: '', param: {} })(ClassComponent);
// 函数式组件应用
const WrapPage = requestWrapper({url: '', param: {} })(Page);
export default WrapPage;
// 应用
<WrapPage prop1="1" />;

自定义 Hook 的编写和应用

自定义 Hook 的编写,一个简略的数据申请的 Hook

function useRequest(options) {const [loading, setLoading] = useState(false);
  const [hasError, setHasError] = useState(false);
  const [hasData, setHasData] = useState(false);
  const [data, setData] = useState(null);
  useEffect(() => {async function reqeust() {setLoading(true);
      /** 这里仍旧是申请,没写 */
      setHasError(false);
      setHasData(true);
      setData({/** 申请回来的数据 */});
      setLoading(false);
    }
    reqeust();}, []);
  return {loading, hasError, hasData, data};
}

自定义 hook 只能在函数式组件应用,不能在类组件外面用

function Page(props) {
  // 这次的 props 只有 prop1 这个属性
  const {loading, hasError, hasData, data} = useRequest({
    url:‘’,
    param: {},});
}
<Page prop1={1} />;

函数式式组件和类组件默认属性

class Button extends React.PureComponent {static defaultProps = { type: "primary", onChange: () => {}};
}

// 不论是函数式还是类都能够这么玩,这也是类动态属性的另外一种写法
Button.defaultProps = {
  type: "primary",
  onChange: () => {}
};

function Button({type, onChange}) {}

// 这样写看起来没问题,然而实际上,如果父组件没传 onChange,onChange
// 每次组件渲染都会生成一个新的函数,援用类型都有这个问题
function Button({type = "primary", onChange = () => {}}) {}

// 这很 OK,你可真是个小机灵鬼
const changeMet = () => {}
function Button({type =“primary”, onChange = changeMet}) {}

类组件的问题被解决了么?

复用组件状态逻辑难

  • 依赖自定义的 Hook,能够解决组件状态和逻辑复用的问题,然而自定义 Hook 编写须要对 Hook 运行机制十分理解,门槛并不比高阶组件低

生命周期带来的负面影响,逻辑拆分重大

  • 生命周期拆分逻辑的问题,在 Hook 外面切实被解决了,不会存在同一个逻辑被拆分在 N 个生命周期外面了

This 的指向问题

  • 这个问题在 Hook 外面也是解决了,因为函数没有 this,就不会有 this 的问题,然而绝对的,如果须要一个不变的对象,请应用 useRef

简略总结

  • useState能够实现相似 statesetState 的成果
  • useEffect 能够实现 componentDidMount componentDidUpdate componentWillUnmount 这几个生命周期的性能,并且写法更加简略,在每次渲染后都会触发,触发的条件是依赖项有扭转
  • useRef 返回一个援用,每次渲染都返回同一个对象,和类组件 this 属性统一
  • useCallback 返回一个记忆化的回调函数,在依赖项扭转的时候,回调函数会批改,否则返回之前的回调函数,对于一些须要传给子组件的函数,能够应用这个,防止子组件因为回调函数扭转而扭转
  • useMemo 返回一个记忆化的值,依赖项扭转,返回的值才会变,可用来记忆化值,和 Vue 计算属性相似,防止反复计算,防止反复渲染
  • 自定义的 Hook 是实现状态和逻辑复用,作用和高阶组件还有渲染属性差不多
  • useReduceruseState 的底层实现,能够治理多个 state,把 state 从组件外部抽离进去
  • useContext 能够实现批量传值,注入多个组件,和 useReducer useMemo 应用能够实现 Redux 的性能

应用感触

集体应用方面

  • 函数式组件自身写起来就比类组件少写不少代码
  • 闭包问题很影响开发和调试,进步了不少调试老本,如果不相熟闭包机制,很难发现问题。Hook 中的闭包问题,大多还是因为依赖项没有填写导致
  • 闭包带来的问题,比类组件 This 的更加宜人,次要调试不好发现问题,填不填依赖项也是一个让人纠结的活
  • Hook 的依赖不能自动识别,必须手动申明,尽管有插件辅助增加,然而应用起来还是不如 VueHook
  • 在相熟 Hook 的机制的状况下,Hook 开发体验还是比类组件好很多

团队合作方面

  • 其实在推广 Hook 的时候,团队成员的 Hook 程度是不太统一的,很多人员就遇到了闭包问题,还有依赖死循环的问题,这个可能大大小小都遇到过,就如同下面提到的,解决闭包问题,形式形形色色,其实也是我本人摸索过去的,而后看到团队成员其实差不多还使用者 state 更新之后,从新设置监听的形式,这个并不是太好,只能说闭包问题解决了
  • 绝对的,React 官网也没有总结太多最佳实际,很多都靠本人实际过去的,所以团队成员在刚接触 Hook 的时候,都是 useEffect useState 两把 API,甚至在 React Hook 的官网文档外面 Hook 简介,对于这两个 Hook 介绍的很多
  • 但对于其余罕用的 Hook,比方 useRefuseCallback 应用场景其实没有太好的例子去撑持这些 API 的应用。倒是其实团队外面不少成员,面对着不参加渲染的属性,也是用 useState,而不是应用 useRef。就是很多新人接触 Hook 容易犯的一个谬误。
  • 有不少同学有些插件没有装上,导致 React 自动检测依赖项的插件没有失效,这无疑会给自身就难以发现的闭包问题加了一层霜
  • 所以我也定期在团队外面分享我认为是比拟好的实际,去疏导团队外面的同学
  • 对于不喜爱用 React Hook 的同学,间接用类组件,类组件尽管代码写起来繁琐,然而起码没有闭包这些问题,而且代码被接手之后容易读懂,React Hook 只是一个工具,会应用会给你加分,然而不会应用只会用类组件,也不会对其他人代码有影响,比拟类组件和函数组件是能够共存的

正文完
 0