React中创建组件的方式

发布于 大漠

学习React也有一段时间了,虽然天天都在围绕着组件打转转,但在React中怎么构建组件呢并没有去深入的了解。事实上呢?在React中的组件还是有些复杂的,从概念上来说就不简单。比如说,类组件函数组件无状态组件高阶组件等。另外创建组件的方式也有所不同,比如最早使用React.createClass来创建组件,有了ES6之后使用extends React.Component(借助ES6的class特性)创建组件,而今天又流行使用函数(Hooks)方式来创建组件。那么他们之间如何创建组件,又有何区别呢?在这篇文章中我们就先来学习和探讨在React中如何创建组件。

先来看React.createClass如何创建组件?

React.createClass

如果你一直以来都在使用React的话,那么对React.createClass这个API并不陌生。在React中,最初就是用这个API来创建React组件。将描述组件的所有信息都将作为对象传递给createClass

createClass方法为开发人员提供了一个工厂方法(Factory Method),可以在不使用JavaScript 类的情况下创建React类组件。这是在ES之前创建React组件方法之一,因为在ES5中没有可用的类语法:

const App = React.createClass({
    getInitialState: function() {
        return {
            value: '大漠'
        }
    }

    onChange: function(e) {
        this.setState({
            value: e.target.value
        })
    }

    render: function() {
        return (
            <div className="card">
                <h1>使用 React.createClass 创建组件</h1>
                <input 
                    value={this.state.value}
                    type="text"
                    onChange={this.onChange}
                />
                <p>Hello, {this.state.value} (^_^)!</p>
            </div>
        )
    }
})

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

上面的Demo在React V15.5版本上运行。

createClass()方法接受一个对象,该对象为React组件定义方法。getInitialState()函数用于为React组件设置初始状态,强制使用render()方法在JSX中用于输出;额外的方法(比如onChange)是通过向对象传递更多的函数而添加的。

React中的生命周期也是可用的。例如,为了每次将值从input中输入存到浏览器的本地存储中(localStorage),我们可以使用componentDidUpdate()生命周期,该方法是将一个函数传递给对象,对象键以React的生命周期方法命名。此外,当组件接收到初始状态时,可以从本地存储中读取该值:

const App = React.createClass({
    getInitialState: function() {
        return {
            value: localStorage.getItem('userName') || '@大漠'
        }
    },
    componentDidUpdate: function(){
        localStorage.setItem('userName', this.state.value)
    },
    onChange: function(e) {
        this.setState({
            value: e.target.value
        })
    },
    render: function() {
        return (
            <div className="card">
                <h1>使用React.createClass创建组件</h1>
                <input 
                    type="text"
                    value={this.state.value}
                    onChange={this.onChange}
                />
                <p>Hello, {this.state.value} (^_^)!</p>
            </div>
        )
    }
})

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

这个示例具有本地存储的功能,每当重新加载或刷新浏览器时,当组件第一次挂载时,应该会显示之前在input中输入的本地存储的初始状态。

注意:React核心包中不再提供React.createClass()方法。如果你想尝试它,必须安装一个额外的包:npm i create-react-class。时至今日,应该尽可能地避免使用它。在这里可以获取到React.createClass()创建组件更多的信息

React Mixins

React中引入了React Mixins,作为React的第一个可重用组件逻辑,这是一种高级模式。使用Mixin,可以将React组件的逻辑提取出来成为一个独立对象。当在组件中使用Mixin时,所有来自Mixin的特性都被引入组件:

var localStorageMixin = {
    getInitialState: function(){
        return {
            value: localStorage.getItem('userName') || '@大漠'
        }
    },
    setLocalStorage: function(val) {
        localStorage.setItem('userName', val)
    }
}

var App = React.createClass({
    mixins: [localStorageMixin],
    componentDidUpdate: function(){
        this.setLocalStorage(this.state.value)
    },
    onChange: function(e) {
        this.setState({
            value: e.target.value
        })
    },
    render() {
        return (
            <div className="card">
                <h1>使用React Mixin和createClass创建组件</h1>
                <input 
                    type="text"
                    value={this.state.value}
                    onChange={this.onChange}
                />
                <p>Hello, {this.state.value} (^_^)!!!</p>
            </div>
        )
    } 
})

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

本例中,Mixin提供从本地存储中读取组件的初始状态,并使用setLocalStorage()方法扩展组件,该方法稍后将在实际组件中使用。为了使用Mixin更加灵活,我们可以使用一个函数来返回一个对象:

function getLocalStorageMixin(localStorageKey) {
    return {
        getInitialState: function(){
            return {
                value: localStoragee.getItem(localStorageKey) || ''
            }
        },
        setLocalStorage: function(value) {
            localStorage.setItem(localStorageKey, value)
        }
    }
}

