React 【hooks + 路由 + 状态管理】

143 阅读12分钟

hooks

useState

  • 函数式更新的主要好处
  1. 基于最新状态更新,函数式更新总是基于最新的状态值,而不是更新时的闭包值:
// 可能有问题的方式
setCount(count + 1); // 依赖当前闭包中的count值

// 更好的方式
setCount(prevCount => prevCount + 1); // 基于最新状态值
  1. 解决多次更新问题,当需要连续多次更新同一状态时,函数式更新能确保所有更新都生效
// 这样只会增加1,因为两次setCount使用的count值相同
setCount(count + 1);
setCount(count + 1);

// 这样会增加2,因为每次更新都基于前一次的结果
setCount(prev => prev + 1);
setCount(prev => prev + 1);

3.函数式更新配合 useEffect 可以有效地避免某些情况下的无限循环问题

import { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [age, setAge] = useState(18);

  // 以下代码会导致死循环
  useEffect(() => {
    setCount(count + 1);
  }, [age, count]);
  
  // 以下代码使用函数格式更新从而避免直接依赖count
  useEffect(() => {
    setCount((pre) => pre + 1);
  }, [age]);

  const onSetAge = () => {
    setAge((pre) => pre + 1);
  };
  return (
    <>
      <p>count:{count}</p>
      <p>age:{age}</p>
      <button onClick={onSetAge}>setAge</button>
    </>
  );
}

export default App;

useImmer

用于以不可变的方式更轻松地更新复杂的状态对象。 因为useState 状态管理, setState()设置时引起页面重新渲染的机制是地址值发生改变。对于基本类型来说就是值改变,对于对象或数组等引用类型来说,就是前后地址值需要不一样,才会更新视图。

// 安装库
npm install immer use-immer

与 useState 对比的优势

场景useStateuseImmer
深层更新需要多层展开操作符直接修改路径
数组操作需要创建新数组直接使用 push/pop/splice
可读性代码冗长代码简洁直观
维护性容易出错(忘记展开)不易出错
  • 传统方式使用useState更新嵌套对象:
const [state, setState] = useState({
  user: {
    name: "John",
    address: {
      city: "New York",
      zip: "10001"
    }
  }
});

// 传统方式 - 需要多层展开
setState(prev => ({
  ...prev,
  user: {
    ...prev.user,
    address: {
      ...prev.user.address,
      city: "Boston"
    }
  }
}));
  • 使用 useImmer
import { useImmer } from "use-immer";

function UserProfile() {
  const [user, updateUser] = useImmer({
    name: "Alice",
    age: 25,
    hobbies: ["reading", "coding"],
    profile: {
      bio: "Developer",
      social: {
        twitter: "@alice",
        github: "alice"
      }
    }
  });

  const updateAge = () => {
    updateUser(draft => {
      draft.age += 1;
    });
  };

  const addHobby = (hobby) => {
    updateUser(draft => {
      draft.hobbies.push(hobby);
    });
  };

  const updateTwitter = (handle) => {
    updateUser(draft => {
      draft.profile.social.twitter = handle;
    });
  };
}

useEffect

  1. 依赖数组的三种形式
// 1. 空依赖数组 - 只在挂载时运行
useEffect(() => {
  // 初始化逻辑
}, []);

// 2. 无依赖数组 - 每次渲染都运行(谨慎使用)
useEffect(() => {
  // 每次渲染都执行
});

// 3. 有具体依赖 - 挂载时运行 + 依赖变化时 运行
useEffect(() => {
  
}, [count, name]);

2.依赖项的比较机制,React 使用 Object.is() 比较依赖项的前后值,为了提高性能,尽量避免依赖项的非必要改变。譬如使用useCallback来包装函数或者把使用到函数定义在useEffect内部,再或者碰到对象类型(普通对象或数组)使用useMemo来缓存。

  • 基本类型:值比较
  • 引用类型:引用比较(对象、数组、函数每次都会创建新引用)
const [user, setUser] = useState({ id: 1, name: '张三', age: 25 });
// 对象依赖的问题(危险!)
useEffect(() => {
    setLogs(prev => [...prev, `⚠️  依赖user对象: ${user.name}`]);
}, [user]); // 每次渲染user对象引用都不同!


const stableUser = useMemo(() => ({ ...user }), [user.name, user.age]);
useEffect(() => {
  setLogs(prev => [...prev, `🎯 使用useMemo优化的依赖`]);
}, [stableUser]);


const handleClick = () => {
  setCount(prev => prev + 1);
};
useEffect(() => {
  setLogs(prev => [...prev, `🔄 依赖函数: 每次都会运行(函数引用变化)`]);
}, [handleClick]); // 函数引用每次渲染都不同


const stableHandleClick = useCallback(() => {
  setCount(prev => prev + 1);
}, []); // 空依赖,函数引用稳定
useEffect(() => {
  setLogs(prev => [...prev, `🏆 使用useCallback优化的函数依赖`]);
}, [stableHandleClick]);

3.useEffect清理函数执行时机

// 1. 组件卸载时
useEffect(() => {
  // effect逻辑
  return () => {
    // 组件卸载时执行清理
  };
}, []);

// 2. 依赖变化时
useEffect(() => {
  // effect逻辑
  return () => {
    // 依赖变化时先执行清理,再执行新effect
  };
}, [dependency]);

// 3. 每次渲染前(无依赖数组)
useEffect(() => {
  // 每次渲染都执行
  return () => {
    // 每次渲染前都执行清理
  };
});


// 声明多个effect时
useEffect(() => {
  console.log('Effect 1');
  return () => console.log('Cleanup 1');
}, []);

useEffect(() => {
  console.log('Effect 2');
  return () => console.log('Cleanup 2');
}, []);
// 执行顺序:
// 挂载: Effect 1 → Effect 2
// 卸载: Cleanup 2 → Cleanup 1 (反向顺序)

4.useEffect处理竞态条件和异步请求

  • 问题演示,如下代码当 userId 变化频繁譬,譬如是输入框的某个绑定值时,且网络状态不稳定时,userId由2变成3,且3返回的速度快于2。最终页面错误的显示userId为2时的脏数据。
// ❌ 有竞态条件的代码
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      const response = await fetch(`/api/users/${userId}`);
      const userData = await response.json();
      setUser(userData); // 如果旧的请求后完成,会覆盖新的
      setLoading(false);
    };

    fetchUser();
  }, [userId]); // userId 变化时重新请求

  return (
    <div>
      {loading ? '加载中...' : user?.name}
    </div>
  );
};

解决方案

  • 4.1 使用 AbortController
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 创建 AbortController
    const abortController = new AbortController();
    const signal = abortController.signal;

    const fetchUser = async () => {
      setLoading(true);
      setError(null);
      
      try {
        const response = await fetch(`/api/users/${userId}`, { 
          signal // 传递 signal
        });
        
        if (!response.ok) {
          throw new Error('请求失败');
        }
        
        const userData = await response.json();
        
        // 检查请求是否已被取消
        if (!signal.aborted) {
          setUser(userData);
        }
      } catch (err) {
        // 如果是取消错误,不更新状态
        if (err.name !== 'AbortError') {
          setError(err.message);
          setUser(null);
        }
      } finally {
        if (!signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // cleanup 函数:取消未完成的请求
    return () => {
      abortController.abort();
    };
  }, [userId]); // 依赖 userId

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  
  return (
    <div>
      <h2>{user?.name}</h2>
      <p>{user?.email}</p>
    </div>
  );
};
  • 4.2 使用标志变量 [核心原理:每次渲染都是独立的闭包]
const UserProfile = ({ userId }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    let isMounted = true; // 标志变量

    const fetchUser = async () => {
      setLoading(true);
      
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        
        // 只有组件仍然挂载时才更新状态
        if (isMounted) {
          setUser(userData);
          setLoading(false);
        }
      } catch (error) {
        if (isMounted) {
          console.error('请求失败:', error);
          setLoading(false);
        }
      }
    };

    fetchUser();

    // cleanup 函数:设置标志为 false
    return () => {
      isMounted = false;
    };
  }, [userId]);

  // 渲染逻辑...
};

使用标志变量能解决的核心在于 每次渲染都是新的函数调用

// 第一次渲染
function Component() {
  let isMounted = true; // 闭包变量1
  // ... effect 逻辑1
  return () => { isMounted = false; }; // cleanup1 清理的是闭包变量1
}

// 第二次渲染(完全是新的函数调用)
function Component() {
  let isMounted = true; // 闭包变量2 - 全新的变量!
  // ... effect 逻辑2
  return () => { isMounted = false; }; // cleanup2 清理的是闭包变量2
}

