深入了解JSX
最近开始学着使用React写东西。在写代码时会使用JSX,不了解JSX的相关知识写起代码的效率还是蛮低的。为了能更好的进入状态,打算先把JSX相关的知识和细节了解一下。在这篇文章中我们主要就是来学习一下JSX的相关知识。希望对于像我这样的初学React(或初次接触JSX)的同学有所帮助。
什么是JSX
JSX是JavaScript中的一种语法扩展。是React组件编写UI逻辑的语言扩展(JSX除了能在React中使用之外还可以用于别的地方)。虽然在不使用JSX的情况之下,React可以完全正常工作,但它是一种理想的组件处理技术,所以React从JSX中获益良多。
首先,你可能认为使用JSX就是将HTML和JavaScript混合在一起,事实并非如此,因为在使用JSX语法时,描述这个UI不是用字符串,使用的是JavaScript。这让我们能做很多事。
将JavaScript和标记(Markup)放在同一位置被认为是一种坏习惯,但事实证明,将视图与功能结合起来可以直接对视图进行推理。
为了了解这意味着什么,假设我们有一个React组件,它只渲染一个<h1>标签。JSX允许我们以种非常类似HTML的方式声明这个元素:
class HelloWorld extends React.Component {
    render() {
        return (
            <h1 className="title">Hello World</h1>
        )
    }
}
HelloWorld组件中的render()函数看起来返回的是HTML,但实际上这是JSX。JSX在运行时被转换为常规则JavaScript。翻译后的组件看起来是这样的:
class HelloWorld extends React.Component {
    render() {
        return (
            React.createElement(
                'h1',
                {className: 'title'},
                'Hello World'
            )
        )
    }
}
虽然JSX看起来像HTML,但实际上它只是一种编写React.createElement()声明的更简洁的方法。当组件渲染时,它输出一个React元素树或该组件输出的HTML元素的虚拟DOM。然后,React将根据这个React元素表示确定对实际DOM进行哪些更改。HelloWorld组件渲染出来的React的HTML看起来像下面这样:
<h1 class='title'>Hello World</h1>
为什么使用JSX
React认为渲染逻辑本质上与其他UI逻辑内在耦合,比如,在UI中需要绑定处理事件、在某些时刻状态发生变化时需要通知到UI,以及需要在UI中展示准备好的数据。
React并没有采用将标记与逻辑进行分离到不同文件这种人为地分离方式,而是通过将二者共同存放在称之为组件的松散耦合单元之中,来实现“关注点分离”。React不强制要求使用JSX,但大多数人发现,在JavaScript代码中将JSX和UI放在一起时,会在视觉上有辅助作用。它还可以使React显示更多有用的错误和警告消息。
JSX原理
在继续往下阅读之前,先稍微的了解一些有关于JSX原理相关的内容。即JSX是经过怎么样的转化变成页面的元素的。
比如说下面这个最简单的DOM元素为例,怎么使用JavaScript的对象来表现一个DOM元素的结果:
<h1 class="title">Hello World!</h1>
每个DOM元素的结构都可以用JavaScript的对象来表示。你会发现,DOM元素包含的信息其实只有三个:标签名(HTML元素)、属性(HTML元素属性)和子元素(HTML元素或文本节点)。那么上面的这个HTML标签对应的所有信息可以用下面这个对象来描述:
{
    tag: 'div',
    attrs: {className: 'title'},
    children: 'Hello World!'
}
你会发现,HTML的信息和JavaScript所包含的结构和信息其实是一样的,我们可以用JavaScript对象来描述所有能用HTML表示的UI信息。只不过写起来有点麻烦,结构看起来不太清晰。
在React中会把类似HTML的JSX结构转换成JavaScript的对象结构。比如:
<h1 class="title">Hello World!</h1>
JSX转换成JavaScript就会像下面这样:
React.createElement(
    "h1", 
    {
        class: "title"
    }, 
    "Hello World!"
);
React.createElement()会构建一个JavaScript对象来描述HTML结构的信息,包括标签名、属性和子元素等。上面是一个较为简单的示例,对于复杂的示例,我们可以借助在线的工具来帮助我们,比如Babel:

所谓的JSX其实就是JavaScript对象。
有了之个表示HTML结构和信息的对象以后,就可以去构造真正的DOM元素,然后使用ReactDOM.render()函数把构造好的DOM元素塞到页面中:
ReactDOM.render(
    <HelloWorld />,
    document.getElementById('root')
)
ReactDOM.render()函数就是把组件渲染并且构造DOM树,然后插入到页面上某个特定的元素上div#root。
简单地总结一下,JSX经过Babel编译和React构造器转换成了JavaScript对象,然后通过ReactDOM.render()函数转换成DOM元素,再插入到页面中渲染出来:

接下来简单看看JSX的具体使用。
JSX的使用
在JSX中可以像下面这样定义一个包含字符串的<h1>标签:
const element = <h1> Hello World!~</h1>
看起来有点像是JavaScript和HTML的结合特,但实际上它就是JavaScript。在JSX中是用来定义组件及其在标记中的位置的语法糖。事实element是一个Object:

JSX中嵌入表达式
在下面的示例中,声明了一个名为eleId的变量,然后在JSX中使用它,并且放置在{}中,比如:
const eleId = 'title'
const element = <h1 id={eleId}>Hello World!</h1>
在JSX语法中,可以在大括号内放置任何有效的JavaScript表达式,比如:
const Hello = (user) => {
    return `Hello ${user}`;
}
const element = <h1>{ Hello('W3cplus.com') }</h1>
在属性中嵌入JavaScript表达式时,不要在大括号外面加上引号。应该仅使用引用(字符串)或大括号(表达式)中的一个,对于同一个属性不能同时使用这两种符号。
JSX中的特定属性
在HTML中标签中有时候会用到带有-中折号的属性,比如aira-hidden,也会有多个词组合在一起的属性,比如tabindex。类似这些属性在JSX中需要使用驼峰写法,比如airaHidden或tabIndex。
const eleId='title'
const element = <h1 id={eleId} tabIndex="0">Hello World!</h1>
需要特别注意的是,在JSX中要是会用到class和for属性时,需要将class换成 className,for换成 htmlFor。那是因为class和for是JavaScript中的关键字。
const element = (
    <div className="control">
        <label htmlFor="user">Name:</label>
        <input id="user" type="text" placholder="Enter your name" />
    </div>
)
在JSX中有些写法和HTML类似,比如一个标签里没有内容,可以使用/>来闭合标签,比如上面的<input>元素:
<input id="user" type="text" placholder="Enter your name" />
如果在ReactDOM.render()包含多个子元素时(JSX标签里包含多个子元素),需要用一个标签元素括起来,比如上面示例中的<div className="control">。另外将其放置在一个括号()中。这是因为render()函数只能返回一个节点,所以如果你想返回两个兄弟节点,就需要添加一个父节点,如上面示例所示。
JSX是一个对象
浏览器不能直接执行包含JSX代码的JavaScript文件。它们必须首先转换成普通的JavaScript。需要一个叫做转置的过程。当然,在React中JSX是可选的,因为对于每个JSX代码都有对应的纯JavaScript代码替代,这就是JSX的换位符。
正如文章开头所提到的,Babel会把JSX转译成一个名为React.createElement()函数调用。比如下面的两段代码,起到的作用是同等的:
// JSX代码
ReactDOM.render(
    <div id="box">
        <h1>title</h1>
        <p>paragraph</p>
    </div>,
    document.getElementById('root')
)
// JavaScript
ReactDOM.render(
    React.createElement(
        "div", 
        {id: "box"},
        React.createElement(
            "h1", 
            null, 
            "title"
        ), 
        React.createElement(
            "p", 
            null, 
            "paragraph"
        )
    ), 
    document.getElementById('root')
);
JSX和JavaScript两段代码同样可以借助在线的Babel工具查看。

上面的示例也再次验证了,虽然JSX和JavaScript所起的效果是等效的,但相比而言,JavaScript要比JSX复杂的多。另外React.createElement()会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:
// 简化后的结构
React.createElement(
    "h1",
    null, 
    "title"
)
实际上,JSX仅仅只是React.createElement(component, props, ...children)函数的语法糖。如下JSX代码:
// JSX代码
<MyButton color="blue" shadowSize={2}>
    Click Me
