引言
效果图

 在这个教程中,我们将介绍如何使用 React、Zustand 状态管理库和 Mock.js 来构建一个包含热门推荐和关键词搜索功能的搜索页面。我们将逐步讲解每一个组件的功能以及它们之间的交互。
 在这个教程中,我们将介绍如何使用 React、Zustand 状态管理库和 Mock.js 来构建一个包含热门推荐和关键词搜索功能的搜索页面。我们将逐步讲解每一个组件的功能以及它们之间的交互。
完整代码
1. 创建 Zustand Store 实现全局状态管理
store/useSearchStore.js
在 src/store/useSearchStore.js 文件中创建一个 Zustand Store 来管理搜索建议列表 (suggestList) 和热门搜索列表 (hotList) 的状态。
            
            
              js
              
              
            
          
          // search 全局状态共享
import {
    create
} from 'Zustand'
import {
    getSuggestList,
    getHotList
} from '@/api/search'
const useSearchStore = create((set,get) => {
    // get 读操作
    const searchHistory = JSON.parse(localStorage.getItem('searchHistory')) || []
    return {
        searchHistory,
        suggestList: [], // 返回list
        hotList:[], // 热门搜索
        setSuggestList: async(keyword)=>{
            const res = await getSuggestList(keyword);
            console.log(res);
            set({
                suggestList: res.data
            })
        },
        setHotList: async () => {
            const res = await getHotList();
            console.log(res);
            set({
                hotList: res.data
            })
        }
    }
})
export default useSearchStore2. API 请求封装与模拟数据配置
在 src/api/search.js 中定义获取搜索建议和热门搜索列表的方法:
api/search.js
            
            
              js
              
              
            
          
          // search模块
import axios from './config'
export const getSuggestList = (keyword) => {
    return axios.get(`/search?keyword=${keyword}`);
}
export const getHotList = async () => {
    return axios.get('/hotList');
}3. 模拟后端接口响应
为了便于本地开发,我们可以使用 Mock.js 来模拟后端接口响应。在 src/mock/index.js 中添加如下代码:
mock/data.js
            
            
              js
              
              
            
          
          import Mock from 'mockjs';
Mock.mock('/api/search', 'get', (req, res) => {
    const keyword = req.query.keyword;
    let num = Math.floor(Math.random() * 10);
    let list = [];
    for (let i = 0; i < num; i++) {
        const randomData = Mock.mock({
            title: '@ctitle'
        });
        list.push(`${randomData.title} ${keyword}`);
    }
    return {
        code: 0,
        data: list
    };
});
Mock.mock('/api/hotlist', 'get', {
    code: 0,
    data: [
        { id: '101', city: "北京" },
        { id: '102', city: "上海" },
        { id: '103', city: "福州" }
    ]
});不要忘记配置 Axios 基础 URL 和拦截器:
api/config.js
            
            
              js
              
              
            
          
          import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:5173/api'
axios.interceptors.request.use((config) => {
    return config
})
// 响应拦截
axios.interceptors.response.use((data) => {
    return data.data
})
export default axios4. 构建 Search 组件
接下来,在 src/components/Search.js 中创建 Search 组件,它将负责渲染整个搜索界面,包括搜索框、热门推荐和搜索建议列表。
Pages/Search/index.jsx
            
            
              jsx
              
              
            
          
          import SearchBox from '@/components/SearchBox'
import useTitle from '@/hooks/useTitle'
import useSearchStore from '@/store/useSearchStore'
import styles from './search.module.css'
import { useState, useEffect, memo } from 'react';
// 性能优化
const HotListItems = memo((props) => {
  console.log('___________',props)
  const {hotList} = props;
  return (
    <div className={styles.hot}>
      <h1>热门推荐</h1>
      {
        hotList.map((item) => (
          <div key={item.id} className={styles.item}>
            {item.city}
          </div>
        ))
      }
    </div> 
  )
})
const Search = () => {
  const [query, setQuery] = useState('');
  const {
    hotList,
    setHotList,
    suggestList,
    setSuggestList,
  } = useSearchStore()
  useEffect(() => {
    setHotList();
  }, [])
  const handleQuery = (query) => {
    setQuery(query);
    if(!query.trim()) return
    setSuggestList(query);
  }
  const suggestListStyle = {
    display: query.trim() == '' ? 'none' : 'block'
  }
  
  useTitle('搜索')
  return (
    <div className={styles.container}>
      <div className={styles.wrapper}>
        <SearchBox handleQuery={handleQuery} />
        {/* 组件化,维护性更好 */}
        <HotListItems hotList={hotList} />
        <div className={styles.suggestList} style={suggestListStyle}>
          {
            suggestList.map( item => (
              <div key={item} className={styles.suggestItem}>
                {item}
              </div>
            ))
          }
        </div>
      </div>
    </div>
  )
}
export default Search5. 构建 SearchBox 组件
在 src/components/SearchBox.js 中创建 SearchBox 组件,它是一个输入框,允许用户输入搜索关键词,并触发搜索操作。
components/SearchBox/index.jsx
            
            
              jsx
              
              
            
          
          import { memo, useEffect, useRef, useState, useMemo } from 'react';
