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
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.