</MyButton>
编译成JavaScript代码如下:
// JavaScript代码
React.createElement(
    MyButton, 
    {
        color: "blue",
        shadowSize: 2
    }, 
    "Click Me"
);
JSX中的JS
JSX接受任何混合的JavaScript。当你需要添加一些JavaScript时,只需要将它放到大括号{}中(前面也有简单的提到过)。例如,下面的示例就是向你展示了在JSX中怎么引用其他地方声明的常量:
const name = '大漠'
ReactDOM.render(
    <h1> Hello, { name }</h1>,
    document.getElementById('root')
)
正如你所看到的,我们可以将JavaScript嵌套在JSX中定义的JSX中,而且你想多深就可以多深(你想放置在JSX代码中的位置):
const items = ['大漠', 'w3cplus', '杭州']
ReactDOM.render(
    <ul>
        {items.map((item, i) => {
            return <li>({i}):{item}</li>
        })}
    </ul>,
    document.getElementById('root')
)
JSX中的HTML
JSX看上去和HTML非常相似,但实际上是XML语法。但在页面中最终呈现的依旧是HTML,因此我们有必要了解HTML中如何定义某些内容与在JSX中定义的差异性。
需要关闭所有标记
就像在XHTML中一样,需要关闭所有的标记,比如<img>标签需要使用自闭标记<img />,其他标记也是类似。<img>标记有点特殊,那么对于其他HTML标记也是有相通的原理,比如<div>这样的常规标记,如果没有包含子元素(或子节点)也可以像这样使用<div />来关闭标签,如果有子元素之类的就需要<div></div>这样使用。
驼峰写法是新的标准
在HTML中,你会发现属性没有任何大小写,比如onclick。但在JSX中,它们被重命名,需要用驼峰的方式来命名:
onchange => onChange
onclick => onClick
onsubmit => onSubmit
前面也提到过了,HTML中的
class和for是JavaScript的关键词,所以在JSX中需要以新的方式提供class=>className,for=>htmlFor。
JSX中的表单
JSX中对于表单的部分做一些更改,主要目的是让开发人员更容易地完成工作。
value和defaultValue
value属性始终保存字段的当前值;defaultValue属性保存创建字段时设置的默认值。这有助于解决在检查输入(input)时常规DOM交互的一些奇怪行为。input.value和input.getAttribute('value') 返回一个当前值和一个初始默认值。
这个也适用于textarea标签,比如:
<textarea>多行文本域的文本</textarea>
需要用下面的方式来替代:
<textarea defaultValue={'多行文本域的文本'} />
<select>也类似:
<select>
    <option value="x" selected></option>
</select>
用下面的这种方式来替代:
<select defaultValue="x">
    <option value="x"></option>
</select>
JSX中的HTML实体符
在HTML中编写代码的时候,有时候会使用到HTML的实体符,比如©用©来表示。在JSX中为了降低XSS攻击的风险,JSX强制在表达式中自动会将HTML实体符进行转义。比如:
<p>{'© 2019'}</p>
在JSX中,©会被转义为"\xA9 2019"。那么上面的代码,在JSX中应该:
<p>{'\xA9 2019'}</p>
<!-- 或者 -->
<p>{'\u00A9 2019'}</p>
渲染出来的结果如下:

JSX中的空格符
在JSX中添加空格符有两条规则:
- 水平空白符会被裁剪为一个空白符:如果在同一行元素之间有空白,则所有元素都会被裁剪为一个空白符
 - 消除垂直换行的空白符:如果是垂直换行的空白符会完全消除
 
比如:
<!-- JSX -->
<p>Something       becomes               this</p>
<p>
    Something
    becomes
    this
</p>
<!-- 编译后结果 -->
<p>Something becomes this</p>
<p>Somethingbecomesthis</p>
要解决这个问题,需要显式地通过添加一个空格符表达式来处理,像下面这样:
<p>
    Something
    {' '}becomes
    {' '}this
</p>
<!-- 或像下面这样 -->
<p>
    Something
    {' becomes '}
    this
