CBU React Hook 实践指南

622 阅读5分钟

一、如何用Hooks替代组件的生命周期


react组件生命周期如图所示,分为render阶段、pre-commit阶段、commit阶段,这些常用的及不常的生命周期如何使用hooks来替代呢,下面一一为大家介绍:


  1. 使用hook替代componentDidMount方案,使用useEffect,第二个参数传入空数组
import React, { useState, useEffect } from 'react';
const columns = {...};

function Example() {
  const [dataSource, setdataSource] = useState([]);

  useEffect(() => { 
    const dataSource = await getSceneList();
    setDataSource(dataSource);
  }, []);

  return <Table dataSource={dataSource} columns={columns} />
}
  1. 使用hook替代componentDidUpdate方案,使用useEffect,第二个参数更新依赖
import React, { useState, useEffect } from 'react';
const columns = {...};

function Example() {
  const [query, setQuery] = useState({});
  const [dataSource, setDataSource] = useState([]);

  useEffect(() => { 
    const dataSource = await getSceneList();
    setDataSource(setDataSource);
  }, [query]);

  return (
    <div>
        <Filter query={query} onChange={setQuery} />
        <Table dataSource={dataSource} columns={columns} />
    </div>);
}
  1. 使用hook替代componentWillUnmount方案,使用useEffect,返回函数会在组件卸载前执行
import React, { useState, useEffect } from 'react';
const columns = {...};

function Example() {
  useEffect(() => {
    const listener = e => {
        console.log(e);
    };
    document.addEventListener('onClick', listener, false);
    return () => {
        document.removeEventListener('onClick', listener, false);
    };
  }, []);

  return (<div></div>);
}
  1. 使用hook替代shouldComponentUpdate方案,使用React.memo
import React, { useState, useEffect } from 'react';

function Example(props) {
  const {label, value} = props;
  return (
    <div>{label}:{value}</div>
  );
}

export default React.memo(Example, (prevProps, nextProps) => {
    return prevProps.label === nextProps.label 
        && prevProps.value === nextProps.value;
});
  1. 使用hook替代getDerivedStateFromProps方案,记录preState进行比较
import React, { useState, useEffect } from 'react';

function ScrollView({row}) {
  let [isScrollingDown, setIsScrollingDown] = useState(false);
  let [prevRow, setPrevRow] = useState(null);

  if (row !== prevRow) {
    setIsScrollingDown(prevRow !== null && row > prevRow);
    setPrevRow(row);
  }

  return `Scrolling down: ${isScrollingDown}`;
}
  1. 使用hook替代getSnapshotBeforeUpdate方案,使用useLayoutEffect,执行时机差不多可以替代
import React, { useState, useEffect } from 'react';

function Example() {
  useLayoutEffect(() => {
    //todo getSnapshot
  });

  return <div>...</div>;
}


二、什么时候该使用什么Hooks?


  1. 状态管理useState VS useReducer
    这个非常简单,就好比页面是否需要引入redux,页面只需要要简单状态useState,页面状态比较复杂需要状态管理useReducer
  2. 缓存处理useCallback vs useMemo vs React.memo,目的都是为了缓存提高性能,它们区别:
  • React.memo前面已经介绍了类似shouldComponentUpdate功能,可以控制组件是否更新,可以参照生命周期中示例。
  • useCallback返回一个缓存的函数,在函数式编程中使用剪头符号创建函数,如果不使用useCallback则每次都会返回一个新函数对象增加不必要的渲染
const handleSubmit = useCallback(data => {
    console.log(data);
  }, []);
  
return (
    <div className="form-test">
      <AForm initialValue={data} onSubmit={handleSubmit}/>
      <button className="submit" form={form.id} type="submit">表单提交</button>
    </div>
  );
  • useMemo返回一个缓存的对象,和useCallback用法一样,一般用作数据处理。
const columns = React.useMemo(() => {
    return columnsFactory(keys, fields, child, handleClick);
}, [fields]);

3. 副作用useEffect vs useLayoutEffect vs useMutationEffect(removed)

浏览器渲染过程


  • useMutationEffect
    同步阻塞,触发动作发生在重排 layout 前,DOM结构已经发生了变化。但不能读取重排信息。不过该hook由于一些问题已经被官方移除:github.com/facebook/re…
  • useLayoutEffect
    同步阻塞,触发动作发生在重排 layout 后但在重绘 paint 前,DOM结构已经发生了变化,这时可以从DOM中读取DOM的layout结果比如元素的高度宽度等重排信息。一般可以用来解决页面闪动问题。
useLayoutEffect(() => {
    form.register({ name });
    form.setValue(name, state);

    return () => {
      form.unregister(name);
    };
  }, [form, name]);
  • useEffect
    异步非阻塞,在渲染commit后,也就是完全在浏览器DOM变化渲染完成后,其实大部分的情况下你应该使用这个hook。

使用useLayoutEffect在服务端渲染会报大量的warning,所以要避免在SSR组件中使用。


三、Hooks特殊feature


