react概念

本文深入讲解React框架的核心概念,如JSX语法、组件与Props、State及生命周期、事件处理、条件渲染、列表与Keys、表单组件、状态提升、组合与继承等。通过实例解析,帮助读者掌握React的最佳实践。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

参考网站

https://react.docschina.org/docs/state-and-lifecycle.html

jsx

//jsx

//因为 JSX 的特性更接近 JavaScript 而不是 HTML , 所以 React DOM 使用 camelCase 小驼峰命名 来定义属性的名称,而不是使用 HTML 的属性名称。
//例如,class 变成了 className,而 tabindex 则对应着 tabIndex。

// javasctrip表达式用{}包裹
// 整个jsx用()包裹
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';



function a() {
    return (
        <div>
            {/* 这里的className */}
            <a href="#" className="active">111</a>
        </div>
    );
}

// Babel 转译器会把 JSX 转换成一个名为 React.createElement() 的方法调用。
const element = (
    <h1 className="greeting">
        Hello, world!
    </h1>
);
//等同
const element = React.createElement(
    'h1',
    { className: 'greeting' },
    'Hello, world!'
);

// React.createElement() 这个方法首先会进行一些避免bug的检查,之后会返回一个类似下面例子的对象:
// 注意: 以下示例是简化过的(不代表在 React 源码中是这样)
// 这样的对象被称为 “React 元素”。
const element = {
    type: 'h1',
    props: {
        className: 'greeting',
        children: 'Hello, world'
    }
};

元素渲染

要将React元素渲染到根DOM节点中,我们通过把它们都传递给 ReactDOM.render() 的方法来将其渲染到页面上:

const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));

更新元素渲染

 React 元素都是immutable 不可变的。

 新界面的唯一办法是创建一个新的元素,然后将它传入 ReactDOM.render() 方法:

function tick() {
    const element = (
        <div>
            <h1>Hello, world!</h1>
            <h2>It is {new Date().toLocaleTimeString()}.</h2>
        </div>
    );
    ReactDOM.render(element, document.getElementById('root'));
}

setInterval(tick, 1000);

React 只会更新必要的部分

React DOM 首先会比较元素内容先后的不同,而在渲染过程中只会更新改变了的部分。

根据我们以往的经验,将界面视为一个个特定时刻的固定内容(就像一帧一帧的动画),而不是随时处于变化之中(而不是处于变化中的一整段动画),将会有利于我们理清开发思路,减少各种bug。

组件 & Props

当React遇到的元素是用户自定义的组件,它会将JSX属性作为单个对象传递给该组件,这个对象称之为“props”。

组件名称必须以大写字母开头

function Welcome(props) {
    return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
    element,
    document.getElementById('root')
);

组件的返回值只能有一个根元素。这也是我们要用一个<div>来包裹所有<Welcome />元素的原因。

function App() {
    return (
        <div>
            <Welcome name="Sara" />
            <Welcome name="Cahal" />
            <Welcome name="Edite" />
        </div>
    );
}

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

Props的只读性

无论是使用函数或是类来声明一个组件,它决不能修改它自己的props。来看这个sum函数:

纯函数”,它没有改变它自己的输入值,当传入的值相同时,总是会返回相同的结果。

function sum(a, b) {
    return a + b;
}

“非纯函数”,它会改变它自身的输入值

function withdraw(account, amount) {
    account.total -= amount;
}

所有的React组件必须像纯函数那样使用它们的props。(单向传递数据的基础,只能父传子且只有父能修改)

 

State & 生命周期

使用类就允许我们使用其它特性,例如局部状态、生命周期钩子

 

