You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中LocalStorage失效时如何持久化Composition状态?

React Context 结合 LocalStorage 持久化状态失效问题

我正在开发一个React项目,通过Context Provider为用户提供基于所选参数生成的Composition链接,希望持久化Composition状态,但使用LocalStorage实现后,刷新页面状态无法保留。以下是Context Provider及相关组件的代码:

Context Provider 代码

import {createContext, useContext, useState, useEffect} from 'react';
import { storage } from '../../../Config/firebaseConfig';
import {ref, deleteObject} from 'firebase/storage'

 const CompositionsContext = createContext(['There are no Compositions']);

 const useCompositions = () => useContext(CompositionsContext);

 const CompositionContextProvider = ({children}) => {
    const[compositions, setCompositions] = useState([]);

    useEffect(() => {
        const storedState = localStorage.getItem('compositions');
        if (storedState) {
          setCompositions(JSON.parse(storedState));
        }
      }, []);
    
      useEffect(() => {
        localStorage.setItem('compositions', JSON.stringify(compositions));
      }, [compositions]);

    const addNewComposition = (newComposition) => {
        setCompositions(compositions => compositions.concat(newComposition));
    }

    const deleteComposition = (indexValue) => {
        setCompositions(compositions => compositions.filter((_,index) => index !== indexValue))
        const storageRef = ref(storage, `pdfs/${compositions[indexValue ]}.pdf`)
        deleteObject(storageRef);
    }


    return (
        <CompositionsContext.Provider value={{addNewComposition, compositions, deleteComposition}}>
            {children}
        </CompositionsContext.Provider>
    )
 }

 export {useCompositions};
 export default CompositionContextProvider;

CreateComposition 组件代码

import React, { useState } from 'react';
import { useCompositions } from './Contexts/CompositionContextProvider';


const CreateComposition = () =>{
    const {addNewComposition} = useCompositions();
    const [taal, setTaal] = useState('');
    const [bpm, setBpm] = useState('');
    const [name, setName] = useState('');

    const options = [
      {label : "", value: ""},
      {label: "Teentaal", value: "Teentaal"},
      {label: "Jhaptaal", value: "Jhaptaal"},
      {label: "Ektaal", value: "Ektaal"},
      {label: "Rupak", value: "Rupak"}
    ]
  
    const sayKayeda = () => {
      alert(taal + " " + bpm + " " + name);
    }
  
    const changeTaal = (event) => {
      setTaal(event.target.value);
    }
  
    const changeBPM = (event) => {
      setBpm(event.target.value);
    }
  
    const changeName = (event) => {
      setName(event.target.value);
    }

  
    const addNewCompositionHandler = () => {
      const newComposition = `${taal}_${bpm}_${name}`;
      addNewComposition(newComposition)
      setTaal('');
      setBpm('');
      setName('');
      
    }


    return (
    <>
        <h2>Input Kayeda Details Below</h2>
        <br></br>
        <br></br>
        <label> <b>Select a Taal</b>&nbsp; 
          {/* <input onChange={changeTaal} value={taal}></input> */}
          <select onChange = {changeTaal}>
            {options.map(option =>(
              <option value={option.value}>{option.label}</option>
            ))}
          </select>
        </label>
        <p></p>
        <label> <b>BPM</b>&nbsp; 
            <input onChange={changeBPM} value={bpm}></input>
        </label>
        <p></p>
        <label> <b>Name</b>&nbsp; 
            <input onChange={changeName} value={name}></input>
        </label>
        <p></p>
        <button onClick={sayKayeda}>Current Kayeda</button>
        &nbsp; 
        <button onClick={addNewCompositionHandler}>Add Composition</button>
        <p></p>
    </>
    );
}


export default CreateComposition;

Compositions 组件代码

// Compositions.js
import React, { useEffect, useState } from 'react';
import { useCompositions } from './Contexts/CompositionContextProvider';
import TeentaalPDF from '../../PDFs/TeentaalE.pdf';
import JhaptaalPDF from '../../PDFs/JhaptaalE.pdf';
import EktaalPDF from '../../PDFs/EktaalE.pdf';
import RupakPDF from '../../PDFs/RupakE.pdf';
import { storage } from '../../Config/firebaseConfig';
import { ref, getDownloadURL, uploadBytes} from 'firebase/storage';