useRef

特性useRefuseState
触发重新渲染❌ 不会触发组件重新渲染✅ 会触发组件重新渲染
数据可变性✅ 可变,直接修改 .current❌ 不可变,必须通过 setter 函数
返回值{ current: value } 对象[value, setter] 数组
主要用途访问 DOM、存储可变值、保持引用管理组件状态、UI 响应数据
同步/异步同步更新异步更新(批量处理)
使用场景副作用、DOM 操作、计时器 ID用户界面、表单数据、业务逻辑
import { useState, useRef } from "react";

const Home = () => {
  console.log("Home---start");

  const [user, setUser] = useState({ name: "张三", age: 25 });
  const userRef = useRef({ name: "张三", age: 25 });

  const updateState = () => {
    // ❌ 错误:直接修改不会触发重新渲染
    // user.name = '李四';

    // ✅ 正确:创建新对象, 会触发组件渲染, 会引起页面响应式变化
    setUser((prev) => ({ ...prev, name: "李四" }));
  };

  const updateRef = () => {
    // ✅ 正确:直接修改 ref 的值, 不会触发组件渲染, 当然也不会引起页面响应式变化
    userRef.current.name = "李四";
    userRef.current.age = 30;
    console.log("Ref 更新:", userRef.current); // { name: "李四", age: 30 }
  };

  return (
    <div>
      <h3>数据可变性对比</h3>

      <div>
        <h4>useState (不可变)</h4>
        <p>
          姓名: {user.name}, 年龄: {user.age}
        </p>
        <button onClick={updateState}>更新 State</button>
      </div>

      <div>
        <h4>useRef (可变)</h4>
        <p>
          姓名: {userRef.current.name}, 年龄: {userRef.current.age}
        </p>
        <button onClick={updateRef}>更新 Ref</button>
        <span style={{ color: "gray", marginLeft: "10px" }}>
          (界面不会更新,但值已改变)
        </span>
      </div>
    </div>
  );
};
export default Home;

  • 1.访问dom元素
const DomAccessDemo = () => {
  const inputRef = useRef(null);
  const divRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus();
    inputRef.current.style.border = '2px solid red';
  };

  const measureDiv = () => {
    const rect = divRef.current.getBoundingClientRect();
    console.log('Div 尺寸:', rect);
    divRef.current.textContent = `宽度: ${rect.width}px, 高度: ${rect.height}px`;
  };

  return (
    <div>
      <input ref={inputRef} placeholder="点击按钮聚焦我" />
      <button onClick={focusInput}>聚焦输入框</button>
      
      <div
        ref={divRef}
        style={{ width: '200px', height: '100px', border: '1px solid black', margin: '10px 0' }}
      >
        测量这个 div
      </div>
      <button onClick={measureDiv}>测量尺寸</button>
    </div>
  );
};
  • 2. 存储计时器 ID 或事件监听器
const TimerDemo = () => {
  const [time, setTime] = useState(0);
  const timerRef = useRef(null);

  const startTimer = () => {
    if (timerRef.current) return; // 防止重复启动
    
    timerRef.current = setInterval(() => {
      setTime(prev => prev + 1);
    }, 1000);
  };

  const stopTimer = () => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  };

  const resetTimer = () => {
    stopTimer();
    setTime(0);
  };

  // 组件卸载时清理
  useEffect(() => {
    return () => stopTimer();
  }, []);

  return (
    <div>
      <h3>计时器: {time} 秒</h3>
      <button onClick={startTimer}>开始</button>
      <button onClick={stopTimer}>停止</button>
      <button onClick={resetTimer}>重置</button>
    </div>
  );
};

useRef || forwardRef + useImperativeHandle

  • 通过useRef获取DOM标签, 并通过.current操作DOM的属性和方法
  • 通过useRef获取子组件实例:如果子组件是类组件,那能通过useRef获取子组件实例以及可以直接调用子组件定义的方法; 但子组件若是函数组件,通过.current访问到的是null。必须通过forwardRef包裹子组件+useImperativeHandle暴露子组件内部方法才行。
子组件类型能否获取实例默认能获取什么需要什么条件
类组件✅ 可以类组件的完整实例直接使用 ref
函数组件❌ 不能(默认)null 或 undefined需要使用 forwardRef + useImperativeHandle