var App = React.createClass({
    mixins: [getLocalStorageMixin('userName')],
    // ...
})

注意:现在在React中不再使用Mixins了,因为它们有几个缺点。有关于React Mixins更多的信息,可以点击这里进行了解

createClass()是创建React组件的一种简单而有效的方法。React最初使用createClass API的原因是,当时JavaScript没有内置的class。当然,这种情况最终改变了。ES6开始引入了class这个关键字,也可以使用类来创建组件。这让React进入了一个两难的境地,要么继续使用createClass,要么跟进ES6,使用class来创建组件。事实证明,React选择了后者。

React.Component

React v3.13.0版本引入了React.Component API,允许你使用JavaScript的类(class)来创建React组件。在React中使用class创建的组件常常被称为React 类组件

我们可以使用React.Component来重构上面使用React.createClass()方法创建的组件。

class App extends React.Component {
    constructor(props) {
        super(props)
        this.state = {
            value: localStorage.getItem('userName') || '@大漠'
        }
        this.onChange = this.onChange.bind(this)
    }

    componentDidUpdate() {
        localStorage.setItem('userName', this.state.value)
    }

    onChange(e) {
        this.setState({
            value: e.target.value
        })
    }

    render() {
        return (
            <div className="card">
                <h1>使用ES6 Class创建组件(React.Component)</h1>
                <input 
                    type="text"
                    value={this.state.value}
                    onChange={this.onChange}
                />
                <p>Hello, {this.state.value} (^_^)!!!</p>
            </div>
        )
    }
}

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

使用JavaScript类编写React组件带有类构造函数constructor()(主要用于React中设置初始状态或绑定方法)和render()方法。React组件内部所有逻辑都来自于React.Component。通过类组件中使用面向对象继承的组件。但是,不建议在更多的地方使用继承这个概念。相反,建议使用组合而不是继承

在React中使用React.Component创建组件,有几个重要的概念需要掌握。

构造函数 constructor()

使用类组件,可以在构造函数constructor()内部将组件的状态初始化为实例(this)上的状态属性。但是,根据ECMAScript规范,如果要扩展子类(即React.Component),必须要先调用super(),然后才能使用this。具体来说,在使用React时,还必须记住将props传递给super()

class App extends React.Component {
    constructor(props) {
        super(props)
        // ...
    }
    // ...
}

自动绑定

