React+Hook

动机

一直在考虑为什么要使用,我们明明使用state使用的好好的,那会发明HOOK的动机是什么呢?

  • 在组件中复用状态逻辑很复杂
    使用Hooks,可以从组件中提取有状态逻辑,以便可以独立测试并复用。Hooks允许在不更改组件层次结构的情况下复用有状态的逻辑。 这样可以轻松地在许多组件之间共享Hooks。
  • 复杂组件变得难以理解
    使用state会导致相关的逻辑被切分,一些相关的代码却被分割在不同的函数文件中,这样导致代码的可读性变弱,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据),而并非强制按照生命周期划分,也可以使用reducer来进行相关逻辑的复用
  • 难以理解的 class
    class使用的时候,要先了解JavaScript的this使用,而且还要进行事件的绑定,对于函数组件与 class 组件的差异也存在分歧,甚至还要区分两种组件的使用场景。hook 使用非 class 的情况下可以使用更多的 React 特性。 从概念上讲,React 组件一直更像是函数。而 Hook 则拥抱了函数,同时也没有牺牲 React 的精神原则。

概览

hooks其实就是一种特殊的函数,这个函数有勾住状态和生命周期的功能,但我们希望在组件中使用状态的时候,我们就会使用hook用来代替class中的state。Hook 不能在 class 组件中使用。

hook和class的对比

使用class的形式来写组件的方法

import React from 'react'
class Person extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            username: 'kim'
        }
    }
    componentDidMount() {
        console.log('组件挂载后做的操作')
    }
    componentWillUnmount() {
        console.log('组件将要卸载')
    }
    componentDidUpdate(prevProps, prevState) {
        // 当username发生改变的时候进行渲染
        if (prevState.username !== this.state.username) {
            console.log('组件更新后的操作')
        }
    }
    render() {
        return (<Input onChange={(event) => this.setState({ username: event.target.value })} />)
    }
}

用hook来写函数组件

import React, { useEffect, useState } from 'react'

export const Person = () => {
    const [name, setName] = useState('');

    useEffect(() => {
        console.log('组件挂载后要做的操作');
        return () => {
            console.log('组件卸载要做的操作')
        }
    }, [])

    useEffect(() => {
        console.log('当name组件发生改变的时候显示的样式')
    }, [name])

    return (<div>
        <p>欢迎 {name}</p>
        <input type="text" placeholder="input a username" onChange={(event) => setName(event.target.value)}></input>
    </div>
    )
}

API

useState
  • 参数:是一个常量,组件初始化的时候就会进行定义
  • 参数:是一个函数,只有开始渲染的时候函数才会执行
  • 返回值:长度为2的数组,第一项是返回的state的值,第二项是改变该state的函数
    用来初始化状态,在函数的内部调用,为函数添加内部的state,useState会返回一对值,分别是要更新的状态和一个让你更新他的函数,useState唯一的参数就是初始化的值。
// 声明多个 state 变量!
  const [age, setAge] = useState(42);
  const [fruit, setFruit] = useState('banana');
  const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]);
//  声明一个叫 "count" 的 state 变量,初始值为0,后续通过setCount改变它能让视图重新渲染
export const Count = () => {
    const [count, setCount] = useState(0);
    // initiState 只会在组件初渲染的时候起作用,后续的渲染会被忽略不计
    const [value,setValue] = setValue(()=>{
        const initialCount = someExpensiveComputation(props);
        return initialState;
    })

    return (<div>
        <p> 你点击了{count}次</p>
        <button onClick={() => setCount(count + 1)}> Click me</button>
    </div>)
}
useEffect
  • 参数:第一个是含有副作用的命令式的代码,
  • 参数:第二个参数是一个数组,数组中的值用来控制第一个参数中的函数是否执行,当第二个参数不传的时候,是每一次有数据更新的时候,执行第一个参数中的函数,相当于class组件中的componentDidMount和componentDidupdate的生命周期。
export const Count = () => {
    const [count, setCount] = useState(0);
    // 功能类似componentDidMount and componentDidUpdate
    useEffect(() => {
        document.title = `You clicked ${count} times`;
    });

    // 只有count改变时才会执行
    useEffect(() => {
        document.title = `You clicked ${count} times`;
    }, [count]);

    // 组件挂载时只执行一次
    useEffect(() => {
        console.log("只执行一次,类似componentDidMount")
    }, []);

    return (<div>
        <p> 你点击了{count}次</p>
        <button onClick={() => setCount(count + 1)}> Click me</button>
    </div>)
}

