reactjstypescriptreact-hookstypes

useSyncExternalStore throws signature error for a subscribe method with a Function return type


I am attempting to make a TypeScript interface that will allow for a given data structure to be compatible with useSyncExternalStore. This is to follow to reduce the load on larger Zustand stores in the application.

This is a modification of the code found at: https://react.dev/reference/react/useSyncExternalStore

First, I make a Subscribable interface that is implemented on the TodoStore class.

The code below can be found at: https://codesandbox.io/s/usesyncexternalstore-test-dxgg5k?file=/src/App.tsx

export interface Subscribable {
  subscribe(listener: Function): Function;
  getSnapshot(): any;
  emitChange(): void;
}

export class TodosStore {
  todos: any[] = [];
  nextId: number = 0;
  listeners: Function[] = [];
  addTodo() {
    this.todos = [
      ...this.todos,
      { id: this.nextId++, text: "Todo #" + this.nextId }
    ];
    this.emitChange();
  }
  subscribe(listener: Function): Function {
    this.listeners = [...this.listeners, listener];
    return () => {
      this.listeners = this.listeners.filter((l) => l !== listener);
    };
  }
  getSnapshot(): any[] {
    return this.todos;
  }
  emitChange() {
    for (let listener of this.listeners) {
      listener();
    }
  }
}

Then on App.tsx, I create a new instance of this store and subscribe to it.

import { useSyncExternalStore } from "react";
import { TodosStore } from "./todoStore";

let list = new TodosStore();

export default function App() {
  const subscribedList = useSyncExternalStore(list.subscribe, list.getSnapshot); // Error here
  return (
    <>
      <button onClick={() => list.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {subscribedList.map((todo) => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
}

useSyncExternalStore throws a type error for my subscribe method

Argument of type '(listener: Function) => Function' is not assignable to parameter of type '(onStoreChange: () => void) => () => void'.
  Type 'Function' is not assignable to type '() => void'.
    Type 'Function' provides no match for the signature '(): void'.ts(2345)

To my understanding, the subscribe method must return an unsubscribe function for the hook. However, setting the return type as Function results in a signature error.

It also throws an error within the getSnapshot method for referencing class variables:

Cannot read properties of undefined (reading 'todos')


Solution

  • Give it a better TS type. Instead of Function, use () => void.