Should I build a local data layer/app state to maintain state in a React Native/Firestore App? - reactjs

A main selling point of Firestore is the ability to use it as a online/offline source of truth. I'm using it in this way right now: updating the Firestore document directly on an action, then listening to Firestore DB changes and mapping this back to local state. However, relying on this latency compensation and mapping back to local state is not sufficient for quick updates (taps, toggles even with a small document size). For example, toggles will "jitter" as the RN toggle presumptively shifts on tap, and the local state hasn't been updated until it already returns see video example. It appears worse on Android and the problem isn't strictly limited to basic toggles.
Does document size or query result size have a bigger impact on latency compensation? Our document size is very small right now with a worst case ~1000 query result set. We could make the documents 1000x bigger (100kb) and have a query result set of size 1. Update: Testing appears inconsistent here, latency compensation is not ideal in either case
Which of the following other things may impact latency compensation?
Using queries with custom indexes. Note: we're not currently reading from cache, we're using the JS SDK
Multiple writes. Would multiple writes to the same document make it worse (4 quick writes vs. 2 quick writes). Update: not clear this makes a big difference.
Using the native vs. JS module. We're currently using the Firestore Web SDK with an Expo app. Update: switching to native module via React-Native Firestore has no apparent performance improvement.
Is it common for people to build a local data shim layer / local app state with React Native / Firestore apps to help improve local performance speed? Are there any suggested libraries for this?
On app load, mount the listener, and export the result to context to be used through the app
const [user, setUser] = useState();
firebase.firestore().collection(`users/${user.uid}`).onSnapshot(qs => setUser(oldState => {
const newState = {};
qs.docChanges().forEach(change => {
if (change.type === "added" || change.type === "modified") {
newState[change.doc.id] = {
docid: change.doc.id,
...change.doc.data(),
};
} else if (change.type === "removed") {
delete oldState[change.doc.id];
}
});
return {
...oldState,
...newState,
};
}))
Sample component and function to toggle notifications: (switch is jittery)
const toggleNotifications = (user, value) => {
firebase.firestore().doc(`users/${user.uid}`).update({
wantNotifications: value,
});
};
const TestComponent = () => {
//gets from context, set in listener mounted on app load
const { user } = useUserContext();
return (
<Switch
value={user.wantNotifications}
onValueChange={value => toggleNotifications(user, value)}
/>
);
};

This not an answer, just a long comment :)
#learningAngular For example, in toggleNotifications just need to call an async action creator and don't worry about putting any logic inside react component.
Instead Redux pattern gives space to do some logics, in this case because user's last moment decision is source of truth so dispatch function would always set a local tempState and updatingStatus before start updating firestore, then after firestore promise either resolved or rejected dispatches an action to reducer to reset updatingStatus. Then a selector would check if updatingStatus is true to just rely on local tempState otherwise rely on listened firestore state. Finally, react component use selector result as currently valid state.
I am not answering this question because I don't have that much experience. I am also curious if a good solution is out there, but this is what I think is the solution at this moment.

I updated the answer with specific learnings, but after a lot of testing, my biggest general learnings so far are
Latency compensation can be very inconsistent even with the same data and environment. Listeners can take time to "warm up", as is mentioned in other questions. It is hard to have a standard metric here.
Document size DOES impact latency compensation. Everything else so far is inconclusive.

Related

Next.js: How to cache initial data on the server (same for all users)