class Clock extends React.Component {
  // 类组件应始终使用props调用基础构造函数。
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }
  // 每当Clock组件第一次加载到DOM中的时候,我们都想生成定时器,这在React中被称为挂载
  // 挂在事件钩子
  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  // 同样,每当Clock生成的这个DOM被移除的时候,我们也会想要清除定时器,这在React中被称为卸载。
  // 卸载事件钩子
  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {
    this.setState({
      date: new Date()
    });
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

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

应当使用 setState():

// Correct
this.setState({comment: 'Hello'});

构造函数是唯一能够初始化 this.state 的地方。

状态更新可能是异步的

React 可以将多个setState() 调用合并成一个调用来提高性能。

因为 this.props 和 this.state 可能是异步更新的,你不应该依靠它们的值来计算下一个状态。

例如,此代码可能无法更新计数器:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

要修复它,请使用第二种形式的 setState() 来接受一个函数而不是一个对象。 该函数将接收先前的状态作为第一个参数将此次更新被应用时的props做为第二个参数

// Correct
this.setState((prevState, props) => ({
  counter: prevState.counter + props.increment
}));

状态更新合并

当你调用 setState() 时,React 将你提供的对象合并到当前状态。

你可以调用 setState() 独立地更新它们:

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts
      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments
      });
    });
  }

这里的合并是浅合并,也就是说this.setState({comments})完整保留了this.state.posts,但完全替换了this.state.comments

数据自顶向下流动

这就是为什么状态通常被称为局部或封装。 除了拥有并设置它的组件外,其它组件不可访问。

组件可以选择将其状态作为属性传递给其子组件:

<h2>It is {this.state.date.toLocaleTimeString()}.</h2>

这也适用于用户定义的组件:

<FormattedDate date={this.state.date} />

FormattedDate 组件将在其属性中接收到 date 值,并且不知道它是来自 Clock 状态、还是来自 Clock 的属性、亦或手工输入

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

这通常被称为自顶向下单向数据流。 任何状态始终由某些特定组件所有,并且从该状态导出的任何数据或 UI 只能影响树中下方的组件。

在 React 应用程序中,组件是有状态还是无状态被认为是可能随时间而变化的组件的实现细节。 可以在有状态组件中使用无状态组件,反之亦然。

事件处理

React 元素的事件处理和 DOM元素的很相似。但是有一点语法上的不同:

  • React事件绑定属性的命名采用驼峰式写法,而不是小写。
  • 如果采用 JSX 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串(DOM元素的写法)

例如,传统的 HTML:

<button onclick="activateLasers()">
  Activate Lasers
</button>

React 中稍稍有点不同:

<button onClick={activateLasers}>
  Activate Lasers
</button>

在 React 中另一个不同是你不能使用返回 false 的方式阻止默认行为。你必须明确的使用 preventDefault

function ActionLink() {
  // e 是一个合成事件。React 根据 W3C spec 来定义这些合成事件,所以你不需要担心跨浏览器的兼容性问题。
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

当你使用 ES6 class 语法来定义一个组件的时候,事件处理器会成为类的一个方法。

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // This binding is necessary to make `this` work in the callback
    // es6特性,类的方法默认是不会绑定 this 的。如果你忘记绑定 this.handleClick 并把它传入 onClick, 当你调用这个函数的时候 this 的值会是 undefined
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn
    }));
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

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

属性初始化器语法,你可以使用属性初始化器来正确的绑定回调函数

class LoggingButton extends React.Component {
  // This syntax ensures `this` is bound within handleClick.
  // Warning: this is *experimental* syntax.
  // 将一个匿名函数付给handleClick
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

如果你没有使用属性初始化器语法,你可以在回调函数中使用 箭头函数

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // This syntax ensures `this` is bound within handleClick
    return (
       // 使用 箭头函数
      <button onClick={(e) => this.handleClick(e)}>
        Click me
      </button>
    );
  }
}

然而如果这个回调函数作为一个属性值传入低阶组件,这些组件可能会进行额外的重新渲染。我们通常建议在构造函数中绑定或使用属性初始化器语法来避免这类性能问题

向事件处理程序传递参数

通常我们会为事件处理程序传递额外的参数。例如,若是 id 是你要删除那一行的 id,以下两种方式都可以向事件处理程序传递参数:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

通过箭头函数的方式,事件对象必须显式的进行传递,但是通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

值得注意的是,通过 bind 方式向监听函数传参,在类组件中定义的监听函数,事件对象 e 要排在所传递参数的后面,例如:

class Popper extends React.Component{
    constructor(){
        super();
        this.state = {name:'Hello world!'};
    }
    
    preventPop(name, e){    //事件对象e要放在最后
        e.preventDefault();
        alert(name);
    }
    
    render(){
        return (
            <div>
                <p>hello</p>
                {/* Pass params via bind() method. */}
                {/* this.state.name为preventPop方法的第一个参数   */}   
                <a href="https://reactjs.org" onClick={this.preventPop.bind(this,this.state.name)}>Click</a>
            </div>
        );
    }
}

条件渲染

使用 JavaScript 操作符 if 或条件运算符来创建表示当前状态的元素

function UserGreeting(props) {
    return <h1>Welcome back!</h1>;
}

function GuestGreeting(props) {
    return <h1>Please sign up.</h1>;
}


function Greeting(props) {
    const isLoggedIn = props.isLoggedIn;
    if (isLoggedIn) {
        return <UserGreeting />;
    }
    return <GuestGreeting />;
}
ReactDOM.render(
    // Try changing to isLoggedIn={true}:
    <Greeting isLoggedIn={false} />,
    document.getElementById('root')
);

元素变量

function LoginButton(props) {
    return (
        <button onClick={props.onClick}>
            Login
      </button>
    );
}

function LogoutButton(props) {
    return (
        <button onClick={props.onClick}>
            Logout
      </button>
    );
}

class LoginControl extends React.Component {
    constructor(props) {
        super(props);
        this.handleLoginClick = this.handleLoginClick.bind(this);
        this.handleLogoutClick = this.handleLogoutClick.bind(this);
        this.state = { isLoggedIn: false };
    }

    handleLoginClick() {
        this.setState({ isLoggedIn: true });
    }

    handleLogoutClick() {
        this.setState({ isLoggedIn: false });
    }

    render() {
        const isLoggedIn = this.state.isLoggedIn;
        // 你可以使用变量来储存元素(此处用button变量)。它可以帮助你有条件的渲染组件的一部分,而输出的其他部分不会更改。
        let button = null;
        if (isLoggedIn) {
            button = <LogoutButton onClick={this.handleLogoutClick} />;
        } else {
            button = <LoginButton onClick={this.handleLoginClick} />;
        }

        return (
            <div>
                <Greeting isLoggedIn={isLoggedIn} />
                {button}
            </div>
        );
    }
}

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

与运算符 &&

function Mailbox(props) {
    const unreadMessages = props.unreadMessages;
    return (
        <div>
            <h1>Hello!</h1>
            {/* 因为在 JavaScript 中,true && expression 总是返回 expression,而 false && expression 总是返回 false。 */}
            {unreadMessages.length > 0 &&
                <h2>
                    You have {unreadMessages.length} unread messages.
          </h2>
            }
        </div>
    );
}

const messages = ['React', 'Re: React', 'Re:Re: React'];
ReactDOM.render(
    <Mailbox unreadMessages={messages} />,
    document.getElementById('root')
);

三目运算符

function sanmu(props) {
    const isLoggedIn =true;
    return (
        <div>
            {isLoggedIn ? (
                <LogoutButton onClick={this.handleLogoutClick} />
            ) : (
                    <LoginButton onClick={this.handleLoginClick} />
                )}
        </div>
    );
}

阻止组件渲染

在极少数情况下,你可能希望隐藏组件,即使它被其他组件渲染。让 render 方法返回 null 而不是它的渲染结果即可实现。

function WarningBanner(props) {
    // render 方法返回 null 并不会影响该组件生命周期方法的回调。例如,componentWillUpdate 和 componentDidUpdate 依然可以被调用
    if (!props.warn) {
      return null;
    }
  
    return (
      <div className="warning">
        Warning!
      </div>
    );
  }
  
  class Page extends React.Component {
    constructor(props) {
      super(props);
      this.state = {showWarning: true}
      this.handleToggleClick = this.handleToggleClick.bind(this);
    }
  
    handleToggleClick() {

      // setState((prevState, props) => ({})
      this.setState(prevState => ({
        showWarning: !prevState.showWarning
      }));
    }
  
    render() {
      return (
        <div>
          <WarningBanner warn={this.state.showWarning} />
          <button onClick={this.handleToggleClick}>
            {this.state.showWarning ? 'Hide' : 'Show'}
          </button>
        </div>
      );
    }
  }
  
  ReactDOM.render(
    <Page />,
    document.getElementById('root')
  );