即使不发生任何变化也能更新当前组件


import React, { useState } from 'react';
const useForceUpdate = () => useState()[1];
const App = () => {
  const forceUpdate = useForceUpdate();
  console.log('rendering');
  return <button onClick={forceUpdate}>Click To Render</button>;
};


四、自定义Hooks实践


把状态存储在localStoreage中,useStorageState


import { useState } from 'react';

function useStorageState<T>(storage: Storage, key: string, defaultValue?: T | (() => T)) {
  const [state, setState] = useState<T | undefined>(() => {
    const raw = storage.getItem(key);
    if (raw !== null) {
      return JSON.parse(raw);
    }
    if (typeof defaultValue === 'function') {
      return (defaultValue as () => T)();
    }
    return defaultValue;
  });
  function updateState(value?: T) {
    if (typeof value === 'undefined') {
      storage.removeItem(key);
      setState(defaultValue);
    } else {
      storage.setItem(key, JSON.stringify(value));
      setState(value);
    }
  }
  return [state, updateState];
}

export default useStorageState;


包装useReducer实现多状态支持字段切面,useMultipleState with AOP


import * as React from 'react';
import { getStorage, setStorage } from '../../utils/storage';

declare type kv = Record<string, any>;
declare type func = (value: any) => void;
declare type hook = [any, (state: any) => void];
declare type aop = {
  getInitState: (initValue: any) => any;
  setState: (setValue: func) => func;
};

export default function useMultipleState(initState: kv, aops: {[key: string]: aop} = {}): hook {
  const initStateAop = {};
  Object.keys(initState).forEach(key => {
    const aop = aops[key] || defaultAop;
    initStateAop[key] = aop.getInitState(initState[key]);
  });
  const reducer = (state: any, newState: any) => ({...state, ...newState});
  const [state, dispatch] = React.useReducer(reducer, initStateAop);
  const setState = (newState: any) => {
    Object.keys(newState).forEach(key => {
      const aop = aops[key] || defaultAop;
      aop.setState(value => newState[key] = value)(newState[key]);
    });
    dispatch(newState);
  };

  return [ state, setState];
}

export function createStorageAop(storageKey: string, dataKey?: string): aop {
  return {
    getInitState: initValue => {
      if (dataKey) {
        const dataValue = getStorage(storageKey, initValue[dataKey]);
        return {...initValue, [dataKey]: dataValue };
      } else {
        return getStorage(storageKey, initValue);
      }
    },
    setState: setValue => value => {
      setValue(value);
      const storageValue = dataKey ? value[dataKey] : value;
      setStorage(storageKey, storageValue);
    }
  };  
}

const defaultAop: aop = {
  getInitState: initValue => initValue,
  setState: setState => setState,
};


五、社区一些Hooks


Hooks可以很大程度上简化代码开发,社区已经有很多的hook类库,这里有一个收集Hooks的仓库,这里介绍几个:


useArray,简化数组操作


const App = () => {
  const todos = useArray(['hello', 'world', 'hooks']);
  return (
    <div>
        <h3>Todos</h3>
        <button onClick={() => todos.add(Math.randown())}>add</button>
        <url>
          {todos.value.map((todo, i) => {
            <li key={i}>
              {todo}
              <button onClick={() => todos.removeIndex(i)}>delete</button>
            </li>
          })}
            
        </ul>
        <button onClick={todos.clear}>clear todos</button>
    </div>
  );
}


useFetch,简化异步请求


import React from "react";
import useFetch from "react-fetch-hook";

const Component = () => {
  const { isLoading, data } = useFetch("https://swapi.co/api/people/1");

  return isLoading ? (
    <div>Loading...</div>
  ) : (
    <UserProfile {...data} />
  );
};


useOnClickOutside


import React from 'react'
import useOnClickOutside from 'use-onclickoutside'

export default function Modal({ close }) {
  const ref = React.useRef(null)
  useOnClickOutside(ref, close)

  return <div ref={ref}>{'Modal content'}</div>
}


六、Hooks do more


我在想Hooks是不是能做一些复杂的事情,所以动态表单Aform hook诞生了 @alife/use-aform,主要需要实现的能力


  • 拓展配置
  • 结构渲染
  • 取值赋值
  • 表单验证
  • 数据监听
  • 组件联动


useAform使用简单示例


import * as React from 'react';
import useAform from '@alife/use-aform';

const aformSchema = {...}; // aform结构
const initData = {...};    // 初始化数据

function App() {
  const { useCallback } = React;
  const { form, AForm } = useAform(aformSchema);

  const title = form.watch('title');
  
  const handleSubmit = useCallback(data => {
    console.log(data);
    // form.setValue(); form.getValue();
  }, []);

  return (
    <div className="form-wrap">
      <h3>{title}</h3>
      <AForm initialValue={initData} onSubmit={handleSubmit}/>
      <button className="submit" form={form.id} type="submit">表单提交</button>
    </div>
  );
}

ReactDOM.render(<App />, mountNode);