[React 2021新书]107、组件思维

358 阅读5分钟

React新手村

组件思维 - 创建一个todo app

本节我们将学习:

  • 如何将一个app拆分成一个一个的组件
  • 概念介绍:presentation/dumb组件和容器组件

组件思维简单来说就是将app拆分成一个一个的组件。以搭建一个简单待办事项应用程序为例,它可以拆分很多组件,我们一起来完成它。

todos列表

我们从待办事项列表开始设计,在考虑展示数据前,先设计底层数据结构。

首先待办事项有描述字段,然后考虑如何完成一个待办事项。待办事项应该包含一个事项列表,因为我们希望在某个时间点执行清单上的一些事项。在事项完成后,是把事项删除还是标记完成呢?我们选择后者,为了让自己对完成它感到满意,同事还可以随时查看以前所做的事情。我们的数据结构的信息。它很可能看起来像这样:

[
  {
    title: 'clean',
    done: false
  }, {
    title: 'do dishes',
    done: false
  }
]

草拟的数据结构设计完成,尝试在 React 中渲染它。

渲染列表

先来试一试:

{todos.map(todo => (
  <div>
    <input type="checkbox" checked={todo.done} /> {todo.title}
  </div>
)}

呈现了待办事项列表,但不能修改待办事项的值,我们需要构建一个真正的 React 组件类,添加事项更改功能。

import React, { Component } from 'react';
import styled from 'styled-components';

const todos = [
  {
    title: 'clean',
    done: false,
  },
  {
    title: 'do the dishes',
    done: true,
  }
];

const Todos = styled.div`
  padding: 30px;
`;

const Todo = styled.div`
  box-shadow: 0 0 5px gray;
  padding: 30px;
  margin-bottom: 10px;
`;

class App extends Component {
  render() {
    return (
      <Todos>
        <h2>Todos</h2>
        {todos.map(todo => (
        <Todo>
          <input type="checkbox" checked={todo.done} /> {todo.title}
        </Todo>
        ))}
      </Todos>
    );
  }
}

export default App;

需要安装 styled-components 支持 npm install --save styled-components

上面我们已经创建了一个完整的组件,但还没有添加对更改待办事项的支持。让我们接下来这样做。我们需要考虑:

  • 为 复选框 添加监听 onChange 事件
  • 修改待办事项,调用事件绑定事件的方法

使用箭头函数 绑定 onChange事件 和监听函数:

<input type="checkbox" checked={todo.done} onChange={() => this.handleChange(todo)} />

然后找出一种方法来更改列表中的待办事项。我们可以直接改变 todos 列表,但在 React更好的方式是在组件中创建一个 todos 状态,就像这样:

state = {
  todos
}

最终代码:

import React, { Component } from 'react';
import styled from 'styled-components';

const todos = [
  {
    title: 'clean',
    done: false,
    id: 1,
  },
  {
    title: 'do the dishes',
    done: true,
    id: 2,
  }
];

const Todos = styled.div`
  padding: 30px;
`;

const Todo = styled.div`
  box-shadow: 0 0 5px gray;
  padding: 30px;
  margin-bottom: 10px;
`;

class App extends Component {
  state = {
    todos,
  };

  handleChecked = (todo) => {
    const newTodos = this.state.todos.map(t => {
      if (t.id === todo.id) {
        return { ...t, done: !t.done };
      }
      return t;
    });

    this.setState({
      todos: newTodos,
    });
  }

render() {
  return (
    <Todos>
      <h2>Todos</h2>
      {this.state.todos.map(todo => (
        <Todo key={todo.id}>
        <input type="checkbox" onChange={() => this.handleChecked(todo)} checked={todo.done} />
      {todo.title}
      </Todo>
      ))}
    </Todos>
  );
}
}

export default App;

handleChecked()onChange绑定的方法,点击修改复选框:

handleChecked = (todo) => {
  const newTodos = this.state.todos.map(t => {
    if (t.id === todo.id) {
    return { ...t, done: !t.done };
    }
    return t;
  });

  this.setState({
    todos: newTodos,
  });
}

通过遍历找到被点击的待办事项,使用对象展开操作符修改:

return { ...t, done: !t.done }

注意todo 现在由三个属性组成:

  • title
  • done
  • id

id 属性用来标识修改哪个 todo.

创建 todos 组件

现在我们完成了App的基本功能,把所有功能逻辑放到一起看起来很混乱,现在我们通过创建 Todos 组件来重构代码,代码将拆分成下面的文件:

  • App.js - App入口文件
  • Todos.js - 列表逻辑文件

将列表逻辑文件 挪到 Todos.js

// Todos.js

import React, { Component } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';

const TodosContainer = styled.div`
  padding: 30px;
`;

const Todo = styled.div`
  box-shadow: 0 0 5px gray;
  padding: 30px;
  margin-bottom: 10px;
`;

class Todos extends Component {
  static propTypes = {
    todos: PropTypes.array.isRequired,
  }

  state = {
    todos: this.props.todos,
  };

  handleChecked = (todo) => {
    const newTodos = this.state.todos.map(t => {
      if (t.id === todo.id) {
        return { ...t, done: !t.done };
      }
      return t;
    });

    this.setState({
      todos: newTodos,
    });
}

  render() {
    return (
    <TodosContainer>
      <h2>Todos</h2>
      {this.state.todos.map(todo => (
        <Todo key={todo.id}>
          <input type="checkbox" onChange={() => this.handleChecked(todo)} checked={todo.done} /> {todo.title}
        </Todo>
      ))}
    </TodosContainer>
    );
  }
}

export default Todos;

自从React 15.5.0后,PropTypes需要用户自己引入 npm install --save prop-types

App入口文件如下:

import React, { Component } from 'react';

import Todos from './Todos';

const todos = [
  {
    title: 'clean',
    done: false,
    id: 1,
  },
  {
    title: 'do the dishes',
    done: true,
    id: 2,
  }
];

class App extends Component {
  render() {
    return (
    <Todos todos={todos} />
    );
  }
}

export default App;

拆分 todos 组件

我们看到Todos.js组件包含了不少内容. 我们可以继续按照责任原则拆分它. 虽然它运行起来没问题,但通常我们会把它拆分成更小的组件,这样更容易管理和扩展。另外,更小的组件可以让组件更易于被调用。如何拆分Todos.js 组件呢?

  • Todos组件, 渲染Todo组件列表,负责处理所有事件
  • Todo组件,负责渲染Todo和向上发送任何更改操作

重构代码,添加Todo.js :

// Todo.js

import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';

const TodoContainer = styled.div`
  box-shadow: 0 0 5px gray;
  padding: 30px;
  margin-bottom: 10px;
`;

const Todo = ({ todo, handleChecked }) => (
  <TodoContainer key={todo.id}>
    <input type="checkbox" onChange={() => handleChecked(todo)} checked={todo.done} />
    {todo.title}
  </TodoContainer>
);

Todo.propTypes = {
  todo: PropTypes.shape({
    title: PropTypes.string,
    done: PropTypes.bool,
    id: PropTypes.number,
  }),
  handleChecked: PropTypes.func,
};

export default Todo;

把Todo放到单独的组件中。该组件不继承React,是一个简单的函数组件,通常称为演示或哑组件,它对上下文一无所知,只依赖于输入。 todo的结果依赖于 传入的方法· handleChecked() 和 props。

Todos.js 修改为:

// Todos.js

import React, { Component } from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';

import Todo from './Todo';

const TodosContainer = styled.div`
  padding: 30px;
`;

class Todos extends Component {
  static propTypes = {
    todos: PropTypes.array.isRequired,
  }

  state = {
    todos: this.props.todos,
  };

  handleChecked = (todo) => {
    const newTodos = this.state.todos.map(t => {
    if (t.id === todo.id) {
      return { ...t, done: !t.done };
    }
    return t;
  });

  this.setState({
    todos: newTodos,
  });
}

  render() {
    return (
      <TodosContainer>
        <h2>Todos</h2>
        {this.state.todos.map(todo => (
        <Todo todo={todo} key={todo.id} handleChecked={this.handleChecked} />
        ))}
      </TodosContainer>
    );
  }
}

export default Todos;

Todos.js 引入Todo组件:

import Todo from './Todo';

调用Todo组件

<Todo todo={todo} key={todo.id} handleChecked={this.handleChecked} />

上述代码中,通过传入 数据 todo 和方法 handleChecked() 给 Todo 组件,在组件中完成渲染,这种组件称为展示组件。

还有一种组件叫容器组件,容器组件是数据和逻辑所在的组件,它“包含”了应用程序的内容。

在我们的App中 Todo组件是展示组件,Todos组件是容器组件。践行组件思维构建的App应该包含大部分展示组件和少部分容器组件。

总结

创建了一个待办事项App。

把App拆分成若干个专注某一项功能的小组件,小的组件便于后期维护和调用,也更简单。

介绍了两个术语,展示/哑组件和容器组件。解释了它们的概念,它们的区别处决于传入的内容。