{"id":19136972,"url":"https://github.com/singcl/react-hooks-todolist","last_synced_at":"2025-10-24T08:11:22.410Z","repository":{"id":94726512,"uuid":"381298414","full_name":"singcl/react-hooks-todolist","owner":"singcl","description":null,"archived":false,"fork":false,"pushed_at":"2021-08-31T03:27:55.000Z","size":598,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-22T18:23:01.540Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/singcl.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-29T08:47:19.000Z","updated_at":"2021-08-31T03:27:58.000Z","dependencies_parsed_at":null,"dependency_job_id":"bb8f4f3a-a00d-4e22-a8fa-f5bc71310df1","html_url":"https://github.com/singcl/react-hooks-todolist","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/singcl/react-hooks-todolist","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/singcl%2Freact-hooks-todolist","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/singcl%2Freact-hooks-todolist/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/singcl%2Freact-hooks-todolist/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/singcl%2Freact-hooks-todolist/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/singcl","download_url":"https://codeload.github.com/singcl/react-hooks-todolist/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/singcl%2Freact-hooks-todolist/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280761890,"owners_count":26386265,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-24T02:00:06.418Z","response_time":73,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-09T06:36:01.380Z","updated_at":"2025-10-24T08:11:22.345Z","avatar_url":"https://github.com/singcl.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Travis](https://travis-ci.com/singcl/react-hooks-todolist.svg?branch=master)\n# React Hooks 实现 todoList 程序 \nReact Hooks 是 React 16.8及以上版本新增的功能。该功能可以让React函数组件与类组件一样具有状态和生命周期。这使得我们用简单的函数组件就可以完成复杂的逻辑操作。  \nReact Hooks 是一组简单的 API，这些API只能运行在函数组件当中，不能运行在类组件中。本程序主要用到了以下API：  \n- `useState()` 管理状态的函数，返回一个数组。\n- `useEffect()` 这个函数可以处理副作用。而且可以用这个函数来模拟类组件中的生命周期函数 —— `componentDidMount`、`componentDidUpdata`以及`componentWillUnmount`。\n- `useRef()` 这个函数可以是我们在函数组件中获取DOM元素。\n- `useContext()` 这个函数可以更好的在函数组件中使用`context`。\n- `useCallback()` 这个函数类似于React当中的 `memo` 方法，使用它可以避免非必要渲染。  \n以上就是完成该项目使用到的 `React Hooks` ，而React自身的比较重要的Hooks大概就是那么多，当然还有别的Hooks，而且你也可以自定义Hook。可以 **[参考官网](https://react.docschina.org/docs/hooks-reference.html)** 。  \n\n## 项目最终程序\n以下的动图就是最终的运行程序：  \n\u003cimg alt=\"最终程序\" src=\"./src/img/qoe.gif\" /\u003e\n  \n## 项目初始化\n这里用的是React脚手架工具：`create-react-app`，自动生成框架后就可以编写程序了。在命令行输入 `yarn start` 命令查看运行结果。  \n### HTML结构\n在 App.js 文件中，删除无用的代码。写入以下代码：\n```js\nimport React from 'react';\nimport './App.css';\n\nfunction TodoList(){\n    return (\n        \u003cdiv className=\"wrapper\"\u003e\n            \u003ch1\u003etodoList\u003c/h1\u003e\n            \u003cTodoBox /\u003e\n            {/* TodoBox 用来盛放输入以及提交按钮 */}\n            \u003cList /\u003e\n            {/* List 用来盛放任务列表 */}\n        \u003c/div\u003e\n    );\n}\n\nfunction TodoBox(props){\n    return (\n        \u003cform className=\"todo-form\"\u003e\n        \u003cinput ref={ipt} type=\"text\" placeholder=\"请输入代办事务\" /\u003e\n        \u003cbutton\n            onClick={handleAddItem}\n            type=\"submit\"\n        \u003esubmit\u003c/button\u003e\n    \u003c/form\u003e\n    );\n}\n\nfunction List(props){\n    return (\n        \u003cul\u003e\n            {/*\n                这里渲染的每一项事务：\u003cItem /\u003e\n            */}\n        \u003c/ul\u003e\n    );\n}\n\nfunction Item(props){       // 渲染列表\n    return (\n        \u003cli className=\"todo-item\"\u003e\n        \u003cinput\n            onChange={handleChange}\n            type=\"checkbox\"\n        /\u003e\n        \u003clabel\u003e{info.item}\u003c/label\u003e\n        \u003cspan\n            onClick={handleRemove}\n            className=\"remove\"\n        \u003e\u0026#215;\u003c/span\u003e\n    \u003c/li\u003e\n    );\n}\n\nexport default TodoList;\n```\nCSS 样式可以参考这个文件 —— **[CSS 样式](./src/App.css)**  \n骨架搭建完毕下面开始编写逻辑代码。  \n## Hooks 的几个 API 介绍\n本文之介绍一下使用到的Hook，这几个也是很常用到的，特别是 `useState`、`useEffect`，下面一一介绍。  \n### `useState` \n这个方法，可以使你能在函数组件中使用状态。它有一个参数：状态的初始值，并返回一个数组，这个数组有两个元素，分别是这个状态的变量名和操作这个状态的方法。用法如下：\n```js\nconst [counter,setCounter] = useState(0);\n```\n注意这里的写法，看着有点怪，其实是用到了 ES6 中的解构赋值。这句话相当于：\n```js\nconst counterAry = useState(0),\n        counter = counterAry[0],\n        setCounter = counterAry[1];\n```\ncounter和setCounter变量名是可以随便取的。`useState(0)` 中的 0 就是将初始化 counter 的值为 0。  \n#### setCounter（第二个元素）的用法：\n在类组件中，我们不能使用setState直接改变state的值，在useState中也是如此。比如，我想让状态 counter 加 1，你不能这么干：\n```js\nsetState({\n    counter: counter ++\n});\n```\n而应：\n```js\nsetState({\n    counter: counter + 1\n});\n```\nsetCounter 是一个方法，里面可以传入一个语句，但不可以直接改变 state。应该这么来写：\n```js\nsetCounter(counter + 1);\n```\n以上就是 `useState()` API 的用法。接下来是 `useEffect`。  \n## `useEffect()` \n该方法，比 `useState()` 方法能理解一些。可以将副作用编写到该方法当中，比如：异步请求、DOM事件。在React的类组建当中，常把副作用编写到 `componentDidMount` 和 `componentDidUpdata` 两个生命周期函数中。`componentDidMount` 表示组件已经挂载完毕，这个函数只会被调用一次在组件生命周期中，因此成为异步请求不错的发起位置。而 `componentDidUpdata` 函数在函数每次更新后几乎都会被调用（当然除了使用`shouldComponentUpdata` 或者 `memo`函数可能不会更新组件），因此使用该函数可以完成一些频繁的副作用操作，比如：DOM的滚动事件、窗口事件等使状态频繁被触发的情况肯能就会用到。  \n那么在函数组件的Hook中又如何来实现呢？  \n### `useEffect()` 函数模拟 `componentDidMount` 函数\n`useEffect`函数接收两个参数，一个是 `callback` 另一个是个数组（可选）。这个数组很关键，它关系到前面的提到的生命周期函数的实现。先说第一个callback参数。这个参数就是为了处理副作用所设计的回调函数。在里面可以很好的处理副作用：\n```js\nuseEffect(() =\u003e {\n    axios.get('www.example.com/aaa?xxx').then((res) =\u003e {\n        // ....\n    });\n});\n```\n第二个参数是个可选的参数，如果不写，则说明 **每次挂载以及更新操作都会被出发回调函数**。这也就相当于 `componentDidMount + componentDidUpdata` 。而如果填入一个空数组 (`[]`) 则表示你的 effect 不依赖于 props 或 state 中的任何值，所以它永远都不需要重复执行。（相当于只在 `componentDidMount` 中执行）。如果传入元素，必须传入的是 `state` 或 `props` 变量，意思就是只有当 state 或 props 值改变才会触发回调函数，这麽做可以避免不必要的重复渲染。  \n以上就是对 `useEffect` 的整体介绍。  \n## `useRef` \n`useRef` 方法可以很方便的才函数组件中获取DOM。它返回一个可变的 ref 对象，其 .current 属性被初始化为传入的参数（initialValue）。返回的 ref 对象在组件的整个生命周期内保持不变。  \n#### 访问子组件：\n```js\nfunction Example(){\n    const div = useRef();\n    useEffect(() =\u003e {\n        div.current.innerText = 'Hooks';\n    },[]);\n    return (\n        \u003cdiv ref={div}\u003e\u003c/div\u003e\n    );\n}\n```\n在组件中ref值等于什么变量，在外部调用时变量名应一致，不然会报错。  \n## `useContext` \n`useContext` 方法相当于类组建当中的 `static contextType = MyContext` 以及 `const context = this.context` 两个语句。或者早期的 `\u003cContext.Consumer\u003e`。使用 `useContext()` 函数使代码更加简洁。语法：\n```js\nconst value = useContext(MyContext);\n```\n接收一个 context 对象（React.createContext 的返回值）并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 \u003cMyContext.Provider\u003e 的 value prop 决定。当然，前面的 `\u003cProvider\u003e` 和创建 `context` 方式并没有变，只是后面获取 value 值时更见方便了。  \n## `useCallback` \n该方法是为了优化函数的。与 `memo` 很像，只不过 `useCallback` 是为了不它的参数（一个函数）做不必要的重复调用，这个函数是个纯函数，可根据传入的props来判断要不要重复执行函数。因为在纯函数中，相同的参数返回的结果是相同的，因此没有必要对相同的参数做重复调用，也就避免的不必要的渲染。它的返回值还是它的回调函数，但是这个返回的函数具有记忆功能，就像React中的`memo`函数。  \n以上就是对各个 Hook 的功能介绍。下面步入正题：使用 Hooks 写一个 todoList 小程序。  \n## 整体构思\n这个程序很简单，输入内容，点击按钮，List 组件里就会多处一项事务。这个事务有三个功能：  \n1. 选中复选框，表示事务已经办完，不然就是表示未完成；\n2. 点击右边的红色×符号，该事务将会被移除；\n3. 能够接收新的事务数据，点击 submit 按钮后会更新 List 组件；  \n\n因此就有了这些方法：  \n+ `addItem(info)` 添加事务；\n+ `removeItem()` 移除事务；\n+ `toggleItem()` 变换事务，表示完成与否；  \n这里利用 ID 的便利性，通过查询 ID 来判断要移除或变换的是哪一个事务，因此，后两个方法需传入 id 作为参数。  \n## TodoList 组件的编写\nTodoList 是整个程序的最外层组件。它或多或少会传递一些 props 给里面的子组件，也会有自己的 state。代码如下：\n```js\nfunction TodoList(){\n\n    const [itemList,setitemList] = useState([]);\n    // 定义 state 初始化为一个空数组，用以存放事务的数据\n\n    // 添加事务\n    const addItem = useCallback((info) =\u003e {\n        // 这里使用扩展运算符，实现浅拷贝\n        // 因为 state 不能直接做更改\n        setitemList([...itemList, info]);\n    },[itemList]);\n\n    // 根据 ID 移除事务\n    // 这里使用数组的 filter 方法，\n    // 根据ID的不同过滤掉要移除的事务\n    // 值得注意的是，filter 方法返回的是一个新的数组，\n    // 原数组并没有改变\n    const removeItem = useCallback((id) =\u003e {\n        setitemList(itemList.filter(value =\u003e value.id !== id));\n    },[itemList]);\n\n    // 根据 ID 变换事务的完成状态\n    // 这里使用 map 在map前，使用concat方法实现数组拷贝\n    const toggle = useCallback((id) =\u003e {\n        setitemList([].concat(itemList).map(item =\u003e {\n        if (item.id === id) {\n            // 点击 checkbox 复选框时，完成变成未完成\n            // 未完成可变成完成状态\n            item.haveDone = !item.haveDone;\n        }\n        return item;\n        }));\n    },[itemList]);\n\n    // 渲染组件\n    return (\n        \u003cdiv className=\"wrapper\"\u003e\n        \u003ch1 className=\"title\"\u003etodoList\u003c/h1\u003e\n        {/* 注意这里的传值！\n            TodoBox 组件只做添加事务的功能\n        */}\n        \u003cTodoBox addItem={addItem}  /\u003e\n        {/* 这里传入 itemList 是为了渲染出事务列表 */}\n        \u003cList\n            infoList={itemList}\n            remove={removeItem}\n            toggle={toggle}\n        /\u003e\n        \u003c/div\u003e\n    );\n}\n\nexport default TodoList;\n```\n需要注意的是对事务的操作都使用了 `useCallback` 方法，这个方法前面已经提到，返回一个和其参数一样却具有 memoized 的函数，useCallback 的第二个参数是一个数组，这和 `useEffect` 的第二个参数一样，为了避免不必要的渲染。这三个 `useCallback` 的第三个参数都是 `itemList` 是因为，在useCallback函数中，state有且正好只有 `itemList` ，即：只有 itemList 发生改变后页面才做渲染。`itemList` 是个数组，它的每一项则是个对象，每个对象对应一个事务的状态，对象中的属性分别是：  \n- `id` 事务的唯一标识符；\n- `haveDone` 该事务是否已完成（默认是未完成）；\n- `name` 该事务的名称；  \n \n## `TodoBox` 的编写\nTodoList 组件初步完成，下面编写 `TodoBox` 组件，这个组件也是个容器组建，里面包含 输入框，为了输入一个待办项；还有一个按钮，为了提交这个待办项，提交后把结果渲染到 List 组件上，展示出待办项。   \n```js\nfunction TodoBox(props){\n\n    // 注意这里接收的props，是 addItem 方法\n    // useRef 用来获取元素\n    const ipt = useRef(null);\n\n    // 点击提交事务\n    function handleAddItem(e){\n        // 这里是为了阻止默认事件触发\n        // form 表单提交会默认跳转页面\n        e.preventDefault();\n        if(!ipt.current.value){\n            // 如果提交时发现 input 框没值，则什么都不做\n            return;\n        }\n        // 不然就添加事务\n        props.addItem({\n            name: ipt.current.value,\n            haveDone: false,\n            id: +new Date()\n        });\n        // 添加完后清空输入框\n        ipt.current.value = '';\n    }\n\n    return (\n        \u003cform className=\"todo-form\"\u003e\n        \u003cinput\n            ref={ipt}\n            type=\"text\"\n            placeholder=\"请输入代办事务\" \n        /\u003e\n        \u003cbutton\n            onClick={handleAddItem}\n            type=\"submit\"\n        \u003esubmit\u003c/button\u003e\n        \u003c/form\u003e\n    );\n}\n```\n## `List` 组件编写\nList 是为了展示待办项数据数据列表的组件。因此需要获取到 itemList 数组做迭代渲染。一个 list 大概长这样：  \n\n![](./src/img/list.png)  \n左边复选框为了改变状态，中间是事务名称，右边是删除事务按钮。因此List组件需要传入 `toggle`、`removeItem`、`itemList` 三个 props。  \n```js\nfunction List(props){\n    const { infoList, remove, toggle } = props;\n    return (\n    \u003cul className=\"listWrapper\"\u003e\n        {\n            infoList.length ? \n            infoList.map(item =\u003e {\n                return (\n                    \u003cItem \n                        key={item.id} \n                        remove={remove} \n                        toggle={toggle} \n                        info={item} \n                    /\u003e\n                )\n            }) : ''\n        }\n    \u003c/ul\u003e\n    );\n}\n```\n注意上面代码中有一句判断 `infoList.length` 有没有，没有当然什么都不渲染。  \n之后是 `Item` 组件：\n```js\nfunction Item(props){\n\n    const info = props.info;\n\n    // 移除事务\n    function handleRemove(){\n        props.remove(info.id);\n    }\n\n    // 切换事务状态\n    function handleChange(){\n        props.toggle(info.id);\n    }\n\n    return (\n    \u003cli className=\"todo-item\"\u003e\n        \u003cinput\n            onChange={handleChange}\n            type=\"checkbox\"\n            checked={info.haveDone} \n        /\u003e\n\n        \u003clabel\n            className={info.haveDone ? \"routine\" : \"\"}\n        \u003e{info.name}\n        \u003c/label\u003e\n\n        \u003cspan \n            onClick={handleRemove}\n            className=\"remove\"\n        \u003e\u0026#215;\u003c/span\u003e\n    \u003c/li\u003e\n    );\n}\n```\n代码完毕，`npm start` 运行程序，添加、切换、移除基本都好使，当刷新时，发现待办项全没了，这又该怎么办呢？这里就要用到 `useEffect` 方法，给程序添加副作用了。只需在 `TodoList` 组件里写入这几行即可：\n```js\nconst NOTES = 'notes'; \n\nuseEffect(() =\u003e {\n    let data = JSON.parse(localStorage.getItem(NOTES));\n    setitemList(data);\n  },[]);\n\n  useEffect(() =\u003e {\n    localStorage.setItem(NOTES,JSON.stringify(itemList));\n  },[itemList]);\n```\n注意，两个 `useEffect` 函数是不能调换顺序的，原因如下：  \n1. 第一个 useEffect 函数是为了从本地存储中找数据，找数据定是在组件挂载之后，即 `componentDidMount` 函数中调用。因此useEffect第二个参数是个空数组。  \n2. 第二个 `useEffect` 函数是设置新的存储数据在 `itemList` 的值变化后就会执行。  \n3. 如果两者调换，当再次刷新时，是先设置存储数据，再获取数据。先设置数据，此时 `itemList` 的值是空数组，因此再获取数据时，获取的就会是空的数据。因此两者不能调换\n\n还有一个需要注意的地方，就是当输入相同的内容时，List 不应该渲染相同的数据项，因此需要去重，只需在 `addItem` 重新写一些去重即可：\n```js\nconst addItem = useCallback((info) =\u003e {\n    // find 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined\n    if(itemInfo.find((elem) =\u003e elem.name === info.name)){\n      return;\n    }\n    setItemInfo([...itemInfo, info]);\n  },[itemInfo]);\n```\n说了那么多，还有一个 Hook 没有用到，那就是 `useContext` 该方法配合 `context` 使用，使得无需为每层组件手动添加 props，就能在组件树间进行数据传递。  \n很明显，在 todoList 程序中，TodoList 组件传递的部分 props 经过了两次才传递给 最里层 `Item` 组件。因此可以使用 context 进行跨层传递 porps 。\n全局写入：\n```js\nimport React,{ useState, useRef ,useEffect,useContext , useCallback, createContext} from 'react';\nimport './App.css';\n\nconst TodoContext = createContext();\n```\n`TodoList` 组件的 return 部分改写成：\n```js\nreturn (\n    \u003cdiv className=\"wrapper\"\u003e\n      \u003ch1 className=\"title\"\u003etodoList\u003c/h1\u003e\n      \u003cTodoBox addItem={addItem}  /\u003e\n      {/* value 部分使用了对象的简写语法 */}\n      \u003cTodoContext.Provider value={{itemList,removeItem,toggle}}\u003e\n        \u003cList /\u003e\n      \u003c/TodoContext.Provider\u003e\n    \u003c/div\u003e\n);\n```\n`List` 部分写入：\n```js\nfunction List(){\n    const context = useContext(TodoContext);\n    // context 是个对象\n    return (\n    \u003cul className=\"listWrapper\"\u003e\n        {\n        context.itemList.length === 0 ? '' :\n        context.itemList.map(item =\u003e {\n            return (\n            \u003cItem key={item.id} info={item} /\u003e\n            );\n        })\n        }\n    \u003c/ul\u003e\n    );\n}\n```\n`Item` 组件写入：\n```js\nconst info = props.info;\n    const context = useContext(TodoContext);\n\n    function handleRemove(){\n        context.removeItem(info.id);\n    }\n\n    function handleChange(){\n        context.toggle(info.id);\n    }\n    // return 部分不变\n    return (\n        // ....\n    );\n}\n```\n\n// 可以看到，使用 useContext 还是可以的，只是会多出一个全局变量。以上就是 TodoList 程序的编写内容。  \n最后，还可以使用 `memo` 对函数组件进行优化，避免不必要的渲染：\n```js\nimport { memo } from 'react';\n\nconst List = memo(/* \n    之前编写的 List 函数内容\n*/);\n```\n\nmemo 功能与 `useCallback`很相似，有两个参数。这里不做多介绍。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsingcl%2Freact-hooks-todolist","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsingcl%2Freact-hooks-todolist","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsingcl%2Freact-hooks-todolist/lists"}