使用 React hooks 构建待办事项列表应用

学习使用函数式组件和状态管理构建 React 应用。
90 位读者喜欢这篇文章。
Team checklist and to dos

React 是最流行和简单的 JavaScript 库之一,用于构建用户界面 (UI),因为它允许您创建可重用的 UI 组件。

React 中的组件是独立的、可重用的代码片段,作为应用程序的构建块。React 函数式组件是 JavaScript 函数,它将表示层与业务逻辑分离开来。根据 React 文档,一个简单的函数式组件可以这样编写:

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

React 函数式组件是无状态的。无状态组件被声明为没有状态并返回相同标记的函数(给定相同的 props)。状态在组件中使用 hooks 进行管理,hooks 在 React 16.8 中引入。它们实现了函数式组件的状态和生命周期管理。有几个内置的 hooks,您也可以创建自定义 hooks。

本文解释了如何使用函数式组件和状态管理在 React 中构建一个简单的待办事项应用。此应用的完整代码可在 GitHubCodeSandbox 上找到。完成本教程后,该应用将如下所示:

先决条件

  • 要在本地构建,您必须安装 Node.js v10.16 或更高版本,yarn v1.20.0 或更高版本,以及 npm 5.6
  • JavaScript 的基本知识
  • 对 React 的基本了解将有所帮助

创建一个 React 应用

Create React App 是一个允许您开始构建 React 应用的环境。在本教程中,我使用了 TypeScript 模板来添加静态类型定义。TypeScript 是一种构建在 JavaScript 之上的开源语言

npx create-react-app todo-app-context-api --template typescript

npx 是一个包运行器工具;或者,您可以使用 yarn

yarn create react-app todo-app-context-api --template typescript

执行此命令后,您可以导航到该目录并运行该应用

cd todo-app-context-api
yarn start

您应该看到启动器应用和 React 徽标,这是由样板代码生成的。由于您正在构建自己的 React 应用,因此您将能够修改徽标和样式以满足您的需求。

构建待办事项应用

待办事项应用可以:

  • 添加项目
  • 列出项目
  • 将项目标记为已完成
  • 删除项目
  • 根据状态(例如,已完成、全部、活动)筛选项目

Header 组件

创建一个名为 components 的目录,并添加一个名为 Header.tsx 的文件

mkdir components
cd  components
vi  Header.tsx

Header 是一个函数式组件,用于保存标题

const Header: React.FC = () => {
    return (
        <div className="header">
            <h1>
                Add TODO List!!
            </h1>
        </div>
        )
}

AddTodo 组件

AddTodo 组件包含一个文本框和一个按钮。单击该按钮会将项目添加到列表中。

components 目录下创建一个名为 todo 的目录,并添加一个名为 AddTodo.tsx 的文件

mkdir todo
cd todo 
vi AddTodo.tsx

AddTodo 是一个接受 props 的函数式组件。Props 允许单向传递数据,即仅从父组件到子组件

const AddTodo: React.FC<AddTodoProps> = ({ todoItem, updateTodoItem, addTaskToList }) => {
    const submitHandler = (event: SyntheticEvent) => {
        event.preventDefault();
        addTaskToList();
    }
    return (
        <form className="addTodoContainer" onSubmit={submitHandler}>
            <div  className="controlContainer">
                <input className="controlSpacing" style={{flex: 1}} type="text" value={todoItem?.text ?? ''} onChange={(ev) => updateTodoItem(ev.target.value)} placeholder="Enter task todo ..." />
                <input className="controlSpacing" style={{flex: 1}} type="submit" value="submit" />
            </div>
            <div>
                <label>
                    <span style={{ color: '#ccc', padding: '20px' }}>{todoItem?.text}</span>
                </label>
            </div>
        </form>
    )
}

您已经创建了一个名为 AddTodo 的函数式 React 组件,它接受父函数提供的 props。这使得该组件可重用。需要传递的 props 是:

  • todoItem: 一个空的待办事项状态
  • updateToDoItem: 一个辅助函数,用于在用户键入时向父组件发送回调
  • addTaskToList: 一个将项目添加到待办事项列表的函数

还有一些样式和 HTML 元素,例如 form、input 等。

TodoList 组件

要创建的下一个组件是 TodoList。它负责列出待办事项状态中的项目,并提供删除和标记项目为已完成的选项。

TodoList 将是一个函数式组件