当使用React.createClass创建组件时,React会自动将所有方法绑定到组件的实例(this。而React.Component并非如此,很多开发人员都意识到他们不知道this关键字是如何工作的。因为必须记住类构造函数中的.bind()方法(即.bind(this)。如果不这样做的话,浏览器会报“无法读取未定义的setState属性”错误。

class App extends React.Component {
    constructor(props) {
        super(props)
        //...
        this.onChange = this.onChange.bind(this)
    }
    // ...
}

调用super(props)并要记住.bind(this)方法是比较烦人,但这里并没有什么根本的错误。但当你一天要像这样处理很多次的时候,也会令人感到烦感。庆幸的是,在从createClass切换到React.Component之后不久,TC39就提出Class Fields相关的建议

类字段(Class Fields)

类字段允许我们直接将实例属性作为属性添加到类上,而无需使用构造函数。这样一来,我们就不再需要使用构造函数来设置组件的初始状态,也不再需要在构造函数中使用.bind(this),因为我们可以使用箭头函数。

class App extends React.Component {
    state = {
        value: localStorage.getItem('userName') || '@w3cplus'
    }

    componentDidUpdate() {
        localStorage.setItem('userName', this.state.value)
    }

    onChange = (e) => {
        this.setState({
            value: e.target.value
        })
    }

    render () {
        const {value} = this.state

        return (
            <div className="card">
                <h1>使用React.Component创建组件(Class Fields)</h1>
                <input 
                    type="text"
                    value={value}
                    onChange={this.onChange}
                />
                <p>Hello, {value} (^_^)!!!</p>
            </div>
        )
    }
}

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

React.createClass VS. React.Component

当新开发人员在创建React组件时遇到两种不同方式时,常常会感到困惑。不知道怎么来选择更为合适。前面我们也花了一些时间简单的了解了React.createClassReact.Component各自怎么创建组件。虽然最终结果是一样的,但他们之间还是有所差异的。简单地归纳起来有以下几点:

  • 语法区别
  • propTypegetDefaultProps
  • 状态的区别
  • 绑定this的方式区别
  • Mixins

语法区别

前面也提到了,React.createClass是ES6之前创建组件的方式(历史原因),通过React.createClass(从字面上来理解:创建一个class)方法创建了一个组件,并赋值给一个常量。在该方法中强制使用render()函数来完成最基本的组件定义(组件所需要的逻辑和JSX模板都在这里):

const MyComponent = React.createClass({
    render() {
        return <h1>使用React.createClass()创建的组件</h1>
    }
})

接下来我们可以使用React.Component来替换上面使用React.createClass构建的组件,将会采用ES6:

class MyComponent extends React.Component {
    constructor(props) {
        super(props)
    }

    render() {
        return <h1>使用React.Component创建组件</h1>
    }
}

现在我们从JavaScript的角度来使用 ES6 类,一般情况下,这通常会用像Babel的编译器在其他浏览器器上将ES6向ES5编译转换。在这个变化过程中,我们引入了constructor(), 调用super() 进而传递这个属性给React.Component

对于React的变化,我们现在创建一个名为MyComponentclass,并从React.Componentextends,而非用较少的React模板和更多JavaScript直接访问React.createClass。这是语法互换带来的一个重要的改变。

propTypegetDefaultProps

React有一个很大的特性就是可以根据不同的状态来渲染不同的UI效果。因此在设计组件的时候会有一些默认属性的声明,同时会有类型和初始状态的设置。

React.createClass中使用propTypes属性是可以为每个props声明类型的对象,getDefaultProps属性则能返回一个对象来创建初始属性的函数。

const ToggleCheckbox = React.createClass({
    propTypes: {
        // 为每个prop声明类型的对象
    },

    getDefaultProps() {
        return {
            // 能返回一个对象来创建初始属性的函数
        }
    },

    render() {
        return <h1></h1>
    }
})

简单地说,React.createClass创建组件时是通过proTypes对象和getDefaultProps()方法来设置和获取props

React.Componet构建组件时也会用到propTypes来声明类型,但和React.createClass有所不同。在React.Component中的propTypes作为MyComponent类(由extends React.Component创建)的一种属性;而React.createClass中的propTyppes仅是createClass定义对象部分的属性。这种创建类属性的方法相比于自己定义的对象,React接口更让人清楚明白。

React.createClass中的getDefaultProps()方法现在也变成了defaultProps对象,不再是一个get函数:

class MyComponent extends React.Component {
    constructor(props) {
        super(props)
    }

    render() {
        return <h1></h1>
    }
}

MyComponent.propTypes = {

}

MyComponent.defaultProps = {

}

接下来,我们分别来看看在React.createClassReact.Component中使用propTypesgetDefaultProps以及defaultProps实现Bootstrap中button组件。

先来看React.createClass中的使用:

const Button = React.createClass({
    propTypes: {
        theme: React.PropTypes.string,
        text: React.PropTypes.string
    },

    getDefaultProps() {
        return {
            theme: 'primary',
            text: 'Primary'
        }
    },

    render() {
        const {theme, text, ...props} = this.props
        return <button className={`btn btn-${theme}` } {...props} type="button"> {text}</button>
    }
})

const App = React.createClass({
    render() {
        return (
            <div className="card">
                <h1>Create Bootstrap's Button with React.createClass</h1>
                <p>
                    <Button theme="primary" text="Primary" />
                    <Button theme="secondary" text="Secondary" />
                    <Button theme="success" text="Success" />
                    <Button theme="danger" text="Danger" />
                    <Button theme="warning" text="Warning" />
                    <Button theme="info" text="Info" />
                    <Button theme="light" text="Light" />
                    <Button theme="dark" text="Dark" />
                    <Button theme="link" text="Link" />
                </p>
            </div>
        )
    }
})

const rootElement = document.getElementById('app');
ReactDOM.render(<App />, rootElement);

接下来,再看React.Component创建的BootStrap的button

class Button extends React.Component {
    constructor(props) {
        super(props)
    }

    render() {
        const {theme, text, ...props} = this.props
        return <button className={`btn btn-${theme}` } {...props} type="button"> {text}</button>
    }
}

Button.defaultProps = {
    theme: 'default',
    text: 'BootStrap Button'
}

class App extends React.Component {
    render() {
        return (
            <div className="card">
                <h1>Create Bootstrap's Button with React.Component</h1>
                <p>
                    <Button theme="primary" text="Primary" />
                    <Button theme="secondary" text="Secondary" />
                    <Button theme="success" text="Success" />
                    <Button theme="danger" text="Danger" />
                    <Button theme="warning" text="Warning" />
                    <Button theme="info" text="Info" />
                    <Button theme="light" text="Light" />
                    <Button theme="dark" text="Dark" />
                    <Button theme="link" text="Link" />
                    <Button />
                </p>
            </div>
        )
    }
}

const rootElement = document.getElementById('app');
ReactDOM.render(<App />, rootElement);

状态差异

React.createClass中使用一个特殊的API getInitialState()方法声明状态,而在React.Component中使用构造函数constructor()来声明状态。在构造函数的顶部调用super(),这将调用React.Component定义的构造函数constructor(),它为组件执行一些必要的设置代码。

const ToggleCheckbox = React.createClass({
    getInitialState() {
        return {
            checked: false
        }
    }

    render() {
        retun (
            <div className="card">
                <input
                    type="checkbox"
                    name="pubilc"
                    id="public"
                />
                <label htmlFor="public">Subscribe to weekly newsletter</label>
            </div>
        )
    }
})

我们再来看看React.Component

class ToggleCheckbox extends React.Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            checkbox: false
        }
    }
    render() {
        retun (
            <div className="card">
                <input
                    type="checkbox"
                    name="pubilc"
                    id="public"
                />
                <label htmlFor="public">Subscribe to weekly newsletter</label>
            </div>
        )
    }
}

