当前位置:首页 > 文章列表 > 文章 > 前端 > ReactContext实现收藏列表不可变更新与本地存储

ReactContext实现收藏列表不可变更新与本地存储

2025-10-18 16:27:36 0浏览 收藏

还在为React应用中收藏功能的状态管理而烦恼吗?本文深入探讨如何利用React Context API优雅地实现收藏列表的不可变更新与本地存储,解决状态不同步、数据重置等常见难题。通过构建`FavoritesContext`,集中管理收藏数据和操作函数,无需繁琐的props传递,轻松实现组件间的数据同步。文章还详细讲解了如何利用`localStorage`进行数据持久化,确保用户收藏数据在刷新或页面切换后依然保留。更有`useMemo`、`useCallback`等优化技巧,提升应用性能,打造流畅的用户体验。无论你是React新手还是经验丰富的开发者,都能从中获得实用的技巧和灵感,构建更健壮、可维护的React应用。

React中利用Context API实现收藏列表的不可变更新与本地存储

本文详细介绍了如何在React应用中,使用Context API管理共享状态,实现收藏列表的不可变更新,并将其持久化存储到本地。通过构建一个`FavoritesContext`,我们能够优雅地在多个组件间同步收藏数据,解决传统组件状态管理中数据不同步和重置的问题,确保用户收藏体验的连贯性。

1. 引言:收藏功能的状态管理挑战

在构建现代Web应用时,用户收藏功能(如收藏食谱、商品等)是常见的需求。然而,在React等前端框架中实现此类功能时,开发者常面临以下挑战:

  • 状态不同步: 当收藏数据分散在不同组件的本地状态中时,修改一个组件的收藏状态可能无法及时反映到其他展示收藏列表的组件。
  • 数据重置: 用户在不同页面间切换或刷新页面后,未持久化的收藏数据会丢失,导致用户体验不佳。
  • 不可变更新: React中推荐对状态进行不可变更新,直接修改原数组或对象可能导致组件不重新渲染或产生难以追踪的副作用。
  • 组件通信复杂: 在组件层级较深时,通过props逐层传递收藏数据和操作函数会变得繁琐。

本文将通过React的Context API,提供一个优雅的解决方案,实现收藏列表的不可变更新,并利用本地存储(localStorage)进行数据持久化。

2. 共享状态解决方案:React Context API

为了解决上述挑战,特别是数据同步和组件通信问题,我们可以引入React的Context API。Context API提供了一种在组件树中共享数据的方式,无需通过props逐层传递。它允许我们将收藏列表的状态及其操作函数集中管理,并使其在任何需要访问或修改收藏数据的组件中变得可用。

3. 构建 FavoritesContext

我们将创建一个名为FavoritesContext的上下文,包含收藏列表数据以及添加/移除收藏的方法。

3.1 创建上下文定义文件 (FavoritesContext.js)

首先,定义FavoritesContext及其提供者FavoritesContextProvider。

// FavoritesContext.js
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
} from "react";

// 1. 创建 FavoritesContext,并设置默认值
const FavoritesContext = createContext({
  favorites: [], // 收藏列表
  toggleFavorite: () => {}, // 切换收藏状态的函数
  isFavorite: () => false, // 判断是否已收藏的函数
});

// 2. 收藏提供者组件
export const FavoritesContextProvider = ({ children }) => {
  // useRef 用于跳过 useEffect 的首次执行,避免在初始化时覆盖 localStorage
  const preloadRef = useRef(false);

  // useState 管理收藏列表,从 localStorage 初始化
  const [favorites, setFavorites] = useState(() => {
    try {
      const storedFavorites = localStorage.getItem("favorites");
      return storedFavorites ? JSON.parse(storedFavorites) : [];
    } catch (error) {
      console.error("Failed to parse favorites from localStorage:", error);
      return [];
    }
  });

  // useMemo 优化,根据 favorites 列表生成一个 Set,用于快速判断某个ID是否已收藏
  const ids = useMemo(
    () => new Set(favorites.map(({ id }) => id)),
    [favorites]
  );
  // useCallback 优化,判断给定ID的食谱是否在收藏列表中
  const isFavorite = useCallback((id) => ids.has(id), [ids]);

  // useEffect 监听 favorites 变化,并同步到 localStorage
  useEffect(() => {
    // 只有在 preloadRef.current 为 true 时(即非首次加载),才执行 localStorage 写入
    if (preloadRef.current) {
      localStorage.setItem("favorites", JSON.stringify(favorites));
    }
    preloadRef.current = true; // 标记为已预加载,后续的 favorites 变化都会触发写入
  }, [favorites]); // 依赖 favorites 数组

  // 切换收藏状态的函数:添加或移除食谱
  const toggleFavorite = ({ id, image, title, route }) => {
    if (isFavorite(id)) {
      // 如果已收藏,则从列表中移除(不可变更新)
      setFavorites((prev) => prev.filter((fav) => fav.id !== id));
    } else {
      // 如果未收藏,则添加到列表中(不可变更新)
      setFavorites((prev) => [...prev, { id, image, title, route }]);
    }
  };

  // 3. 提供上下文值
  return (
    <FavoritesContext.Provider
      value={{ favorites, toggleFavorite, isFavorite }}
    >
      {children}
    </FavoritesContext.Provider>
  );
};