列表 & Keys

渲染多个组件

我们使用Javascript中的map()方法遍历numbers数组。对数组中的每个元素返回<li>标签,最后我们得到一个数组listItems

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

我们把整个listItems插入到ul元素中,然后渲染进DOM:

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

Keys

Keys可以在DOM中的某些元素被增加或删除的时候帮助React识别哪些元素发生了变化。因此你应当给数组中的每一个元素赋予一个确定的标识。

一个元素的key最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用来自数据的id作为元素的key:

const todoItems = todos.map((todo) =>
  <li key={todo.id}>
    {todo.text}
  </li>
);

当元素没有确定的id时,你可以使用他的序列号索引index作为key

const todoItems = todos.map((todo, index) =>
  // Only do this if items have no stable IDs
  <li key={index}>
    {todo.text}
  </li>
);

如果列表项目的顺序可能会变化,我们不建议使用索引来用作键值,因为这样做会导致性能的负面影响,还可能引起组件状态问题。

用keys提取组件

如果你提取出一个ListItem组件,你应该把key保存在数组中的这个<ListItem />元素上,而不是放在ListItem组件中的<li>元素上(哪里有map哪里加key

function ListItem(props) {
  // 对啦!这里不需要指定key:
  return <li>{props.value}</li>;
}

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 又对啦!key应该在数组的上下文中被指定
    <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')
);

key会作为给React的提示,但不会传递给你的组件。如果您的组件中需要使用和key相同的值,请用其他属性名显式传递这个值:(key是不会被子组件prop读取的

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

在jsx中嵌入map()

在上面的例子中,我们声明了一个单独的listItems变量并将其包含在JSX中

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

JSX允许在大括号中嵌入任何表达式,所以我们可以在map()中这样使用:

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

表单

受控组件

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

我们通过使react变成一种单一数据源的状态来结合二者。React负责渲染表单的组件仍然控制用户后续输入时所发生的变化。相应的,其值由React控制的输入表单元素称为“受控组件”(通过绑定事件改变state值)。

例如,我们想要使上个例子中在提交表单时输出name,我们可以写成“受控组件”的形式:

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 was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
           {/* 每次按键都会触发 handleChange 来更新当前React的state */}
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

使用”受控组件”,每个状态的改变都有一个与之相关的处理函数。这样就可以直接修改或验证用户输入。

textarea 标签

在React中,<textarea>会用value属性来代替。这样的话,表单中的<textarea> 非常类似于使用单行输入的表单:

class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 'Please write an essay about your favorite DOM element.'
    };

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

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

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
            {/* value属性来赋值 */}
          <textarea value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

select 标签