const Compositions = () => {
  const {compositions, deleteComposition} = useCompositions();
  const [pdfLinks, setPdfLinks] = useState({});
  const [fileUpload, setFileUpload] = useState(null);

  useEffect(() => {
    const fetchPDFLinks = async () => {
      const links = {};
      for (const composition of compositions) {
        const downloadURL = await getPDFDownloadURLFromStorage(composition);
        links[composition] = downloadURL || getDefaultPDF(composition);
      }
      setPdfLinks(links);
    };

    fetchPDFLinks();
  }, [compositions]);

  const getPDFDownloadURLFromStorage = async (composition) => {
    const storageRef = ref(storage, `pdfs/${composition}.pdf`);
    try {
      const downloadURL = await getDownloadURL(storageRef);
      return downloadURL;
    } catch (error) {
      console.error(error);
      return null
    }
  };

  const getDefaultPDF = (composition) => {
    if (composition.includes('Teentaal')) {
      return TeentaalPDF;
    } else if (composition.includes('Jhaptaal')) {
      return JhaptaalPDF;
    } else if (composition.includes('Ektaal')) {
      return EktaalPDF;
    } else if (composition.includes('Rupak')) {
      return RupakPDF;
    } else {
      return null; // Return null or a default PDF for unmatched compositions
    }
  };

  const onSubmitComposition = async (newCompositionName) => {
    if(!fileUpload) return;
    const storageRef = ref(storage, `pdfs/${newCompositionName}.pdf`);
    try {
      await uploadBytes(storageRef, fileUpload); // Uploading file to Firebase Storage

    } catch (error) {
      console.error('Error uploading file:', error);
    }
  };

  return (
    <>
      <h2>List of Compositions</h2>
      <h2>{compositions.length}</h2>
      <ul>
        {compositions.map((composition, index) => (
          <li key={index}>
            <a href={pdfLinks[composition]} target="_blank" rel="noreferrer">{composition} </a> &nbsp;
            <button onClick={() => deleteComposition(index)}>Delete Composition</button>
            <p>
              <input type = "file" onChange={(e) => setFileUpload(e.target.files[0])}/>
              <button onClick={() => onSubmitComposition(composition)}>
                Store Composition
              </button>
            </p>
          </li>
        ))}
      </ul>
    </>
  );
};

export default Compositions;

问题原因及修复方案

1. LocalStorage 初始覆盖问题

当前代码中,组件挂载时会先执行第二个useEffect,将初始空数组[]存入LocalStorage,直接覆盖之前存储的有效数据。后续第一个useEffect读取到的是被覆盖后的空值,导致刷新后状态丢失。

修复:初始化State时直接读取LocalStorage
将State初始化改为函数式,直接从LocalStorage读取数据,避免初始覆盖:

const[compositions, setCompositions] = useState(() => {
  const storedState = localStorage.getItem('compositions');
  // 解析存储的数据,不存在则返回空数组
  return storedState ? JSON.parse(storedState) : [];
});

同时移除第一个useEffect,因为初始化已经完成了读取操作。

2. 删除操作中的状态异步问题

deleteComposition函数中,setCompositions是异步更新,直接访问compositions[indexValue]拿到的是旧状态的数组,可能导致删除错误的文件。

修复:在函数式更新中获取最新状态
修改deleteComposition,在setCompositions的回调中获取当前最新数组,确保删除正确的文件:

const deleteComposition = (indexValue) => {
  setCompositions(currentCompositions => {
    // 获取要删除的Composition名称
    const targetComposition = currentCompositions[indexValue];
    // 删除Firebase Storage中的对应文件
    const storageRef = ref(storage, `pdfs/${targetComposition}.pdf`);
    deleteObject(storageRef).catch(err => console.error('删除文件失败:', err));
    // 返回过滤后的新数组
    return currentCompositions.filter((_, index) => index !== indexValue);
  });
}

3. 可选:添加输入校验

在addNewCompositionHandler中,添加输入校验,避免空值或无效值被添加到数组:

const addNewCompositionHandler = () => {
  // 校验必填字段
  if (!taal || !bpm || !name) {
    alert('请填写完整的Composition信息');
    return;
  }
  const newComposition = `${taal}_${bpm}_${name}`;
  addNewComposition(newComposition);
  setTaal('');
  setBpm('');
  setName('');
}

修改后的完整Context Provider代码

import {createContext, useContext, useState, useEffect} from 'react';
import { storage } from '../../../Config/firebaseConfig';
import {ref, deleteObject} from 'firebase/storage'

const CompositionsContext = createContext({
  addNewComposition: () => {},
  compositions: [],
  deleteComposition: () => {}
});

const useCompositions = () => useContext(CompositionsContext);

const CompositionContextProvider = ({children}) => {
  // 初始化时直接读取LocalStorage
  const[compositions, setCompositions] = useState(() => {
    const storedState = localStorage.getItem('compositions');
    return storedState ? JSON.parse(storedState) : [];
  });

  useEffect(() => {
    // 状态变化时存入LocalStorage
    localStorage.setItem('compositions', JSON.stringify(compositions));
  }, [compositions]);

  const addNewComposition = (newComposition) => {
    setCompositions(compositions => compositions.concat(newComposition));
  }

  const deleteComposition = (indexValue) => {
    setCompositions(currentCompositions => {
      const targetComposition = currentCompositions[indexValue];
      const storageRef = ref(storage, `pdfs/${targetComposition}.pdf`);
      deleteObject(storageRef).catch(err => console.error('删除文件失败:', err));
      return currentCompositions.filter((_, index) => index !== indexValue);
    });
  }

  return (
    <CompositionsContext.Provider value={{addNewComposition, compositions, deleteComposition}}>
      {children}
    </CompositionsContext.Provider>
  )
}

export {useCompositions};
export default CompositionContextProvider;

内容的提问来源于stack exchange,提问作者51014

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 00:54:50