绑定this的差异

在JavaScript中,this关键词总是让人琢磨不透,例如,在render()中,我们说this是绑定到组件的,换句话说,this是引用组件。对于初级的React开发人员来说,更难理解这之间的差异,但一开始不理解所有的细节没什么问题,随着后续的学习,会更理解的。

回到上面ToggleChecked()这个组件。在不同的方式之下,this的绑定是不同的。

使用React.createClass创建的组件会天然的绑定this,React会将每个方法绑定到组件。而使用React.Componet创建的组件,需要手动将其绑定到组件。

接着上面的示例,使用两种不同的方式来完成ToggleChecked()组件。

const ToggleChecked = React.createClass({
    propTypes: {
        checked: React.PropTypes.boolean,
        subscribe: React.PropTypes.string,
    },

    getInitialState() {
        return {
            checked: false
        }
    },

    getDefaultProps() {
        return {
            subscribe: '欢迎订阅每周期刊!'
        }
    },

    toggleChecked() {
        this.setState((prevState) => (
            { checked: !prevState.checked}
        ))
    },

    render() {
        const {subscribe} = this.props
        const className = this.state.checked ? `toggle checkbox checked` : `toggle checkbox`
        const subscribeText = !this.state.checked ? `${subscribe}` : `恭喜你,订阅成功!`
        
        return (
            <div className="card">
                <h1>使用React.createClass创建ToggleChecked组件</h1>
                <div className={className}>
                    <input
                        type="checkbox"
                        name="public"
                        id="bubble"
                        onClick={this.toggleChecked}
                    />
                    <label htmlFor="bubble" className="bubble">{subscribeText}</label>
                </div>
            </div>
        )
    }
})

const rootElement = document.getElementById('app');
ReactDOM.render(<ToggleChecked />, rootElement);

接着使用React.Component来重构上面的这个示例:

class ToggleChecked extends React.Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            checked: false
        }
        this.toggleChecked = this.toggleChecked.bind(this)
    }

    static defaultProps = {
        subscribe: '欢迎订阅每周期刊!'
    }

    toggleChecked() {
        this.setState(prevState => ({checked: !prevState.checked}))
    }

    render() {
        const {subscribe} = this.props
        const className = this.state.checked ? `toggle checkbox checked` : `toggle checkbox`
        const subscribeText = !this.state.checked ? `${subscribe}` : `恭喜你,订阅成功!`
            
        return (
            <div className="card">
                <h1>使用React.Component创建ToggleChecked组件</h1>
                <div className={className}>
                    <input
                        type="checkbox"
                        name="public"
                        id="bubble"
                        onClick={this.toggleChecked}
                    />
                    <label htmlFor="bubble" className="bubble">{subscribeText}</label>
                </div>
            </div>
        )
    }
}

// ToggleChecked.defaultProps = {
//     subscribe: '欢迎订阅每周期刊!'
// }

const rootElement = document.getElementById('app');
ReactDOM.render(<ToggleChecked />, rootElement);

Mixins

使用React.createClass创建组件时可以使用mixins属性向组件中添加mixins,它可组成mixins数组。然后将其扩展为组件类。

var SomeMixin = {
    doSomething() {
        // ...
    }
};

const Contacts = React.createClass({
    mixins: [SomeMixin],
    handleClick() {
        this.doSomething(); // use mixin
    },
    render() {
        return (
        <div onClick={this.handleClick}></div>
        );
    }
});

React.Component不再支持mixins

高阶组件(HOCs)

高阶组件简称HOCs,即Higher-Order Components的简写。高阶组件是React中创建组件的高级用法,在这里暂不会深入的探讨这方面的知识,只是让大家简单的了解,在React还有这种模式来创建组件。

