https://tkdodo.eu/blog/react-query-data-transformations
数据变化
服务器端给前端的数据不能总 (或者总不能) 满足前端的需求,需要对其进行一定程度的转换。
0. 在服务器端处理
- 好处:服务器端的数据总能在 devtool 上清晰看到
- 一般:不在前端,需要单独的维护
- 不好:不总是可能
1. 响应函数处理
const fetchTodos = async (): Promise<Todos> => {
const response = await axios.get('todos')
const data: Todos = response.data
return data.map((todo) => todo.name.toUpperCase())
}
export const useTodosQuery = () => useQuery(['todos'], fetchTodos)
- 好处: 离着服务端代码很近
- 一般: 不能复用缓存
- 不好: 每次都需要处理
- 不好: 如果前端有单独的 api 层,不能随意修改
2. 在 Render 中修改
- 好处: 通过 useMemo 优化,实际上,与响应函数处理没啥区别,也是每次都要处理
- 一般: 数据结构不能在 devtool 里面清晰看到
- 不好: 代码不清晰
- 不好: 数据可能是空的,或者错误,或者未准备好的。
实际上,在 useMemo 中还能读去其他组件状态,能更方便的处理数据。
3. 通过 useQuery 的 select 处理
const transformTodoNames = (data: Todos) =>
data.map((todo) => todo.name.toUpperCase())
export const useTodosQuery = () =>
useQuery(['todos'], fetchTodos, {
// ✅ uses a stable function reference
select: transformTodoNames,
})
- 好处: 最佳实践
- 好处: 支持部分的监听
- 一般: 每个监听者对于数据的处理可以有所不同
- 一般: 结构化共享被执行了两次
React Query 渲染优化
无必要的渲染,是大家尽量避免的,然而,更要避免的事需要渲染而没有渲染。在优化之前,需要阅读一下的一些 topic
使用 notifyOnChangeProps 控制响应的变化
export const useTodosQuery = (select, notifyOnChangeProps) =>
useQuery(['todos'], fetchTodos, { select, notifyOnChangeProps })
export const useTodosCount = () =>
useTodosQuery((data) => data.length, ['data'])
可以使用 notifyOnChangeProps='tracked'
,自动跟踪用到的变量。
结构分享
重新查询时,会尽可能的重用状态。例如 todo 如果是一个列表,而下面的代码尝试监听第二个元素,如果列表重取。而第二条没有变化,下面的状态就不会更新。
// ✅ will only re-render if _something_ within todo with id:2 changes
// thanks to structural sharing
const { data } = useTodo(2)
有效的查询 key
值得再次强调,query key 应该与用它的组件放在一起。queries可以复合一些 hooks。让 query key 只包含在局部作用域。
- src
- features
- Profile
- index.tsx
- queries.ts
- Todos
- index.tsx
- queries.ts
使用 query function 上下文
babel-plugin-react-query-key-gen 可以检查 query key 不全的情况如下:
export const useTodos = () => {
const { state, sorting } = useTodoParams()
// 🚨 can you spot the mistake ⬇️
return useQuery(['todos', state], () => fetchTodos(state, sorting))
}
然而,可以直接使用 queryKey 参数
const fetchTodos = async ({ queryKey }) => {
// 🚀 we can get all params from the queryKey
const [, state, sorting] = queryKey
const response = await axios.get(`todos/${state}?sorting=${sorting}`)
return response.data
}
export const useTodos = () => {
const { state, sorting } = useTodoParams()
// ✅ no need to pass parameters manually
return useQuery(['todos', state, sorting], fetchTodos)
}
使用 更严格的类型约束
const todoKeys = {
all: ['todos'] as const,
lists: () => [...todoKeys.all, 'list'] as const,
list: (state: State, sorting: Sorting) =>
[...todoKeys.lists(), state, sorting] as const,
}
const fetchTodos = async ({
queryKey,
}: // 🤯 only accept keys that come from the factory
QueryFunctionContext<ReturnType<typeof todoKeys['list']>>) => {
const [, , state, sorting] = queryKey
const response = await axios.get(`todos/${state}?sorting=${sorting}`)
return response.data
}
export const useTodos = () => {
const { state, sorting } = useTodoParams()
// ✅ build the key via the factory
return useQuery(todoKeys.list(state, sorting), fetchTodos)
}
react query 是一个状态管理库
定制 staleTime
太小的 staleTime 会导致大量的数据重取。一般建议在 20s 以上,这个参数默认是 0。
React Router 也可以加载数据
react router 6.4.0 开始支持在 route 上挂载 loader / action
createBrowserRouter([
{
path: "/teams/:teamId",
loader: ({ params }) => {
return fakeGetTeam(params.teamId);
},
},
]);
可以跟 react query 混用
// ⬇️ define your query
const contactDetailQuery = (id) => ({
queryKey: ['contacts', 'detail', id],
queryFn: async () => getContact(id),
})
// ⬇️ needs access to queryClient
export const loader =
(queryClient) =>
async ({ params }) => {
const query = contactDetailQuery(params.contactId)
// ⬇️ return data or fetch it
return (
queryClient.getQueryData(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}
export default function Contact() {
const params = useParams()
// ⬇️ useQuery as per usual
const { data: contact } = useQuery(contactDetailQuery(params.contactId))
// render some jsx
}
预先查询缓存
- 如果支持服务器端渲染,在服务器端进行预先获取数据。
- 如果能在 route 中支持 loader,在route 中预先加载数据。
可以通过两种方式来预先缓存,一种是拉的方式,即通过 initialData 来指定数据。另一种是推的方式。例如在 list 获取完后,直接给每个 id 设置对应的缓存。
const useTodos = () => {
const queryClient = useQueryClient()
return useQuery({
queryKey: ['todos', 'list'],
queryFn: async () => {
const todos = await fetchTodos()
todos.forEach((todo) => {
// ⬇️ create a detail cache for each item
queryClient.setQueryData(['todos', 'detail', todo.id], todo)
})
return todos
},
})
}
- 好处: query 可以明确的知道对应数据的挂载时间,从而考量数据的新鲜度。
- 一般:回调无法正常执行,例如 onSuccess, onQuery 的调用,可能会导致相同的数据会写入缓存多次。
- 一般:可能会在缓存中写入无用的数据。
- 不好:推的数据可能会被垃圾回收。导致缓存失效。
网友评论