const TodoList: React.FC = ({ listData, removeItem, toggleItemStatus }) => {
    return listData.length > 0 ? (
        <div className="todoListContainer">
            { listData.map((lData) => {
                return (
                    <ul key={lData.id}>
                        <li>
                            <div className="listItemContainer">
                                <input type="checkbox" style={{ padding: '10px', margin: '5px' }} onChange={() => toggleItemStatus(lData.id)} checked={lData.completed}/>
                                <span className="listItems" style={{ textDecoration: lData.completed ? 'line-through' : 'none', flex: 2 }}>{lData.text}</span>
                                <button type="button" className="listItems" onClick={() => removeItem(lData.id)}>Delete</button>
                            </div>
                        </li>
                    </ul>
                )
            })}
        </div>
    ) : (<span> No Todo list exist </span >)
}

TodoList 也是一个可重用的函数式 React 组件,它接受来自父函数的 props。需要传递的 props 是:

  • listData: 待办事项列表,包含 ID、文本和已完成属性
  • removeItem: 一个辅助函数,用于从待办事项列表中删除项目
  • toggleItemStatus: 一个函数,用于在已完成和未完成之间切换任务状态

还有一些样式和 HTML 元素(例如列表、input 等)。

Footer 将是一个函数式组件;在 components 目录下创建它,如下所示:

cd ..


const Footer: React.FC = ({item = 0, storage, filterTodoList}) => {
    return (
        <div className="footer">
            <button type="button" style={{flex:1}} onClick={() => filterTodoList(ALL_FILTER)}>All Item</button>
            <button type="button" style={{flex:1}} onClick={() => filterTodoList(ACTIVE_FILTER)}>Active</button>
            <button type="button" style={{flex:1}} onClick={() => filterTodoList(COMPLETED_FILTER)}>Completed</button>
            <span style={{color: '#cecece', flex:4, textAlign: 'center'}}>{item} Items | Make use of {storage} to store data</span>
        </div>
    );
}

它接受三个 props:

  • item: 显示项目数量
  • storage: 显示文本
  • filterTodoList: 一个函数,用于根据状态(活动、已完成、所有项目)筛选任务

Todo 组件:使用 contextApi 和 useReducer 管理状态

Context 提供了一种在组件树中传递数据的方法,而无需在每个级别手动传递 props。ContextApiuseReducer 可用于管理状态,方法是在整个 React 组件树中共享状态,而无需将其作为 prop 传递给树中的每个组件。

现在您已经有了 AddTodo、TodoList 和 Footer 组件,您需要将它们连接起来。

使用以下内置 hooks 来管理组件的状态和生命周期:

  • useState: 返回状态值和更新器函数以更新状态
  • useEffect: 帮助管理函数式组件中的生命周期并执行副作用
  • useContext: 接受一个 context 对象并返回当前 context 值
  • useReducer: 与 useState 类似,它返回状态值和更新器函数,但当您有复杂的状态逻辑时(例如,多个子值或新状态取决于前一个状态时),它将代替 useState 使用

首先,使用 contextApiuseReducer hooks 管理状态。为了关注点分离,在 components 下添加一个名为 contextApiComponents 的新目录

mkdir contextApiComponents
cd contextApiComponents

创建 TodoContextApi.tsx

const defaultTodoItem: TodoItemProp = { id: Date.now(), text: '', completed: false };

const TodoContextApi: React.FC = () => {
    const { state: { todoList }, dispatch } = React.useContext(TodoContext);
    const [todoItem, setTodoItem] = React.useState(defaultTodoItem);
    const [todoListData, setTodoListData] = React.useState(todoList);

    React.useEffect(() => {
        setTodoListData(todoList);
    }, [todoList])

    const updateTodoItem = (text: string) => {
        setTodoItem({
            id: Date.now(),
            text,
            completed: false
        })
    }
    const addTaskToList = () => {
        dispatch({
            type: ADD_TODO_ACTION,
            payload: todoItem
        });
        setTodoItem(defaultTodoItem);
    }
    const removeItem = (id: number) => {
        dispatch({
            type: REMOVE_TODO_ACTION,
            payload: { id }
        })
    }
    const toggleItemStatus = (id: number) => {
        dispatch({
            type: UPDATE_TODO_ACTION,
            payload: { id }
        })
    }
    const filterTodoList = (type: string) => {
        const filteredList = FilterReducer(todoList, {type});
        setTodoListData(filteredList)

    }

    return (
        <>
            <AddTodo todoItem={todoItem} updateTodoItem={updateTodoItem} addTaskToList={addTaskToList} />
            <TodoList listData={todoListData} removeItem={removeItem} toggleItemStatus={toggleItemStatus} />
            <Footer item={todoListData.length} storage="Context API" filterTodoList={filterTodoList} />
        </>
    )
}

