彻底理解react中useSyncExternalStore的用法

下面我们来一点一点读懂官方文档的解释

useSyncExternalStore is a React Hook that lets you subscribe to an external store.

解释:

  1. useSyncExternalStore 是一个hooks
  2. 可以让你订阅外部的store

语法

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

参数

subscribe: A function that takes a single callback argument and subscribes it to the store. When the store changes, it should invoke the provided callback, which will cause React to re-call getSnapshot and (if needed) re-render the component. The subscribe function should return a function that cleans up the subscription.

解释:

subscribe 是一个订阅函数,可以订阅你的store,返回值是一个函数,可以取消订阅,也就是说你的store应该提供一个 subscribe方法,可以注册listener,例如:

typescript 复制代码
 type IState = {
     todos: any[]
 }

 let state: IState = {
     todos: []
 }
 const listeners = new Set<() => void>();
 const store = {
     // 提供一个订阅方法
     subscribe: (listener: () => void) => {
        listeners.add(listener);
        // 返回值是一个取消订阅的方法
        return () => listeners.delete(listener);
     },
 }
  • store改变时,需要调用这些callback,也就是 listener,例如
typescript 复制代码
 type IState = {
     todos: any[]
 }

 let state: IState = {
     todos: []
 }
 const listeners = new Set<() => void>();

 const store = {
     // 提供一个订阅方法
     subscribe: (listener: () => void) => {
        listeners.add(listener);
        // 返回值是一个取消订阅的方法
        return () => listeners.delete(listener);
     },
     // 调用callback
     update: () => {
         console.log('update', listeners);
         listeners.forEach((listener) => listener());
     },

     // 添加一个todo
     addTodo: (todo: any) => {
         // 修改值,返回一个新的对象,getSnapshot要求返回一个新对象,否则将无法触发更新
         state = {
             ...state,
             todos: [...state.todos, todo]
         }
         store.update();
     },
 }

getSnapshot: A function that returns a snapshot of the data in the store that's needed by the component. While the store has not changed, repeated calls to getSnapshot must return the same value. If the store changes and the returned value is different (as compared by Object.is), React re-renders the component.

解释:

  • getSnapshot 是一个方法,返回store中的数据,例如:
typescript 复制代码
  
   type IState = {
       todos: any[]
   }

   let state: IState = {
       todos: []
   }
   const listeners = new Set<() => void>();

   const store = {
       // 提供一个订阅方法
       subscribe: (listener: () => void) => {
          listeners.add(listener);
          // 返回值是一个取消订阅的方法
          return () => listeners.delete(listener);
       },
       // 调用callback
       update: () => {
           console.log('update', listeners);
           listeners.forEach((listener) => listener());
       },

       // 添加一个todo
       addTodo: (todo: any) => {
           // 这里可以做一些业务逻辑,比如添加一个todo
           console.log('addTodo', todo);
           // 修改值
           state = {
               ...state,
               todos: [...state.todos, todo]
           }
           store.update();
       },
       getSnapshot: () => {
           console.log('getSnapshot', state);
           return state;
       },
   }


   export default store
  
  • store 数据如果没有发生变化,getSnapshot必须返回相同的数据,如果返回的值不同,react就会触发重新render, 例如
typescript 复制代码
    getSnapshot: () => {
       // 即使数据相同,但是会造成死循环,因为不是一个对象了
       return { state }
   }

optional getServerSnapshot: A function that returns the initial snapshot of the data in the store. It will be used only during server rendering and during hydration of server-rendered content on the client. The server snapshot must be the same between the client and the server, and is usually serialized and passed from the server to the client. If you omit this argument, rendering the component on the server will throw an error.

  • 这个是服务端渲染用的,我这里就不解释了

以上就是useSyncExternalStore的详解了

使用示例

typescript 复制代码
import {  useRef, useSyncExternalStore } from 'react'
import store from './store'
import './App.css'