通过前面的学习我们了解到在React.Component中删除了React.createClass中的mixins,但在创建组件的时候有些逻辑是可重用的,因此我们就可以采用高阶组件的方式。那么什么是高阶组件呢?

简单地说,高阶组件它也是一个组件,它可以接受组件作为输入,也可以把返回的组件作为输出,但具有扩展功能。我们来看一个简单的示例:

const withLocalStorage = localStorageKey => Component => 
    class WithLocalStorage extends React.Component {
        constructor(props) {
            super(props)
            this.state = {
                [localStorageKey]: localStorage.getItem(localStorageKey)
            }
        }
        setLocalStorage = value => {
            localStorage.setItem(localStorageKey, value)
        }

        render() {
            return (
                <Component
                    {...this.state}
                    {...this.props}
                    setLocalStorage={this.setLocalStorage}
                />
            )
        }
    }

class App extends React.Component {
    constructor(props){
        super(props)
        this.state = {
            value: this.props['userName'] || '@大漠'
        }
    }

    componentDidUpdate() {
        this.props.setLocalStorage(this.state.value)
    }

    onChange = e => {
        this.setState({
            value: e.target.value
        })
    }

    render() {
        return (
            <div className="card">
                <h1>使用高阶组件创建组件</h1>
                <input 
                    type="text"
                    value={this.state.value}
                    onChange={this.onChange}
                />
                <p>Hellow, {this.state.value} (^_^)!!!</p>
            </div>
        )
    }
}

const AppWithLocalStorage = withLocalStorage('userName')(App)

const rootElement = document.getElementById('app');
ReactDOM.render(<AppWithLocalStorage />, rootElement);

如果你想深入了解React中的高阶组件相关的知识点的话,可以阅读下面相关的教程:

函数组件

React函数组件相当于React类组件,但表示为函数而不是类。在过去,不可能在函数组件中使用状态(state)或副作用(Side-effects),正因此,它们也被称为无状态组件。自React v16.8版本开始,React的Hooks出现之后,函数组件功能也得到了更大的提高和完善,也成为当下构建组件的主要方式之一。

React Hook为函数组件提供了状态和副作用,并且提供了各种内置的Hooks以及可以自己定义需要的Hook。

const App = () => {
    const [value, setValue] = React.useState('@w3cplus')
    const onChange = e => setValue(e.target.value)

    return (
        <div className="card">
            <h1>使用React Hooks创建组件</h1>
            <input 
                type="text"
                value={value}
                onChange={onChange}
            />
            <p>Hello, {value} (^_^)!</p>
        </div>
    )
}

const rootElement = document.getElementById('app');
ReactDOM.render(<App />, rootElement);

这个示例向大家演示了一个最基础的函数组件。由于需要捕获input输入的值的状态,因此在组件中使用了内置的React Hook,即useState

引入React Hook其实也会给组件带来副作用。一般来说,每当组件的propsstate发生变化时,都会使用内置的useEffect Hook来执行一个函数。在上例的基础上,给组件添加浏览器本地存储的功能,那么组件可以像下面这样来改:

const App = () => {
    const [value, setValue] = React.useState(
        localStorage.getItem('userName') || '@w3cplus'
    )

    React.useEffect(() => {
        localStorage.setItem('userName', value)
    }, [value])

    const onChange = e => setValue(e.target.value)

    return (
        <div className="card">
            <h1>使用React Hooks创建组件</h1>
            <input 
                type="text"
                value={value}
                onChange={onChange}
            />
            <p>Hello, {value} (^_^)!</p>
        </div>
    )
}

const rootElement = document.getElementById('app');
ReactDOM.render(<App />, rootElement);

该示例使用了React的useEffect 钩子,每当组件中的input的值发生变化时,都会执行这个钩子。useEffect在函数中执行时,它将使用来自状态的最近值更新本地存储的值。此外,useState钩子的初始状态是从本地存储中读取的。

上面仅演示了最简单的函数组件的构建。

在前面我们学习了如何使用React.createClassReact.Component构建Bootstrap的button组件。事实上,在React中,使用函数组件更适用于只渲染元素的视觉效果而不跟踪其状态或生命周期的组件(这些组件通常称为函数组件无状态组件)。我们来看看如何使用函数组件来重新写Bootstrap按钮组件:

const Button = (props) => {
    const {theme, text, ...rest} = props

    return <button type="button" className={`btn btn-${theme}`} {...rest}>{text}</button>
}

Button.defaultProps = {
    theme: 'default',
    text: 'Bootstrap Button'
}

