React 中hooks之useSyncExternalStore使用总结
1. 基本概念
useSyncExternalStore 是 React 18 引入的一个 Hook,用于订阅外部数据源,确保在并发渲染下数据的一致性。它主要用于:
- 订阅浏览器 API(如 window.width)
- 订阅第三方状态管理库
- 订阅任何外部数据源
1.1 基本语法
const state = useSyncExternalStore(
subscribe, // 订阅函数
getSnapshot, // 获取当前状态的函数
getServerSnapshot // 可选:服务端渲染时获取状态的函数
);
2. 基础示例
2.1 订阅窗口大小变化
getSnapshot 是一个函数,用于返回当前浏览器窗口的宽度和高度。window.innerWidth 和 window.innerHeight 分别获取浏览器窗口的宽度和高度。
该函数返回一个对象,包含 width 和 height 两个属性。
subscribe 函数接受一个回调函数 callback,并将其作为事件监听器绑定到 resize 事件上。
每当浏览器窗口的尺寸发生变化时,resize 事件会触发,进而调用 callback。
subscribe 函数还返回一个清理函数(return () => window.removeEventListener(‘resize’, callback)),用于在组件卸载时移除事件监听器,防止内存泄漏。
当callback回调触发的时候就会触发组件更新
function useWindowSize() {
const getSnapshot = () => ({
width: window.innerWidth,
height: window.innerHeight
});
const subscribe = (callback) => {
window.addEventListener('resize', callback);
return () => window.removeEventListener('resize', callback);
};
return useSyncExternalStore(subscribe, getSnapshot);
}
function WindowSizeComponent() {
const { width, height } = useWindowSize();
return (
<div>
Window size: {width} x {height}
</div>
);
}
2.2 订阅浏览器在线状态
function useOnlineStatus() {
const getSnapshot = () => navigator.onLine;
const subscribe = (callback) => {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
};
return useSyncExternalStore(subscribe, getSnapshot);
}
function OnlineStatusComponent() {
const isOnline = useOnlineStatus();
return (
<div>
Status: {isOnline ? '在线' : '离线'}
</div>
);
}
3. 进阶用法
3.1 创建自定义存储
useTodoStore 是一个自定义 Hook,它使用了 useSyncExternalStore 来同步外部存储(即 todoStore)的状态。
todoStore.subscribe:订阅状态更新。每当 todoStore 中的状态变化时,useSyncExternalStore 会触发重新渲染。
todoStore.getSnapshot:获取当前的状态快照,在此返回的对象包含 todos 和 filter。
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
getSnapshot() {
return state;
},
setState(newState) {
state = newState;
listeners.forEach(listener => listener());
}
};
}
const todoStore = createStore({
todos: [],
filter: 'all'
});
function useTodoStore() {
return useSyncExternalStore(
todoStore.subscribe,
todoStore.getSnapshot
);
}
function TodoList() {
const { todos, filter } = useTodoStore();
return (
<ul>
{todos
.filter(todo => filter === 'all' || todo.completed === (filter === 'completed'))
.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
3.2 与服务端渲染集成
function useSharedState(initialState) {
const store = useMemo(() => createStore(initialState), [initialState]);
// 提供服务端快照
const getServerSnapshot = () => initialState;
return useSyncExternalStore(
store.subscribe,
store.getSnapshot,
getServerSnapshot
);
}
3.3 订阅 WebSocket 数据
function useWebSocketData(url) {
const [store] = useState(() => {
let data = null;
const listeners = new Set();
const ws = new WebSocket(url);
ws.onmessage = (event) => {
data = JSON.parse(event.data);
listeners.forEach(listener => listener());
};
return {
subscribe(listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
if (listeners.size === 0) {
ws.close();
}
};
},
getSnapshot() {
return data;
}
};
});
return useSyncExternalStore(store.subscribe, store.getSnapshot);
}
function LiveDataComponent() {
const data = useWebSocketData('wss://api.example.com/live');
if (!data) return <div>Loading...</div>;
return (
<div>
<h2>实时数据</h2>
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
);
}
4. 性能优化
4.1 选择性订阅
function useStoreSelector(selector) {
const store = useContext(StoreContext);
const getSnapshot = useCallback(() => {
return selector(store.getSnapshot());
}, [store, selector]);
return useSyncExternalStore(
store.subscribe,
getSnapshot
);
}
// 使用示例
function TodoCounter() {
const count = useStoreSelector(state => state.todos.length);
return <div>Total todos: {count}</div>;
}
4.2 避免不必要的更新
function createStoreWithSelector(initialState) {
let state = initialState;
const listeners = new Map();
return {
subscribe(listener, selector) {
const wrappedListener = () => {
const newSelectedValue = selector(state);
if (newSelectedValue !== selector(previousState)) {
listener();
}
};
listeners.set(listener, wrappedListener);
return () => listeners.delete(listener);
},
getSnapshot() {
return state;
},
setState(newState) {
const previousState = state;
state = newState;
listeners.forEach(listener => listener());
}
};
}
5. 实际应用场景
5.1 主题切换系统
function createThemeStore() {
let theme = 'light';
const listeners = new Set();
return {
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
getSnapshot() {
return theme;
},
toggleTheme() {
theme = theme === 'light' ? 'dark' : 'light';
listeners.forEach(listener => listener());
}
};
}
const themeStore = createThemeStore();
function useTheme() {
return useSyncExternalStore(
themeStore.subscribe,
themeStore.getSnapshot
);
}
function ThemeToggle() {
const theme = useTheme();
return (
<button onClick={() => themeStore.toggleTheme()}>
Current theme: {theme}
</button>
);
}
5.2 表单状态管理
function createFormStore(initialValues) {
let values = initialValues;
const listeners = new Set();
return {
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
getSnapshot() {
return values;
},
updateField(field, value) {
values = { ...values, [field]: value };
listeners.forEach(listener => listener());
},
reset() {
values = initialValues;
listeners.forEach(listener => listener());
}
};
}
function useForm(initialValues) {
const [store] = useState(() => createFormStore(initialValues));
return useSyncExternalStore(
store.subscribe,
store.getSnapshot
);
}
function Form() {
const formData = useForm({ name: '', email: '' });
return (
<form>
<input
value={formData.name}
onChange={e => formStore.updateField('name', e.target.value)}
/>
<input
value={formData.email}
onChange={e => formStore.updateField('email', e.target.value)}
/>
</form>
);
}
6. 注意事项
-
保持一致性
- subscribe 函数应该返回清理函数
- getSnapshot 应该返回不可变的数据
-
避免频繁更新
- 考虑使用节流或防抖
- 实现选择性订阅机制
-
服务端渲染
- 提供 getServerSnapshot
- 确保服务端和客户端状态同步
-
内存管理
- 及时清理订阅
- 避免内存泄漏
通过合理使用 useSyncExternalStore,我们可以安全地订阅外部数据源,并确保在 React 并发渲染下的数据一致性。这个 Hook 特别适合需要与外部系统集成的场景。 还可以用来实现浏览器localStrorage的持久化存储