function App() {
  const state = useSyncExternalStore(store.subscribe, store.getSnapshot)
  const titleRef = useRef<HTMLInputElement>(null)
  const bodyRef = useRef<HTMLInputElement>(null)
  
  return (
    <div className='App'>
      <div>
        {
          state.todos?.map((item, index) => (
            <div key={index}>
              <h3>{item.title}</h3>
              <p>{item.body}</p>
            </div>
          ))
        }
      </div>
      <div className='add-todo'>
        <input type="text" placeholder='title' ref={titleRef} />
        <input type="text" placeholder='body' ref={bodyRef} />
        <button onClick={() => store.addTodo({ title: titleRef.current?.value, body: bodyRef.current?.value })}>
          Add Todo
        </button>
      </div>
    </div>
  )
}

export default App

持续优化,我们来用useSyncExternalStore写一个简单的store

  1. 介绍我希望的api的样子
typescript 复制代码
// const store = createStore(initialState, reducers)

// 创建api 示例
const store = createStore({
    todos: []
}, {
    addTodo: (todo: any) => (state) => {
        state.todo = [...state.todo, todo]
        // 这里也可以return值,让页面可以立马拿到最新的值
        return anyValue
    }
})
// 使用api示例
const [state, actions] = store.useStore(selector?)
  1. 写代码
typescript 复制代码
import { useSyncExternalStore } from "react";