// 4. 便捷钩子:useFavorites,用于在组件中消费上下文
export const useFavorites = () => useContext(FavoritesContext);

3.2 关键点解析

  • useState初始化与localStorage: favorites状态在初始化时尝试从localStorage读取数据。为了避免JSON.parse失败,加入了try-catch块。
  • useRef跳过首次useEffect: preloadRef用于确保useEffect在组件首次渲染时(即从localStorage加载数据时)不立即将空数组或初始数据写回localStorage,只有在favorites真正发生改变后才进行写入操作。
  • useMemo和useCallback优化:
    • ids通过useMemo缓存了所有收藏项的ID集合,避免每次渲染都重新计算。
    • isFavorite通过useCallback缓存,确保其引用稳定,避免不必要的子组件重新渲染。
  • useEffect同步到localStorage: 当favorites状态发生变化时,useEffect会将最新的收藏列表序列化为JSON字符串并存储到localStorage,实现数据持久化。
  • toggleFavorite实现不可变更新:
    • 无论是添加还是移除收藏,都使用了函数式更新setFavorites((prev) => ...),并且通过filter或展开运算符...prev创建新数组,而不是直接修改prev,从而确保状态的不可变性。
  • useFavorites自定义钩子: 这是一个简单的封装,让组件更方便地使用FavoritesContext。

4. 集成 FavoritesContext 到应用

现在,我们将FavoritesContext集成到React应用中。

4.1 在应用根部包裹提供者 (App.js 或 index.js)

为了让整个应用或应用的相关部分都能访问到收藏状态,我们需要将FavoritesContextProvider包裹在组件树的顶层。

// App.js 或 index.js
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom'; // 假设使用了路由
import App from './App'; // 你的主应用组件
import { FavoritesContextProvider } from './FavoritesContext'; // 引入收藏上下文提供者

function Root() {
  return (
    <Router>
      <FavoritesContextProvider>
        <App /> {/* 你的主应用组件 */}
      </FavoritesContextProvider>
    </Router>
  );
}

export default Root; // 或者在 index.js 中渲染 Root 组件

4.2 在组件中消费上下文 (AddToFavorites.js 和 Favorites.js)

现在,任何子组件都可以通过useFavorites钩子访问和修改收藏状态。

AddToFavorites 组件示例:

此组件负责显示“添加/移除收藏”按钮,并根据当前食谱是否已收藏来更新状态。

// AddToFavorites.js
import React from 'react';
import { useFavorites } from './FavoritesContext'; // 引入 useFavorites 钩子
import { BsFillSuitHeartFill } from 'react-icons/bs'; // 假设使用了此图标

// AddToFavBtn 是一个样式组件,这里仅作占位
const AddToFavBtn = ({ children, className, onClick }) => (
  <button className={className} onClick={onClick}>{children}</button>
);

function AddToFavorites({ recipeDetails }) {
  // 从上下文中获取 isFavorite 检查函数和 toggleFavorite 操作函数
  const { isFavorite, toggleFavorite } = useFavorites();

  // 判断当前食谱是否已收藏
  const isCurrentRecipeFavorite = isFavorite(recipeDetails.id);

  // 处理点击事件,切换收藏状态
  const handleToggle = () => {
    toggleFavorite({
      id: recipeDetails.id,
      image: recipeDetails.image,
      title: recipeDetails.title,
      // route: `/recipe/${recipeDetails.id}` // 根据需要添加路由信息
    });
  };

  return (
    <AddToFavBtn className={isCurrentRecipeFavorite ? 'active' : ''} onClick={handleToggle}>
      {!isCurrentRecipeFavorite ? 'Add to favorites' : 'Remove from favorites'}
      <div>
        <BsFillSuitHeartFill />
      </div>
    </AddToFavBtn>
  );
}

export default AddToFavorites;

Recipe 组件中集成 AddToFavorites: 确保Recipe组件将完整的食谱详情(包含id, image, title等)作为recipeDetails prop传递给AddToFavorites。

// Recipe.js (部分代码)
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import AddToFavorites from './AddToFavorites'; // 引入 AddToFavorites 组件

// DetailWrapper, Info, ButtonContainer, Button 等是样式组件,这里仅作占位
const DetailWrapper = ({ children }) => <div>{children}</div>;
const Info = ({ children }) => <div>{children}</div>;
const ButtonContainer = ({ children }) => <div>{children}</div>;
const Button = ({ children, className, onClick }) => <button className={className} onClick={onClick}>{children}</button>;