可以在组件渲染后实现各种不同的副作用。有些副作用可能需要清除,比如说全局设定鼠标监听事件。

useEffect(() => {
  function handleStatusChange(status) {
    setIsOnline(status.isOnline);
  }

  ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
  return () => {
    ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
  };
});
useContext

当hook接受到一个context的对象的时候并返回context的当前值,当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext(themes.light);

const App = () => {
    // 在root中传入这个值,使用context进行包裹,便于后续取值
    return (
        <ThemeContext.Provider value={themes.dark}>
            <ToolBar />
        </ThemeContext.Provider>
    )
}

// 中间层不用传递参数
const ToolBar = () => {
    return (
        <ThemedButton />
    )
}

//  在使用层直接进行使用就可以了
const ThemedButton = () => {
    const theme = useContext(ThemeContext)
    return (
        <button style={{ background: theme.background, color: theme.foreground }}>
            I am styled by theme context!
        </button>
    )
}

Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。

我们看一下上面的代码以class的形式编写,会是什么样子的:

// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树。
// 为当前的 theme 创建一个 context(“light”为默认值)。
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // 使用一个 Provider 来将当前的 theme 传递给以下的组件树。
    // 无论多深,任何组件都能读取这个值。
    // 在这个例子中,我们将 “dark” 作为当前的值传递下去。
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // 指定 contextType 读取当前的 theme context。
  // React 会往上找到最近的 theme Provider,然后使用它的值。
  // 在这个例子中,当前的 theme 值为 “dark”。
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

useReducer
  • 第一个参数是reducer纯函数
  • 第二个参数是初始的state
  • 第三个参数可以修改初始state,将初始 state 设置为 init(initialArg)

是useState的一种替代场景,如果我们都是使用useState就会散落到程序的各个地方,我们通过代码比较一下二者的差别:

我们使用setState来完成这一段代码

const LoginPage = () => {
    const [name, setName] = useState(''); // 用户名
    const [err, setError] = useState(''); //错误信息
    const [pwd, setPwd] = useState(''); // 密码
    const [isLoading, setIsLoading] = useState(false); // 发送请求之后数据是否已经返回
    const [isLogged, setIsLogged] = useState(false) // 是否已经登陆

    const login = (e) => {
        e.preventDefault();
        setError('');
        setIsLoading(true);
        login({ name, pwd }).then(() => {
            setIsLoggedIn(true); // 标示登陆成功
            setIsLoading(false); // 标示loading完成
        }).catch((error) => {
            setError(error.message);
            setName('');
            setPwd('');
            setIsLoading(false);
        })
    }
    return(<div>jsx的界面</div>)
}

我们发现setState散落到方法的各个地方,导致代码的已读性大大的减弱了,我们再看一下 我们使用useReducer来进行这个操作:

const loginReducer = (state, action) => {
    switch (action.type) {
        case 'login':
            return {
                ...state,
                isLoading: false,
                error: ''
            }
        case 'success':
            return {
                ...state,
                isLoggedIn: true,
                isLoading: false,
            }
        case 'error':
            return {
                ...state,
                error: action.payload.error,
                name: '',
                pwd: '',
                isLoading: false
            }
        default:
            return state;
    }
}

function LoginPage() {
    const [state, dispatch] = useReducer(loginReducer, initState);
    const { name, pwd, isLoading, error, isLoggedIn } = state;
    const login = (event) => {
        event.preventDefault();
        dispatch({ type: 'login' });
        login({ name, pwd })
            .then(() => {
                dispatch({ type: 'success' });
            })
            .catch((error) => {
                dispatch({
                    type: 'error',
                    payload: { error: error.message }
                });
            });
    }
    return (
        <div></div>
        //  返回页面JSX Element
    )
}
  • 我们发现使用reducer写的代码,虽然代码长度变长了,但是代码的可读性大大的增加了,我们可以更加清晰的看到这么写代码的意图
  • LoginPage不需要关心如何处理这几种行为,那是loginReducer需要关心的,表现和业务分离。
  • 所有的state处理都集中到了一起,使得我们对state的变化更有掌控力,同时也更容易复用state逻辑变化代码,比如在其他函数中也需要触发登录error状态,只需要dispatch({ type: 'error' })。

总结一下使用state的场景:

  • 如果你的state是一个数组或者对象
  • 如果你的state变化很复杂,经常一个操作需要修改很多state
  • 如果你希望构建自动化测试用例来保证程序的稳定性
  • 如果你需要在深层子组件里面去修改一些状态(关于这点我们下篇文章会详细介绍)
  • 如果你用应用程序比较大,希望UI和业务能够分开维护
useCallback
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容