function createStore<S, R>(initialState: S, reducers: R) {

  let state: S = initialState;
  const listeners = new Set<() => void>();

  const update  = () => {
    state = {
      ...state,
    }
    listeners.forEach((listener) => listener());
  };

  const proxy = new Proxy(reducers, {
    get(target: any, key: string) {
      return (...args: any[]) => {
        const action = reducers[key](...args);
        const result = action(state);
        if (result instanceof Promise) {
          result.then(update);
        } else {
          update(result);
        }
      }
    },
  });


  const store = {
    subscribe: (listener: () => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot: () => state,
    useStore: () => {
        const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
        return [state, proxy as any];
    }
  };

  return store;

}

const store = createStore({
  todos: [],
}, {
  addTodo: (todo: any) => (state: any) => {
      state.todos = [...state.todos, todo]
  }
})
export default store
  1. 测试代码
typescript 复制代码
import {  useRef } from 'react'
import store from './store/index'
import './App.css'

function App() {

  const [state, actions] = store.useStore()

  const titleRef = useRef<HTMLInputElement>(null)
  const bodyRef = useRef<HTMLInputElement>(null)

  console.log('====come state', state)

  return (
  
    <div className='App'>
      <div>
        {
          state.todos?.map((item, index) => (
            <div key={index}>
              <h3>{item.title}</h3>
              <p>{item.body}</p>
            </div>
          ))
        }
      </div>
      <div className='add-todo'>
        <input type="text" placeholder='title' ref={titleRef} />
        <input type="text" placeholder='body' ref={bodyRef} />
        <button onClick={() => actions.addTodo({ title: titleRef.current?.value, body: bodyRef.current?.value })}>
          Add Todo
        </button>
      </div>
    </div>
  )
}

export default App
  1. useStore增加selector
typescript 复制代码
import { useRef, useSyncExternalStore } from "react";
import { shallow } from "./shallow";

function createStore<S, R>(initialState: S, reducers: R) {

  let state: S = initialState;
  const listeners = new Set<() => void>();

  const update  = () => {
    state = {
      ...state,
    }
    listeners.forEach((listener) => listener());
  };

  const proxy = new Proxy(reducers, {
    get(target: any, key: string) {
      return (...args: any[]) => {
        const action = reducers[key](...args);
        const result = action(state);
        if (result instanceof Promise) {
          result.then(update);
        } else {
          update(result);
        }
      }
    },
  });


  const store = {
    getState: () => state,
    subscribe: (listener: () => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    getSnapshot: () => state,
    useStore: (selector?: Function) => {
        // 这里用 useRef 来缓存上一次的值,selector 每次都会返回一个新对象,所以要做浅比较,不然会造成死循环
        const pre = useRef();
        const state = useSyncExternalStore(store.subscribe, () => {
          if (selector) {
            const next = selector(store.getState());
            if (shallow(pre.current, next)) {
              return pre.current;
            }
            pre.current = next;
            return pre.current;
          }
          return store.getState()
        });
        return [state, proxy as any];
    }
  };

  return store;

}

const store = createStore({
  todos: [],
}, {
  addTodo: (todo: any) => (state: any) => {
      state.todos = [...state.todos, todo]
  }
})

export default store

shallow.js

typescript 复制代码
const isIterable = (obj: object): obj is Iterable<unknown> =>
  Symbol.iterator in obj

const hasIterableEntries = (
  value: Iterable<unknown>,
): value is Iterable<unknown> & {
  entries(): Iterable<[unknown, unknown]>
} =>
  // HACK: avoid checking entries type
  'entries' in value

const compareEntries = (
  valueA: { entries(): Iterable<[unknown, unknown]> },
  valueB: { entries(): Iterable<[unknown, unknown]> },
) => {
  const mapA = valueA instanceof Map ? valueA : new Map(valueA.entries())
  const mapB = valueB instanceof Map ? valueB : new Map(valueB.entries())
  if (mapA.size !== mapB.size) {
    return false
  }
  for (const [key, value] of mapA) {
    if (!Object.is(value, mapB.get(key))) {
      return false
    }
  }
  return true
}

// Ordered iterables
const compareIterables = (
  valueA: Iterable<unknown>,
  valueB: Iterable<unknown>,
) => {
  const iteratorA = valueA[Symbol.iterator]()
  const iteratorB = valueB[Symbol.iterator]()
  let nextA = iteratorA.next()
  let nextB = iteratorB.next()
  while (!nextA.done && !nextB.done) {
    if (!Object.is(nextA.value, nextB.value)) {
      return false
    }
    nextA = iteratorA.next()
    nextB = iteratorB.next()
  }
  return !!nextA.done && !!nextB.done
}

export function shallow<T>(valueA: T, valueB: T): boolean {
  if (Object.is(valueA, valueB)) {
    return true
  }
  if (
    typeof valueA !== 'object' ||
    valueA === null ||
    typeof valueB !== 'object' ||
    valueB === null
  ) {
    return false
  }
  if (!isIterable(valueA) || !isIterable(valueB)) {
    return compareEntries(
      { entries: () => Object.entries(valueA) },
      { entries: () => Object.entries(valueB) },
    )
  }
  if (hasIterableEntries(valueA) && hasIterableEntries(valueB)) {
    return compareEntries(valueA, valueB)
  }
  return compareIterables(valueA, valueB)
}

测试代码

typescript 复制代码
import {  useRef } from 'react'
import store from './store/index'
import './App.css'

function App() {

  const [state, actions] = store.useStore(s => s.todos)

  const titleRef = useRef<HTMLInputElement>(null)
  const bodyRef = useRef<HTMLInputElement>(null)

  return (
  
    <div className='App'>
      <div>
        {
          state?.map((item, index) => (
            <div key={index}>
              <h3>{item.title}</h3>
              <p>{item.body}</p>
            </div>
          ))
        }
      </div>
      <div className='add-todo'>
        <input type="text" placeholder='title' ref={titleRef} />
        <input type="text" placeholder='body' ref={bodyRef} />
        <button onClick={() => actions.addTodo({ title: titleRef.current?.value, body: bodyRef.current?.value })}>
          Add Todo
        </button>
      </div>
    </div>
  )
}

export default App

以上,简易代码就完成了,下面会解释useSyncExternalStore实现原理

useSyncExternalStore 源码实现解释

  1. 使用setState 创建一个hooks,通过forceUpdate(可理解为setState)做更新
  2. 将handleStoreChange(里面包含了forceUpdate function)传给subscribe
  3. 外部store收集所有订阅的handleStoreChange
  4. listener调用会判断checkIfSnapshotChanged,如果改变,则触发forceUpdate

useSyncExternalStore 源码细节

typescript 复制代码
import * as React from 'react';
import is from 'shared/objectIs';
const {useState, useEffect, useLayoutEffect, useDebugValue} = React;

let didWarnOld18Alpha = false;
let didWarnUncachedGetSnapshot = false;

export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T {
  // 暴露给外部使用的value
  const value = getSnapshot();

  // To force a re-render, we call forceUpdate({inst}). That works because the
  // new object always fails an equality check.
  // 通过forceUpdate更新,forceUpdate({inst})相当于每次创建一个新对象,类似setState({})
  const [{inst}, forceUpdate] = useState({inst: {value, getSnapshot}});

  // Track the latest getSnapshot function with a ref. This needs to be updated
  // in the layout phase so we can access it during the tearing check that
  // happens on subscribe.
  useLayoutEffect(() => {
    inst.value = value;
    inst.getSnapshot = getSnapshot;

    // Whenever getSnapshot or subscribe changes, we need to check in the
    // commit phase if there was an interleaved mutation. In concurrent mode
    // this can happen all the time, but even in synchronous mode, an earlier
    // effect may have mutated the store.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
  }, [subscribe, value, getSnapshot]);

  useEffect(() => {
    // Check for changes right before subscribing. Subsequent changes will be
    // detected in the subscription handler.
    if (checkIfSnapshotChanged(inst)) {
      // Force a re-render.
      forceUpdate({inst});
    }
    // 
    const handleStoreChange = () => {
      // check变更,如果发生变化,则触发rerender
      if (checkIfSnapshotChanged(inst)) {
        // Force a re-render.
        forceUpdate({inst});
      }
    };
    // 自定义store订阅handleStoreChange,并将返回的卸载函数抛给react
    return subscribe(handleStoreChange);
  }, [subscribe]);

  useDebugValue(value);
  return value;
}

function checkIfSnapshotChanged<T>(inst: {
  value: T,
  getSnapshot: () => T,
}): boolean {
  const latestGetSnapshot = inst.getSnapshot;
  const prevValue = inst.value;
  try {
    const nextValue = latestGetSnapshot();
    // 这里用相同对象对比(地址对比),正如官方文档解释
    return !is(prevValue, nextValue);
  } catch (error) {
    return true;
  }
}

我基于以上思想封装了一个简单的store,欢迎star

sample-store具备以下优点

  • Simple and intuitive API (简单直观的API)
  • TypeScript support (TypeScript支持)
  • Lightweight with minimal dependencies (轻量级,依赖少)
  • React hooks integration (React钩子集成)
  • Efficient state updates with shallow comparison (使用浅比较的高效状态更新)

参考文档

相关推荐
冴羽19 分钟前
SvelteKit 最新中文文档教程(17)—— 仅服务端模块和快照
前端·javascript·svelte
uhakadotcom20 分钟前
Langflow:打造AI应用的强大工具
前端·面试·github
前端小张同学29 分钟前
AI编程-cursor无限使用, 还有谁不会🎁🎁🎁??
前端·cursor
yanxy51233 分钟前
【TS学习】(15)分布式条件特性
前端·学习·typescript
uhakadotcom1 小时前
Caddy Web服务器初体验:简洁高效的现代选择
前端·面试·github
前端菜鸟来报道1 小时前
前端react 实现分段进度条
前端·javascript·react.js·进度条
花楸树1 小时前
前端搭建 MCP Client(Web版)+ Server + Agent 实践
前端·人工智能
wuaro1 小时前
RBAC权限控制具体实现
前端·javascript·vue
专业抄代码选手2 小时前
【JS】instanceof 和 typeof 的使用
前端·javascript·面试