父组件

import { useState, useRef } from "react";
import Child from "./Child";

const Home = () => {
  console.log("Home---start");
  const childRef = useRef(null);
  const h3Ref = useRef(null);

  const onSetMessage = () => {
    childRef.current?.showMessage("haha");
  };

  return (
    <>
      <h3 ref={h3Ref}>Home page</h3>
      <button onClick={onSetMessage}>设置子组件内容</button>
      <Child ref={childRef}></Child>
    </>
  );
};
export default Home;

子组件

import React, { forwardRef, useImperativeHandle, useState } from "react";

const Child = (props, ref) => {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState("Hello from Child component");

  // 使用 useImperativeHandle 暴露特定的方法和值
  useImperativeHandle(
    ref,
    () => ({
      // 暴露给父组件的方法
      incrementCount: () => {
        setCount((prev) => prev + 1);
      },

      showMessage: (text) => {
        setMessage(text);
        return `Message set to: ${text}`;
      },

      // 暴露给父组件的值
      getComponentInfo: () => {
        return {
          count,
          message,
          timestamp: new Date().toISOString(),
        };
      },

      // 只读的当前值
      currentCount: count,

      // 重置方法
      reset: () => {
        setCount(0);
        setMessage("Hello from function component");
      },
    }),
    [count, message]
  ); // 依赖数组,当 count 或 message 变化时重新创建暴露的方法

  return (
    <div style={{ border: "2px solid green", padding: "10px", margin: "10px" }}>
      <h3>函数子组件(使用 forwardRef)</h3>
      <p>Count: {count}</p>
      <p>Message: {message}</p>
      <button onClick={() => setCount((c) => c + 1)}>内部增加</button>
    </div>
  );
};
// 使用 forwardRef 包装函数组件
export default forwardRef(Child);

createPortal

createPortal 允许你将子元素渲染到 DOM 节点中的不同位置,常用于模态框、提示框、全局通知等场景。这是因为 position: fixed 在理论上是相对于浏览器窗口定位的,但父元素如果加了 transform perspective filter等属性时,他就相对于父元素而定位了。这样就导致模态框、提示框、全局通知等全局显示组件定位不准了。

import React, { useState } from "react";
import { createPortal } from "react-dom";

// 模态框组件
function Modal({ children, isOpen, onClose }) {
  if (!isOpen) return null;

  return createPortal(
    <div
      style={{
        position: "fixed",
        top: 0,
        left: 0,
        right: 0,
        bottom: 0,
        backgroundColor: "rgba(0, 0, 0, 0.5)",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        zIndex: 1000,
      }}
    >
      <div
        style={{
          backgroundColor: "white",
          padding: "20px",
          borderRadius: "8px",
          minWidth: "300px",
          maxWidth: "500px",
        }}
      >
        {children}
        <button onClick={onClose} style={{ marginTop: "20px" }}>
          关闭
        </button>
      </div>
    </div>,
    document.body // 渲染到 body 下
  );
}

function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    // 父元素加了 transform 样式属性,但是 Modal 使用了 createPortal api包裹,使其父元素是body,所以能处于浏览器正中间显示;
    // 父元素加了 transform 样式属性,Modal 不使用 createPortal , Modal 定位的参考点是div,导致 Modal 无法在浏览器正中间显示。
    <div style={{ padding: "20px", transform: "scale(0.5)" }}>
      <h1>基础模态框示例</h1>
      <button onClick={() => setIsModalOpen(true)}>打开模态框</button>

      <Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
        <h2>这是模态框内容</h2>
        <p>这个模态框是使用 createPortal 渲染到 body 元素下的。</p>
        <p>即使父元素有 overflow: hidden,模态框也不会被裁剪。</p>
      </Modal>
    </div>
  );
}

export default App;

路由 react-router-dom

  • 一个简单的路由配置,Routes 和 Route 来定义路由
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
import { BrowserRouter } from 'react-router-dom' // 导入 BrowserRouter

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter> {/* 使用 BrowserRouter 包裹 App */}
      <App />
    </BrowserRouter>
  </React.StrictMode>,
)


// App.jsx
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';

function App() {
  return (
    <div className="App">
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} /> {/* 404 页面 */}
      </Routes>
    </div>
  );
}

export default App;
  • useRoutes,允许你使用 JavaScript 对象 来定义路由,而不是使用 <Route> 组件

