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

Next.js页面刷新时sortingWay状态重置问题求助

问题:Next.js动态路由页面刷新后排序状态丢失

我开发的Next.js应用包含动态路由products/[category]/[subcategory],这些页面复用带有排序功能的ProductsList组件,通过更新组件内的sortingWay状态实现页面间路由跳转。
根据Next.js官方文档:

在Next.js中导航至同一页面时,默认不会重置页面状态,因为React不会卸载组件,除非父组件发生变化。

当前同页面导航时状态正常,但当sortingWay有值时刷新页面,该状态会被重置。尝试通过localStorage保存状态但未生效。


相关代码

[category].js([subcategory].js与其基本一致)

import { useRouter } from 'next/router';
import ProdList from '../../../components/ProdList';
import React from "react";

export default function SubCategory({ products }) {
  const router = useRouter();
  const category = router.query.category;

  const filtered = products.filter(product => category === product.category);

  return (
    <>
      <h1>Товари підкатегорії {category}</h1>
      <ProdList data={filtered}/>
    </>
  )
}

export async function getStaticProps() {
  const response = await fetch(`http://localhost:4000/products`);
  const data = await response.json();

  return {
    props: {
      products: data
    }
  }
}

export async function getStaticPaths() {
  const response = await fetch('http://localhost:4000/products');
  const data = await response.json();

  const paths = data.map(product => {
    return {
      params: { 
        category: product.category,
      },
    }
  })
  
  return {
    paths,
    fallback: false
  }
}

ProductList.jsx组件

import React from 'react'
import { useEffect, useState } from "react";
import { useRouter } from 'next/router';
import ProductItem from '../ProductItem';
import { productList } from './styles';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';

export default function ProdList({ data }) {
  const [ sortingWay, setSortingWay ] = useState('');
  const [ productsList, setProductList ] = useState(data);

  const router = useRouter();
  const { pathname, query } = router
  const category = router.query.category;
  const subCategory = router.query.subCategory;

  useEffect(() => {
    setSortingWay('')
  }, [router.query.subCategory])

  useEffect(() => {
    setProductList(data)
  }, [router.query.category, router.query.subCategory])
  
  useEffect(() => {
    addEventListener('beforeunload', event => {
      const data = window.localStorage.getItem('sorting___Way');
      if(data.length > 0 ) setSortingWay(data) ;
    })
    
  }, []);
  
  useEffect(() => {
    window.localStorage.setItem('sorting___Way', sortingWay);
    setSortingWay(sortingWay) 
  }, [sortingWay]);

  // sorting
  const calcSalePrice = (price, sale) => {
    const normalized = parseInt(price.replace(/\s+/g, ''));
    return (normalized / 100) * (100 - sale);
  } 

  const sortByPriceAscending = () => {
    return data.sort((a, b) => {
      const priceA = calcSalePrice(a.price, a.sale);
      const priceB = calcSalePrice(b.price, b.sale);
      return priceA - priceB;
    })
  }
  
  const sortByPriceDescending = () => {
    return data.sort((a, b) => {
      const priceA = calcSalePrice(a.price, a.sale);
      const priceB = calcSalePrice(b.price, b.sale);
      return priceB - priceA;
    })
  }

  const sortBySale = () => data.sort((a, b) => b.sale - a.sale);

  const sortByDefault = () => data.sort((a, b) => a.id - b.id);

  // https://nextjs.org/docs/routing/shallow-routing
  const shallowRoute = (targetValue) => {
    if (category && !subCategory) {
      return router.push(`/prod/${category}/?sortBy=${targetValue}`, undefined, { shallow: true })
    } else if (category && subCategory) {
      return router.push(`/prod/${category}/${subCategory}/?sortBy=${targetValue}`, undefined, { shallow: true })
    }
  }
  
  // handler
  const handleChange = ({ target }) => {
    const { value } = target;
    
    if (value === "asc") {
      setProductList(sortByPriceAscending());
    } else if (value === "des") {
      setProductList(sortByPriceDescending()); 
    } else if (value === "sale") {
      setProductList(sortBySale());
    } else if (value === "default") {
      setProductList(sortByDefault());
    }

    setSortingWay(value);
    shallowRoute(value)
  }

  return (
    <div>
    <FormControl variant="standard" sx={{ m: 1, minWidth: 120 }}>
      <InputLabel id="sort-products-label">Сортувати</InputLabel>
      <Select
        labelId="sort-products-label"
        id="sort-products"
        value={sortingWay}
        label="Сортувати"
        onChange={handleChange}
      >
        <MenuItem value="asc">За зростанням ціни</MenuItem>
        <MenuItem value="des">За спаданням ціни</MenuItem>
        <MenuItem value="sale">За знижками</MenuItem>
        <MenuItem value="default">За замовчуванням</MenuItem>
      </Select>
    </FormControl>
    
      <div>
        {
          productsList.map(product => 
            <ProductItem 
              key={product.id} 
              id={product.id} 
              subCategory={product.subCategory}
              title={product.title} 
              price={product.price} 
              image={product.image}
              sale={product.sale}
            />
          )
        }
      </div>
    </div>
  )
}