import { ArrowLeft, Close } from '@react-vant/icons';
import styles from './search.module.css';
import { debounce } from '@/utils';
const SearchBox = ({ handleQuery }) => {
    const [query, setQuery] = useState('');
    const queryRef = useRef(null);
    const handleChange = (e) => {
        if (e.target.value.trim() === '') {
            setQuery('');
            return;
        }
        setQuery(e.target.value.trim());
    };
    const clearQuery = () => {
        setQuery('');
        queryRef.current.value = '';
        queryRef.current.focus();
    };
    const handleQueryDebounce = useMemo(() => debounce(handleQuery, 300), []);
    const displayStyle = query.trim() ? { display: 'block' } : { display: 'none' };
    useEffect(() => {
        handleQueryDebounce(query);
    }, [query]);
    return (
        <div className={`${styles.wrapper}`}>
            <ArrowLeft onClick={() => history.go(-1)} />
            <input type="text" className={styles.ipt} placeholder="搜索旅游相关" ref={queryRef} onChange={handleChange} />
            <Close onClick={clearQuery} style={displayStyle} />
        </div>
    );
};
export default memo(SearchBox);关键词搜索整体流程梳理
从用户输入到数据展示
首先,用户在
SearchBox组件中输入框中输入关键词(如:"北京"),组件使用useEffect和防抖debounce,调用通过props传入的handleQuery函数,作为防抖函数的参数传入,在用户停止输入约300ms后执行,将"北京"作为参数传递。
子组件searchBox.jsx
            
            
              jsx
              
              
            
          
          <input 
    type="text"
    className={styles.ipt} 
    placeholder="搜索旅游相关"
    ref={queryRef}
    onChange={handleChange}
/>
            
            
              jsx
              
              
            
          
          const handleQueryDebounce = useMemo(() => {
        return debounce(handleQuery, 300)
    }, [])
// 非受控组件
useEffect(() => {
    handleQueryDebounce(query);
}, [query])父组件的
handleQuery("北京")被调用,更新输入框query的状态,随后调用 Zustand Store 中的setSuggestList("北京")方法。 父组件search.jsx
            
            
              jsx
              
              
            
          
           const handleQuery = (query) => {
    setQuery(query);
    if(!query.trim()) return
    setSuggestList(query);
  }
  /*....其他*/
  <SearchBox handleQuery={handleQuery} />
setSuggestList方法是一个异步函数,接收一个参数,即关键词"北京",在它内部会调用getSuggestList("北京")。 而getSuggestList方法会进行API请求 ,函数执行axios.get('/search?keyword=北京')。
状态管理:useSearchStore.js
            
            
              js
              
              
            
          
          setSuggestList: async(keyword)=>{
    const res = await getSuggestList(keyword);
    console.log(res);
    set({
        suggestList: res.data
    })
}api请求
            
            
              js
              
              
            
          
          export const getSuggestList = (keyword) => {
    return axios.get(`/search?keyword=${keyword}`);
}找到匹配的配置 (url) 后,
Mock.js会执行该配置项中的response函数,并将请求信息(req)和一个响应对象(res,虽然这里没用到)作为参数传递进去。在
response函数内部,const keyword = req.query.keyword;这行代码就是用来从拦截到的 HTTP 请求中提取查询参数keyword的值。
mock.js
            
            
              js
              
              
            
          
          {
    url: '/api/search',
    method: 'get',
    timeout: 1000,
    response:(req, res)=>{
        // ?keyword=释小龙
        const keyword = req.query.keyword;
        let num = Math.floor(Math.random() * 10); //生成0-9的随机数
        let list = [];
        for(let i = 0; i < num; i++) {
            const randomData = Mock.mock({
                title: '@ctitle'
            }) //Mock.mock 返回一个对象
            console.log(randomData)
            list.push(`${randomData.title} ${keyword}`)
        }
        return {
            code: 0,
            data: list
        }
    }
}使用
Mock.js生成随机数据,并将提取到的keyword值("北京")拼接到生成的标题后面,形成建议列表。最后将生成的列表包装成
{ code: 0, data: list }的格式返回给axios。而
axios收到Mock.js返回的模拟数据,getSuggestList函数的 Promise 被 resolve。
Search组件因为订阅了suggestList状态,所以重新渲染,将新的建议列表展示在页面上。
总结
通过这个教程,我们学习了如何使用 React 结合 Zustand 状态管理库来实现一个简单的搜索页面,该页面支持热门推荐和关键词搜索功能。同时我们也了解了如何利用 Mock.js 来为前端开发提供稳定的模拟数据环境。希望这篇教程对你有所帮助!