const App = (props) =>{
    return (
        <div className="card">
            <h1>Create Bootstrap's Button with React Hooks</h1>
            <p>
                <Button theme="primary" text="Primary" />
                <Button theme="secondary" text="Secondary" />
                <Button theme="success" text="Success" />
                <Button theme="danger" text="Danger" />
                <Button theme="warning" text="Warning" />
                <Button theme="info" text="Info" />
                <Button theme="light" text="Light" />
                <Button theme="dark" text="Dark" />
                <Button theme="link" text="Link" />
                <Button />
            </p>
        </div>
    )
}

const rootElement = document.getElementById('app');
ReactDOM.render(<App />, rootElement);

另外,在构建Button组件时,还可以借助ES6的解构特性,可以不显式的使用defaultProps来设置默认值,而改用下面这样的方式:

const Button = (props) => {
    const {
        theme = 'default',
        text = 'Bootstrap Button',
        ...rest
    } = props
    return <button type="button" className={`btn btn-${theme}`} {...rest}>{text}</button>
}

// 或者
const Button = ({theme='default', text='Bootstrap Button', ...rest}) {
    return <button type="button" className={`btn btn-${theme}`} {...rest}>{text}</button>
}

类组件如何转换成函数组件

使用React Hooks创建组件还是较新的一种方式,但这种方式很有可能会成为一种主流方式。那么常常使用class来构建组件的同学,又应该如何快速过渡到使用Hooks构建组件呢?接下来我们来通过一些示例来演示如何使用Hook来重构class声明的组件,以及要注意的一些点。

不带状态和生命周期的组件

先从最简单的示例着手吧。比如下面这个使用class创建的组件,该组件是一个没有状态组件,也没有生命周期的方法。当用户点击一个按钮,浏览器会弹出一个警告框:

class Alert extends React.Component {
    handleClick = () => {
        alert('Hello, React!')
    }

    render() {
        return (
            <div className="card">
                <h1>这是一个类组件</h1>
                <p>
                    <button type="button" className="btn btn-primary" onClick={this.handleClick}>点击我,弹出一个警告框</button>
                </p>
            </div>
        )
    }
}

const rootElement = document.getElementById('app');
ReactDOM.render(<Alert />, rootElement);

这就是一个最普通的React类组件。如果我们用React Hooks来重构的话,大致像下面这样:

const Alert = () => {
    const handleClick = () => {
        alert('Hello, React!')
    }

    return (
        <div className="card">
            <h1>这是一个功能组件(Functional Component)</h1>
            <p>
                <button type="button" className="btn btn-primary" onClick={handleClick}>点击我,弹出一个警告框</button>
            </p>
        </div>
    )
}

const rootElement = document.getElementById('app');
ReactDOM.render(<Alert />, rootElement);
v

和前面的类组件一样,在这个函数组件中也没有使用任何钩子函数或任何新东西。

接下来,我们让示例稍微变得复杂一点点,在组件中加入状态

带状态的组件

我们来创建一个changeName组件,功能很简单,当用户在input框输入内容时会对应更改用户名称。在这个示例中,需要一个全局变量name。在React中class创建的组件时通过在state对象中定义name这个变量,并在input有新的输入值时通过setState()更新name变量:

class ChangeName extends React.Component {
    state = {
        name: '@w3cplus'
    }

    handleChange = e => {
        this.setState({name: e.target.value})
    }

    render() {
        return (
            <div className="card">
                <h1>这是一个类组件(Class Component)</h1>
                <input 
                    type="text"
                    value={this.state.name}
                    onChange={this.handleChange}
                />
                <p>Hello, {this.state.name} (^_^)!</p>
            </div>
        )
    }
}

const rootElement = document.getElementById('app');
ReactDOM.render(<ChangeName />, rootElement);

我们可以使用Hooks将整个组件转换成一个功能性组件:

const ChangeName = () => {
    const [name, setName] = React.useState('@w3cplus')

    const handleChange = e => {
        setName(e.target.value)
    }

    return (
        <div class="card">
            <h1>这是一个功能组件(Functional Component)</h1>
            <input 
                type="text"
                value={name}
                onChange={handleChange}
            />
            <p>Hello, {name} (^_^)!</p>
        </div>
    )
}

const rootElement = document.getElementById('app');
ReactDOM.render(<ChangeName />, rootElement);

这里我们用到了useState钩子,使用useState()钩子,可以在这个函数组件中使用state。它使用了ES6的解构语法来赋值。

const [name, setName] = React.useState('@w3cplus')

在函数组件中的这个name和类组件的this.state.name是等价的,setName等价于this.setState。在使用useState()钩子函数时,它接受一个参数作为状态的初始值。简单地说,useState()参数是状态的初始值。在我们的示例中,将它设置为@w3cplus,这样状态中的name的初始状态就是@w3cplus

带有多个状态的组件