此组件包括 AddTodoTodoListFooter 组件及其各自的辅助函数和回调函数。

为了管理状态,它使用 contextApi,它提供状态和 dispatch 方法,而 dispatch 方法又更新状态。它接受一个 context 对象。(接下来,您将创建 context 的 provider,称为 contextProvider)。

 const { state: { todoList }, dispatch } = React.useContext(TodoContext);

TodoProvider

添加 TodoProvider,它创建 context 并使用 useReducer hook。useReducer hook 接受一个 reducer 函数以及初始值,并返回状态和更新器函数 (dispatch)。

  • 创建 context 并导出它。导出它将允许任何子组件使用 hook useContext 获取当前状态
    export const TodoContext = React.createContext({} as TodoContextProps);
  • 创建 ContextProvider 并导出它
    const TodoProvider : React.FC = (props) => {
        const [state, dispatch] = React.useReducer(TodoReducer, {todoList: []});
        const value = {state, dispatch}
        return (
            <TodoContext.Provider value={value}>
                {props.children}
            </TodoContext.Provider>
        )
    }
  • 如果您使用 provider(例如 TodoProvider)包装父组件(例如 TodoContextApi)或应用本身,则层次结构中的任何 React 组件都可以使用 useContext hook 直接访问 context 数据
    <TodoProvider>
      <TodoContextApi />
    </TodoProvider>
  • TodoContextApi 组件中,使用 useContext hook 访问当前 context 值
    const { state: { todoList }, dispatch } = React.useContext(TodoContext)

TodoProvider.tsx

type TodoContextProps = {
    state : {todoList: TodoItemProp[]};
    dispatch: ({type, payload}: {type:string, payload: any}) => void;
}

export const TodoContext = React.createContext({} as TodoContextProps);

const TodoProvider : React.FC = (props) => {
    const [state, dispatch] = React.useReducer(TodoReducer, {todoList: []});
    const value = {state, dispatch}
    return (
        <TodoContext.Provider value={value}>
            {props.children}
        </TodoContext.Provider>
    )
}

Reducers

Reducer 是一个没有副作用的纯函数。这意味着对于相同的输入,预期的输出将始终相同。这使得 reducer 更容易在隔离状态下进行测试并有助于管理状态。TodoReducerFilterReducer 在组件 TodoProviderTodoContextApi 中使用。

src 下创建一个名为 reducers 的目录,并在其中创建一个名为 TodoReducer.tsx 的文件

const TodoReducer = (state: StateProps = {todoList:[]}, action: ActionProps) => {
    switch(action.type) {
        case ADD_TODO_ACTION:
            return { todoList: [...state.todoList, action.payload]}
        case REMOVE_TODO_ACTION:
            return { todoList: state.todoList.length ? state.todoList.filter((d) => d.id !== action.payload.id) : []};
        case UPDATE_TODO_ACTION:
            return { todoList: state.todoList.length ? state.todoList.map((d) => {
                if(d.id === action.payload.id) d.completed = !d.completed;
                return d;
            }): []}
        default:
            return state;
    }
}

创建一个 FilterReducer 以维护筛选器的状态

const FilterReducer =(state : TodoItemProp[] = [], action: ActionProps) => {
    switch(action.type) {
        case ALL_FILTER:
            return state;
        case ACTIVE_FILTER:
            return state.filter((d) => !d.completed);
        case COMPLETED_FILTER:
            return state.filter((d) => d.completed);
        default:
            return state;
    }
}

您已经创建了所有必需的组件。接下来,您将在 App 中添加 HeaderTodoContextApi 组件,并将 TodoContextApiTodoProvider 一起添加,以便所有子组件都可以访问 context。

function App() {
  return (
    <div className="App">
      <Header />
      <TodoProvider>
              <TodoContextApi />
      </TodoProvider>
    </div>
  );
}

确保 App 组件在 index.tsx 中的 ReactDom.render 内。ReactDom.render 接受两个参数:React Element 和 HTML 元素的 ID。React Element 在网页上呈现,id 指示哪个 HTML 元素将被 React Element 替换

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

结论

您已经学习了如何使用 hooks 和状态管理在 React 中构建一个函数式应用。您将用它做什么呢?

接下来阅读什么
标签
Jai
我是一位开源软件爱好者,在 Red Hat 开发者工程组担任高级软件工程师。我为 OpenShift、fabric8-analytics 等项目以及围绕 IDE、用户界面/Web 的技术做出贡献。

评论已关闭。

Creative Commons License本作品根据 知识共享许可协议 4.0 版本国际许可协议授权。
© . All rights reserved.