在React中,并不使用之前的selected属性,而在根select标签上value属性来表示选中项。这在受控组件中更为方便,因为你只需要在一个地方来更新组件。例如:

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

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

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

  handleSubmit(event) {
    alert('Your favorite flavor is: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Pick your favorite La Croix flavor:
            {/* 用value属性来表示选中项 */}
          <select value={this.state.value} onChange={this.handleChange}>
            <option value="grapefruit">Grapefruit</option>
            <option value="lime">Lime</option>
            <option value="coconut">Coconut</option>
            <option value="mango">Mango</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

总之,<input type="text"><textarea>, 和 <select> 都十分类似 - 他们都通过传入一个value属性来实现对组件的控制

file input 标签

<input type="file" />

由于该标签的 value 属性是只读的, 所以它是 React 中的一个非受控组件。我们会把它和其他非受控组件一起在后面的章节进行详细的介绍。

多个输入的解决方法

当你有处理多个受控的input元素时,你可以通过给每个元素添加一个name属性,来让处理函数根据 event.target.name的值来选择做什么。

例如:

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

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    // 使用ES6当中的计算属性名语法来更新与给定输入名称相对应的状态键:
    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

同样由于 setState() 自动将部分状态合并到当前状态,因此我们只需要使用发生变化的部分调用它。

受控组件的替代方法

有时使用受控组件可能很繁琐,因为您要为数据可能发生变化的每一种方式都编写一个事件处理程序,并通过一个组件来管理全部的状态。当您将预先存在的代码库转换为React或将React应用程序与非React库集成时,这可能变得特别烦人。在以上情况下,你或许应该看看非受控组件,这是一种表单的替代技术。

状态提升

使用 react 经常会遇到几个组件需要共用状态数据的情况。这种情况下,我们最好将这部分共享的状态提升至他们最近的父组件当中进行管理。我们来看一下具体如何操作吧。

const scaleNames = {
  c: 'Celsius',
  f: 'Fahrenheit'
};

function toCelsius(fahrenheit) {
  return (fahrenheit - 32) * 5 / 9;
}

function toFahrenheit(celsius) {
  return (celsius * 9 / 5) + 32;
}

function tryConvert(temperature, convert) {
  const input = parseFloat(temperature);
  if (Number.isNaN(input)) {
    return '';
  }
  const output = convert(input);
  const rounded = Math.round(output * 1000) / 1000;
  return rounded.toString();
}

function BoilingVerdict(props) {
  if (props.celsius >= 100) {
    return <p>The water would boil.</p>;
  }
  return <p>The water would not boil.</p>;
}

class TemperatureInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e) {
    this.props.onTemperatureChange(e.target.value);
  }

  render() {
    const temperature = this.props.temperature;
    const scale = this.props.scale;
    return (
      <fieldset>
        <legend>Enter temperature in {scaleNames[scale]}:</legend>
        <input value={temperature}
               onChange={this.handleChange} />
      </fieldset>
    );
  }
}

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }

  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

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

经验教训

在React应用中,对应任何可变数据理应只有一个单一“数据源”。通常,状态都是首先添加在需要渲染数据的组件中。然后,如果另一个组件也需要这些数据,你可以将数据提升至离它们最近的共同祖先中。你应该依赖 自上而下的数据流,而不是尝试在不同组件中同步状态。

状态提升要写更多的“炉墙代码”,比起双向绑定方式,但带来的好处是,你也可以花更少的工作量找到和隔离bug。因为任何生活在某些组件中的状态数据,也只有该组件它自己能够操作这些数据,发生bug的表面积就被大大地减小了。此外,你也可以使用自定义逻辑来拒绝(reject)或转换(transform)用户的输入。

如果某些数据可以由props或者state推导出来,那么它很有可能不应该在state中出现。举个例子,我们没有同时保存 celsiusValue 和 fahrenheitValue,而只是保存最新编辑的temperature和它的scale值。另一个输入框中的值总是可以在 render() 函数中由这些保存的数据计算出来。这样我们在不损失任何用户输入精度的情况下,可以对另一字段清除或应用四舍五入。

当你在开发UI界面遇到问题时,你可以使用 React 开发者工具来检查props属性,并且可以点击查看组件树,直到你找到负责目前状态更新的组件。这能让你到追踪到产生 bug 的源头。

Monitoring State in React DevTools

组合 vs 继承

React 具有强大的组合模型,我们建议使用组合而不是继承来复用组件之间的代码。

包含关系

一些组件不能提前知道它们的子组件是什么。这对于 Sidebar 或 Dialog 这类通用容器尤其常见。

我们建议这些组件使用 children 属性将子元素直接传递到输出。(有点像vue的匿名插槽)

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}

这样做还允许其他组件通过嵌套 JSX 来传递子组件。

function WelcomeDialog() {
  return (
    <FancyBorder color="blue">
       // FancyBorder 组件内部的jsx将作为prop.children传入组件
      <h1 className="Dialog-title">
        Welcome
      </h1>
      <p className="Dialog-message">
        Thank you for visiting our spacecraft!
      </p>
    </FancyBorder>
  );
}

