通过ReactHooks直接操作api神库:ReactQuery
React Query 是一个以react hooks为基础进行异步获取,缓存和更改数据的库,react query仓库文档原文描述:
Hooks for fetching, caching and updating asynchronous data in React
换言之,是一个异步处理数据的库。这么重要而且好用的库当然要收入囊中,把他学起来,本篇着重介绍整个项目的构建过程。
p.s 该篇的后端服务用这篇 配置的服务端
前端基础服务搭建
初始化项目:
1
| npx create-react-app client && cd client
|
安装依赖并运行:
其中 react-hook-from @rebass/forms @rebass/preset styled-components react-loader-spinner
均为样式组件
1 2
| yarn add react-query react-router-dom react-hook-from @rebass/forms @rebass/preset styled-components react-loader-spinner yarn start
|
修改src/client.js
文件如下: QueryClientProvider文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import React from 'react' import ReactDOM from 'react-dom' import "react-loader-spinner/dist/loader/css/react-spinner-loader.css" import App from './App' import { BrowserRouter } from "react-router-dom" import { ThemeProvider } from "styled-components" import preset from "@rebass/preset"
import { QueryClientProvider, QueryClient } from "react-query"
const queryClient = new QueryClient()
ReactDOM.render( {} <React.StrictMode> {} <QueryClientProvider client={queryClient}> {} <ThemeProvider theme={preset}> <BrowserRouter> <App /> </BrowserRouter> </ThemeProvider> </QueryClientProvider> </React.StrictMode>, document.getElementById('root') )
|
基础路由模块及基础架构:
src/App.js
利用 'react-router-dom'
作为路由的管理器,也是react常见的结构模式:
修改src/App.js
文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { Switch, Route } from 'react-router-dom' import { BooksList } from './BookList' import { CreateBook } from './CreateBook' import { UpdateBook } from './UpdateBook'
function App() { return <> <Switch> <Route path='/update-book/:id'><UpdateBook/></Route> <Route path='/create-book'><CreateBook /></Route> <Route path='/'><BooksList /></Route> </Switch> </> }
export default App;
|
获取所有图书
创建 src/BookList/index.js
和 src/BookList/BookList.jsx
:
src/BookList/index.js
:
1
| export * from './BooksList'
|
src/BookList/BookList.jsx
:
1 2 3
| export const BooksList = () => { return null }
|
创建图书
创建 src/BookList/index.js
和 src/BookList/BookList.jsx
:
src/CreateBook/index.js
:
1
| export * from './CreateBook'
|
src/CreateBook/CreateBook.jsx
:
1 2 3 4 5 6 7 8 9 10
| export const CreateBook = () => { return null } ``` ### 更新图书 创建 `src/BookList/index.js` 和 `src/BookList/BookList.jsx` : `src/UpdateBook/index.js`: ```js export * from './UpdateBook'
|
src/UpdateBook/UpdateBook.jsx
:
1 2 3
| export const UpdateBook = () => { return null }
|
基本结构:
1 2 3 4 5 6 7 8 9 10 11
| src |_App.js |_BookList | |_index.js | |_BookList.jsx |_CreateBook | |_CreateBook.jsx | |_index.js |_UpdateBook |_UpdateBook.jsx |_index.js
|
添加导航栏样式
src/shared/NavBar.jsx
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { Flex, Box, Link as StyledLink, Image } from 'rebass/styled-components' import { Link } from 'react-router-dom' import { Container } from './Container' import logo from './logo.svg'
export const NavBar = () => { return <Flex bg="black" color="white" justifyContent="center"> <Container> <Flex px={2} width='100%' alignItems='center'> <Image size={20} src={logo} /> <Link component={StyledLink} variant='nav' to='/'> React Query CRUD </Link> <Box mx="auto"/> <Link component={StyledLink} variant='nav' to='/create-book'> + Add new book </Link> </Flex> </Container> </Flex> }
|
src/shared/Container.jsx
:
1 2 3 4 5 6 7
| import { Box } from 'rebass/styled-components'
export const Container = ({children}) => { return <Box sx={{ width: "100%", maxWidth: 1024, mx: "auto" }}> {children} </Box> }
|
返回 src/App.js
把导航栏加上:
1 2 3
| import { NavBar } from './shared/NavBar'
<NavBar/>
|
创建 api.js 接口文件获取并数据:
配制文件 .env
路径必须在根目录
1
| REACT_APP_SERVER = http://localhost:4800
|
src/api.js
1 2 3 4 5
| export const getAllBooks = async () => { const response = await fetch(`${process.env.REACT_APP_SERVER}/books`) if (!response.ok) throw new Error('something wrong') return response.json() }
|
【 useQuery
】 应用 —— src/BookList.jsx
中查询所有图书
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { useQuery } from 'react-query' import { Flex } from 'rebass' import { getAllBooks } from '../api' import { Container } from '../shared/Container' import Loader from 'react-loader-spinner'
export const BooksList = () => { const { data, error, isLoading, isError } = useQuery('books', getAllBooks)
if (isLoading) return <Container> <Flex> <Loader type='ThreeDots' color='#ccc' height={30} /> </Flex> </Container>
if (isError) return <span> Error: {error.message} </span>
return <Container> <Flex flexDirection='column' alignItems='center'> { data.map(({author, title, id}) => <div key={id}> {author} - {title} </div> )} </Flex> </Container> }
|
此时界面可看到效果:
每条记录单独抽离做模块 src\BookList\BookItem.jsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { Flex, Text, Button, Link as StyledLink } from 'rebass/styled-components' import { Link } from 'react-router-dom'
export const BookItem = ({ author, title, id }) => {
return <Flex p={3} width="100%" alignItems='center'> <Link component={StyledLink} to={`/update-book/${id}`} mr="auto"> { title } </Link> <Text>{author}</Text> <Button ml="3"> remove </Button> </Flex> }
|
从 src\BookList\BookList.jsx
导入并取代具体记录的位置
1 2 3 4 5
| import { BookItem } from './BookItem'
data.map(({author, title, id}) => <BookItem author={author} title={title} id={id} key={id}/> )
|
看看页面的效果:
在 src\api.js
增加删除接口:
1 2 3 4 5 6 7
| export const removeBook = async id => { const response = await fetch(`${process.env.REACT_APP_API_SERVER}/books/${id}`, { method: 'DELETE' }) if (!response.ok) throw new Error(response.json().message) return true }
|
为删除图书修改界面
并在 src\BookList\BookList.jsx
里增加相应的 useMutation 和 queryClient 的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { removeBook } from '../api'
const queryClient = useQueryClient()
const { mutateAsync, isLoading } = useMutation(removeBook)
const remove = async () => { await mutateAsync(id) queryClient.invalidateQueries('books') }
<Button ml="3" onClick={remove}> { isLoading ? <Loader type='ThreeDots' color='#fff' height={10} />: 'Remove' } </Button>
|
queryClient.invalidateQueries() 用于清除缓存并刷新页面:
来自中文官方文档的解释:
可以智能地将查询标记为过时的,并使之可用重新获取数据,
简言之,queryClient.invalidateQueries('books')
可清除旧 ‘books’ 的显示缓存,并直接刷新最新的 ‘books’ 接口数据,如果不加,页面就不会刷新,需要手动刷新
变更一本图书信息
src\api.js
增加获取一本书的接口:
1 2 3 4 5 6 7 8
| export const getBook = async ({ queryKey }) => { const [_key, { id }] = queryKey const response = await fetch(`${process.env.REACT_APP_API_SERVER}/books/${id}`) if (!response.ok) throw new Error(response.json().message)
return response.json() }
|
src\api.js
增加 updateBook
接口:
1 2 3 4 5 6 7 8 9 10 11 12
| export const updateBook = async ({ id, ...data }) => { const response = await fetch(`${process.env.REACT_APP_API_SERVER}/books/${id}`, { method: 'PUT', headers: { 'Content-Type' : 'application/json' }, body: JSON.stringify(data) }) if (!response.ok) throw new Error(response.json().message) return response.json() }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { Box, Button } from 'rebass/styled-components' import { Label, Input } from '@rebass/forms' import { useForm } from 'react-hook-form' import Loader from 'react-loader-spinner'
export const BookForm = ({ defaultValues, onFormSubmit, isLoading }) => { const { register, handleSubmit } = useForm({ defaultValues }) const onSubmit = handleSubmit( data => { onFormSubmit(data) })
return <form onSubmit={onSubmit}> <Box sx={{ marginBottom : 3 }}> <Label htmlFor="title">Title</Label> <Input ref={register} id='title' name='title' type='text' /> </Box> <Box sx={{ marginBottom : 3 }}> <Label htmlFor='author'>Author</Label> <Input ref={register} id='author' name='author' type='text' /> </Box> <Button> { isLoading ? <Loader type='ThreeDots' color='#fff' height={10} /> : 'Submit' } </Button> </form> }
|
src\UpdateBook\UpdateBook.jsx
更改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import Loader from "react-loader-spinner" import { useMutation, useQuery } from "react-query" import { useHistory, useParams } from "react-router-dom" import { Box, Flex, Heading } from "rebass/styled-components" import { getBook, updateBook } from "../api" import { BookForm, Container } from "../shared"
export const UpdateBook = () => { const { id } = useParams() const history = useHistory() const { data, error, isLoading, isError } = useQuery(['book', {id}], getBook)
const { mutateAsync, isLoading: isMutating } = useMutation(updateBook) const onFormSubmit = async data => { await mutateAsync({ ...data, id }) history.push('/') }
if ( isLoading ) return <Container> <Flex> <Loader type='ThreeDots' color='#ccc' height={30} /> </Flex> </Container>
if ( isError ) return <Container> <Flex py='5' justifyContent='center'> Error: {error.message} </Flex> </Container>
return <Container> <Box sx={{ py: 3 }}> <Heading sx={{ marginBottom: 3 }}>Update Book</Heading> <BookForm defaultValues={data} onFormSubmit={onFormSubmit} isLoading={isMutating} /> </Box> </Container> }
|
新建 src\shared\index.js
将所有共用组件导出:
1 2 3
| export * from './Container' export * from './BookForm' export * from './NavBar'
|
看看页面的效果:
新建一本图书
src\api.js
里添加
1 2 3 4 5 6 7 8 9 10 11
| export const createBook = async (data) => { const response = await fetch(`${process.env.REACT_APP_API_SERVER}/books/`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) })
if (!response.ok) throw new Error(response.json().message)
return response.json() }
|
更改创建图书界面 src\CreateBook.jsx
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { useMutation } from "react-query" import { useHistory } from "react-router-dom" import { Box, Heading } from "rebass" import { createBook } from "../api" import { BookForm, Container, } from '../shared'
export const CreateBook = () => { const history = useHistory()
const { mutateAsync, isLoading } = useMutation(createBook) const onFormSubmit = async data => { await mutateAsync(data) history.push('/') }
return <Container> <Box sx={{ py: 3 }}> <Heading sx={{ marginBottom: 3 }}>Create New Book</Heading> <BookForm onFormSubmit={onFormSubmit} isLoading={isLoading} /> </Box> </Container> }
|
看看页面的效果: