React Hooks 用法

刚学了一下 React Hooks 的用法,就写篇博客记录一下。因为学得也比较浅,所以这篇博客只讲怎么用。

useState

普通用法

就是用来管理组件的内部状态。

const UseState: React.FC = () => {
  const [x, setX] = useState(0)
  const [y, setY] = useState(100)

  const onClickX = () => setX(x + 1)
  const onClickY = () => setY((prevX) => prevX + 1)

  return (
    <div>
      <h1>useState</h1>
      <button onClick={onClickX}>x + 1</button>
      <p>x: {x}</p>

      <button onClick={onClickY}>y + 1</button>
      <p>y: {y}</p>
    </div>
  )
}

这里注意,x 是直接通过 setX 去改变的,而 y 是传入一个函数(操作)去改变。传入函数去改变的一个好处就是可以连续多次改变 y 值,而 setX 只会执行最后一次的 setX。

改变对象

对于改变对象一定不能只改变其中的属性,而是整个对象都要改。

const [obj, setObj] = useState({name: 'Jack', age: 18})

// 错误
obj.age = 19
setObj(obj)

// 正确
setObj({
  ...obj,
  age: 19
})

复杂计算

如果初始值需要复杂计算,则可以在 useState 直接传入一个 factory 函数,而不是默认值。

const [onlyRunAtFirstTime, change] = useState(() => {
  console.log('只在第一次渲染时执行,这里可以假设 9 + 9 是一些复杂的操作')

  return {name: 'Jack', age: 9 + 9}
})

useReducer

useReducer 可以看成复杂版的 useState,或者理解成 Redux 里的 reducer,再或者理解成 Vuex。返回值依然是一个数组,第一项是读,第二项是写。

const initState: TState = {
  name: 'Jack',
  age: 18
}

const reducer = (state: TState, action: TAction) => {
  const {type} = action

  switch (type) {
    case 'addAge':
      return {...state, age: state.age + 1}
    case 'minusAge':
      return {...state, age: state.age - 1}
    case 'update':
      return {...state, ...action.user}
    default:
      throw new Error('没有传入 type')
  }
}

const LearnUseReducer: React.FC = () => {
  const [state, dispatch] = useReducer(reducer, initState)

  const onAdd = () => dispatch({type: 'addAge'})
  const onMinus = () => dispatch({type: 'minusAge'})
  const onUpdate = () => dispatch({type: 'update', user: {name: 'Tom', age: 20}})

  return (
    <div>
      <h1>useReducer</h1>
      <button onClick={onAdd}>age + 1</button>
      <button onClick={onMinus}>age - 1</button>
      <button onClick={onUpdate}>更新整个user</button>
      <p>{JSON.stringify(state)}</p>
    </div>
  )
}

这里要注意的是,reducer 是第一个参数,initState 是第二个参数,非常奇怪的传参逻辑。

useContext

普通用法

这个 Hook 的意义就是造一些局部变量,在这个局部下的组件都能访问到这些变量。

一般可以配合上面的 useReducer 去代替 Redux,或者在 styled-components 里可直接使用主题色。

const initTheme = {
  success: 'green'
}

const Context = createContext<TTheme | null>(null)

const LearnUseContext: React.FC = () => {
  const [theme] = useState(initTheme)

  return (
    <Context.Provider value={theme}>
      <div>
        爸爸得到的值: {theme.success}
        <Child/>
      </div>
    </Context.Provider>
  )
}

const Child: React.FC = () => {
  const theme = useContext(Context)

  return (
    <div>
      儿子得到的值: {theme!.success}
      <GrandChild/>
    </div>
  )
}

const GrandChild: React.FC = () => {
  const theme = useContext(Context)

  return (
    <div>孙子得到的值: {theme?.success}</div>
  )
}

上面的例子可以看到,在外面定义好的主题色,通过 Context.Provider 传递到下面的所有组件。当然也可以再传个 setTheme 函数,让儿子、孙子组件去修改 theme。

代替 Redux

那怎么去代替 Redux 呢?很简单,把上面的讲的 reducerstate 提到 App,然后,通过 Context.Provider 传到下面所有组件就好了。

const initState = {/*初始状态*/}
const reducer = () => {/*你的 reducer*/}

const App: React.FC = () => {
  const [store, dispatch] = useReducer(reducer, initState)

  return (
    <Context.Provider value={{store, dispatch}}>
      <div>各种组件</div>
    </Context.Provider>
  )
}

useEffect

useEffect 一般是去代替 componentDidMount, componentDidUpdate 和 componentWillUnmount。简单来讲,就是活了,变了,死了的时候要干啥。

下面先给个例子。

const LearnUseEffect: React.FC = () => {
  const [movies, setMovies] = useState<any[]>([])

  useEffect(() => { // 对应 componentDidMount
    console.log('刚开始就执行,要在这里去获取 movies')

    ajax('movies', (newMovies: any[]) => setMovies(newMovies))
  }, [])
  useEffect(() => { // 对应 componentDidUpdate
    console.log('只要有东西更新了,我就执行')
  })
  useEffect(() => { // 对应 componentDidUpdate,但是只有 movies 更新才执行
    console.log('movies 更新了,所以我执行了')
  }, [movies])

  return (
    <div>
      <h1>useEffect</h1>
      {
        movies.length === 0 ?
          <div>加载中</div> :
          <ul>
            {movies.map(m => <li key={m.id}>{m.name}</li>)}
          </ul>
      }
    </div>
  )
}

执行后可以看到

为什么会有两次的 “只要有东西...” 和 “movies 更新了..” 呢?是因为第一次是使用了 useStatemoviesnull 变成了 [],第二次才是发 ajax 去请求数据。

关于替换 componentWillUnmount,只需要返回一个函数就可以了。

useEffect(() => { // 对应 componentDidMount
  console.log('刚开始就执行,要在这里去获取 movies')

  ajax('movies', (newMovies: any[]) => setMovies(newMovies))
  
  return () => {
    console.log('这个组件就要死翘翘了')
  }
}, [])

注意,这里可以同时使用多个 useEffect,执行顺序就是按代码的顺序。

useLayoutEffect

useEffectuseLayoutEffect 是差不多的东西,区别是执行时机不一样。这个 Hook 是在渲染之前,先去执行一些东西,然后再去渲染。而 useEffect 就是在渲染之后,才去执行函数。也就是说:

  1. App() 执行
  2. 生成虚拟 DOM
  3. 变成真实 DOM
  4. 执行 useLayoutEffect 回调
  5. 渲染 render
  6. 执行 useEffect 回调

这样的好处就是如果你有下面的代码:

useEffect(() => {
  document.querySelector('#xxx').textContent = Math.random()
})

你会看到页面的闪烁,本来是 0 变成了 随机数。

而如果用 useLayoutEffect 就不会出现上面的情况,因为还渲染,已经改成了随机数了。

既然不会闪烁,那是不是都用 useLayoutEffect 好呢?其实不是,因为如果把所有的操作都放在渲染之前,用户一打开网页就会是一片白,等数据来了或者计算结束了,页面才渲染。这会严重影响用户体验。

大部分情况下应该是使用 useEffect,在获取数据或者大量计算时候显示一个 Loading 菊花就好了。

useMemo

在写 React 的时候,我们经常会有这种场景,App 下有一个 Child 组件:<App> -> <Child>。当改变了 App 组件的变量时,Child 会再次执行。

const LearnUseMemo: React.FC = () => {
  const [x, setX] = useState(0)

  const onClickX = () => setX(x + 1)

  return (
    <div>
      <h1>useMemo</h1>
      <button onClick={onClickX}>x + 1</button>
      <Child/>
    </div>
  )
}

const Child: React.FC = () => {
  console.log('我是儿子,为什么要搞我')

  return <div>儿子</div>
}

像这个例子,点一下 x + 1 按钮,x 的值变了,但是依然会打出“我是儿子,别搞我”。

这里就有问题了:改变的是 App 的东西,又不是 Child 的东西,Child 怎么还要执行呢?useMemo 就是来解决这个问题的。

什么是 memo

这里有个解决方法就是用 React.memo,将 Child 组件传入会得到一个组件,这个新的组件只有在 props 变了才会再次执行。

const Child2 = React.memo(Child)

然后

return (
    <div>
      <h1>useMemo</h1>
      <button onClick={onClickX}>x + 1</button>
      <Child2/>
    </div>
)

这样就不会打出“我是儿子,为什么要搞我”。但是,这里还是有问题,假如 Child 的 props 要传入一个函数,那么 props 就有可能发生变化:

const LearnUseMemo: React.FC = () => {
  const [x, setX] = useState(0)

  const onClickX = () => setX(x + 1)

  const onChild3Click = () => {}

  return (
    <div>
      <button onClick={onClickX}>x + 1</button>
      <Child3 onChild3Click={onChild3Click}/>
    </div>
  )
}

const Child3 = React.memo((props: any) => {
  console.log('我是儿子3,用了 memo 还能搞到我?')

  return <div onClick={props.onChild3Click}>我是儿子3,用了 memo 还能搞到我?</div>
})

这里点击了 x + 1 按钮,还是会打出 “我是儿子3,用了 memo 还能搞到我”,因为在执行 LearnUseMemo 的时候, onChild3Click 已经换成另一个函数了(虽然函数体是一样的,但是函数是对象,对象地址变了),所以 props 会变,props 一变,就会执行 Child3。

最后的 useMemo

为了解决上面的问题,终于引出我们的 useMemo 了。useMemo 可以缓存一切东西,比如上面的函数:

const onMemoClick = useMemo(() => {
  return () => console.log("点击了")
}, [])

然后再把这个 onMemoClick 传给 Child3 就OK了!第二个参数是监听的依赖,只有依赖变了,才会更新,和 useEffect 是差不多的。

为了更简洁,我们还可以使用 useCallback,就不用像上面那样疯狂俄罗斯套娃了。

const onMemoClick = useCallback(() => {
  console.log('点击了')
}, [])