<FancyBorder> JSX 标签内的任何内容都将通过 children 属性传入 FancyBorder。由于 FancyBorder 在一个 <div> 内渲染了 {props.children},所以被传递的所有元素都会出现在最终输出中。

组件中有多个入口,这种情况下你可以使用自己约定的属性而不是 children:(vue的具名插槽)

function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}

类似 <Contacts /> 和 <Chat /> 这样的 React 元素都是对象,所以你可以像任何其他元素一样传递它们。

特殊实例

在 React 中,这也是通过组合来实现的,通过配置属性用较特殊的组件来渲染较通用的组件。

function Dialog(props) {
  return (
    <FancyBorder color="blue">
      <h1 className="Dialog-title">
        {props.title}
      </h1>
      <p className="Dialog-message">
        {props.message}
      </p>
      {props.children}
    </FancyBorder>
  );
}

class SignUpDialog extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.handleSignUp = this.handleSignUp.bind(this);
    this.state = {login: ''};
  }

  render() {
    return (
      <Dialog title="Mars Exploration Program"
              message="How should we refer to you?">
        <input value={this.state.login}
               onChange={this.handleChange} />
        <button onClick={this.handleSignUp}>
          Sign Me Up!
        </button>
      </Dialog>
    );
  }

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

  handleSignUp() {
    alert(`Welcome aboard, ${this.state.login}!`);
  }
}

那么继承呢?

在 Facebook 网站上,我们的 React 使用了数以千计的组件,然而却还未发现任何需要推荐你使用继承的情况。

属性和组合为你提供了以清晰和安全的方式自定义组件的样式和行为所需的所有灵活性。请记住,组件可以接受任意元素,包括基本数据类型、React 元素或函数。

如果要在组件之间复用 UI 无关的功能,我们建议将其提取到单独的 JavaScript 模块中。这样可以在不对组件进行扩展的前提下导入并使用该函数、对象或类。

React 理念

第一步:把 UI 划分出组件层级

如果你的模型构建正确,你的 UI (以及你的组件结构)会被很好的映射。这是因为 UI 和数据模型往往遵循着相同的信息架构,这意味着将 UI 划分成组件的工作往往是很容易的。只要把它划分成能准确表示你数据模型的一部分的组件就可以。

Component diagram

在这里你会看到,我们的简单应用中有5个组件。我们把每个组件展示的数据用斜体表示。

  1. FilterableProductTable (橙色): 包含了整个例子
  2. SearchBar (蓝色): 接受所有的用户输入
  3. ProductTable (绿色): 根据用户输入过滤并展示数据集合
  4. ProductCategoryRow (绿松石色): 展示每个分类的标题
  5. ProductRow (红色): 用行来展示每个产品

现在我们已经确定了原型图中的组件,让我们把它们整理成层级结构。这很容易。原型图中的子组件在层级结构中应该作为子节点。

  • FilterableProductTable

    • SearchBar
    • ProductTable

      • ProductCategoryRow
      • ProductRow

第二步:用 React 创建一个静态版本

先创建一个静态版本:传入数据模型,渲染 UI 但没有任何交互。最好把这些过程解耦,因为创建一个静态版本更多需要的是码代码,不太需要逻辑思考,而添加交互则更多需要的是逻辑思考,不是码代码。

要构建一个用于呈现数据模型的静态版本的应用程序,你需要创建能够复用其他组件的组件,并通过 props 来传递数据props 是一种从父级向子级传递数据的方法。如果你熟悉 state 的概念, 在创建静态版本的时候不要使用 state。State 只在交互的时候使用,即随时间变化的数据。由于这是静态版本的应用,你不需要使用它。

 

在这步的最后,你会拥有一个用于呈现数据模型的可重用组件库。这些组件只会有 render()方法,因为这只是你的应用的静态版本。层级最高的组件(FilterableProductTable)会把数据模型作为 prop 传入如果你改变你的基础数据模型并且再次调用 ReactDOM.render(), UI 会更新。很容易看到你的 UI 是如何更新的,哪里进行了更新。因为没有什么复杂的事情发生。React 的单向数据流(也叫作单向绑定)保证了一切是模块化并且是快速的。