问题原因

  1. localStorage读取时机错误:当前在beforeunload事件中读取localStorage,该事件仅在页面卸载前触发,刷新后组件重新挂载时未执行读取逻辑。
  2. 冗余状态更新:监听sortingWay的useEffect中重复调用setSortingWay(sortingWay),导致不必要的重渲染和状态混乱。
  3. 未利用URL参数同步状态:虽然通过shallow routing将排序参数写入URL,但刷新后未从URL的sortBy参数读取状态,仅依赖localStorage存在可靠性问题(如隐私模式下存储被禁用)。
  4. 直接修改原始数据:排序函数使用data.sort()原地修改传入的原始数组,违反React状态不可变原则,导致状态异常。

解决方案

1. 正确初始化sortingWay状态

组件挂载时优先从URL参数读取,其次读取localStorage,最后使用默认值:

const [sortingWay, setSortingWay] = useState(() => {
  // 服务端渲染时window不存在,需做判断
  if (typeof window === 'undefined') return '';
  // 优先取URL参数
  const urlSort = router.query.sortBy;
  if (urlSort) return urlSort;
  // 其次取localStorage
  const storedSort = window.localStorage.getItem('sorting___Way');
  return storedSort || '';
});

2. 修复localStorage读写逻辑

移除错误的beforeunload监听,仅在sortingWay变化时写入存储:

useEffect(() => {
  if (sortingWay && typeof window !== 'undefined') {
    window.localStorage.setItem('sorting___Way', sortingWay);
  }
}, [sortingWay]);

3. 从URL参数同步状态

组件挂载或路由参数变化时,同步URL中的sortBy参数到状态:

useEffect(() => {
  if (router.query.sortBy && router.query.sortBy !== sortingWay) {
    setSortingWay(router.query.sortBy);
    // 同步执行排序
    handleSort(router.query.sortBy);
  }
}, [router.query.sortBy]);

4. 修复排序函数,避免修改原数据

使用扩展运算符复制数组后再排序,保证原始数据不被修改:

const sortByPriceAscending = () => {
  return [...data].sort((a, b) => {
    const priceA = calcSalePrice(a.price, a.sale);
    const priceB = calcSalePrice(b.price, b.sale);
    return priceA - priceB;
  });
};

const sortByPriceDescending = () => {
  return [...data].sort((a, b) => {
    const priceA = calcSalePrice(a.price, a.sale);
    const priceB = calcSalePrice(b.price, b.sale);
    return priceB - priceA;
  });
};

const sortBySale = () => [...data].sort((a, b) => b.sale - a.sale);

const sortByDefault = () => [...data].sort((a, b) => a.id - b.id);

5. 优化状态更新逻辑

抽离排序逻辑为单独函数,简化handleChange:

const handleSort = (value) => {
  let sortedList = [...data];
  switch(value) {
    case 'asc':
      sortedList = sortByPriceAscending();
      break;
    case 'des':
      sortedList = sortByPriceDescending();
      break;
    case 'sale':
      sortedList = sortBySale();
      break;
    default:
      sortedList = sortByDefault();
  }
  setProductList(sortedList);
};

const handleChange = ({ target }) => {
  const { value } = target;
  setSortingWay(value);
  handleSort(value);
  shallowRoute(value);
};

6. 清理不必要的状态重置

若切换子分类时不需要重置排序状态,移除以下代码;若需要重置,需同步更新URL参数和localStorage:

// useEffect(() => {
//   setSortingWay('')
// }, [router.query.subCategory])

内容的提问来源于stack exchange,提问作者Катерина

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 23:45:42