React+antd实现监听localStorage变化多页面更新+纯js单页面table模糊、精确查询、添加、展示功能
React+antd实现监听localStorage变化多页面更新
在开发中遇见一个问题,就是一个后台页面,需要通过改变业态来影响table的查询内容,如下图
这个需要改变本地缓存的localStorage,再通知其他组件进行对应操作
由于我是后端开发,对react说熟也熟,说不熟也不熟
而页面之间的通信我只会 pubsub-js 和 props,props是父与子的互传,pubsub-js 又需要引入,本来打包就够慢了,不想引入新的包,而且localStorage后续还得监听多个值,用 pubsub 就颇为麻烦了
还有个方法就是轮训去查localStorage的值如果改变就执行对应操作,但如果一个页面多组件需要获取到localStorage新值的情况下有点浪费性能了
工具类utils.ts
storage 事件:
localStorage
和sessionStorage
对象都会触发storage
事件,当其他窗口或标签页对存储进行更改时会触发该事件。您可以通过添加storage
事件监听器来捕获变化,并在回调函数中执行相应的操作。删除监听器(移除事件监听)原因:
防止内存泄漏
如果不移除监听器,每次组件重新渲染时都会添加新的监听器
旧的监听器会继续存在于内存中,无法被垃圾回收
随着时间的推移,会导致内存使用量不断增加
避免重复执行
多个相同的监听器会同时响应同一个事件
导致回调函数被多次执行,可能引发意外的副作用
特别是在
setValue
被多次调用时,会造成组件状态混乱组件卸载时的清理
当使用该 Hook 的组件被卸载时,需要清理所有与之相关的事件监听
确保不会尝试更新已卸载组件的状态(避免 React 警告)
// 设置localStorage值的辅助函数
export function setLocalStorageValue(key: string, value: string) {try {window.localStorage.setItem(key, value);// 触发自定义事件,通知同标签页的监听器window.dispatchEvent(new CustomEvent('localStorageChange', {// 事件携带的数据detail: { key, value }}));} catch (error) {console.error(`Error setting localStorage key "${key}":`, error);}
}// 自定义Hook:用于监听localStorage特定键的变化
export function useLocalStorage(key: any, callback: (newValue: string | undefined) => void) {const [value, setValue] = useState(() => {try {// 直接从localStorage获取当前值const item = window.localStorage.getItem(key);return item || undefined;} catch (error) {console.error(`Error reading localStorage key "${key}":`, error);return undefined;}});// 监听storage变化useEffect(() => {const handleStorageChange = (e: any) => {if (e.key === key) {setValue(e.newValue);}};// 监听storage事件(跨标签页)window.addEventListener('storage', handleStorageChange);// 监听自定义事件(同标签页)const handleCustomEvent = (e: any) => {if (e.detail.key === key) {setValue(e.detail.value);}};// 监听自定义事件window.addEventListener('localStorageChange', handleCustomEvent);// 删除监听器是 React useEffect Hook 的标准清理模式,确保应用程序的性能和稳定性return () => {window.removeEventListener('storage', handleStorageChange);window.removeEventListener('localStorageChange', handleCustomEvent);};}, [key]);// 使用 useRef 保存最新的回调const callbackRef = useRef(callback);callbackRef.current = callback;// 使用 ref 来调用回调,避免重复触发useEffect(() => {if (value !== undefined) {callbackRef.current(value);}}, [value]); // 只依赖 valuereturn value;
}
使用
存入操作
import { setLocalStorageValue } from "@/utils/utils";
import { Button } from "antd";
import React from 'react';// 存入操作
const DepositOperation: React.FC = () => {// 点击操作const onClick = () => {setLocalStorageValue('format', "666");};return (<Button onClick={onClick}></Button>);
}export default DepositOperation;
监听操作
import { useLocalStorage } from "@/utils/utils";
import React from 'react';// 监听操作
const ListeningOperation: React.FC = () => {// ....其他无关代码// 业态监听,修改时做相应动作useLocalStorage('format', (value) => {console.log('format', value)});return (<></>);
}export default ListeningOperation;
测试
为啥不是 666,是因为我没有存 666,存的是一个对象,上面的代码只是示例,下面的图片只是验证方法可行可用
纯js单页面table模糊、精确查询、添加、展示功能
下面只是记录一个纯js实现的页面,通过list生产列表,js精确、模糊查询,点击行选中
代码
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable array-callback-return */
import type { ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { Button, Modal, Space, Table } from 'antd';
import type { ProFormInstance } from '@ant-design/pro-form';
import React, { useState } from 'react';type DataInfoProps = {from: React.RefObject<ProFormInstance | undefined>;visible: (visible: boolean) => void;data: any[];
}const { confirm } = Modal;
const Info: React.FC<DataInfoProps> = ({ from, visible, data }) => {const [dataSelected, setDataSelected] = useState<React.Key[]>(from.current?.getFieldValue("permissions") as React.Key[] || [])const columns: ProColumns[] = [{title: 'ID',dataIndex: 'value',hideInTable: true,},{title: '名称',dataIndex: 'label',hideInTable: true,},{title: '状态',dataIndex: 'status',hideInTable: true,valueEnum: {"0": {text: '未选择',},"1": {text: '已选择',}},},// {// title: "序号",// dataIndex: 'index',// valueType: 'index',// width: 80,// align: 'center',// },{title: 'ID',dataIndex: 'value',align: 'center',search: false,},{title: '名称',dataIndex: 'label',align: 'center',search: false,},// {// title: '操作',// valueType: 'option',// key: 'option',// width: 120,// render: (t, record) => {// return (// <a// key="enable"// onClick={() => {// from?.current?.setFieldsValue({// Id: record.id,// Name: record.Name,// })// visible(false)// }}// >// 选择// </a>// )// }// },];return (<><ProTablecolumns={columns}cardBorderedrequest={async (params = {}, sort, filter) => {// 获取搜索参数const Id = params.value || ''; // ID - 精确匹配const Name = params.label || ''; // 名称 - 模糊查询const { status } = params; // 状态查询// 过滤数据const filteredData = data.filter(item => {// ID精确匹配const idMatch = !Id || (item.value && item.value.toString() === Id);// 名称模糊查询const nameMatch = !Name ||(item.label && item.label.toString().toLowerCase().includes(Name.toLowerCase()));// 状态查询:根据dataSelected判断是否已选择let statusMatch = true;if (status !== undefined && status !== null && status !== '') {const isSelected = dataSelected.includes(item.value);if (status === "1") {statusMatch = isSelected; // 已选择} else if (status === "0") {statusMatch = !isSelected; // 未选择}}return idMatch && nameMatch && statusMatch;});// 应用排序const sortedData = [...filteredData];if (sort && Object.keys(sort).length > 0) {const sortField = Object.keys(sort)[0];const sortOrder = sort[sortField];sortedData.sort((a, b) => {const aValue = a[sortField] || '';const bValue = b[sortField] || '';if (sortOrder === 'ascend') {return aValue.toString().localeCompare(bValue.toString());}return bValue.toString().localeCompare(aValue.toString());});}return {data: sortedData,success: true,total: filteredData.length,};}}options={false}rowKey="value"pagination={{defaultPageSize: 10,pageSizeOptions: [10, 20, 30, 40, 50, 100, 200, 500, 1000],showSizeChanger: true,showQuickJumper: true,}}search={{labelWidth: 'auto',defaultCollapsed: false,showHiddenNum: true,}}scroll={{ x: 'max-content' }}rowSelection={{selectedRowKeys: dataSelected,// selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],// onChange: (newSelectedRowKeys: React.Key[], selectedRows: any[], info: { type: string }) => {// console.log('info', info)// console.log('type', info.type)// // 普通选择操作:直接设置新的选择// setDataSelected(newSelectedRowKeys);// },onSelect: (record: any, selected: boolean, selectedRows: any, changeRows: any) => {const newSelectedRowKeys = [...dataSelected];const index = newSelectedRowKeys.indexOf(record.value);if (index > -1) {// 如果已经选中,则取消选择newSelectedRowKeys.splice(index, 1);} else {// 如果未选中,则添加选择newSelectedRowKeys.push(record.value);}setDataSelected(newSelectedRowKeys);},onSelectAll: (selectedRowKeys: any, selectedRows: any, changeRows: any) => {const newSelectedRowKeys = [...dataSelected];changeRows.forEach((item: any) => {const index = newSelectedRowKeys.indexOf(item.value);if (index > -1) {// 如果已经选中,则取消选择newSelectedRowKeys.splice(index, 1);} else {// 如果未选中,则添加选择newSelectedRowKeys.push(item.value);}})setDataSelected(newSelectedRowKeys);}}}dateFormatter="string"headerTitle=""onRow={(record) => ({onClick: (event) => {// 阻止事件冒泡,避免与复选框点击冲突if ((event.target as HTMLElement).closest('.ant-checkbox-wrapper')) {return;}const key = record.value;const newSelectedRowKeys = [...dataSelected];const index = newSelectedRowKeys.indexOf(key);if (index > -1) {// 如果已经选中,则取消选择newSelectedRowKeys.splice(index, 1);} else {// 如果未选中,则添加选择newSelectedRowKeys.push(key);}setDataSelected(newSelectedRowKeys);},style: { cursor: 'pointer' }})}tableAlertOptionRender={({ selectedRowKeys, selectedRows, onCleanSelected }) => {return (<Space size={24}><Buttontype="primary"style={{ marginInlineStart: 8 }}onClick={() => {confirm({title: `确定添加选中吗?`,maskClosable: true,okText: '确定',cancelText: '取消',content: '',onOk() {from.current?.setFieldsValue({"permissions": dataSelected,})visible(false)},onCancel() { },});}}>添加选择项</Button>{selectedRowKeys.length > 0 && (<Buttontype="primary"style={{ marginInlineStart: 8 }}onClick={() => setDataSelected([])}>取消选择</Button>)}</Space>);}}/></>);
};export default Info;