重学React:一个案例通关React核心知识点

开发 前端
页面刷新之后我们待办状态就失效了。很容易想到把待办列表存入localStorage,初始化时取出,列表有更新时保存。有没有发现,这又是一个典型的副作用使用场景。


图片

前言

小羊们好,本文希望用一个经典案例TodoMVC带大家掌握React中最核心的知识点。

我们将学到如下核心知识点:

  • 如何创建一个React项目
  • 如何掌握组件开发思想
  • 如何玩转JSX语法糖
  • 如何用好React hooks
  • ...

搭建项目

官方推荐的SPA应用创建工具链是create-react-app,我是个vue爱好者,比较青睐vite,借此也可以多踩踩坑:

npm create vite@latest

图片

用 VSCode 打开这个项目,再回到命令行中执行启动命令:

cd oh-my-react
code .
npm install
npm run dev

图片

编写应用基本结构

调整页面结构,将src/App.js 替换为以下代码:

import reactLogo from "./assets/react.svg";
import "./App.css";

function App() {
return (
<div className="App">
<header>
<h1>我的待办事项</h1>
<img src={reactLogo} className="logo" alt="logo" />
</header>
<main>
<section>
<h2>待办项</h2>
<ul className="todo-list">
<li className="todo">创建项目</li>
<li className="todo">JSX</li>
<li className="todo">组件化</li>
<li className="todo">hooks</li>
</ul>
</section>
</main>
</div>
);
}

export default App;

稍微修改一下样式,效果如下:

图片

循环输出项目

现在我们数据是写死的,最终我们需要一个数组保存所有todo,就像下面这样:

const todos = ['创建项目', '组件化开发', 'JSX语法糖', '掌握hooks']

我们需要循环输出它们,React中我们只需要对数组进行map转换即可:

{/* 使用map将数据转换为JSX */}
{todos.map((todo) => (
<li className="todo" key={todo}>
{/* 属性是动态值使用{xxx} */}
{/* 渲染列表时务必指定key */}
{todo}
</li>
))}

此时效果和之前是完全一样的!

请注意:渲染列表时务必添加key属性用于唯一区分列表项,否则会有警告提示信息!

图片

为todo加入状态

我们的待办是有状态的,比如我们需要知道待办是否完成,因此需要像下面这样修改数据结构:

const todos = [
{ id: 1, title: "创建项目", completed: true },
{ id: 2, title: "组件化开发", completed: false },
{ id: 3, title: "掌握JSX", completed: false },
{ id: 4, title: "掌握hooks", completed: false },
];

相应的,li中绑定要做相应调整

<li className="todo" key={todo.id.toString()}>
{todo.title}
</li>

受控组件

todo的完成状态需要反映在视图中,我们可以用一个checkbox的勾选状态来表示。

这就需要引入一个react的概念:受控组件,即使用react中的state作为表单输入元素唯一数据源,同时还控制用户输入过程中表单发生的操作。我们修改视图如下:

<li className="todo" key={todo.title}>
{/* todo.completed作为checkbox输入源 */}
{/* 同时控制用户输入操作 */}
<input
className="toggle"
type="checkbox"
checked={todo.completed}
notallow={(e) => changeState(e, todo)}
/>
<span>{todo.title}</span>
</li>

既然todo.computed要求是react状态,我们就要使用useState将前面的todos声明为一个组件状态:

// 将前面的`todos`创建为一个状态
const [todos, setTodos] = useState([
{ id: 1, title: "创建项目", completed: true },
{ id: 2, title: "组件化开发", completed: false },
{ id: 3, title: "掌握JSX", completed: false },
{ id: 4, title: "掌握hooks", completed: false },
]);
// 控制用户输入过程中表单发生的操作
const changeState = (e, currentTodo) => {
currentTodo.completed = e.target.checked;
// 必须重新设置状态,否则组件不会重新渲染
// 更新数组需要全新对象,否则组件不会重新渲染
setTodos([...todos])
};

效果如下:

图片

新增待办

我们再加一个新增代码的功能。

图片

这同样是受控组件的应用,我们添加一个输入框:

<div>
<input
className="new-todo"
autoFocus
autoComplete="off"
placeholder="该学啥了?"
value={newTodo}
notallow={changeNewTodo}
notallow={addTodo}
/>
</div>

同样需要对应的状态和事件控制:

const [newTodo, setNewTodo] = useState("");
const changeNewTodo = (e) => {
setNewTodo(e.target.value);
};
// 用户回车且输入框有内容则添加一个新待办
const addTodo = (e) => {
if (e.code === 'Enter' && newTodo) {
setTodos([
...todos,
{
id: todos.length + 1,
title: newTodo,
completed: false,
},
]);
setNewTodo("");
}
};

删除待办

我们增加一个删除待办功能

图片

这是事件处理和状态修改的应用,新增一个删除按钮:

<button className="destroy" notallow={() => removeTodo(todo)}>X</button>

修改状态,移除数组项时过滤掉删除项,将返回的新数组指定为给todos:

const removeTodo = (todo) => {
setTodos(todos.filter((item) => item.id !== todo.id));
};

修改待办

我们还想设计一个行内修改待办的交互,效果如下:

图片

这是一个动态样式、受控组件的综合应用,来看一下react中如何做。

首先修改一下视图结构:

<div class="view">
{/* 双击开启行内编辑:隐藏.view,显示.edit */}
<span notallow={() => editTodo(todo)}>{todo.title}</span>
<button className="destroy" notallow={() => removeTodo(todo)}>
X
</button>
</div>
{/* 声明editedTodo状态, onChange处理状态变化 */}
{/* onKeyUp处理修改确认,onBlur退出编辑模式 */}
<input
className="edit"
type="text"
value={editedTodo.title}
notallow={onEditing}
notallow={onEdited}
notallow={cancelEdit}
/>

然后给li加一个动态样式:

{/* editedTodo.title不为空且id和上下文中todo.id相同添加.editing */}
<li
className={[
"todo",
todo.completed ? "completed" : "",
editedTodo.title && editedTodo.id === todo.id
? "editing"
: "",
].join(" ")}
key={todo.id.toString()}
>

添加相关样式:

.todo-list li.completed span {
color: #949494;
text-decoration: line-through;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing .edit {
display: block;
padding: 12px 16px;
}
.todo-list li.editing .view {
display: none;
}

实现相关逻辑:

const initial = {
title: "",
completed: false,
};
const [editedTodo, setEditedTodo] = useState(initial);

// 用户双击触发编辑模式
const editTodo = (todo) => {
// 克隆一个todo用于编辑
// setBeforeEditCache(todo.title);
setEditedTodo({ ...todo });
};
// 受控组件要求的事件处理
const onEditing = (e) => {
const title = e.target.value;
if (title) {
setEditedTodo({ ...editedTodo, title: e.target.value });
} else {
// title为空删除该项
removeTodo(editedTodo);
}
};
const onEdited = (e) => {
// 监听enter
if (e.code === "Enter") {
if (editedTodo.title) {
// 获取对应待办并更新
const todo = todos.find((todo) => todo.id === editedTodo.id);
todo.title = editedTodo.title;
setTodos([...todos]);
}
setEditedTodo(initial);
}
};
const cancelEdit = (e) => {
setEditedTodo(initial);
};

添加副作用

进入编辑模式之后,我们希望可以自动获取焦点。

分析一下发现,设置焦点的条件是:editedTodo被设置为一个具体的todo时。那么也就是说,在editedTodo这个状态改变后产生了一个自动获取输入框焦点的副作用。

react提供了声明副作用方法useEffect(effect, deps)专门完成此类任务,表示deps中的依赖状态若发生变化则执行effect函数。

此处正好用于实现这个需求:

useEffect(() {
// 如果editedTodo存在则设置焦点
if(editedTodo.id) {}
}, [editedTodo])

但是此处有两个问题:如何设置input的焦点?设置哪个input的焦点?

这实际上是另一个知识点,react中如何获取dom元素的引用,从而可以操作dom,答案是:ref。

<input
className="edit"
{/* 设置一个函数到ref,根据上下文中todo的情况动态设置期望的input元素 */}
ref={e => setEditInputRef(e, todo)}
/>

setEditInputRef中根据todo和editedTodo即可判断是否是我们想要的input元素:

let inputRef = null
const setEditInputRef = (e, todo) => {
if (editedTodo.id === todo.id) {
inputRef = e
}
}

现在可以在副作用中执行设置焦点操作了!

useEffect(() {
// 如果id存在说明切换到了编辑模式
if (editedTodo.id) {
inputRef.focus()
}
}, [editedTodo])

状态持久化

还有个问题,页面刷新之后我们待办状态就失效了。很容易想到把待办列表存入localStorage,初始化时取出,列表有更新时保存。有没有发现,这又是一个典型的副作用使用场景。

我们实现一个todoStorage负责存取:

const STORAGE_KEY = 'todomvc-react'
const todoStorage = {
fetch () {
const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]')
return todos
},
save (todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
}
}

