愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

React学习笔记(三):进阶知识

一、条件渲染

在React里,我们可以创建不同的组件来封装我们需要的功能。我们也可以根据组件的状态,只渲染组件中的一部分内容,而条件渲染就是为此而准备的。在React中,我们可以像在JavaScript中写条件语句一样地写条件渲染语句,如:

function Greet(props) {
    const isLogined = props.isLogined;
    if (isLogined) {
        return <div>Hello !</div>;
    }
    return <div>Please sign in</div>;
}

ReactDOM.render(
    <Greet isLogined={true} />,
    document.getElementById('root')
);

这将渲染出:

<div>Hello !</div>

1、使用变量来存储元素

我们也可以使用变量来存储元素,如:

function LogBtn(props) {
    var button;
    const isLogined = props.isLogined;
    if (isLogined) {
        button = <button>退出</button>
    } else {
        button = <button>登陆</button>
    }
    return <div>You can {button}</div>;
}

ReactDOM.render(
    <LogBtn isLogined={false} />,
    document.getElementById('root')
);

2、使用&&运算符进行渲染

由于JavaScript语法对待&&运算符的性质,我们也可以使用&&运算符来完成条件渲染,如:

function LogBtn(props) {
    var button;
    const isLogined = props.isLogined;
    return (
        <div>Hello
        {!isLogined && (
            <button>请登陆</button>
        )}
        </div>
    )
}

props.isLogined为false的时候,就会渲染出:

<div>Hello <button>请登录</button></div>

3、使用三目运算符进行渲染

我们可能已经发现了,其实JSX可以像一个表达式那样子灵活使用,所以,我们自然也可以使用三目运算符进行渲染,如:

function LogBtn (props) {
    const isLogined = props.isLogined;
    return (
        <div>You can 
            <button>{isLogined ? '退出' : '登陆'}</button>
        </div>
    )
}

4、阻止整个组件的渲染

有时候,我们希望是整个组件都不渲染,而不仅仅是局部不渲染,那么这种情况下,我们就可以在render()函数里返回一个null,来实现我们想要的效果,如:

function LogBtn (props) {
    const isLogined = props.isLogined;
    const isShow = props.isShow;
    if (isShow) {
        return (
            <div>You can 
                <button>{isLogined ? '退出' : '登陆'}</button>
            </div>
        )
    }
    return null;
}

注意: 组件里返回null不会影响组件生命周期的触发,如componentWillUpdatecomponentDidUpdate仍然会被调用


二、列表渲染与keys

在JavaScript中,我们可以使用map()函数来对一个数组列表进行操作,如:

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(number => number*2);
console.log(doubled); // 得到[2, 4, 6, 8, 10]

同样的,在React里,我们也可以使用map()来进行列表渲染,如:

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map(number => {
    return (
        <li>{number}</li>
    )
});

ReactDOM.render(
    <ul>{listItems}</ul>,
    document.getElementById('root')
)

这将得到:

<ul><li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>

当然,我们还可以进行更好的封装,如:

function NumberList (props) {
    const numbers = props.numbers;
    const listItems = numbers.map(number => {
        return (
            <li>{number}</li>
        )
    });

    return <ul>{listItems}</ul>
}

当我们运行以上的代码的时候,会发现控制台提示:Each child in an array or iterator should have a unique "key" prop,因此,我们需要为列表项的每一个项分配一个key,来解决这个问题,通常而言,我们可以使用以下几种方式来提供key

  • 使用数据项自身的ID,如<li key={item.itemId}>
  • 使用索引下标(index),如:
const listItems = numbers.map((number, index) => {
    <li key={index}>{number}</li>
});

但是React不推荐在需要重新排序的列表里使用索引下标,因为会导致变得很慢。

注意: 只有在一个项的同胞里区分彼此的时候,才需要使用到key,key不需要全局唯一,只需要在一个数组内部区分彼此时唯一便可。key的作用是给React一个提示,而不会传递给组件。如果我们在组件内需要同样的一个值,可以换个名字传递,如:

const content = posts.map(post => (
    <Post key={post.id} id={post.id} title={post.title} />
));


三、表单

表单和其他的React中的DOM元素有所不同,因为表单元素生来就是为了保存一些内部状态。在React中,表单和HTML中的表单略有不同

1、受控组件

HTML中,<input><textarea><select>这类表单元素会维持自身状态,并根据用户输入进行更新。不过React中,可变的状态通常保存在组件的this.state中,且只能用setState()方法进行更新,如:

class NameForm extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            value: ''
        }
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }
    handleChange (event) {
        this.setState({
            value: event.target.value
        });
    }
    handleSubmit (event) {
        alert('Your name is '+this.state.value);
        event.preventDefault();
    } 
    render () {
        return (
            <form onSubmit={this.handleSubmit}>
            Name: <input type="text" value={this.state.value} onChange={this.handleChange} />
            <input type="submit" value="Submit" />
            </form>
        )
    }
}

和HTML中不同的是,React中的textarea并不需要写成<textarea></textarea>的形式,而是写成<textarea value="" ... />的形式便可。而对于HTML中的select标签,通常做法是:

<select>
    <option value="A">A</option>
    <option value="B" selected>B</option>
    <option value="C">C</option>
</select>

但是React中,不需要在需要选中的option处加入selected,而只需要传入一个value,就会自动根据value来选中相应的选项,如:

<select value="C">
    <option value="A">A</option>
    <option value="B">B</option>
    <option value="C">C</option>
</select>

那么如上述例子,C所在的这个option就会被选中

2、多个输入的解决办法

通常一个表单都有多个输入,如果我们为每一个输入添加处理事件,那么将会非常繁琐。好的一个解决办法是,使用name,然后根据event.target.name来选择做什么。如:

class Form extends React.Component {
    constructor (props) {
        super(props);
        this.state = {
            name: '',
            gender: '男',
            attend: false,
            profile: ''
        };
        this.handleInputChange = this.handleInputChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }
    handleInputChange (event) {
        const target = event.target;
        const value = target.type==='checkbox' ? target.checked : target.value;
        const name = target.name;
        this.setState({
            [name]: value
        });
    }
    handleSubmit (event) {
        this.setState({
            profile: `姓名:${this.state.name},${this.state.gender},${this.state.attend ? '参加' : '不参加'}活动`
        });
        event.preventDefault();
    } 
    render () {
        return (
            <form>
            <p>姓名:<input name="name" value={this.state.name} onChange={this.handleInputChange} /></p>
            <p>性别:
                <select name="gender" value={this.state.gender} onChange={this.handleInputChange}>
                    <option value="男">男</option>
                    <option value="女">女</option>
                </select>
            </p>
            <p>是否参加:<input name="attend" type="checkbox" onChange={this.handleInputChange} checked={this.state.attend} /></p>
            <input type="submit" value="Submit" onClick={this.handleSubmit} />
            <p>您的报名信息:{this.state.profile}</p>
            </form>
        )
    }
}

3、非受控组件

大多数情况下,使用受控组件实现表单是首选,在受控组件中,表单数据是交由React组件处理的。如果想要让表单数据由DOM处理(即数据不保存在React的状态里,而是保存在DOM中),那么可以使用非受控组件,使用非受控组件,可以无需为每个状态更新编写事件处理程序,使用ref即可实现,如:

class NameForm extends React.Component {
    constrcutor(props) {
        super(props)
    }
    handleSubmit: (event) => {
        console.log('A name was submitted: ', this.input.value)
        event.preventDefault()
    }
    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                Name: <input type="text" ref={input => this.input = input} />
                </label>
                <input type="submit" value="submit" />
            </form>
        )
    }
}

对于非受控组件,如果要指定默认值,那么可以使用defaultValue,如:

<input type="text" defaultValue="Hello" ref={input => this.input = input} />

相应的,type="checkbox"type="radio",则使用defaultChecked


四、状态提升

当需要几个组件共用状态数据的时候,可以使用状态提升技术。核心思想在于:把数据抽离到最近的共同父组件,父组件管理状态(state),然后通过属性(props)传递给子组件。如实现一个货币转换的组件,可以如下:

1、首先定义转换函数

function USD2RMB (amount) {
    return amount * 6.7925;
}

function RMB2USD (amount) {
    return amount * 0.1472;
}

function convert (amount, typeFn) {
    return typeFn(amount);
}

2、定义组件

我们希望在RMB的输入表单上上输入的时候,USD的输入表单上的数值也同步更新,这种情况下,如果RMB组件自己管理自己的状态,是很难以实现的,因此,我们需要让这个状态提升自父组件进行管理。如下:

class CurrencyInput extends React.Component {
    constructor (props) {
        super(props)
        this.handleChange = this.handleChange.bind(this)
    }
    handleChange (event) {
        this.props.onInputChange(event.target.value)
    }
    render () {
        const value = this.props.value
        const type = this.props.type
        return (
            <p>{type}: <input type="text" value={value} onChange={this.handleChange} /></p>
        );
    }
}

最后定义一个共同的父组件,如下:

class CurrencyConvert extends Component {
    constructor (props) {
        super(props);
        this.state = {
            type: 'RMB',
            amount: 0
        }
        this.handleRMBChange = this.handleRMBChange.bind(this);
        this.handleUSDChange = this.handleUSDChange.bind(this);
    }
    handleRMBChange (amount) {
        this.setState({
            type: 'RMB',
            amount
        });
    }
    handleUSDChange (amount) {
        this.setState({
            type: 'USD',
            amount
        });
    }
    render () {
        const type = this.state.type;
        const amount = this.state.amount;
        const RMB = type==='RMB' ? amount : convert(amount, USB2RMB);
        const USD = type==='USD' ? amount : convert(amount, RMB2USB);
        return (
            <div>
                <p>Please Input:</p>
                <CurrencyInput type="RMB" value={RMB} onInputChange={this.handleRMBChange} />
                <CurrencyInput type="USD" value={USD} onInputChange={this.handleUSDChange} />
            </div>
        );
    }
}


五、组合vs继承

React推崇更多的是使用组合,而非使用继承。对于一些使用场景,React给出的建议如下:

1、包含关系

当父组件不知道子组件可能的内容是什么的时候,可以使用props.children,如:

function Article (props) {
    return (
        <section>
            <aside>侧边栏</aside>
            <article>{props.children}</article>
        </section>
    );
}

function App () {
    return (
        <Article>这是一篇文章</Article>
    );
}

这将渲染得到:

<section>
    <aside>侧边栏</aside>
    <article>这是一篇文章</article>
</section>

我们还可以自定义名称,因为JSX实际上会被转化为合法的JS表达式,所以,还可以有:

function Article (props) {
    return (
        <section>
            <aside>{props.aside}</aside>
            <article>{props.children}</article>
        </section>
    );
}

function App () {
    return (
        <Article aside={
            <h1>这是一个侧栏</h1>
        }>这是一篇文章</Article>
    );
}

这将渲染得到:

<section>
    <aside><h1>这是一个侧栏</h1></aside>
    <article>这是一篇文章</article>
</section>

2、何时使用继承?

在Facebook的网站上,使用了数以千计的组件,但是实践证明还没有发现需要使用继承才能解决的情况。
属性和组合为我们提供了清晰的、安全的方式来自定义组件的样式和行为,组件可以接受任意元素,包括:基本数据类型、React元素、函数。
如果要在组件之间复用UI无关的功能,那么应该将其提取到单独的JavaScript模块中,这样子可以在不对组件进行扩展的前提下导入并使用函数、对象、类