一个案例通关 React 核心知识点
搭建项目
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 的使用似乎有那么一点点理解了。嘻嘻。