逻辑实现如下:

const [todos, setTodos] = useState(todoStorage.fetch());
useEffect(() {
todoStorage.save(todos)
}, [todos])

效果如下:

图片

提取组件

我们的代码量现在已经相当庞大,在它变得难以维护之前我们最好提前做重构。这里可以将app拆分为若干组件,这能够有效分割代码,提高复用性和可维护性。

这里我们将前面div.todo-list部分提取为可复用的TodoList组件,src/TodoList.jsx:

import { useState, useEffect } from "react";

// 传入todos和setTodos
const TodoList = ({ todos, setTodos }) => {
const changeState = (e, currentTodo) => {};
const removeTodo = (todo) => {};

const initial = {};
const [editedTodo, setEditedTodo] = useState(initial);
const editTodo = (todo) => {};
const onEditing = (e) => {};
const onEdited = (e) => {};
const cancelEdit = (e) => {};

let inputRef = null;
const setEditInputRef = (e, todo) => {};
useEffect(() {}, [editedTodo]);

return (
<ul className="todo-list">
{todos.map((todo) => (
//...
))}
</ul>
);
};

export default TodoList;

相对应的,App中使用做一些修改:

<TodoList todos={todos} setTodos={setTodos}></TodoList>

此时效果和之前是完全一样的!

此时TodoList还是有些臃肿的,我们还可以继续将列表项拆分,比如提取TodoItem,从而进一步拆分代码出去。如果你感兴趣可以自己试试!

自定义hooks

前面提取的TodoList中实际上只包含更新和删除操作,新增操作被留在App.jsx中,这看起来有些奇怪。分析发现,todos和其操作是通用逻辑,如果提取出来不仅可以保存状态,且能传递给多个组件使用。比如我们可以提取AddTodo组件,它就会关心新增逻辑;我们又可以提供过滤逻辑,提供给TodoFilter用于筛选不同状态的待办出来。

像上面这种需求就是react中的自定义hooks功能的应用。

我们提取一个hook:useTodos,具体实现如下:

// 接收初始数据,将其声明为状态,同时提供状态操作方法给外界使用
function useTodos(data) {
const [todos, setTodos] = useState(data);
const addTodo = (title) => {
setTodos([
...todos,
{
id: todos.length + 1,
title,
completed: false,
},
]);
}
const removeTodo = (id) => {
setTodos(todos.filter((todo) => todo.id !== id));
}
const updateTodo = (editedTodo) => {
const todo = todos.find((todo) => todo.id === editedTodo.id);
Object.assign(todo, editedTodo)
setTodos([...todos]);
}
return {todos, addTodo, removeTodo, updateTodo}
}

现在我们调整App.jsx中使用方式:

// 获取todos状态和操作方法
const {todos, addTodo, removeTodo, updateTodo} = useTodos(todoStorage.fetch())

// 原先新增的部分略微调整:
// 1.addTodo改为onAddTodo
// 2.调用addTodo实现新增
const [newTodo, setNewTodo] = useState("");
const changeNewTodo = (e) => {
setNewTodo(e.target.value);
};
const onAddTodo = (e) => {
if (e.code === "Enter" && newTodo) {
addTodo(newTodo)
setNewTodo("");
}
};

