搭建项目
npm create vite@latest
下载依赖,然后启动
编写应用基本结构
将scr/App.jsx改为一下代码:
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import './App.css'
function App() {
return (
<div className='bg'>
<h2>
我的待办事项<img src={reactLogo}></img>
</h2>
<ul>
<li className='item'>学习vue</li>
<li className='item'>学习react</li>
</ul>
</div>
)
}
export default App
循环输出项目
用一个数组表示所有todo项
const todoList = ['vue', 'react', '后台管理系统', '组件源码']
,然后渲染到页面上。
<>
<h2>
我的待办事项<img src={reactLogo}></img>
</h2>
<ul>
{
todoList.map(item => <li className='item' key={item}>{item}</li>)
}
</ul>
</>
为todo加入是否完成字段
todo是否完成需要有一个字段来判断,所以改下todoList的结构
const todoList = [
{ title: 'vue', completed: true, id: 1 },
{ title: 'react', completed: false, id: 2 },
{ title: '后台管理系统', completed: false, id: 3 },
{ title: '组件源码', completed: false, id: 4 },
]
顺便改下视图结构。
<>
<h2>
我的待办事项<img src={reactLogo}></img>
</h2>
<ul>
{
todoList.map(item => <li className='item' key={item.id}>{item.title}</li>)
}
</ul>
</>
用checkbox来显示todo是否完成
用户的todo是否完成可以用checkbox来显示。
为了让我们改变todo时视图也会跟着变化,这里用react自带的hook useState将todoList包裹。
这里村长也提到了受控组件的概念:react的state作为表单的唯一数据源,同时表单触发的一系列事件也由react处理。
const [todoList, setTodoList] = useState([
{ title: 'vue', completed: true, id: 1 },
{ title: 'react', completed: false, id: 2 },
{ title: '后台管理系统', completed: false, id: 3 },
{ title: '组件源码', completed: false, id: 4 },
])
function changeState(e, item) {
// 因为是引用数据类型,所以todoList也会跟着改变
item.completed = e.target.checked
// 想要让视图更新,必须setTodoList一下
// 这样是不会触发视图更新的,因为react比较的时候会发现和原来的值相同就不去更新了。
// setTodoList(todoList)
setTodoList([...todoList])
}
<>
<h2>
我的待办事项<img src={reactLogo}></img>
</h2>
<ul>
{
todoList.map(item => {
return (
<li className='item' key={item.id}>
<input type='checkbox' checked={item.completed} onChange={(e) => changeState(e, item)}/>
<span>{item.title}</span>
</li>
)
})
}
</ul>
</>
效果如下
新增待办事项
这同样是受控组件的应用。
添加一个输入框:
<input
className="new-todo"
autoFocus
autoComplete="off"
placeholder="该学啥了?"
value={newTodo}
onChange={changeNewTodo}
onKeyUp={addTodo}
/>
对应的状态和事件控制:
const [newTodo, setNewTodo] = useState("")
function changeNewTodo(e) {
setNewTodo(e.target.value);
}
// 用户回车且输入框有内容则添加一个新待办
function addTodo(e) {
if (e.code === 'Enter' && newTodo) {
setTodoList([
...todoList,
{
id: todoList.length + 1,
title: newTodo,
completed: false,
},
]);
setNewTodo("");
}
};
删除待办事项
新增一个删除按钮:
<button className='x-button' onClick={() => removeTodo(item)}>X</button>
修改状态,filter会返回一个新的数组
function removeTodo(item) {
setTodoList(todoList.filter(todoItem => todoItem.id !== item.id))
}
修改待办事项
想要实现的功能:双击title,进入编辑模式。按回车如果输入不为空,则修改成功,否则提示标题不能为空。失去焦点退出编辑模式时,会提示确认修改。
首先修改下视图,用一个editedTodo表示当前被编辑的todo副本。
<li className='item' key={item.id}>
<input className='checkbox' type='checkbox' checked={item.completed} onChange={(e) => changeState(e, item)}/>
{
editedTodo.id !== item.id ?
<>
<span onDoubleClick={(e) => {onDoubleClick(item)}}>{item.title}</span>
<button className='x-button' onClick={() => removeTodo(item)}>X</button>
</>
:
<>
<input
type='text'
value={editedTodo.title}
onChange={onEditing}
onKeyUp={onEditComplete}
onBlur={onEditBlur}
/>
</>
}
</li>
实现相关js逻辑
// 当前正在被编辑的todo的副本
const [editedTodo, setEditedTodo] = useState(inital)
// 双击进入编辑模式
function onDoubleClick(item) {
setEditedTodo({...item})
}
// 编辑输入
function onEditing(e) {
const title = e.target.value;
setEditedTodo({ ...editedTodo, title });
}
// 按回车编辑完成
function onEditComplete(e) {
if(e.code === 'Enter') {
const title = e.target.value;
if(!title) {
alert('title不能为空!')
} else {
noName(title)
}
setEditedTodo(inital)
}
}
// 无名函数,作用为修改todoList
function noName(title) {
const todo = todoList.find(item => item.id === editedTodo.id)
todo.title = title
setTodoList([...todoList])
}
// 编辑时失去焦点
function onEditBlur(e) {
const title = e.target.value;
if(!title) {
alert('title不能为空!')
} else {
if(confirm('确认修改吗?')) {
noName(title)
}
}
setEditedTodo(inital)
}
自动获取焦点
现在进入编辑模式后,还不能自动获取焦点。
虽然我发现只要在编辑模式下的input上加一个autoFocus属性就可以实现了,不用ref。
不过在这里还是用ref实现下,顺便复习下ref和useEffect的用法。
<input
ref={e => setEditInputRef(e, item)}
...
/>
js逻辑
let inputRef = null
const setEditInputRef = (e, todo) => {
if (editedTodo.id === todo.id) {
inputRef = e
}
}
useEffect(() => {
if (editedTodo.id) {
inputRef.focus()
}
}, [editedTodo])
状态持久化
存储在本地
const STORAGE_KEY = 'todomvc-react'
const todoStorage = {
fetch () {
// get到了一个新写法,以前我都是用三元运算符写的,很麻烦
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
return todos
},
save (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
}
const [todoList, setTodoList] = useState(todoStorage.fetch())
useEffect(() => {
todoStorage.save(todoList)
}, [todoList])
提取组件
如果是vue的话,我一般不习惯把这种列表封装成组件,但是经常把列表项封装为一个组件。像下面这样:
<ListItem {...{isEditing: editedTodo.id === item.id, editedTodo}}></ListItem>
自定义hooks
自定义组件,一般是现在父组件中写好,再提取为自定义组件。
自定义hooks,则是先自定义一个hook,再引入使用。(个人观点)
首先自定义一个hook,useTodoList.jsx
import { useState } from "react";
// 接收初始数据,将其声明为状态,同时提供状态操作方法给外界使用
// 我感觉有点先定义了一个state,然后把操作state的增删改查方法都定义好了,再都抛出去。
// 复用的都是一些操作state的方法
export function useTodoList(data) {
const [todoList, setTodoList] = useState(data)
function addTodo(title) {
setTodoList([
...todoList,
{
id: todoList.length + 1,
title,
completed: false,
}
])
}
function removeTodo(id) {
setTodoList(todoList.filter(item => item.id !== id))
}
function updateTodo(editedTodo) {
const todo = todoList.find(item => item.id === editedTodo.id)
Object.assign(todo, editedTodo)
setTodoList([...todoList])
}
return {todoList, addTodo, removeTodo, updateTodo, setTodoList}
}
然后再App.jsx中结构使用:
const {todoList, addTodo, removeTodo, updateTodo, setTodoList} = useTodoList(todoStorage.fetch())
App.jsx中也有一些变化
const [newTodo, setNewTodo] = useState("")
function changeNewTodo(e) {
setNewTodo(e.target.value);
}
// 用户回车且输入框有内容则添加一个新待办
function onAddTodo(e) {
if (e.code === 'Enter' && newTodo) {
addTodo(newTodo)
setNewTodo("");
}
};
// 按回车编辑完成
function onEditComplete(e) {
if(e.code === 'Enter') {
const title = e.target.value;
if(!title) {
alert('title不能为空!')
} else {
// noName(title)
updateTodo(editedTodo)
}
setEditedTodo(inital)
}
}
// 编辑时失去焦点
function onEditBlur(e) {
const title = e.target.value;
if(!title) {
alert('title不能为空!')
} else {
if(confirm('确认修改吗?')) {
// noName(title)
updateTodo(editedTodo)
}
}
setEditedTodo(inital)
}
过滤功能
最后一个过滤功能,
也是使用到了自定义hook,还有react内置钩子useMemo。useMemo字面意思就是使用缓存,也就是说只有当传入依赖项改变时,才会重新执行传入的回调参数。
useFilter.jsx
function useFilter(todos) {
const [visibility, setVisibility] = useState("all");
// 如果todos或者`visibility`变化,我们将重新计算`filteredTodos`
const filteredTodos = useMemo(() => {
if (visibility === "all") {
return todos;
} else if (visibility === "active") {
return todos.filter((todo) => todo.completed === false);
} else {
return todos.filter((todo) => todo.completed === true);
}
}, [todos, visibility]);
return {visibility, setVisibility, filteredTodos}
}
使用:
const {visibility, setVisibility, filteredTodos} = useFilter(todos)
<TodoFilter visibility={visibility} setVisibility={setVisibility}></TodoFilter>
最后感想
跟着村长的教程大概敲了一般,感觉对react hook的使用似乎有那么一点点理解了。嘻嘻。