这篇文章上次修改于 353 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

list 列表和 key

在 JavaScript 中我们通常使用 map method 来对一个 list 的每个元素进行操作:

const numbers = [1, 2, 3, 4, 5];
const double = numbers.map((number) => { return number * 2});
console.log(double)

//output:
//[ 2, 4, 6, 8, 10 ]

在 React 中对一个 list 的元素进行操作方法类似。

我们可以在 JSX 中通过大括号{} 来建立一个 elements 的集合,下面示例中我们将 map 的返回定义为 <li> 元素并赋值给 listItems:

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

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

注意在 render 中我们将 listItems 放在 <ul> 元素中。

通常情况下我们将 lists 放在一个 component 中:

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

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
    <NumberList numbers={numbers}/>,
    document.getElementById('root')
);

当运行以上代码时,在浏览器终端会有一个 warning 警告信息:Each child in a list should have a unique "key" prop.
2021-03-05T06:50:19.png

Key 是一个特殊的 string 字符串属性需要给创建的 list element 添加的。它可以用来定位 list 中的每个元素。

下面我们给 list item 添加 Key 字符串属性:

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

添加后报警就会消除。

Keys

Key 可以帮助 React 识别哪个 item 修改过,被删除,被添加。以上示例中,我们在 map 中创建 item 时给其 key 属性,这样每个 item 可以有确切的属性值。

每个 list item 最好设置一个特殊的标识 key string 来区别于其他 items。最常用的就是使用数据中的 ID 作为 key:

const TodoItems = (props) => {
    const todos = props.todos;
    const listItems = todos.map((todo) => 
        <li key={todo.id}>
            {todo.text}
        </li>
    )
    return (
        <ul>{listItems}</ul>
    );
}
const todos = [
    {id: 1, text: '123'},
    {id: 2, text: '456'}
];
ReactDOM.render(
    <TodoItems todos={todos} />,
    document.getElementById('root')
);

当没有特定的 ID 来作为标识时,作为最后的选择,可以使用 item 的 index 作为 key:

const TodoItems = (props) => {
    const todos = props.todos;
    const listItems = todos.map((todo, index) => 
        <li key={index}>
            {todo.text}
        </li>
    )
    return (
        <ul>{listItems}</ul>
    );
}

如果 items 的顺序可能会发生变化的话,不推荐使用 index 作为 key 使用,因为可能对性能产生影响并且对 component 的 state 造成问题。如果没有定义确切的 key 给 items,React 默认会使用 index 作为 keys。

拆解 component 时 key 的处理

keys 是对应与一个数组的内容而言的,它并不能单独存在。例如我们要拆解上面的 NumberList,提取出 ListItem,则需要将 key 定义在 <ListItem /> 元素中而不是 ListItem component 内部的 <li> 中:

const ListItem = (props) => {
    return (
        <li>{props.value}</li>
    );
}

const NumberList = (props) => {
    const numbers = props.numbers;
    const listItems = numbers.map((number) =>
        <ListItem key={number.toString()} value={number}/>);
    return (
        <ul>{listItems}</ul>
    );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
    <NumberList numbers={numbers} />,
    document.getElementById('root')
);

如果写成下面模式就是错误的:

function ListItem(props) {
  const value = props.value;
  return (
    <li key={value.toString()}>
      {value}
    </li>
  );
}

每个 item 的 key 必须是特定的

数组中每个 items 使用的 key 必须是互相独立且不相同的,但并不需要在全局下互相独立。在两个单独的数组中可以,其元素可以使用相同的 key:

const React = require('react')
const ReactDOM = require('react-dom')

const Blog = (props) => {
    const sideBar = (
        <ul>
            {props.posts.map((post) =>
                <li key={post.id}>{post.title}</li>
            )}
        </ul>
    );
    const content = props.posts.map((post) =>
        <div key={post.id}>
            <h3>{post.title}</h3>
            <p>{post.content}</p>
        </div>
    );
    return (
        <div>
            {sideBar}
            <hr/>
            {content}
        </div>
    );
}

