предисловие
С момента выпуска Reack Hooks в 16.8 в сообществе было довольно много обсуждений и заявок.Интересно, использовали ли вы эту классную функцию в своей компании~
Сегодня я поделюсь с вами тем, как использовать React Hooks для реализации относительно полного списка задач.
Функции:
- Управляйте запросами с помощью настраиваемых хуков
- Используйте хуки для организации кода и логического разделения
предварительный просмотр интерфейса
Адрес опыта
код sandbox.IO/yes/react-ok, о, о…
Подробный код
интерфейс
Во-первых, мы представляем antd как библиотеку пользовательского интерфейса, сохраняем некоторую ненужную логику и быстро строим скелет нашей страницы.
const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
[TAB_ALL]: "全部",
[TAB_FINISHED]: "已完成",
[TAB_UNFINISHED]: "待完成"
};
function App() {
const [activeTab, setActiveTab] = useState(TAB_ALL);
return (
<>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} />
<TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} />
<TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} />
</Tabs>
<div className="app-wrap">
<h1 className="app-title">Todo List</h1>
<Input />
<TodoList />
</div>
</>
);
}
сбор информации
С интерфейсом следующим шагом будет получение данных.
макет API
Здесь я создал новый api.js для имитации интерфейса получения данных, логика в нем приятна на вид, и на нее не нужно обращать особого внимания.
const todos = [
{
id: 1,
text: "todo1",
finished: true
},
{
id: 2,
text: "todo2",
finished: false
},
{
id: 3,
text: "todo3",
finished: true
},
{
id: 4,
text: "todo4",
finished: false
},
{
id: 5,
text: "todo5",
finished: false
}
];
const delay = time => new Promise(resolve => setTimeout(resolve, time));
// 将方法延迟1秒
const withDelay = fn => async (...args) => {
await delay(1000);
return fn(...args);
};
// 获取todos
export const fetchTodos = withDelay(params => {
const { query, tab } = params;
let result = todos;
// tab页分类
if (tab) {
switch (tab) {
case "finished":
result = result.filter(todo => todo.finished === true);
break;
case "unfinished":
result = result.filter(todo => todo.finished === false);
break;
default:
break;
}
}
// 带参数查询
if (query) {
result = result.filter(todo => todo.text.includes(query));
}
return Promise.resolve({
tab,
result
});
});
Здесь мы инкапсулируем метод withDelay для обертывания функции и имитации задержки интерфейса асинхронного запроса, что нам удобно для демонстрации функции загрузки позже.
Сбор основных данных
Самый традиционный способ получить данные — использовать useEffect в компоненте для завершения запроса и объявить зависимое значение для повторной выборки данных после изменения некоторых условий, просто напишите один:
import { fetchTodos } from './api'
const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
[TAB_ALL]: "全部",
[TAB_FINISHED]: "已完成",
[TAB_UNFINISHED]: "待完成"
};
function App() {
const [activeTab, setActiveTab] = useState(TAB_ALL);
// 获取数据
const [loading, setLoading] = useState(false)
const [todos, setTodos] = useState([])
useEffect(() => {
setLoading(true)
fetchTodos({tab: activeTab})
.then(result => {
setTodos(todos)
})
.finally(() => {
setLoading(false)
})
}, [activeTab])
return (
<>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} />
<TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} />
<TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} />
</Tabs>
<div className="app-wrap">
<h1 className="app-title">Todo List</h1>
<Input />
<Spin spinning={loading} tip="稍等片刻~">
<!--把todos传递给组件-->
<TodoList todos={todos}/>
</Spin>
</div>
</>
);
}
Это хорошо, и мои коллеги так пишут в новых запущенных проектах внутри компании, но есть несколько небольших проблем с таким сбором данных.
- Используйте useState для создания состояния загрузки каждый раз
- Каждый раз, когда useState используется для установления состояния результата запроса
- Для запросов, если есть какие-то инкапсуляции более высокого порядка, работать с ними непросто.
Итак, здесь нам нужно инкапсулировать пользовательский хук, предназначенный для запросов.
Пользовательский хук (сбор данных)
Я забыл, где я видел утверждение. Пользовательские хуки фактически размещают содержимое тела метода в компоненте после выполнения метода useXXX. Я думаю, что это утверждение очень удобно для понимания пользовательских хуков.
useTest() {
const [test, setTest] = useState('')
setInterval(() => {
setTest(Math.random())
}, 1000)
return {test, setTest}
}
function App() {
const {test, setTest} = useTest()
return <span>{test}</span>
}
Этот код эквивалентен:
function App() {
const [test, setTest] = useState('')
setInterval(() => {
setTest(Math.random())
}, 1000)
return <span>{test}</span>
}
Считаете ли вы, что настраиваемый хук очень прост в одно мгновение? Основываясь на этой идее, давайте инкапсулируем нужный нам метод useRequest.
export const useRequest = (fn, dependencies) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
// 请求的方法 这个方法会自动管理loading
const request = () => {
setLoading(true);
fn()
.then(setData)
.finally(() => {
setLoading(false);
});
};
// 根据传入的依赖项来执行请求
useEffect(() => {
request()
}, dependencies);
return {
// 请求获取的数据
data,
// loading状态
loading,
// 请求的方法封装
request
};
};
С помощью этого пользовательского хука код внутри нашего компонента можно значительно упростить.
import { fetchTodos } from './api'
import { useRequest } from './hooks'
const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
[TAB_ALL]: "全部",
[TAB_FINISHED]: "已完成",
[TAB_UNFINISHED]: "待完成"
};
function App() {
const [activeTab, setActiveTab] = useState(TAB_ALL);
// 获取数据
const {loading, data: todos} = useRequest(() => {
return fetchTodos({ tab: activeTab });
}, [activeTab])
return (
<>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} />
<TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} />
<TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} />
</Tabs>
<div className="app-wrap">
<h1 className="app-title">Todo List</h1>
<Input />
<Spin spinning={loading} tip="稍等片刻~">
<!--把todos传递给组件-->
<TodoList todos={todos}/>
</Spin>
</div>
</>
);
}
Конечно, есть намного меньше копочка для котельной, не болит талия или ноги, и я могу отправить 5 запросов в одном Go!
Удалите грязные данные, генерируемые частым переключением вкладок
Сценарий, с которым мы особенно склонны сталкиваться в реальной разработке, заключается в том, что переключение вкладок не меняет вид, а повторно запрашивает новые данные списка.В этом случае мы можем столкнуться с проблемой.Возьмем этот тодолист в качестве примера, мы начинаем с全部
вкладка переключиться на已完成
tab, будет запрашивать данные, но если мы находимся в已完成
Если данные вкладки не были запрошены, нажмите待完成
вкладку, нам нужно рассмотреть проблему в это время, время ответа асинхронных запросов неизвестно, очень вероятно, что первый запрос, который мы инициируем已完成
В итоге потребовалось 5 секунд, второй запрос待完成
В конце это занимает 1 с, поэтому возвращаются данные второго запроса.После рендеринга страницы данные первого запроса возвращаются через несколько секунд, но в это время наша вкладка остается на соответствующем втором запросе.待完成
, что создает ошибку с грязными данными.
На самом деле мы можем использовать характеристики useEffect для решения этой проблемы в пакете useRequest.
export const useRequest = (fn, dependencies, defaultValue = []) => {
const [data, setData] = useState(defaultValue);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(false);
const request = () => {
// 定义cancel标志位
let cancel = false;
setLoading(true);
fn()
.then(res => {
if (!cancel) {
setData(res);
} else {
// 在请求成功取消掉后,打印测试文本。
const { tab } = res;
console.log(`request with ${tab} canceled`);
}
})
.catch(() => {
if (!cancel) {
setError(error);
}
})
.finally(() => {
if (!cancel) {
setLoading(false);
}
});
// 请求的方法返回一个 取消掉这次请求的方法
return () => {
cancel = true;
};
};
// 重点看这段,在useEffect传入的函数,返回一个取消请求的函数
// 这样在下一次调用这个useEffect时,会先取消掉上一次的请求。
useEffect(() => {
const cancelRequest = request();
return () => {
cancelRequest();
};
// eslint-disable-next-line
}, dependencies);
return { data, setData, loading, error, request };
};
На самом деле запрос отмены, реализованный в запросе, здесь - это только отмена, которую мы смоделировали. В реальных случаях мы можем использовать методы, предоставляемые библиотеками запросов, такими как axios, для различной инкапсуляции. Здесь мы в основном говорим об идеях. Функция, возвращаемая в useEffect, на самом деле называется функцией очистки. Каждый раз, когда выполняется useEffect, функция очистки будет выполняться первой. Используя эту функцию, мы можем успешно заставить useEffect использовать только последний результат запроса для отображения страницы.
можешь идтиадрес предварительного просмотраБыстро щелкните переключатель вкладки, чтобы увидеть результаты, напечатанные на консоли.
Инкапсуляция активных запросов
Теперь вам нужно добавить функцию, нажмите элемент в списке, переключите состояние завершения, на этот разuseRequest
Это кажется неуместным, потому чтоuseRequest
По сути, это инкапсуляция useEffect, и сценарий использования useEffect заключается в инициировании запроса во время инициализации и изменении зависимостей, но это новое требование фактически заключается в инициировании запроса в ответ на щелчок пользователя. написать setLoading или тому подобное Избыточный код? Ответ, конечно, нет.
Мы используем идею функций высшего порядка для инкапсуляции пользовательского хука:useWithLoading
реализация кода useWithLoading
export function useWithLoading(fn) {
const [loading, setLoading] = useState(false);
const func = (...args) => {
setLoading(true);
return fn(...args).finally(() => {
setLoading(false);
});
};
return { func, loading };
}
По сути, он оборачивает входящий метод и изменяет состояние загрузки до и после выполнения.
использовать:
// 完成todo逻辑
const { func: onToggleFinished, loading: toggleLoading } = useWithLoading(
async id => {
await toggleTodo(id);
}
);
<TodoList todos={todos} onToggleFinished={onToggleFinished} />
организация кода
Добавлена новая функция.Плейсхолдер ввода переключает копию в соответствии с переключением вкладки.Обратите внимание, что здесь мы сначала приводим неправильный пример, что является ошибкой, которую люди, которые только что перешли с Vue2.x и React Class Компонент сделать легко.
❌ Примеры ошибок
import { fetchTodos } from './api'
import { useRequest } from './hooks'
const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
[TAB_ALL]: "全部",
[TAB_FINISHED]: "已完成",
[TAB_UNFINISHED]: "待完成"
};
function App() {
// state放在一起
const [activeTab, setActiveTab] = useState(TAB_ALL);
const [placeholder, setPlaceholder] = useState("");
const [query, setQuery] = useState("");
// 副作用放在一起
const {loading, data: todos} = useRequest(() => {
return fetchTodos({ tab: activeTab });
}, [activeTab])
useEffect(() => {
setPlaceholder(`在${tabMap[activeTab]}内搜索`);
}, [activeTab]);
const { func: onToggleFinished, loading: toggleLoading } = useWithLoading(
async id => {
await toggleTodo(id);
}
);
return (
<>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} />
<TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} />
<TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} />
</Tabs>
<div className="app-wrap">
<h1 className="app-title">Todo List</h1>
<Input />
<Spin spinning={loading} tip="稍等片刻~">
<!--把todos传递给组件-->
<TodoList todos={todos}/>
</Spin>
</div>
</>
);
}
Обратите внимание, что в предыдущей разработке vue и react, потому что способ организации кода vuebased on options
(На основе параметров, таких как данные, методы, вычисленные организации),
React также является СОСТОЯНИЕМ в локальной унифицированной инициализации, затем определяет кучу методов XXX в классе, которые заставят новые задания читать логику.
Таким образом, хуки также решают проблему, то есть способ организации нашего кода можетbased on logical concerns
(организовано на основе логических соображений)
Не разделяйте useState useEffect на категории в соответствии с обычным мышлением, это выглядит аккуратно, но бесполезно! !
Вот сравнительная диаграмма компонента в библиотеке @vue/ui во введении API композиции vue.
Цвета используются для обозначения функциональных точек.С первого взгляда становится ясно, какая организация кода более удобна для сопровождения.Метод организации кода, рекомендованный API композиции Vue, заключается в разбиении логики на пользовательские функции-хуки одну за другой, что согласуется с идеей хуков реакции.
export default {
setup() { // ...
}
}
function useCurrentFolderData(nextworkState) { // ...
}
function useFolderNavigation({ nextworkState, currentFolderData }) { // ...
}
function useFavoriteFolder(currentFolderData) { // ...
}
function useHiddenFolders() { // ...
}
function useCreateFolder(openFolder) { // ...
}
✔️ правильный пример
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import TodoInput from "./todo-input";
import TodoList from "./todo-list";
import { Spin, Tabs } from "antd";
import { fetchTodos, toggleTodo } from "./api";
import { useRequest, useWithLoading } from "./hook";
import "antd/dist/antd.css";
import "./styles/styles.css";
import "./styles/reset.css";
const { TabPane } = Tabs;
const TAB_ALL = "all";
const TAB_FINISHED = "finished";
const TAB_UNFINISHED = "unfinished";
const tabMap = {
[TAB_ALL]: "全部",
[TAB_FINISHED]: "已完成",
[TAB_UNFINISHED]: "待完成"
};
function App() {
const [activeTab, setActiveTab] = useState(TAB_ALL);
// 数据获取逻辑
const [query, setQuery] = useState("");
const {
data: { result: todos = [] },
loading: listLoading
} = useRequest(() => {
return fetchTodos({ query, tab: activeTab });
}, [query, activeTab]);
// placeHolder
const [placeholder, setPlaceholder] = useState("");
useEffect(() => {
setPlaceholder(`在${tabMap[activeTab]}内搜索`);
}, [activeTab]);
// 完成todo逻辑
const { func: onToggleFinished, loading: toggleLoading } = useWithLoading(
async id => {
await toggleTodo(id);
}
);
const loading = !!listLoading || !!toggleLoading;
return (
<>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<TabPane tab={tabMap[TAB_ALL]} key={TAB_ALL} />
<TabPane tab={tabMap[TAB_FINISHED]} key={TAB_FINISHED} />
<TabPane tab={tabMap[TAB_UNFINISHED]} key={TAB_UNFINISHED} />
</Tabs>
<div className="app-wrap">
<h1 className="app-title">Todo List</h1>
<TodoInput placeholder={placeholder} onSetQuery={setQuery} />
<Spin spinning={loading} tip="稍等片刻~">
<TodoList todos={todos} onToggleFinished={onToggleFinished} />
</Spin>
</div>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Суммировать
React Hook дает нам новую идею для лучшей организации логического кода внутри компонентов, упрощая поддержку больших компонентов со сложными функциями. И пользовательская функция Hook очень мощная.В проекте компании я также инкапсулировал много полезных пользовательских крючков, таких как UseTable, useTreeSearch, useTabs и т. д., которые можно комбинировать с библиотеками компонентов и требованиями взаимодействия с пользовательским интерфейсом, используемыми соответствующими компаниями. , Инкапсулируйте детализацию и используйте свое воображение! использовать свое воображение!