1.创建路由配置对象 src/routes/index.jsx

// src/routes/index.jsx
import { useRoutes } from 'react-router-dom';
// 1. 引入你的页面组件
import Home from '../pages/Home';
import About from '../pages/About';
import UserList from '../pages/UserList';
import UserProfile from '../pages/UserProfile';
import NotFound from '../pages/NotFound';

// 2. 定义路由配置数组
const routes = [
  {
    path: '/',
    element: <Home />,
  },
  {
    path: '/about',
    element: <About />,
  },
  {
    path: '/users',
    element: <UserList />,
    // 3. 可以嵌套路由(子路由)
    // children: [...]
  },
  {
    path: '/users/:userId', // 4. 动态路由参数
    element: <UserProfile />,
  },
  {
    path: '*', // 5. 404 通配符路由,匹配所有未定义的路径
    element: <NotFound />,
  },
];

// 6. 创建一个自定义 Hook 来使用路由配置
export default function AppRoutes() {
  // useRoutes Hook 接收路由配置数组,并返回匹配当前 URL 的路由元素
  const element = useRoutes(routes);
  return element;
}

2.在主应用组件中使用, src/App.jsx,使用上面创建的 AppRoutes 组件。

// src/App.jsx
import './App.css';
// 1. 引入路由组件
import AppRoutes from './routes';

// 2. 引入其他需要的组件,比如导航栏
import Navigation from './components/Navigation';

function App() {
  return (
    <div className="App">
      {/* 3. 导航栏,通常包含 <Link> 组件 */}
      <Navigation />
      {/* 4. 这里是路由渲染的位置 */}
      <main>
        <AppRoutes />
      </main>
    </div>
  );
}

export default App;

3.在入口文件设置 Router src/main.jsx,用 <BrowserRouter> 包裹了 App 组件

// src/main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
// 1. 引入 BrowserRouter
import { BrowserRouter } from 'react-router-dom'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    {/* 2. 用 BrowserRouter 包裹 App,为其下属组件提供路由上下文 */}
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>,
)
  • useRoutes + <Outlet /> 嵌套路由
  1. 配置children子路由选项
// src/routes/index.jsx (部分代码)
import { useRoutes } from 'react-router-dom';

const routes = [
  {
    path: '/',
    element: <Home />,
  },
  {
    path: '/dashboard',
    element: <DashboardLayout />, // 布局组件,内部包含 <Outlet/>
    children: [ // 子路由将在布局组件的 <Outlet/> 位置渲染
      {
        index: true, // 等同于 path: '',匹配 /dashboard
        element: <DashboardHome />,
      },
      {
        path: 'settings', // 匹配 /dashboard/settings
        element: <DashboardSettings />,
      },
      {
        path: 'analytics', // 匹配 /dashboard/analytics
        element: <DashboardAnalytics />,
      },
    ],
  },
  // ... other routes
];

// 创建路由 Hook
export default function AppRoutes() {
  const element = useRoutes(routes);
  return element;
}

2.父组件需要包含一个 <Outlet /> 来渲染子路由

// src/layouts/DashboardLayout.jsx
import { Outlet, Link } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div>
      <h1>Dashboard</h1>
      <nav>
        <Link to="/dashboard">Home</Link>
        <Link to="/dashboard/settings">Settings</Link>
        <Link to="/dashboard/analytics">Analytics</Link>
      </nav>
      <hr />
      {/* 子路由对应的组件将在这里渲染 */}
      <Outlet />
    </div>
  );
}
export default DashboardLayout;
  • 路由其他Api

<Link to="/dashboard">Home</Link>

import { useNavigate, useSearchParams, useParams, useLocation } from 'react-router-dom';

// useSearchParams: 获取查询参数,例如获取?后面的 `http://example.com/path?name=Alice&age=25`。
const [searchParams, setSearchParams] = useSearchParams();
// 使用 get 方法获取特定参数
const name = searchParams.get('name');
const age = searchParams.get('age');

// useParams: 获取 URL 中的参数,例如 /users/123 中的 '123'
const { userId } = useParams();

const navigate = useNavigate();
const handleClick = () => {
    // 跳转到关于页面
    navigate('/about');
    // 或者使用 replace: true 替换当前历史记录
    // navigate('/about', { replace: true });
};