</p>
JSX中的注释
在编码时难免少不了添加一些注释。在不同的语言中有不同的注释方式,比如:
<!-- HTML中的注释方式 -->
/* CSS中的单行注释方式 */
/*
 * CSS 中的多行注释方式
 *
*/
// JavaScript中的注释 
// 1. JavaScript的多行注释
// 2. JavaScript的多行注释
/*
 * JavaScript中的多行注释
*/
JSX中的注释会放在{}中,像下面这样:
{/* JSX中的单行注释*/}
{
    // JSX中的单行注释
}
{
    /* 
     * JSX中的多行注释
    */
}
JSX中的条件渲染
在JavaScript中,对于if ... else和switch语句并不会感到陌生,在JSX中其实也可以使用它们。
在JSX中根据条件渲染中,组件根据一个或多个条件决定返回哪些元素。例如可以返回一个项目列表,也可以返回一条消息。当组件具有条件渲染时,渲染组件的实例可以具有不同的外观(组件样式不一样)。
JSX中的if ... else
条件渲染最简单的方式就是使用if ... else方式。例如,当没有项列表时,列表组件不应该渲染列表。我们可以使用if语句从渲染生命周期中提前返回。
function List({ list }) {
    if (!list) {
        return null;
    }
    return (
        <div>
            {list.map(item => <ListItem item={item} />)}
        </div>
    );
}
上面的JSX编译后的结果如下:
function List(_ref) {
    var list = _ref.list;
    if (!list) {
        return null;
    }
    return React.createElement(
        "div", 
        null, 
        list.map(function (item) {
            return React.createElement(
                ListItem, 
                {item: item}
            );
        })
    );
}
返回null的组件将不会渲染任何内容。然而,你可能想在列表为空时显示文本,以便为你的应用程序给用户提供一些反馈,提供较好的用户体验:
function List({ list }) {
    if (!list) {
        return null;
    }
    if (!list.length) {
        return <p>对不起,列表是空的!</p>;
    } else {
        return (
            <div>
                {list.map(item => <ListItem item={item} />)}
            </div>
        );
    }
}
JSX中的三元运算符
使用三元运算符可以使if ... else语句更简洁。
来看一个示例,假设组件中一个切换按钮,可以在高亮和暗黑两种模式之间切换。决定切换到哪种状态只需要一个简单条件的布尔值,该布尔值可以决定要返回哪个元素(切换到哪种模式):
function Item({ item, mode }) {
    const isDarkMode = mode === 'DARK';
    return (
        <div>
            { isDarkMode
                ? <DarkMode item={item} />
                : <HighlightMode item={item} />
            }
        </div>
    );
}
如果三元运算符中的两个分支会用到多个子元素时,可以使用()括起来:
function Item({ item, mode }) {
    const isDarkMode = mode === 'DARK';
    return (
        <div>
            { 
                isDarkMode ? (
                    <DarkMode item={item} />
                ) : (
                    <HighlightMode item={item} />
                )
            }
        </div>
    );
}
JSX中的&&操作符
在写组件的时候,经常会碰到这种情况,要么渲染出组件,要么什么都不渲染,例如有一个Card组件,它渲染一个卡片或什么都不渲染。我们可以使用上面提到的if ... else语句或三元运算符来实现:
{/* if ... else */}
function CardFun({ isCard }) {
    if (isCard) {
        return (
            <Card />
        )
    } else {
        return null
    }
}
{/* 三元操作符 */}
function CardFun({ isCard }) {
    return (
        <div>
            {isCard ? <Card /> : null}
        </div>
    )
}
但还有一种方法会更简单,可以省略返回null的必要性。即使用&&操作符会更简单:
const result = true && 'Hello World';
console.log(result);
// Hello World
const result = false && 'Hello World';
console.log(result);
// false
使用&&运算符,上面的示例可以像下面这样使用:
function CardFun({ isCard }) {
    return (
        <div>
            {isCard && <Card />}
        </div>
    )
}
JSX中的switch操作符
在JSX中如果要根据多个不同的条件渲染出组件不同的状态,那么可以使用switch ... case的方式来处理,比如:
function Notification({ text, state }) {
    switch(state) {
        case 'info':
            return <Info text={text} />;
        case 'warning':
            return <Warning text={text} />;
        case 'error':
            return <Error text={text} />;
        default:
            return null;
    }
}
在React中,如果组件基于状态的条件渲染时,还可以使用React.PropTypes描述组件接口:
function Notification({ text, state }) {
    switch(state) {
        case 'info':
            return <Info text={text} />;
        case 'warning':
            return <Warning text={text} />;
        case 'error':
            return <Error text={text} />;
        default:
            return null;
    }
}
Notification.propTypes = {
    text: React.PropTypes.string,
    state: React.PropTypes.oneOf(['info', 'warning', 'error'])
}
有关于React中的
React.PropTypes相关知识不在这里阐述,后续我们会专门花一定的篇幅来介绍这方面的知识。
现在有一个通用组件来显示不同类型的通知。例如,基于state的值,组件可以有不同的外观。error是红色的,warning可能是黄色的,info可能是蓝色的。
在JSX中还可以用内联开关的方式,需要一个自调用JavaScript函数。
function Notification({ text, state }) {
    return (
        <div>
            {(function() {
                switch(state) {
                case 'info':
                    return <Info text={text} />;
                case 'warning':
                    return <Warning text={text} />;
                case 'error':
                    return <Error text={text} />;
                default:
                    return null;
                }
            })()}
        </div>
    );
}
上面的示例,还可以使用ES6箭头函数来写,会更简洁:
function Notification({ text, state }) {
    return (
        <div>
            {(() => {
                switch(state) {
                case 'info':
                    return <Info text={text} />;
                case 'warning':
                    return <Warning text={text} />;
                case 'error':
                    return <Error text={text} />;
                default:
                    return null;
                }
            })()}
        </div>
    );
}
JSX中使用枚举条件来渲染
在JavaScript中,当对象用作键值的映射时,对象可以用作枚举。
const ENUM = {
    a: '1',
    b: '2',
    c: '3',
};
枚举用作多个条件渲染也是一个较好的方式,比如上面的示例,使用枚举可以改写成下面这样:
function Notification({ text, state }) {
    return (
        <div>
            {{
                info: <Info text={text} />,
                warning: <Warning text={text} />,
                error: <Error text={text} />,
            }[state]}
        </div>
    );
}
state属性键帮助我们从对象中检索值。相比前面的示例是不是更简洁,是吧。而且更具可读性。在本例中,我们必须使用内联对象,因为对象的值依赖于text属性。如果不依赖text,就需要使用外部静态枚举方式:
const NOTIFICATION_STATES = {
    info: <Info />,
    warning: <Warning />,
    error: <Error />,
};
function Notification({ state }) {
    return (
        <div>
            {NOTIFICATION_STATES[state]}
        </div>
    );
}
如果依赖text属性可以使用函数来检索值:
const getSpecificNotification = (text) => ({
    info: <Info text={text} />,
    warning: <Warning text={text} />,
    error: <Error text={text} />,
});
function Notification({ state, text }) {
    return (
        <div>
            {getSpecificNotification(text)[state]}
        </div>
    );
}
JSX中多级条件渲染
JSX中还会有多级条件(嵌套条件)渲染的场景。比如下面这个示例,List组件,它既可以显示列表、空文本,也可以什么都不显示:
function List({ list }) {
    const isNull = !list;
    const isEmpty = !isNull && !list.length;
    return (
        <div>
            { isNull
                ? null
                : ( isEmpty
                ? <p>Sorry, the list is empty.</p>
                : <div>{list.map(item => <ListItem item={item} />)}</div>
                )
            }
        </div>
    );
}
// Usage
<List list={null} />            // => <div></div>
<List list={[]} />              // => <div><p>Sorry, the list is empty.</p></div>
<List list={['a', 'b', 'c']} /> // => <div><div>a</div><div>b</div><div>c</div><div>
上面的示例还可以继续优化,将嵌套条件保持在最小值状态。建议将复杂的组件拆分成更小的组件,这些组件自身具备有条件地渲染。
function List({ list }) {
    const isList = list && list.length;
    return (
        <div>
            { isList
                ? <div>{list.map(item => <ListItem item={item} />)}</div>
                : <NoList isNull={!list} isEmpty={list && !list.length} />
            }
        </div>
    );
}
function NoList({ isNull, isEmpty }) {
    return (!isNull && isEmpty) && <p>Sorry, the list is empty.</p>;
}
特别声明:JSX中条件渲染这部分示例代码来自于@rwieruch的《All React Conditional Rendering Techniques》一文。
有关于条件渲染相关的还可以阅读@Kristina Grujic的《React JSX: How to Do It the Right Way, Part II》和《条件渲染》。
JSX中的循环
如果你有一组需要循环的元素来生成JSX部分,可以创建一个循环,然后将JSX添加到一个数组中:
const elements = [] //..some array
const items = []
for (const [index, value] of elements.entries()) {
    items.push(<Element key={index} />)
}
现在,当渲染JSX时,可以通过用大括号{}来嵌套items数组:
const elements = ['one', 'two', 'three'];
const items = []
for (const [index, value] of elements.entries()) {
    items.push(<li key={index}>{value}</li>)
}
return (
    <div>
        {items}
    </div>
)
如果使用map还可以在JSX中直接执行相同的操作:
const elements = ['one', 'two', 'three'];
return (
    <ul>
        {elements.map((value, index) => {
            return <li key={index}>{value}</li>
        })}
    </ul>
)
有关于JSX中的循环相关的使用,更详细的介绍还可以阅读:
JSX中表达式
前面其实提到过,在JSX中可以通过使用大括号{}来引用表达式,用于将变量传递给元素和属性、计算布尔表达式等。比如下面这样的一个示例:
class App extends Component {
    constructor(props) {
        super(props);
        this.state = {
            address: {
                houseNumber: '12',
                street: 'Oakwood Drive',
                city: 'Red Hook',
                state: 'New York'
                zipcode: '1234',
                country: 'USA'
            }
        }
    }
    formatAddress() {
        const { address } = this.state
        return `${address.houseNumber}
            ${address.street}
            ${address.city}
            ${address.state}
            ${address.zipcode}
            ${address.country}
        `
    }
    render() {
        return (
            <div className="App">
                { this.formatAddress() }
            </div>
        )
    }
}
const rootElement = document.getElementById(root)
ReactDOM.render(<App />, rootElement)
如果想在字符串中显示变量内部的值,那么ES6的模板字符串(Template Literals)就非常有用,比如:
cosnt monthAndYear = `${month} - ${year}`
在标签元素中属性也可以使用表达多:
const profilePicture = <img src = {user.picture} alt="me!" />
另外如果想根据条件给元素添加类名,也可以使用表达式直接处理:
<h1 className={isLogo ? 'logo' : 'title'}></h1>
JSX 中的 Props
在JSX中有多种方式可以指定Props。
JavaScript 表达式作为 Props
你可以把包裹在 {} 中的 JavaScript 表达式作为一个 prop 传递给 JSX 元素。例如,如下的 JSX:
<MyComponent foo={1 + 2 + 3 + 4} />
在 MyComponent 中,props.foo 的值等于 1 + 2 + 3 + 4 的执行结果 10。
if 语句以及 for 循环不是 JavaScript 表达式,所以不能在 JSX 中直接使用。但是,你可以用在 JSX 以外的代码中。比如:
function NumberDescriber(props) {
    let description;
    if (props.number % 2 == 0) {
        description = <strong>even</strong>;
    } else {
        description = <i>odd</i>;
    }
    return <div>{props.number} is an {description} number</div>;
}
字符串字面量
你可以将字符串字面量赋值给 prop。 如下两个 JSX 表达式是等价的:
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />
当你将字符串字面量赋值给 prop 时,它的值是未转义的。所以,以下两个 JSX 表达式是等价的:
<MyComponent message="<3" />
<MyComponent message={'<3'} />
这种行为通常是不重要的,这里只是提醒有这个用法。
Props 默认值为 “True”
如果你没给 prop 赋值,它的默认值是 true。以下两个 JSX 表达式是等价的:
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
通常,我们不建议这样使用,因为它可能与 ES6 对象简写混淆,{foo} 是 {foo: foo} 的简写,而不是 {foo: true}。这样实现只是为了保持和 HTML 中标签属性的行为一致。
属性展开
如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象。以下两个组件是等价的:
function App1() {
    return <Greeting firstName="Ben" lastName="Hector" />;
}
function App2() {
    const props = {firstName: 'Ben', lastName: 'Hector'};
    return <Greeting {...props} />;
}
你还可以选择只保留当前组件需要接收的 props,并使用展开运算符将其他 props 传递下去。
const Button = props => {
    const { kind, ...other } = props;
    const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
    return <button className={className} {...other} />;
};
const App = () => {
    return (
        <div>
            <Button kind="primary" onClick={() => console.log("clicked!")}>
                Hello World!
            </Button>
        </div>
    );
};
在上述例子中,kind 的 prop 会被安全的保留,它将不会被传递给 DOM 中的 <button> 元素。 所有其他的 props 会通过 ...other 对象传递,使得这个组件的应用可以非常灵活。你可以看到它传递了一个 onClick 和 children 属性。
属性展开在某些情况下很有用,但是也很容易将不必要的 props 传递给不相关的组件,或者将无效的 HTML 属性传递给 DOM。我们建议谨慎的使用该语法。
JSX 中的子元素
包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。有几种不同的方法来传递子元素:
字符串字面量
你可以将字符串放在开始和结束标签之间,此时 props.children 就只是该字符串。这对于很多内置的 HTML 元素很有用。例如:
<MyComponent>Hello world!</MyComponent>
这是一个合法的 JSX,MyComponent 中的 props.children 是一个简单的未转义字符串 "Hello world!"。因此你可以采用编写写 HTML 的方式来编写写 JSX。如下所示:
<div>This is valid HTML & JSX at the same time.</div>
JSX 子元素
子元素允许由多个 JSX 元素组成。这对于嵌套组件非常有用:
<MyContainer>
    <MyFirstComponent />
    <MySecondComponent />
</MyContainer>
你可以将不同类型的子元素混合在一起,因此你可以将字符串字面量与 JSX 子元素一起使用。这也是 JSX 类似 HTML 的一种表现,所以如下代码是合法的 JSX 并且也是合法的 HTML:
<div>
    Here is a list:
    <ul>
        <li>Item 1</li>
        <li>Item 2</li>
    </ul>
</div>
React 组件也能够返回存储在数组中的一组元素:
render() {
    // 不需要用额外的元素包裹列表元素!
    return [
        // 不要忘记设置 key :)
        <li key="A">First item</li>,
        <li key="B">Second item</li>,
        <li key="C">Third item</li>,
    ];
}
JavaScript 表达式作为子元素
JavaScript 表达式可以被包裹在 {} 中作为子元素。例如,以下表达式是等价的:
<MyComponent>foo</MyComponent>
<MyComponent>{'foo'}</MyComponent>
这对于展示任意长度的列表非常有用。例如,渲染 HTML 列表:
function Item(props) {
    return <li>{props.message}</li>;
}
function TodoList() {
    const todos = ['finish doc', 'submit pr', 'nag dan to review'];
    return (
        <ul>
            {todos.map((message) => <Item key={message} message={message} />)}
        </ul>
    );
}
JavaScript 表达式也可以和其他类型的子元素组合。这种做法可以方便地替代模板字符串:
function Hello(props) {
    return <div>Hello {props.addressee}!</div>;
}
函数作为子元素
通常,JSX 中的 JavaScript 表达式将会被计算为字符串、React 元素或者是列表。不过,props.children 和其他 prop 一样,它可以传递任意类型的数据,而不仅仅是 React 已知的可渲染类型。例如,如果你有一个自定义组件,你可以把回调函数作为 props.children 进行传递:
// 调用子元素回调 numTimes 次,来重复生成组件
function Repeat(props) {
    let items = [];
    for (let i = 0; i < props.numTimes; i++) {
        items.push(props.children(i));
    }
    return <div>{items}</div>;
}
function ListOfTenThings() {
    return (
        <Repeat numTimes={10}>
            {(index) => <div key={index}>This is item {index} in the list</div>}
        </Repeat>
    );
}
你可以将任何东西作为子元素传递给自定义组件,只要确保在该组件渲染之前能够被转换成 React 理解的对象。这种用法并不常见,但可以用于扩展 JSX。
布尔类型、Null 以及 Undefined 将会忽略
false, null, undefined和 true 是合法的子元素。但它们并不会被渲染。以下的 JSX 表达式渲染结果相同:
<div />
<div></div>
<div>{false}</div>
<div>{null}</div>
<div>{undefined}</div>
<div>{true}</div>
这有助于依据特定条件来渲染其他的 React 元素。例如,在以下 JSX 中,仅当 showHeader 为 true 时,才会渲染 <Header />:
<div>
    {showHeader && <Header />}
    <Content />