useRef

普通用法

使用 useSate 的时候,值是每次都会变,那我希望每次更新值不要变
useRef 的使用场景是如果你需要一个值,在组件不断 render 时保持不变。。useRef 会将值存在一个地方,与当前组件一一对应。例子:

const LearnUseRef: React.FC = () => {
  const [x, setX] = useState(0)
  const y = useRef(0)

  useEffect(() => {
    y.current += 1
  }, [])

  const changeX = () => setX(x + 1)
  const changeY = () => y.current += 1

  return (
    <div>
      <h1>useRef</h1>
      <button onClick={changeX}>x + 1</button>
      <button onClick={changeY}>y + 1</button>
      <div>x 的值 {x}</div>
      <div>y 的值 {y.current}</div>
    </div>
  )
}

但是这里有个问题,就是在 useEffect 的时候改变了 y 的值,页面显示还是0.

这是因为改变了 y.current 的值后,React 并不会帮你更新页面。只有,如 setX 被执行导致页面更新才会更新 y 的值,所以很蛋疼。不过,如果你非要实现点击然后加一这个功能,那就不如用 useState 好了。

forwardRef

之前用过 React 和 Vue 的同学应该知道,我们可以用 ref 去代替 document.querySelect 来获取某个元素。forwardRef 是用来传递 ref 这个 props 的。

const LearnUseRef: React.FC = () => {
  const myButton = useRef(null)
  return (
    <div>
      ...
      <Button ref={myButton}/>
    </div>
  )
}

const Button = (props: any) => {
  return <button ref={props.ref}>用作 ref 的按钮</button>
}

这里我们想获取 Button 组件里的 button 元素,所以我们想从 LearnUseRef 组件传一个 ref 到 Button,好让 Button 去引用到 button 元素。但是报错:

因为 ref 本来就用作引用某个元素的,但是你是想传一个叫 "ref" 的 props,这不就冲突了嘛,所以报错。这里已经提示我们要用 forwardRef 了,所以应该是这么用的:

const LearnUseRef: React.FC = () => {
  const myButton = useRef(null)
  return (
    <div>
      ...
      <OKButton ref={myButton}/>
    </div>
  )
}

const Button = (props: any) => {
  return <button ref={props.ref}>用作 ref 的按钮</button>
}

const OKButton = React.forwardRef(Button)

useImperativeHandle

这个 Hook 其实的意思就是 setRef,怎么理解呢?回看上面的例子,我们传了一个 ref 给 OKButton 组件,那万一 OKButton 组件想修改这个 ref 怎么办呢?这就需要 useImperativeHandle 了。

const LearnUseImperativeHandle: React.FC = () => {
  const myButton = useRef(null)

  useEffect(() => {
    console.log('有人改了我的 ref', myButton)
  })

  return (
    <div>
      <OKButton ref={myButton}/>
    </div>
  )
}

const OKButton = React.forwardRef((props: any, ref: any) => {
  useImperativeHandle(ref, () => {
    return {
      x: 'hello'
    }
  })
  return <button>按钮</button>
})

打开控制台,可以看到

这里的 current 就变成了 {x: 'hello'} 了。用处嘛,我也没想到有什么用。。。

自定义 Hook

这个其实说到底就是一种封装的方法。比如,我们有 books 这个资源,RESTful API 有

  1. get /books
  2. post /books?id=xxx
  3. delete /books?id=xxx
  4. put /books?id=xxx

那么我们代码可能就有4个函数去发这4个请求:

const getBooks = () => {/* get /books */}
const addBooks = () => {/* post /books?id=xxx */}
const editBooks = () => {/* put /books?id=xxx */}
const deleteBooks = () => {/* delete /books?id=xxx */}

最好我们有个东西可以把上面的东西放在一起,然后在对应的组件调用一下就好了,这其实就是自定义 Hooks。

const useBooks = (initBooks: TBook[]) => {
  const [books, setBooks] = useState(initBooks)

  useEffect(() => {
    getBooks()
  }, [])

  const getBooks = () => {
    setTimeout(() => {
      setBooks([
        {id: '1', name: '一体'},
        {id: '2', name: '二体'},
        {id: '3', name: '三体'},
        {id: '4', name: '裸体'},
      ])
    }, 2000)
  }
  const addBooks = (newBook: TBook) => {
    setBooks([...books, newBook])
  }
  const editBooks = () => {/* put /books?id=xxx */}
  const deleteBooks = () => {/* delete /books?id=xxx */}

  return {
    books,
    getBooks,
    addBooks,
    editBooks,
    deleteBooks
  }
}

const LearnCustomizeHook: React.FC = () => {
  const {books} = useBooks([])

  return (
    <div>
      <h1>useBooks</h1>
      {
        books.length === 0?
          <div>加载中</div> :
          <ul>
            {
              books.map(b => <li key={b.id}>{b.name}</li>)
            }
          </ul>
      }
    </div>
  )
}

可以看到,用了自定义 Hooks 会让代码变得清爽很多。

(完)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353