对应的,TodoList实现和使用也有略微变化

<TodoList {...{todos: filteredTodos, removeTodo, updateTodo}}></TodoList>
// 接收数据和操作函数
const TodoList = ({ todos, removeTodo, updateTodo }) => {
// 调用updateTodo更新
const changeState = (e, currentTodo) => {
currentTodo.completed = e.target.checked;
updateTodo(currentTodo)
};

// ...

const onEditing = (e) => {
const title = e.target.value;
if (title) {
setEditedTodo({ ...editedTodo, title });
} else {
// 调用removeTodo执行删除
removeTodo(editedTodo.id);
}
};
const onEdited = (e) => {
if (e.code === "Enter") {
if (editedTodo.title) {
// 调用updateTodo执行更新
updateTodo(editedTodo)
}
setEditedTodo(initial);
}
};

//...

return (
<ul className="todo-list">...</ul>
);
};
export default TodoList;

过滤功能

下面我们按组件化思想继续完成最后一个需求:按状态过滤待办。

图片

我们创建一个组件,TodoFilter.tsx:

我们希望外界传入过滤字段visibility,同时还有一个修改它的setVisibility。前者被修改之后,我们希望能对todos执行一个过滤操作获得filteredTodos,从而让TodoList可以根据它去显示。

export default function TodoFilter({visibility, setVisibility}) {
return (
<div className="footer">
<ul className="filters">
<li>
<button
className={visibility === "all" ? "selected" : ""}
notallow={() => setVisibility("all")}
>
All
</button>
</li>
<li>
<button
className={visibility === "active" ? "selected" : ""}
notallow={() => setVisibility("active")}
>
Active
</button>
</li>
<li>
<button
className={visibility === "completed" ? "selected" : ""}
notallow={() => setVisibility("completed")}
>
Completed
</button>
</li>
</ul>
</div>
);
}

再在App.jsx中实现一个useFilter的hooks:根据visibility的值做不同程度的过滤。

这里是react中内置钩子useMemo的应用:如果todos或者visibility变化,我们将重新计算filteredTodos

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>

补充一些样式

button.selected {
border-color: #646cff;
}

.filters {
list-style: none;
display: flex;
}

.filters li {
margin-right: 6px;
}

后续更新计划

终于写完了!掌握了好多react知识,但我们的应用还有很多不完善的地方,比如:

  • 我们是否可以将状态提取到全局,从而让组件获取状态,以及执行方法的时候更加简洁优雅
  • 我们的样式并没有做到组件隔离,这样很容易产生污染和冲突
  • 我们能否做一些权限限制,使得只有管理员才能创建和删除待办
  • 等等...

这些功能我们会在后面的教程中带大家逐步实现,顺便,我们再学一下状态管理、路由这些功能库的使用

责任编辑:武晓燕 来源: 村长学前端
相关推荐

2021-01-15 08:35:49

Zookeeper

2021-01-06 13:52:19

zookeeper开源分布式

2021-12-30 08:17:27

Springboot数据访问DataSourceB

2020-11-06 00:50:16

JavaClassLoaderJVM

2024-04-23 14:25:16

Python备忘清单

2020-10-26 10:40:31

Axios前端拦截器

2021-04-13 08:25:12

测试开发Java注解Spring

2020-05-19 14:40:08

Linux互联网核心

2020-04-27 09:40:13

Reacthooks前端

2022-01-12 14:24:37

接口Callable程序

2022-04-08 07:51:31

JavaJVM垃圾回收

2022-08-15 17:34:22

react-routv6

2023-08-07 14:44:56

Socket文件描述符

2023-07-13 12:21:18

2020-10-12 10:06:26

技术React代数

2011-03-31 11:15:52

网页设计Web

2023-07-14 07:23:21

ReactuseEffect

2022-03-16 17:01:35

React18并发的React组件render

2021-12-27 10:20:46

JavaNetty网络

2022-11-10 08:02:08

点赞
收藏

51CTO技术栈公众号