React实战_实现待办事项TodoList(Hook版)

企业级产品的开发流程中,产品经理将表达交互的原型图给设计师,设计师最终将UI图提供给工程师,工程师此时需要将UI图分解,逆推需要实现的功能。如何合理分解UI图,正是本系列文章的核心。通过合理地分解UI图,确定组件功能的界限,帮助工程师探索和养成自己的React编程最佳实践。

目录

一、功能分析

二、数据考虑

三、组件拆分

四、目录结构

五、组件实现

AddTodo组件

TodoList组件

TodoItem组件

Todos组件

Filter组件

Link组件

六、总结


一、功能分析

TodoList设计图
图一 TodoList设计图

图一是TodoList设计图,从界面上看分“上、中、下”三个部分:

  • 上:新增待办
  • 中:待办列表
  • 下:改变列表分类
功能分析
图二 TodoList功能分析

 

二、数据分析

当“上”部分添加数据后,“中”部分会显示新增的数据,因此考虑使用数组list保存待办事项数据,具体的每一项是个JS对象

let list = [
{
    // 待办事项JS对象
}, 
{}, 
...];

当“下”部分切换类型之后,“中”部分会切换显示相应类型的待办事项,因此考虑JS对象具有completed字段保存状态,且具有name字段保存事项名称,以及id作为唯一标识。

let plan = {
    id: 0,            // 唯一标识
    name: 'xxx',      // 待办事项名称
    completed: false  // 待办事项完成状态
}

当然,我们需要一个全局状态filter保存“下”部分中选择的类型,便于“中”部分显示相应类型的列表数据。

最终得到组件状态的数据结构如下,这个状态可以考虑放在组件顶层

let state = {
  list =[
    {
      id: 0,            // 唯一标识
      name: 'xxx',      // 待办事项名称
      completed: false  // 待办事项完成状态
    },
    {},
    ...],

  filter: 'all'
};

 

三、组件拆分

最简单的拆分方式,按照界面可以粗粒度地拆为三部分:

  • AddTodo组件:对应界面“上”部分
  • TodoList组件:对应界面“中”部分
  • Filter组件:对应界面“下”部分

深入一点思考,细化组件的功能:

组件拆分
图三 组件拆分

AddTodo组件,可以改变列表list,因此有个方法去执行这个改变;

TodoList组件,可以显示list,因此有个状态list保存数据;可以改变列表list,因此有个方法去执行这个改变;

Filter组件,可以显示当前选中类型filter,因此有个状态filter保存数据;可以改变选中类型filter,因此有个方法去执行这个改变。

更进一步,AddTodo组件和TodoList组件关联度很高,可以考虑都放在Todos组件下;TodoList组件中的每行看起来都是差不多,可以考虑复用成TodoItem组件;Filter组件中的每个按钮也差不多,可以考虑复用成Link组件。

可以看到,显示功能必定对应状态,改变功能必定对应方法,这里涉及组件React组件设计方法,可以参考笔者另一篇博文《React实战_如何设计高质量组件》。

 

四、目录结构

filter文件夹下存放Filter组件相关文件,具有views文件夹和index.js文件。

todos文件夹下存放Todos组件相关文件,具有views文件夹和index.js文件。

views文件夹下存放更细粒度的组件,index.js文件作为组件模块的统一入口(目前仅输出views,后期可扩展输出状态)。

图四 目录结构

五、组件实现

第二部分中说过,将组件的状态统一放在顶层,通过props将传递给子组件。根据“分而治之”的思想,我们并不是将整个state都放在根组件,而是将list放在Todos组件,filter放在Filter组件分别管理,这样能避免混乱。

AddTodo组件

前面分析过,AddTodo组件具有改变list功能,我们需要创建一个组件内部的方法clickHandler,其中能获取input的值,并执行父组件的方法,用于改变父组件中的list。

import React, { createRef } from 'react';
const inputRef = createRef();                // 使用hook

export default function AddTodo({ onAdd }) {
   
    const clickHandler = () => {
        let value = inputRef.current.value.trim();

        if (value) {
            // 执行父组件的方法,修改list数据
            onAdd(value);
            // 添加后将input框置空,提高用户体验
            inputRef.current.value = "";
        }
    }

    return (
        <React.Fragment>
            <input ref={inputRef} />
            <button onClick={clickHandler}>添加</button>
        </React.Fragment>
    )
}