使用useState转换一个状态属性是一回事,但是当你要同时处理多个状态属性时,同样的方法并不十分适用。比如在上面的示例中,我们再增加两个input输入框,用来输入firstNamelastName,这个时候就有三个状态属性,组件可能变成这样:

class LoginForm extends React.Component {
    state = {
        firstName: '大漠',
        lastName: '@w3cplus',
        professional: '码农'
    }

    handleClick = () => {
        document.querySelector('p').textContent =  `Hello, ${this.state.firstName}${this.state.lastName},他是位${this.state.professional}`
    }

    handleChangeFirstName = e => {
        this.setState({firstName: e.target.value})
    }

    handleChangeLastName = e => {
        this.setState({lastName: e.target.value})
    }

    handleChangeProfessional = e => {
        this.setState({professional: e.target.value})
    }

    render() {
        return (
            <div className="card">
                <h1>这是一个类组件(Class Component)</h1>
                <div className="control">
                    <label htmlFor="firstname">First Name:</label>
                    <input 
                        id="firstname"
                        type="text"
                        value={this.state.firstName}
                        onChange={this.handleChangeFirstName}
                    />
                </div>
                <div className="control">
                    <label htmlFor="lastname">Last Name:</label>
                    <input 
                        id="lastname"
                        type="text"
                        value={this.state.lastName}
                        onChange={this.handleChangeLastName}
                    />
                </div>
                <div className="control">
                    <label htmlFor="professional">Professional:</label>
                    <input 
                        id="professional"
                        type="text"
                        value={this.state.professional}
                        onChange={this.handleChangeProfessional}
                    />
                </div>
                <div className="control">
                    <button type="button" className="btn btn-primary" onClick={this.handleClick}>Enter</button>
                </div>
                <p></p>
            </div>
        )
    }
}

const rootElement = document.getElementById('app');
ReactDOM.render(<LoginForm />, rootElement);

同样的,使用useState()钩子函数,上面示例可以改成像下面这样:

const LoginForm = () => {
    const [firstName, setFirstName] = React.useState('大漠')
    const [lastName, setLastName] = React.useState('@w3cplus')
    const [professional, setProfessional] = React.useState('码农')

    const handleClick = () => {
        document.querySelector('p').textContent =  `Hello, ${firstName}${lastName},他是位${professional}`
    }

    const handleChangeFirstName = (e) => {
        setFirstName(e.target.value)
    }

    const handleChangeLastName = (e) => {
        setLastName(e.target.value)
    }

    const handleChangeProfessional = (e) => {
        setProfessional(e.target.value)
    }

    return (
        <div className="card">
            <h1>这是一个功能组件(Functional Component)</h1>
            <div className="control">
                <label htmlFor="firstname">First Name:</label>
                <input 
                    id="firstname"
                    type="text"
                    value={firstName}
                    onChange={handleChangeFirstName}
                        />
            </div>
            <div className="control">
                <label htmlFor="lastname">Last Name:</label>
                <input 
                    id="lastname"
                    type="text"
                    value={lastName}
                    onChange={handleChangeLastName}
                />
            </div>
            <div className="control">
                <label htmlFor="professional">Professional:</label>
                <input 
                    id="professional"
                    type="text"
                    value={professional}
                    onChange={handleChangeProfessional}
                />
            </div>
            <div className="control">
                <button type="button" className="btn btn-primary" onClick={handleClick}>Enter</button>
            </div>
            <p></p>
        </div>
    )
}

const rootElement = document.getElementById('app');
ReactDOM.render(<LoginForm />, rootElement);

带有状态和componentDidMount生命周期的组件

接着我们来看同时带有一个状态和componentDidMount生命周期的组件。构建一个这样的场景,给name设置一个初始状态,并在3s后将它更新为不同的值。为此,我们必须为input声明初始状态的值,并调用componentDidMount()生命周期方法,该方法将在初始渲染之后运行,以更新状态值。

class DynamicChangeName extends React.Component {
    state = {
        name: '大漠'
    }

    componentDidMount() {
        setInterval(()=>{
            this.setState({
                name: '@w3cplus'
            })
        }, 3000)
    }

    handleChange = e => {
        this.setState({name: e.target.value})
    }

    render() {
        return (
            <div className="card">
                <h1>这是一个类组件(Class Component)</h1>
                <input 
                    type="text"
                    value={this.state.name}
                    onChange={this.handleChange}
                />
                <p>Hello, {this.state.name} (3秒后动态更新)</p>
            </div>
        )
    }
}
const rootElement = document.getElementById('app');
ReactDOM.render(<DynamicChangeName />, rootElement);