在跳转时,你需要通过 `state` 属性传递参数:
// 或使用 <Navigate to="/user-detail" state={}>
navigate('/user-detail', {
  state: {
    user: { name: 'Alice', email: 'alice@example.com' },
    from: 'homepage'
  }
});

// 获取 location 对象
const location = useLocation(); // { pathname: '/dashboard', search: '?name=1&age=12', hash: '', state: null }
// 从 location.state 中获取传递的状态参数
const user = location.state?.user; // 假设传递了一个 user 对象
const from = location.state?.from; // 或其他状态数据

通过 state 传递的参数在页面刷新后通常会丢失,因为它们通常存储在内存中(如 React Router 的 history 状态),而不是 URL 中。如果参数需要持久化,应优先考虑使用动态路由参数查询参数

  • 路由守卫
  1. routes/index.jsx
import { useRoutes } from "react-router-dom";
import ProtectedRoute from "./ProtectedRoute";
import Login from "../pages/Login.jsx";
import Dashboard from "../pages/Dashboard";
import Profile from "../pages/Profile";

const routes = [
  {
    path: "/login",
    element: <Login />,
  },
  {
    // 保护路由组
    element: <ProtectedRoute />,
    children: [
      {
        path: "/dashboard",
        element: <Dashboard />,
      },
      {
        path: "/profile",
        element: <Profile />,
      },
    ],
  },
];

// 创建路由 Hook
export default function AppRoutes() {
  const element = useRoutes(routes);
  return element;
}
  1. ProtectedRoute.jsx
import { Navigate, Outlet, useLocation } from "react-router-dom";

const ProtectedRoute = () => {
  // 这里可以根据你的认证逻辑进行修改
  const isAuthenticated = !!localStorage.getItem("authToken");
  const location = useLocation();
  console.log("location", location); // { pathname: '/dashboard', search: '?name=1&age=12', hash: '', state: null, key: 'nedxjr1g' }

  if (!isAuthenticated) {
    // 未认证,重定向到登录页
    return <Navigate to="/login" replace state={{ from: location }} />;
  }

  // 认证通过,渲染子路由
  return <Outlet />;
};

export default ProtectedRoute;
  1. Login.jsx
import { useNavigate, useLocation } from "react-router-dom";

function Login() {
  const navigate = useNavigate();
  const location = useLocation();

  const from = location.state?.from || "/dashboard"; // :cite[4]

  const onLogin = () => {
    localStorage.setItem("authToken", "fake-jwt-token");
    console.log("from", from); // { pathname: '/dashboard', search: '?name=1&age=12', hash: '', state: null, key: 'nedxjr1g' }
    navigate(from);
  };
  return (
    <>
      <button onClick={onLogin}>登录</button>
    </>
  );
}
export default Login;

状态管理 Zustand

  • 创建一个 Store
// store/useCounterStore.js
import { create } from 'zustand';

// 使用 create 函数创建 store,其参数是一个返回状态和操作的函数
const useCounterStore = create((set, get) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
  reset: () => set({ count: 0 }),
  // 也可以获取当前状态
  doubleCount: () => get().count * 2,
}));

export default useCounterStore;
  • 在组件中使用
// components/Counter.jsx
import useCounterStore from '../store/useCounterStore';

function Counter() {
  // 从 store 中提取需要的状态和动作
  // 你可以选择整个 store,或者只选择需要的部分,Zustand 会进行自动的优化
  const { count, increment, decrement } = useCounterStore();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>+</button>
      <button onClick={decrement}>-</button>
    </div>
  );
}

export default Counter;
  • 持久化解决方案
// store/useStoreWithPersist.js
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';

// 使用 persist middleware
const usePersistedStore = create(
  persist(
    (set, get) => ({
      count: 0,
      user: null,
      theme: 'light',
      
      increment: () => set((state) => ({ count: state.count + 1 })),
      decrement: () => set((state) => ({ count: state.count - 1 })),
      
      setUser: (userData) => set({ user: userData }),
      setTheme: (theme) => set({ theme: theme }),
      
      // 计算属性
      getDoubleCount: () => get().count * 2,
    }),
    {
      name: 'app-storage', // 存储的键名
      storage: createJSONStorage(() => localStorage), // 使用 localStorage
      // storage: createJSONStorage(() => sessionStorage), // 或者使用 sessionStorage
    }
  )
);

export default usePersistedStore;