TodoList组件

TodoList组件具有显示特定类型list和改变list功能,我们可以在Todos组件创建方法clickHandler,将其作为props传递给TodoList组件,TodoList以及子组件均为“受控组件”。显然TodoList依赖list和filter做渲染,也能通过props从父组件获取。

import React from 'react';
import TodoItem from './todo-item';
import { FilterTypes } from '../../constants';

export default function TodoList({ list, onClick, filter }) {


    return (
        <ul>
            {
                list.filter(item => {                        // 根据filter过滤不符合的类型
                    if (filter === FilterTypes.COMPLETED) {
                        return item.completed
                    } else if (filter === FilterTypes.UNCOMPLETED) {
                        return !item.completed
                    } else {
                        return item
                    }
                }).map(item => <TodoItem                    // 渲染符合条件的TodoItem
                    key={item.id}
                    text={item.text}
                    onClick={() => onClick(item.id)}
                    completed={item.completed} />)
            }
        </ul>
    )
}

TodoItem组件

TodoItem组件是完全受控的组件,它的显示内容完全由props决定,根据UI图我们可以知道它需要text、completed和onClick属性。

import React from 'react';

export default function TodoItem({ text, onClick, completed }) {
    return (
        <div onClick={onClick}>
            <span>{text}&nbsp;&nbsp;&nbsp;&nbsp;</span>
            <span>
                完成:
                <input type="checkbox" readOnly checked={completed} />
            </span>
        </div>
    )
}

Todos组件

Todos组件相对复杂点,因为list和clickHandler方法都放在里面,它将AddTodo组件和TodoList组件关联了起来。

import React, { useState } from 'react';
import deepcopy from 'deepcopy';

import AddTodo from './add-todo';
import TodoList from './todo-list';

export default function ({ filter }) {
    // 整个todos组件中的状态list保存在这里
    const [list, setList] = useState([]);

    // 传递给AddTodo组件
    const addHandler = (text) => {
        let item = {
            id: (Math.random() * 10000).toFixed(0),
            text: text,
            completed: false
        }
        setList([...list, item]);
    }
    // 传递给能TodoList组件
    const clickHandler = (id) => {
        let newList = deepcopy(list);
        newList.forEach(item => {
            if (item.id === id) {
                item.completed = !item.completed;
            }
        });
        setList(newList);
    }

    return (
        <div className="todos">
            {/* 渲染AddTodo组件 */}
            <AddTodo onAdd={addHandler} />

            {/* 渲染TodoList组件 */}    
            <TodoList list={list} filter={filter} onClick={clickHandler} />
        </div>
    )
}

Filter组件

Filter组件具有显示和改变filter类型的功能,我们可以在TodoApp根组件创建方法setFilter,将其作为props传递给Filter组件,Filter以及子组件均为“受控组件”。显然Filter依赖filter做渲染,也能通过props从父组件获取。

import React from 'react';
import Link from './link';
import { FilterTypes } from '../../constants';

const types = Object.keys(FilterTypes);

export default function Filter({ filter, setFilter }) {

    return (
        <div style={{ display: 'flex' }}>
            {
                types.map(
                    type =>
                        <Link
                            key={type}
                            type={type}
                            filter={filter}
                            onClick={setFilter} />)
            }
        </div>
    )
}

Link组件

Link组件具有显示当前状态功能,其状态完全由父组件控制,可以通过props从父组件获取filter。

import React from 'react';
import { FilterTypes } from '../../constants';

export default function Link({ filter, onClick, type }) {
    return (
        <div
            className={filter === FilterTypes[type] ? 'link link--active' : 'link'}
            onClick={() => onClick(FilterTypes[type])}>
            {type}
        </div>
    )
}

六、总结

本文主要介绍了如何通过React实现,完全基于函数式组件的待办事项TodoList。假设工程师的起点都是拿到UI图,本文对TodoList的UI图进行了功能分析、数据分析,在此基础上进行了组件拆分,定下了初步的目录结构,最终按照之前的分析工作实现各个粒度的组件,大的组件都封装成一个模块,由统一的index.js文件暴露给调用者,保证了组件的高内聚、低耦合。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值