当程序运行时,输入框的值是在state对象中定义的初始值。不过该值在3s后会更新到componentDidMount()方法中定义的值。接下来我们使用Hooks中的useStateuseEffect钩子函数将类组件转换为函数组件。

const DynamicChangeName = () => {
    const [name, setName] = React.useState('大漠')

    React.useEffect(() => {
        setInterval(() => {
            setName('@w3cplus')
        },3000)
    })

    const handleChange = e => {
        setName(e.target.value)
    }

    return (
        <div className="card">
            <h1>这是一个函数组件(Functional Component)</h1>
            <input 
                type="text"
                value={name}
                onChange={handleChange}
            />
            <p>Hello, {name} (3秒后动态更新)</p>
        </div>
    )
}
const rootElement = document.getElementById('app');
ReactDOM.render(<DynamicChangeName />, rootElement);

效果和类组件一样。唯一的区别是,这里使用了useStateuseEffect钩子,而不像在类组件中那样使用传统的状态对象和componentDidMount()生命周期。

使用状态、componentDidMountcomponetDidUpdate生命周期的组件

到目前为止,你也可能发现了,我们在示例中使用到最多的就是useState这个钩子。在接下来的这个示例中,会有一个状态和两个生命周期,而且会更多用到useEffect钩子。

先来看类组件:

class DynamicChangeName extends React.Component {
    state = {
        name: '大漠'
    }

    componentDidMount() {
        const paragraph = document.querySelector('p')
        setTimeout(() => {
            paragraph.textContent =`Hello, ${this.state.name} (_^_)!`
        }, 3000)
    }

    componentDidUpdate() {
        const paragraph = document.querySelector('p')
        paragraph.textContent = `Hello, ${this.state.name} (_^_)!`
    }

    handleChange =  e => {
        this.setState({name: e.target.value})
    }

    render() {
        return (
            <div className="card">
                <h1>这是一个类组件(Class Component)</h1>
                <input 
                    type="text"
                    value={this.state.name}
                    onChange={this.handleChange}
                />
                <p>等我3秒,我就回来....</p>
            </div>
        )
    }
}
const rootElement = document.getElementById('app');
ReactDOM.render(<DynamicChangeName />, rootElement);

当运行上面的示例后,你会发现在componentDidMount()中定义的那样,段落<p>默认内容在3s之后会自动更新为name的初始状态的初始值。当你在input中键入新的内容之后,<p>将会使用componentDidUpdate()input输入的文本进行更新。

现在我们使用useEffect钩子函数来重构这个组件。

const DynamicChangeName = () => {
    const [name, setName] = React.useState('大漠')

    React.useEffect(()=>{
        const paragraph = document.querySelector('p')
        
        setTimeout(() => {
            paragraph.textContent =`Hello, ${name} (_^_)!`
        }, 3000)
    
    })

    const handleChange = e => {
        setName(e.target.value)
    }

    return (
        <div className="card">
            <h1>这是一个功能组件(Functional Component)</h1>
            <input 
                type="text"
                value={name}
                onChange={handleChange}
            />
            <p>等3秒我就回来...</p>
        </div>
    )
}

const rootElement = document.getElementById('app');
ReactDOM.render(<DynamicChangeName />, rootElement);

有关于React中生命周期以及如何使用Hooks来替代生命周期更多的介绍可以阅读下面这些文章:

无状态组件 vs. 有状态组件

前面我们学习了如何在React中创建组件的方式,而React组件根据不同的纬度划分也不太一样。比如前面按创建方式我们可以称为类组件,函数组件之类的。事实上,任何组件都有状态(Stateful)和无状态(Stateless)之分。说实话,一句两句话我们无法讲清楚两者之间的区别,特别是对于初学者来说更是如此。在这里不会深入阐述他们之间的差异,如果你感兴趣的话,请持续关注后续的更新,将和大家一起单独来探讨这方面的知识。这里只用两个简单的示例向大家展示一下两者。

无状态组件(Stateless Components)

一个只依赖props的无状态按钮组件:

const Hello = ({ name }) => (<div>Hello, {name}!</div>);

有状态组件(Stateful Components)

有状态组件总是类组件。比如下面这个示例:

class ButtonCounter extends React.Component {
    constructor() {
        super()
        this.state = { clicks: 0 }
        this.handleClick = this.handleClick.bind(this)
    }

    handleClick() {
        this.setState({ clicks: this.state.clicks + 1 })
    }

    render() {
        return (
            <Button
                onClick={this.handleClick}
                text={`You've clicked me ${this.state.clicks} times!`}
            />
        )
    }
}

小结

这篇文章主要介绍了如何使用React.createClassReact.Componet以及React Hooks来创建React组件,以及介绍了三者之间的差异性。希望该文对于初学React的同学有所帮助。