-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

1 Answer 1

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.