-1

I'm following the guide in https://reactjs.org/docs/thinking-in-react.html which makes sense for data that I request and update on a filter.

However, I want to have some sort of general synchronization job in the background that polls and updates an SQLite database periodically.

The question I have is if the SQLite database has detected that there was an update, how do I signal to a list component that it needs to refresh itself?

Notes:

I am thinking if it was in a context it will trigger a re-render of every component underneath it which may be problematic if I have something like a data entry form since a re-render will cause my focus to be lost

In vue land we have emit to emit custom events, but I don't see anything like that in the API guide for React

Another approach I was thinking of was to useReducer, but it didn't pan out on the context because it will also cause a re-render just like useState

Then I thought of having the reducer on the filtered table component but then I don't know how to trigger it from the context.

I also tried putting in a useRef to prevent the re-render but that does not work for documented reasons which I have checked.

useCallback was supposedly the solution, but the dependencies would be triggering a rerender

Archimedes Trajano
  • 685
  • 1
  • 6
  • 14

1 Answers1

0

After a bit more research I found that the build your own hooks is similar to what I needed. In that it uses a subscribe, unsubscribe pair for their chat API which isn't actually coded, but I made an assumption on how this would work.

So I have a simple "API" that has a fetchData() method that I wrap in a context.

import React, { PropsWithChildren, createContext, useContext } from "react";

type IApiContext = {
  fetchData?: () => Promise<number>;
};
const ApiContext = createContext<IApiContext>({
  fetchData: undefined,
});
export function ApiProvider({ children }: PropsWithChildren<{}>) {
  const fetchData = async () => {
    return Promise.resolve(Date.now());
  };

  return (
    <ApiContext.Provider
      value={{
        fetchData,
      }}
    >
      {children}
    </ApiContext.Provider>
  );
}
export function useApi(): Required<IApiContext> {
  return useContext(ApiContext) as Required<IApiContext>;
}

I then have a data store object with a polling effect (not written in this answer) that will execute every 10 seconds and store the data locally (in this case a simple array ref). Then provide onSubscribe, onUnsubscribe callbacks. I also provided a getDataAsync() method which is what I'd be doing anyway since it does not make sense to reconsititute the data when I would've stored the data in a local database.

import React, {
  createContext, PropsWithChildren, useContext, useRef
} from "react";
import { usePoll } from "../hooks/usePoll";
import { useApi } from "./ApiContext";
export type UpdateCallback = (eventData: number) => Promise<void>;
type IDataStoreContext = {
  subscribe: (onUpdate: UpdateCallback) => void;
  unsubscribe: (onUpdate: UpdateCallback) => void;
  getDataAsync: () => Promise<string[]>;
};
const DataStoreContext = createContext<Partial<IDataStoreContext>>({});
export function DataStoreProvider({ children }: PropsWithChildren<{}>) {
  const { fetchData } = useApi();
  const dataRef = useRef<string[]>([]);
  const subscriptionsRef = useRef<UpdateCallback[]>([]);
  usePoll(
    async () => {
      const result = await fetchData();
      console.log("usePoll", result, subscriptionsRef.current, dataRef.current);
      dataRef.current.push("ds" + result);
      await Promise.all(subscriptionsRef.current.map((f) => f(result)));
      console.log("usePoll done")
    },
    10000,
    true
  );
  function subscribe(onUpdate: UpdateCallback) {
    console.log("subscribe");
    subscriptionsRef.current.push(onUpdate);
  }
  function unsubscribe(onUpdate: UpdateCallback) {
    console.log("unsubscribe");
    subscriptionsRef.current = subscriptionsRef.current.filter(
      (f) => f !== onUpdate
    );
  }
  async function getDataAsync() {
    return Promise.resolve(dataRef.current);
  }
  return (
    <DataStoreContext.Provider value={{ subscribe, unsubscribe, getDataAsync }}>
      {children}
    </DataStoreContext.Provider>
  );
}
export function useDataStore(): Required<IDataStoreContext> {
  return useContext(DataStoreContext) as Required<IDataStoreContext>;
}

I then use it in a component that would need the results

import React, { useEffect, useState } from "react";
import { FlatList, Text, View } from "react-native";
import { useDataStore } from "./DataStoreContext";
export function DebugComponent() {
  const { subscribe, unsubscribe, getDataAsync } = useDataStore();
  const [data, setData] = useState<string[]>([]);

  const onUpdate = async () => {
    const updated = await getDataAsync();
    // don't just use updated because I know it is using the same object, this forces the creation of a new array object.
    setData([...updated]);
  };

  useEffect(() => {
    subscribe(onUpdate);
    return () => {
      unsubscribe(onUpdate);
    };
  }, []);

  return (
    <View style={{ borderWidth: 1, borderColor: "black" }}>
      <FlatList
        data={data}
        keyExtractor={(r) => r}
        renderItem={({ item }) => <Text>{item}</Text>}
      />
    </View>
  );
}

Not exactly sure if this is ideal React thinking, but it seems to make the most sense right now.

Archimedes Trajano
  • 685
  • 1
  • 6
  • 14