</div>
值得注意的是有一些 “falsy” 值,如数字 0,仍然会被 React 渲染。例如,以下代码并不会像你预期那样工作,因为当 props.messages 是空数组时,0 仍然会被渲染:
<div>
    {props.messages.length &&
        <MessageList messages={props.messages} />
    }
</div>
要解决这个问题,确保 && 之前的表达式总是布尔值:
<div>
    {props.messages.length > 0 &&
        <MessageList messages={props.messages} />
    }
</div>
反之,如果你想渲染 false、true、null、undefined 等值,你需要先将它们转换为字符串:
<div>
    My JavaScript variable is {String(myVariable)}.
</div>
指定 React 元素类型
JSX 标签的第一部分指定了 React 元素的类型。
大写字母开头的 JSX 标签意味着它们是 React 组件。这些标签会被编译为对命名变量的直接引用,所以,当你使用 JSX <Foo /> 表达式时,Foo 必须包含在作用域内。
React 必须在作用域内
由于 JSX 会编译为 React.createElement 调用形式,所以 React 库也必须包含在 JSX 代码作用域内。
例如,在如下代码中,虽然 React 和 CustomButton 并没有被直接使用,但还是需要导入:
import React from 'react';
import CustomButton from './CustomButton';
function WarningButton() {
    // return React.createElement(CustomButton, {color: 'red'}, null);
    return <CustomButton color="red" />;
}
如果你不使用 JavaScript 打包工具而是直接通过 <script> 标签加载 React,则必须将 React 挂载到全局变量中。
在 JSX 类型中使用点语法
在 JSX 中,你也可以使用点语法来引用一个 React 组件。当你在一个模块中导出许多 React 组件时,这会非常方便。例如,如果 MyComponents.DatePicker 是一个组件,你可以在 JSX 中直接使用:
import React from 'react';
const MyComponents = {
    DatePicker: function DatePicker(props) {
        return <div>Imagine a {props.color} datepicker here.</div>;
    }
}
function BlueDatePicker() {
    return <MyComponents.DatePicker color="blue" />;
}
用户定义的组件必须以大写字母开头
以小写字母开头的元素代表一个 HTML 内置组件,比如 <div> 或者 <span> 会生成相应的字符串 'div' 或者 'span' 传递给 React.createElement(作为参数)。大写字母开头的元素则对应着在 JavaScript 引入或自定义的组件,如 <Foo /> 会编译为 React.createElement(Foo)。
我们建议使用大写字母开头命名自定义组件。如果你确实需要一个以小写字母开头的组件,则在 JSX 中使用它之前,必须将它赋值给一个大写字母开头的变量。
例如,以下的代码将无法按照预期运行:
import React from 'react';
// 错误!组件应该以大写字母开头:
function hello(props) {
    // 正确!这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签
    return <div>Hello {props.toWhat}</div>;
}
function HelloWorld() {
    // 错误!React 会认为 <hello /> 是一个 HTML 标签,因为它没有以大写字母开头:
    return <hello toWhat="World" />;
}
要解决这个问题,我们需要重命名 hello 为 Hello,同时在 JSX 中使用 <Hello /> :
import React from 'react';
// 正确!组件需要以大写字母开头:
function Hello(props) {
    // 正确! 这种 <div> 的使用是合法的,因为 div 是一个有效的 HTML 标签:
    return <div>Hello {props.toWhat}</div>;
}
function HelloWorld() {
    // 正确!React 知道 <Hello /> 是一个组件,因为它是大写字母开头的:
    return <Hello toWhat="World" />;
}
在运行时选择类型
你不能将通用表达式作为 React 元素类型。如果你想通过通用表达式来(动态)决定元素类型,你需要首先将它赋值给大写字母开头的变量。这通常用于根据 prop 来渲染不同组件的情况下:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
    photo: PhotoStory,
    video: VideoStory
};
function Story(props) {
    // 错误!JSX 类型不能是一个表达式。
    return <components[props.storyType] story={props.story} />;
}
要解决这个问题, 需要首先将类型赋值给一个大写字母开头的变量:
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
    photo: PhotoStory,
    video: VideoStory
};
function Story(props) {
    // 正确!JSX 类型可以是大写字母开头的变量。
    const SpecificStory = components[props.storyType];
    return <SpecificStory story={props.story} />;
}
JSX中行内样式
在HTML中,可以通过style给标签元素添加行内样式:
<h1 class="title" style="color:red;">Hello World!~</h1>
在JSX同样可以使用style来给元素添加样式,但和HTML的使用略有不同,需要以一个对象的方式给style赋值,如下:
<h1 className="title" style={{color: 'red'}}>Hello world!~</h1>
可以通过Babel在线转换工具,把下面的代码转换一下:
{/* JSX 转换前*/}
const element = <h1 className="title" style={{color: 'red'}}>Hello world!~</h1>;
ReactDOM.render(
    <div>
        { element }
    </div>,
    document.getElementById('root')
)
// Babel 转换出来的代码
var element = React.createElement(
    "h1", 
    {
        className: "title",
        style: {
            color: 'red'
        }
    }, 
    "Hello world!~"
);
ReactDOM.render(React.createElement("div", null, element), document.getElementById('root'));
编译后在浏览器中看到的效果和我们平时写HTML并无差异:

在JSX中,可以将上面的代码进行优化,可以声明一个对象,比如titleStyles,然后在需要的地方通过style来引用:
const titleStyle = {
    color: 'red'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;
如果h1中有多个样式时,可以在titleStyle对象中添加你所需要的样式:
const titleStyle = {
    color: 'red',
    backgroundColor: '#604D17',
    padding: '5px 10px',
    display: 'inline-flex',
    'border-radius': '5px'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;
注意,如果CSS的属性带有中折号-时,在JSX中需要将其换成驼峰写法或用引号括起来,比如上面例示中的:
background-color => 换成驼峰写法: backgroundColor
border-radius    => 用引号括起来: 'border-radius'
更建议使用驼峰方式来处理。
JSX中还更强大一些,比如说,可以给CSS属性赋值一个函数值,比如下面示例中的backgroundColor就是一个随机生成的颜色:
let rgb = []
for (var i = 0; i < 3; i++) {
    let r = Math.floor(Math.random() * 256)
    rgb.push(r)
}
const titleStyle = {
    color: 'red',
    backgroundColor: `rgb(${rgb})`,
    padding: '5px 10px',
    display: 'inline-flex',
    'border-radius': '5px'
}
const element = <h1 className="title" style={ titleStyle }>Hello world!~</h1>;
在JSX中,可以给CSS属性添加变量、函数等值。
除此之外,在JSX中还可以共享一些样式,比如:
const TodoComponent = {
    width: "300px",
    margin: "30px auto",
    backgroundColor: "#44014C",
    minHeight: "200px",
    boxSizing: "border-box"
}
const Header = {
    padding: "10px 20px",
    textAlign: "center",
    color: "red",
    fontSize: "22px"
}
const ErrorMessage = {
    color: "white",
    fontSize: "13px"
}
const styles = {
    TodoComponent: TodoComponent,
    Header: Header,
    ErrorMessage: ErrorMessage
}
const element = <h1 className="title" style={ styles.Header }>Hello world!~</h1>;
这只是JSX中添加行内样式的使用。这种方式也常常被称为inline style。其实在React中处理CSS的方式还有其他方式,比如Styled Components,CSS Modules等。据2019年的CSS 状态报告统计,在React中处理样式(CSS-in-JS)就属Styled Components和CSS Modules占比最高:
如果你对2019年CSS状态报告中提到的CSS特性感兴趣的话,可以阅读《从9102年的CSS状态报告中看CSS特性的使用》一文。
上面提到的仅是在JSX中怎么写行内样式,如果你对React项目中怎么写样式,维护样式方面感兴趣的话,欢迎关注后续的更新,我将会花一篇的篇幅来和大家一起探讨如何在React中撸CSS。
小结
对于撸习惯了HTML或其他模板语言的同学来说,使用JSX还是会略有一点不习惯。但JSX本身并不是模板语言,他是JavaScript的扩展。初次使用JSX可能会存在一定疑惑,虽然该文介绍了一些JSX的使用和具有的特性,以及需要注意的一些小细节。但难免无法解决所有的困惑,如果你和我一样,在使用JSX感到困惑的话,可以花点时间阅读@Brad Westfall的这篇博文,还是蛮有帮助的。如果你在这方面有更多的经验和技巧,欢迎在下面的评论中与我们一起共享。
 