function Recipe() {
  let params = useParams();
  const [details, setDetails] = useState({});
  const [activeTab, setActiveTab] = useState('summary');

  const fetchDetails = async () => {
    const data = await fetch(`https://api.spoonacular.com/recipes/${params.name}/information?apiKey=${process.env.REACT_APP_API_KEY}`);
    const detailData = await data.json();
    setDetails(detailData);
  }

  useEffect(() => {
    fetchDetails();
  }, [params.name]);

  return (
    <DetailWrapper>
        <h2>{details.title}</h2>
        <img src={details.image} alt="" />

      <Info>
        <ButtonContainer>
          {/* 将 details 对象作为 recipeDetails prop 传递 */}
          {details.id && <AddToFavorites recipeDetails={details}/>} 
          <Button 
            className={activeTab === 'summary' ? 'active' : ''}
            onClick={() => setActiveTab('summary')}>Nutrition Info
          </Button>
          {/* ... 其他按钮和内容 */}
        </ButtonContainer>
        {/* ... 其他标签页内容 */}
      </Info>
    </DetailWrapper>
  )
}

export default Recipe;

Favorites 组件示例:

此组件负责从上下文中获取收藏列表并进行展示。

// Favorites.js
import React from 'react';
import { useFavorites } from './FavoritesContext'; // 引入 useFavorites 钩子
// FavPageContainer 是一个样式组件,这里仅作占位
const FavPageContainer = ({ children }) => <div>{children}</div>;

function Favorites() {
  // 从上下文中获取收藏列表
  const { favorites } = useFavorites();

  return (
    <FavPageContainer>
      <h3>My Favorites</h3>
      {favorites.length === 0 ? (
        <p>No favorites added yet.</p>
      ) : (
        <ul>
          {favorites.map((listItem) => (
            // 确保每个列表项都有唯一的 key
            <li key={listItem.id}>
              <img 
                src={listItem.image} 
                alt={listItem.title} 
                style={{ width: '50px', height: '50px', marginRight: '10px', verticalAlign: 'middle' }} 
              />
              <span>{listItem.title}</span>
              {/* 如果有路由信息,可以添加链接 */}
              {/* {listItem.route && <Link to={`/recipe/${listItem.route}`}>{listItem.title}</Link>} */}
            </li>
          ))}
        </ul>
      )}
    </FavPageContainer>
  );
}

export default Favorites;

5. 注意事项与最佳实践

  • JSON.parse和JSON.stringify: localStorage只能存储字符串。因此,在存储JavaScript对象或数组时,必须使用JSON.stringify将其转换为字符串;在读取时,使用JSON.parse将其转换回JavaScript对象或数组。
  • localStorage容量限制: localStorage通常有5MB左右的存储限制。对于大型数据集合,应考虑其他持久化方案(如IndexedDB或服务器端存储)。
  • 错误处理: 从localStorage读取数据时,JSON.parse可能会因为存储的数据格式不正确而抛出错误。在useState初始化时加入try-catch块是良好的实践。
  • 性能优化: useMemo和useCallback在Context API中尤其有用,它们可以帮助避免不必要的计算和函数重新创建,从而提高消费者组件的渲染性能。
  • 替代方案: 对于更复杂的全局状态管理需求,例如涉及多个模块、大量状态或复杂异步操作的场景,可以考虑使用Redux、Zustand、Jotai等专业的全局状态管理库。Context API适用于中小型应用或局部共享状态。
  • id的重要性: 确保每个收藏项都有一个唯一的id,这对于列表渲染的key属性以及toggleFavorite函数的正确识别和操作至关重要。

6. 总结

通过采用React Context API来管理收藏列表的共享状态,我们成功解决了在多个组件间同步数据、持久化数据到本地存储以及进行不可变更新的挑战。这种方法不仅使代码结构更清晰,降低了组件间的耦合度,还提升了用户体验的连贯性。理解并熟练运用Context API及其相关的Hooks(useState, useEffect, useMemo, useCallback, useRef),是构建健壮且可维护的React应用的关键。

以上就是本文的全部内容了,是否有顺利帮助你解决问题?若是能给你带来学习上的帮助,请大家多多支持golang学习网!更多关于文章的相关知识,也可关注golang学习网公众号。

PHP表单邮件发送问题排查与优化技巧PHP表单邮件发送问题排查与优化技巧
上一篇
PHP表单邮件发送问题排查与优化技巧
百度网盘磁力链接怎么下
下一篇
百度网盘磁力链接怎么下
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • ChatExcel酷表:告别Excel难题,北大团队AI助手助您轻松处理数据
    ChatExcel酷表
    ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
    3182次使用
  • Any绘本:开源免费AI绘本创作工具深度解析
    Any绘本
    探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
    3393次使用
  • 可赞AI:AI驱动办公可视化智能工具,一键高效生成文档图表脑图
    可赞AI
    可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
    3425次使用
  • 星月写作:AI网文创作神器,助力爆款小说速成
    星月写作
    星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
    4530次使用
  • MagicLight.ai:叙事驱动AI动画视频创作平台 | 高效生成专业级故事动画
    MagicLight
    MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
    3802次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码