React项目实战:GitHub仓库列表应用(路由懒加载、全局状态管理与自定义Hook)

105 阅读3分钟

本文通过一个GitHub仓库列表项目,详细介绍如何用React全家桶开发一个单页应用(SPA)。项目使用React Router v6实现路由懒加载,Context API + useReducer管理全局状态,并封装自定义Hook复用逻辑。

项目概述

我们要实现的功能如下:

  • 首页(Home)
  • 用户仓库列表页(RepoList)
  • 仓库详情页(RepoDetail)
  • 404页面(NotFound)

项目目录结构如下:

src
├── api          // 接口封装
│   └── repos.js
├── components   // 组件
│   └── Loading.jsx
├── context      // 全局状态
│   └── GlobalContext.jsx
├── hooks        // 自定义Hook
│   └── useRepos.js
├── pages        // 页面组件
│   ├── Home
│   │   └── index.jsx
│   ├── RepoList
│   │   └── index.jsx
│   ├── RepoDetail
│   │   └── index.jsx
│   └── NotFound
│       └── index.jsx
├── reducers     // 状态reducer
│   └── repoReducer.js
├── App.jsx      // 根组件
└── main.jsx     // 入口文件

技术亮点

1. 路由懒加载

使用React.lazySuspense实现路由懒加载,减少首屏资源加载体积:

// App.jsx
import { Suspense, lazy } from 'react'
import { Routes, Route } from 'react-router-dom'
import Loading from './components/Loading'

const RepoList = lazy(() => import('./pages/RepoList'))
const RepoDetail = lazy(() => import('./pages/RepoDetail'))
const Home = lazy(() => import('./pages/Home'))
const NotFound = lazy(() => import('./pages/NotFound'))

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users/:id/repos" element={<RepoList />} />
        <Route path="/users/:id/repos/:repoId" element={<RepoDetail />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Suspense>
  )
}

2. 全局状态管理(Context + useReducer)

使用ContextuseReducer管理仓库列表相关的全局状态(包括加载状态、错误信息等):

Reducer设计

// reducers/repoReducer.js
export const repoReducer = (state, action) => {
  switch(action.type) {
    case 'FETCH_START':
      return { ...state, loading: true, error: null }
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, repos: action.payload, error: null }
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload }
    default:
      return state
  }
}

创建Context

// context/GlobalContext.jsx
import { useReducer, createContext } from 'react'
import { repoReducer } from '@/reducers/repoReducer'

export const GlobalContext = createContext()

const initialState = {
  repos: [],
  loading: false,
  error: null
}

export const GlobalProvider = ({ children }) => {
  const [state, dispatch] = useReducer(repoReducer, initialState)
  return (
    <GlobalContext.Provider value={{ state, dispatch }}>
      {children}
    </GlobalContext.Provider>
  )
}

在入口文件中包裹应用

// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { GlobalProvider } from '@/context/GlobalContext'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter>
      <GlobalProvider>
        <App />
      </GlobalProvider>
    </BrowserRouter>
  </React.StrictMode>
)

3. 自定义Hook(useRepos)

将数据获取逻辑封装成自定义Hook,简化组件代码:

// hooks/useRepos.js
import { useEffect, useContext } from 'react'
import { GlobalContext } from '@/context/GlobalContext'
import { getRepos } from '@/api/repos'

export const useRepos = (id) => {
  const { state, dispatch } = useContext(GlobalContext)

  useEffect(() => {
    dispatch({ type: 'FETCH_START' })
    const fetchRepos = async () => {
      try {
        const res = await getRepos(id)
        dispatch({ type: 'FETCH_SUCCESS', payload: res.data })
      } catch (err) {
        dispatch({ type: 'FETCH_ERROR', payload: err.message })
      }
    }
    fetchRepos()
  }, [id, dispatch])

  return state
}

4. API请求封装

将GitHub API请求封装成独立模块:

// api/repos.js
import axios from 'axios'

const BASE_URL = 'https://api.github.com/'

export const getRepos = (username) => {
  return axios.get(`${BASE_URL}users/${username}/repos`)
}

export const getRepoDetail = async (username, repoName) => {
  return await axios.get(`${BASE_URL}repos/${username}/${repoName}`)
}

5. 仓库列表页(RepoList)实现

在RepoList组件中使用自定义Hook获取数据:

// pages/RepoList/index.jsx
import { useParams, useNavigate, Link } from 'react-router-dom'
import { useEffect } from 'react'
import { useRepos } from '@/hooks/useRepos'

const RepoList = () => {
  const { id } = useParams()
  const navigate = useNavigate()
  const { repos, loading, error } = useRepos(id)

  useEffect(() => {
    if (!id?.trim()) {
      navigate('/')
    }
  }, [id, navigate])

  if (loading) return <div>Loading...</div>
  if (error) return <div>Error: {error}</div>

  return (
    <>
      <h2>Repositories for {id}</h2>
      {repos.map(repo => (
        <Link key={repo.id} to={`/users/${id}/repos/${repo.name}`}>
          {repo.name}
        </Link>
      ))}
    </>
  )
}

export default RepoList

性能优化点

  1. 路由懒加载:减少首屏加载时间
  2. 状态管理精细化:避免不必要的重渲染
  3. API请求封装:统一错误处理和基础URL
  4. 用户输入校验:在RepoList组件中检查id是否有效

踩坑记录

1. 路由参数获取

useParams必须在Router的上下文中使用,因此确保组件在路由匹配的组件树中。

2. 依赖数组问题

useEffect中使用dispatch时,注意将其加入依赖数组(ESLint会提示)。由于dispatch是稳定的,不会引起重复执行。

3. 用户输入校验

在RepoList组件中,我们检查id是否为空,如果为空则重定向到首页。这是必要的,因为GitHub API不允许空用户名。

总结

通过这个项目,实践了React Router v6的路由配置和懒加载、使用Context+useReducer管理全局状态、封装自定义Hook复用逻辑、以及API请求的封装。这些技术点可以帮助我们构建一个结构清晰、性能良好的React应用。