const posts = [
    {id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
    {id: 2, title: 'Installation', content: 'You can install React from npm.'}
];

ReactDOM.render(
    <Blog posts={posts} />,
    document.getElementById('root')
);

上面示例中,我们在 Blog component 中定义了两个 JSX,都创建了 list elements,每个元素的 key 使用了对应的 id 属性。在每个 list 内部 key 是互相独立的。可以看到不只是 <li> 元素可以加 key,只要通过 map 定义了一个 array 数组,就可以给每个元素加上 key 属性来互相独立识别。

key 是为了给 React 识别用的。它本身并不作为一个普通 prop 传给 components,也就是在 component 内部并不能使用这个 key 数据,如果想要在 component 中使用这个数据则需要单独定义一个其他 prop 来传入 key 数据:

const Post = (props) => {
    return (
        <li>
            {props.id}: {props.title}
        </li>
    )
}
const Blog = (props) => {
    const sideBar = (
        <ul>
            {props.posts.map((post) =>
                <Post key={post.id} id={post.id} title={post.title} />
            )}
        </ul>
    );
...
...
...
}

上面示例中,Post component 无法直接访问 key 的数据,所以我们在调用 Post 时单独定义一个 id 属性并赋值为 key 相同的数据,这样就间接的可以在 Post component 中通过 id 来读取 key 的数据。

在之前的 ListItem 示例中,我们声明了一个单独的 listItems 变量并在后续返回中将其放在 <ul> 中:

const NumberList = (props) => {
    const numbers = props.numbers;
    const listItems = numbers.map((number) =>
        <ListItem key={number.toString()} value={number}/>);
    return (
        <ul>{listItems}</ul>
    );
}

JSX 支持嵌入任何的 JavaScript 表达式,只需要使用大括号包围即可,所以上面的代码可以修改为以下模式:

    return (
        <ul>
            {numbers.map((number) =>
                <ListItem key={number.toString()} value={number} />);}
        </ul>
    );

使用哪种方式来定义 JSX 取决于对应的使用场景,总的原则是要方便与代码阅读,逻辑清晰。需要注意的是如果 map() method 中层级太复杂,可以考虑将其拆分为多个 components。

Forms 表格

HTML 的 form element 和其他 DOM elements 有点区别,因为 form element 包含有一些内部 state 数据,例如下面的 html 示例包含一个 from 表格:

<form>
    <label>
        Name:
        <input type="text" name="name">
    </label>
    <input type="submit" value="Submit">
</form>

以上示例中的 form 表格会有一个默认的 behavior 动作,那就是当用户点击 submit 按钮时会打开一个新页面。如果你不需要这个默认行为,同时需要提取 input 的信息时,需要在 submit event 事件发生时对其使用 preventDefault() method,标准的实现方法是通过 controlled components 可控构件 来处理。

controlled components

在 html 中,form 的元素如:<input>, <textarea>, 和 <select> 都有他们自己的 state 且随着用户输入信息而自动更新。在 react 中,可变的 state 存储在 component 的 state property 中且只能通过 setState() 更新。

我们可以将 from 元素的 state 和 component 的 state 合并起来作为唯一的数据来源,这样 component 既可以渲染 form 也可以控制 form 中的输入信息。一个 input 输入信息受 react component 控制的 form element 叫做 controlled component

如下示例中,我们构建一个 controlled component 来记录用户 input 的内容:

const React = require('react')
const ReactDOM = require('react-dom')

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(`a name has been submited: ${this.state.value}`);
        event.preventDefault();
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    Name:
                    <input type="text" value={this.state.value} onChange={this.handleChange} />
                </label>
                <input type="submit" value="Submit" />
            </form>
        );
    }
}

ReactDOM.render(
    <NameForm />,
    document.getElementById('root')
);

我们将 input 的 value 属性定义为 this.state.value 的值,这里显示的永远是当前 state value 的值。当 handleChange 被触发时,会将当前用户输入的内容更新到 state value 中,然后触发 render 更新 form。

通过 controlled component 可以使 form 中 input 的内容受控于 state,这样我们就可以操作输入的数据用于其他任何地方了。

textarea 标签

在 html 中我们使用 textarea 来定义一段文本区域:

<textarea>
  Hello there, this is some text in a text area
</textarea>

在 react 中,类似于上面的 input 标签,我们将文本内容放在 value 属性中,如下示例:

const React = require('react')
const ReactDOM = require('react-dom')

class NameForm extends React.Component {
    constructor(props) {
        super(props);
        this.state = {value: 'please write some words to discribe yourself'};

        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    };

    handleChange(event) {
        this.setState({value: event.target.value});
    }