I'm learning Next.js and started building one of my first applications - https://www.codart.io/
If you visit the site, you'll notice a delay until the collections become available: The application makes an initial graphql request to retrieve a json file with a number of NFT collections, which takes a few seconds. This data needs to be available across different components, so I built a global context to store the data (I'm not sure this is the best approach):
https://github.com/bartomolina/codart/blob/main/app/components/collections-context.tsx
const ArtBlocksContext = createContext({
aBCollections: [] as IABCollection[],
cACollections: [] as ICACollection[],
fetchCACollections: () => {},
});
export const useArtBlocks = () => useContext(ArtBlocksContext);
export const ArtblocksProvider = ({ children }: React.PropsWithChildren) => {
...
useEffect(() => {
fetchCACollections();
execute(ArtblocksCollectionsDocument, {}).then((result) => {
I wonder what would be the best way to cache this data (as the data doesn't change often, it could be cached at build time, or ideally, indicating an expiration date i.e. 24 hours). Note that the cache will be shared between all users.
I've been looking at some similar posts, and I got some ideas, although it seems there isn't a clear and simple way to do this:
Using Redux - Seems an overkill for a small project like this.
Use some custom caching libraries - I'd rather not use any external libraries.
Use getStaticProps along with the Context API - It seems you can't getStaticProps within the _app.ts page, so you would need to call it in every page where the context is used?
Use SWR and an API call - SWR will cache the data, but on a per-user basis? i.e. the first time a user visits the site, it will still take a few seconds to load.

Zustand fetch with API call useEffect best practice

When fetching state from an API with Zustand in a useEffect function what is the best practice for doing that? Right now I am using it very simply:
export interface ModeState{
modes: Mode[];
fetchModes: () => void;
}
export const useModeStore = create<ModeState>((set) => ({
modes: [],
fetchModes: async () => {
const modes: AcquisitionMode[] = await API.get(`/acquisition-modes`);
await set({ modes });
},
}));
In component render function:
const modeStore = useModeStore()
const modes = modeStore.modes
useEffect(() => {
modeStore.fetchModes()
}, [])
However the documentation seems to imply there are multiple ways this could be written to be more efficient in terms of performance, especially if my store grows more complex with more values and fetch functions. Is it best practice to make one store per API call? Use slices to get just the part of the store you need in each component? Should I be using the store differently in useEffect? I can't find a clear example online of how you should use the store in useEffect. The subscribe documentation does not seem to apply to the use case where you are using the store to fetch values with an async function.
I have used zustand in a similar fashion in the past. I would often have a sync method on the store which I call in a useEffect and pass to it any state that is available to the component and not the store. Another possibility could be to let a library optimized for fetching get the data and make it available to the store once fetched.
What the documentation refers to with regard to performance is that you can indeed select parts of your store with a provided selector. In these cases a rerender will only happen when
the previous and current selected value are different or
a custom provided equality function states that previous and current values are different.
If you want to get into more detail with regard to performance I can recommend this this article here (disclaimer, I wrote it)
Even so, those performance considerations do not influence so much how you would trigger a fetch from, say, a useEffect hook.

Storing E-Commerce Product Data On LocalStorage

I am busy working on a "Headless" E-Commmerce Application in ReactJS and I Have stumbled upon an issue regarding performance.
My application uses a serverless approach with commercejs meaning I fetch my products and every other data via API calls instead of a traditional approach that involves me setting up a database and having other backend tools.
I already have this:
const App = () => {
const [products, setProducts ] = useState([]);
const getProducts = async () => {
const { data } = await commerce.products.list();
setProducts(data)
}
useEffect(() => {
getProducts()
},[])
}
Which is all used to get products and assign them to the products variable which I map through to display them inside divs and this works perfectly.
Here's what I need help with:
Is it be a good idea for me to use localStorage to store products instead of making a new commerce.products.list() every time the same user visits a page that need to display products?
Also, if so, how would one go about creating a function that knows when to update the products localStorage if there has been any changes (say a new product has been added or there's been price change for a certain product) if the products are now being fetched from localStorage?
Surely the answer to number 2, if is a yes, will be something like: make api requests that will be called on intervals but my main question/concern is how to know exactly when localStorage should be updated in an event like this without calling the API every now and then?
Also the use of sessionStorage did cross my mind, and it seemed like a better idea as the products data will be updated every time a user visits the application but I have considered the possibility of a user resuming a current window which was left open in the background for weeks and not see anything new.
I know this isn't a typical code/error/solution question but still any form of guidance/assistance will be highly appreciated.

Create and Read State for thousands of items using Recoil

I've just started using Recoil on a new project and I'm not sure if there is a better way to accomplish this.
My app is an interface to basically edit a JSON file containing an array of objects. It reads the file in, groups the objects based on a specific property into tabs, and then a user can navigate the tabs, see the few hundred values per tab, make changes and then save the changes.
I'm using recoil because it allows me to access the state of each input from anywhere in my app, which makes saving much easier - in theory...
In order to generate State for each object in the JSON file, I've created an component that returns null and I map over the initial array, create the component, which creates Recoil state using an AtomFamily, and then also saves the ID to another piece of Recoil state so I can keep a list of everything.
Question 1 Is these a better way to do this? The null component doesn't feel right, but storing the whole array in a single piece of state causes a re-render of everything on every keypress.
To Save the data, I have a button which calls a function. That function just needs to get the ID's, loop through them, get the state of each one, and push them into an Array. I've done this with a Selector too, but the issue is that I can't call getRecoilValue from a function because of the Rules of Hooks - but if I make the value available to the parent component, it again slows everything right down.
Question 2 I'm pretty sure I'm missing the right way to think about storing state and using hooks, but I haven't found any samples for this particular use case - needing to generate the state up front, and then accessing it all again on Save. Any guidance?
Question 1
Get accustomed to null-rendering components, you almost can't avoid them with Recoil and, more in general, this hooks-first React world 😉
About the useRecoilValue inside a function: you're right, you should leverage useRecoilCallback for that kind of task. With useRecoilCallback you have a central point where you can get and set whatever you want at once. Take a look at this working CodeSandbox where I tried to replicate (the most minimal way) your use-case. The SaveData component (a dedicated component is not necessary, you could just expose the Recoil callback without creating an ad-hoc component) is the following
const SaveData = () => {
const saveData = useRecoilCallback(({ snapshot }) => async () => {
const ids = await snapshot.getPromise(carIds);
for (const carId of ids) {
const car = await snapshot.getPromise(cars(carId));
const carIndex = db.findIndex(({ id }) => id === carId);
db[carIndex] = car;
}
console.log("Data saved, new `db` is");
console.log(JSON.stringify(db, null, 2));
});
return <button onClick={saveData}>Save data</button>;
};
as you can see:
it retrieves all the ids through const ids = await snapshot.getPromise(carIds);
it uses the ids to retrieve all the cars from the atom family const car = await snapshot.getPromise(cars(carId));
All of that in a central point, without hooks and without subscribing the component to atoms updates.
Question 2
There are a few approaches for your use case:
creating empty atoms when the app starts, updating them, and saving them in the end. It's what my CodeSandbox does
doing the same but initializing the atoms through RecoilRoot' initialState prop
being updated by Recoil about every atom change. This is possible with useRecoilTransactionObserver but please, note that it's currently marked as unstable. A new way to do the same will be available soon (I guess) but at the moment it's the only solution
The latter is the "smarter" approach but it really depends on your use case, it's up to you to think if you really want to update the JSON at every atom' update 😉
I hope it helps, let me know if I missed something 😊

Global variables in React

I know Redux solves this but I came up with an idea.
Imagine I have an app that gets some JSON on start. Based on this JSON I'm setting up the environment, so let's assume the app starts and it downloads an array of list items.
Of course as I'm not using Redux (the app itself is quite simple and Redux feels like a huge overkill here) if I want to use these list items outside of my component I have to pass them down as props and then pass them as props again as deep as I want to use them.
Why can't I do something like this:
fetch(listItems)
.then(response => response.json())
.then(json => {
window.consts = json.list;
This way I can access my list anywhere in my app and even outside of React. Is it considered an anti-pattern? Of course the list items WON'T be changed EVER, so there is no interaction or change of state.
What I usually do when I have some static (but requested via API) data is a little service that acts kind like a global but is under a regular import:
// get-timezones.js
import { get } from '../services/request'
let fetching = false
let timez = null
export default () => {
// if we already got timezones, return it
if (timez) {
return new Promise((resolve) => resolve(timez))
}
// if we already fired a request, return its promise
if (fetching) {
return fetching
}
// first run, return request promise
// and populate timezones for caching
fetching = get('timezones').then((data) => {
timez = data
return timez
})
return fetching
}
And then in the view react component:
// some-view.js
getTimezones().then((timezones) => {
this.setState({ timezones })
})
This works in a way it will always return a promise but the first time it is called it will do the request to the API and get the data. Subsequent requests will use a cached variable (kinda like a global).
Your approach may have a few issues:
If react renders before this window.consts is populated you won't
be able to access it, react won't know it should re-render.
You seem to be doing this request even when the data won't be used.
The only downside of my approach is setting state asynchronously, it may lead to errors if the component is not mounted anymore.
From the React point of view:
You can pass the list from top level via Context and you can see docs here.
Sample of using it is simple and exists in many libraries, such as Material UI components using it to inject theme across all components.
From engineering concept of everything is a trade of:
If you feel that it's gonna take so much time, and you are not going to change it ever, so keep it simple, set it to window and document it. (For your self to not forget it and letting other people know why you did this.)
If you're absolutely certain they won't ever change, I think it's quite ok to store them in a global, especially if you need to access the data outside of React. You may want to use a different name, maybe something like "appNameConfig"..
Otherwise, React has a feature called Context, which can also be used for "deep provision" - Reference

Resources