第三步:定义 UI 状态的最小(但完整)表示

为了使你的 UI 交互,你需要能够触发对底层数据模型的更改。React 使用 state,让这变的更容易。

为了正确构建你的应用,首先你需要考虑你的应用所需要的最小可变状态集。要点是 DRY:不要重复(Don’t Repeat Yourself)。找出应用程序的绝对最小表示并计算你所需要的其他任何请求。

想想我们的实例应用中所有数据。我们有:

  • 原产品列表
  • 用户输入的搜索文本
  • 复选框的值
  • 产品的筛选列表

让我们来看看每一条,找出哪一个是 state。每个数据只要考虑三个问题:

  1. 它是通过 props 从父级传来的吗?如果是,他可能不是 state。
  2. 它随着时间推移不变吗?如果是,它可能不是 state。
  3. 你能够根据组件中任何其他的 state 或 props 把它计算出来吗?如果是,它不是 state。

原产品列表被作为 props 传入,所以它不是 state。搜索文本和复选框似乎是 state,因为它们随时间改变并且不能由其他任何值计算出来。最后,产品的筛选列表不是 state,因为它可以通过将原始产品列表与搜索文本和复选框的值组合计算出来。

最后,我们的 state 有:

  • 用户输入的搜索文本
  • 复选框的值

第四步:确定你的 State 应该位于哪里

现在我们确定了应用 state 的最小集合。接下来,我们需要确定哪个组件会改变,或拥有这个 state。

确认哪个组件应该拥有哪个 state

  • 确定每一个需要这个 state 来渲染的组件。
  • 找到一个公共所有者组件(一个在层级上高于所有其他需要这个 state 的组件的组件)
  • 这个公共所有者组件或另一个层级更高的组件应该拥有这个 state。
  • 如果你没有找到可以拥有这个 state 的组件,创建一个仅用来保存状态的组件并把它加入比这个公共所有者组件层级更高的地方

让我们用这个策略分析我们的应用:

  • ProductTable 需要根据 state 过滤产品列表,SearchBar 需要展示搜索文本和复选框状态。
  • 公共所有者组件是 FilterableProductTable
  • 筛选文本和复选框的值应该放在 FilterableProductTable

很酷,所以我们决定把 state 放在 FilterableProductTable。首先,为 FilterableProductTable 的 constructor 添加一个实例属性 this.state = {filterText: '', inStockOnly: false} 来表示我们应用的初始状态。接下来,把 filterText 和 inStockOnly 作为 prop 传入 ProductTable 和 SearchBar。最后在 ProductTable 中使用这些 props 来筛选每行产品信息,在 SearchBar 中设置表单域的值。

第五步:添加反向数据流

到目前为止,我们已经创建了一个可以正确渲染的应用程序,它的数据在层级中通过函数的 props 和 state 向下流动。现在是时候支持其他方式的数据流了:层级结构中最底层的表单组件需要去更新在 FilterableProductTable 中的 state

React 的数据流很明显,让你可以很轻松的了解你的程序是如何运行的,但相较于传统的双向绑定,它的代码量会稍微多一点。

我们想确保每当用户更改表单时,我们更新状态来反应用户输入。因为组件应该只更新自己的状态, FilterableProductTable 会将一个回调函数传递给 SearchBar ,每当应该更新状态时,它就会触发。我们可以使用输入上的 onChange 事件来调用它。FilterableProductTable 传入的回调函数会调用 setState(),这时应用程序会被更新。

希望这可以让你了解如何使用 React 构建组件和应用程序。虽然这可能会比以前编写更多的代码,但请记住,代码是用来读的,这比写更重要,并且非常容易阅读这个模块化的,思路清晰的代码。当你开始构建大型组件库的时候,你会开始欣赏 React 的思路清晰化和模块性,并且随着代码的复用,你的代码量会开始减少。:)

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值