    handleSubmit(event) {
        alert(`a discribe has been submited: ${this.state.value}`);
        event.preventDefault();
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    TextArea:
                    <textarea type="text" value={this.state.value} onChange={this.handleChange} />
                </label>
                <input type="submit" value="Submit" />
            </form>
        );
    }
}

ReactDOM.render(
    <NameForm />,
    document.getElementById('root')
);

注意我们在构造器中给他 state value 定义了初始值,这样在第一次访问页面时就会有一段默认文字了。

select 标签

在 html 中 select 标签可以创建一个下拉菜单控件:

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

注意上面的示例中,Coconut 选项会默认选中,因为其定义了 selected 属性。在 react 中我们可以在 select 根标签中直接定义 value 属性来定义当前选中的是哪一个 option。这在 controlled component 中可以很方便的管理及更新 select element 的 value:

const React = require('react')
const ReactDOM = require('react-dom')

class NameForm extends React.Component {
    constructor(props) {
        super(props);
        this.state = {value: 'sports'};

        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    };

    handleChange(event) {
        this.setState({value: event.target.value});
    }

    handleSubmit(event) {
        alert(`you favorite is: ${this.state.value}`);
        event.preventDefault();
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    your favorite:
                    <select value={this.state.value} onChange={this.handleChange}>
                        <option value="sleep">Sleep</option>
                        <option value="sports">Sports</option>
                        <option value="takePhoto">Take Photo</option>
                        <option value="work">Work</option>
                    </select>
                </label>
                <input type="submit" value="Submit" />
            </form>
        );
    }
}

ReactDOM.render(
    <NameForm />,
    document.getElementById('root')
);

以上示例中,我们将 state 的 value 赋值给 select 的 value 这样 select 当前选中项总是 state 中的值,在 handleChange 触发时会更新 state 中的 value 并 render 页面。

注意我们可以给 value 赋值一个数组,这样就可以同时选中多个 option:

<select multiple={true} value={['B', 'C']}>

以上几种 form 控件起始基本结构都类似,他们都核心概念就是将元素的 value 属性和 state 挂钩,从而使 controlled component 生效。

处理多个 input 输入源

当我们需要在 component 中同时处理多个 input 元素时,可以给每个 input 添加 name 属性,然后再对应的 handle function 中通过 event.target.name 来区分他们:

const React = require('react')
const ReactDOM = require('react-dom')

class NameForm extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            isGoing: true,
            numberOfGuests: 2
        };

        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    };

    handleChange(event) {
        const target = event.target;
        const value = target.type === 'checkbox' ? target.checked : target.value;
        const name = target.name;
        this.setState({[name]: value});
    }

    handleSubmit(event) {
        alert(`Is Going: ${this.state.isGoing}, Number Of Guests: ${this.state.numberOfGuests}`);
        event.preventDefault();
    }

    render() {
        return (
            <form onSubmit={this.handleSubmit}>
                <label>
                    Is Going:
                    <input
                    name="isGoing"
                    type="checkbox"
                    checked={this.state.isGoing}
                    onChange={this.handleChange} />
                </label>
                <br />
                <label>
                    Number of Guests:
                    <input
                    name="numberOfGuests"
                    type="number"
                    value={this.state.numberOfGuests}
                    onChange={this.handleChange} />
                </label>
                <br />
                <input type="submit" value="Submit" />
            </form>
        );
    }
}

ReactDOM.render(
    <NameForm />,
    document.getElementById('root')
);

以上示例中我们使用了 ES6 中新加入的特性: this.setState({[name]: value}); 再 object 中使用方括号 [] 来调用变量。setState() 会自动合并更新到 state 中。

input Null value

我们可以给一个 input 元素定义初值,默认情况下在页面加载完成后,input 框就可以立刻被用户进行编辑,有时候我们不希望再一开始就让用户修改 input 中的数据,此时可以临时给 input 的 value 属性赋值为 null 或 undefined 就可以了:

const React = require('react')
const ReactDOM = require('react-dom')

ReactDOM.render(
    <input value="hi" />,
    document.getElementById('root')
);

setTimeout(() => {
    ReactDOM.render(
        <input value={null} />,
        document.getElementById('root')
    );
}, 2000);

以上示例中,在页面刚加载的前 2 秒,用户无法修改默认的 hi 字符串。