Cache manegment in recoil js - javascript

I decided to try using recoil instead of redux and ran into the problem of caching mutable collections. To be more precise, the problem is not in the response caching itself, but in the further data management, without re-requesting. For example, I requested an array of free time slots that will be displayed in the calendar, the user can change them (unblock or block). After the request (unblock or block) I can use useRecoilRefresher_UNSTABLE to reset the selector cache and request updated slots again, this works as expected, but why do this if you can remember the result after the first request and then just update it. My code is:
export const _cacheFreeSlotsA = atom<SlotsArray | null>({
key: '_cacheFreeSlotsA',
default: null,
});
export const freeSlotsS = selector<SlotsArray>({
key: 'freeSlotsS',
get: async ({ get }) => {
const cache = get(_cacheFreeSlotsA);
if (cache) {
return cache;
}
const res = await slotsAPI.getFreeSlots();
return res.slots;
},
});
export const useUpdateFreeSlotsS = () => {
const setFreeSlots = useSetRecoilState(_cacheFreeSlotsA);
const currentSlots = useRecoilValue(freeSlotsS);
return async (req: ChangeSlotsRequest) => {
await slotsAPI.changeSlots(req);
switch (req.operation) {
case 'open':
setFreeSlots([...currentSlots, ...req.slots]);
break;
case 'close':
setFreeSlots(difference(currentSlots, req.slots));
break;
}
};
};
// export const useUpdateFreeSlotsS = () => {
// const refresh = useRecoilRefresher_UNSTABLE(freeSlotsS);
// return async (req: ChangeSlotsRequest) => {
// await slotsAPI.changeSlots(req);
// refresh();
// };
// };
It works but look like workaround. Is there a more elegant and clear way to implement this behavior? (it would be just perfect if inside the freeSlotsS get method I could get access to the set method, but unfortunately it is not in the arguments)

Related

How can I read a CSV file from a URL in a Next.js application?

I have a Next.js application here which needs to read a CSV file from a URL in the same repo in multiple places, but I cannot seem to be able to retrieve this data. You can find the relevant file in my repo here.
Note, the URL I'm trying to pull data from is this: https://raw.githubusercontent.com/ivan-rivera/balderdash-next/main/public/test_rare_words.csv
Here is what I've tried so far:
Approach 1: importing the data
let vocab = {};
...
async function buildVocab() {
const words = await import(VOCAB_URL); // this works when I point to a folder in my directory, but it does not work when I deploy this app. If I point at the URL address, I get an error saying that it cannot find the module
for (let i = 0; i < words.length; i++) {
vocab[words[i].word] = words[i].definition;
}
}
Approach 2: papaparse
const papa = require("papaparse");
let vocab = {};
...
export async function buildVocab() {
await papa.parse(
VOCAB_URL,
{
header: true,
download: true,
delimiter: ",",
step: function (row) {
console.log("Row:", row.data); // this prints data correctly
},
complete: function (results) {
console.log(results); // this returns an object with several attributes among which is "data" and "errors" and both are empty
},
}
);
// this does not work because `complete` does not return anything
vocab = Object.assign({}, ...raw.map((e) => ({ [e.word]: e.definition })));
console.log(vocab);
}
Approach 3: needle
const csvParser = require("csv-parser");
const needle = require("needle");
let vocab = {};
...
let result = [];
needle
.get(VOCAB_URL)
.pipe(csvParser())
.on("data", (data) => {
result.push(data);
});
vocab = Object.assign({}, ...result.map((e) => ({ [e.word]: e.definition })));
// This approach also returns nothing, however, I noticed that if I force it to sleep, then I do get the results I want:
setTimeout(() => {
console.log(result);
}, 1000); // now this prints the data I'm looking for
What I cannot figure out is how to force this function to wait for needle to retrieve the data. I've declared it as an async function and I'm calling it with await buildVocab() but it doesn't help.
Any ideas how I can fix this? Sorry, I'm a JS beginner, so it's probably something fundamental that I'm missing :(
After spending hours on this, I think I finally found a solution:
let vocab = {};
export async function buildVocab() {
await fetch(VOCAB_URL)
.then((resp) => resp.text())
.then((text) => {
papa.parse(text, { header: true }).data.forEach((row) => {
vocab[row.word] = row.definition;
});
});
}
The only oddity that I still can't work out is this: I'm calling my buildVocab function inside another async function and I noticed that if I do not include a console.log statement in that function, then the vocab still does not get populated in time. Here is the function:
export async function sampleWord() {
await buildVocab();
const keys = Object.keys(vocab);
const index = Math.floor(Math.random() * keys.length);
console.log(`selected word: ${keys[index]}`); // this is important!
return keys[index];
}

Context is undefined in translation module

I tried to add a call to an endpoint in order to get translation. I have like this :
const loadLocales = async () => {
const context = require.context('./locales', true);
const data = await ApiService.post(`${translationToolUrl}/gateway/translations`, { project: 'myProject' });
const messages = context.keys()
.map((key) => ({ key, locale: key.match(/[-a-z0-9_]+/i)[0] }))
.reduce((msgs, { key, locale }) => ({
...msgs,
[locale]: extendMessages(context(key)),
}), {});
return { context, messages };
};
const { context, messages } = loadLocales();
i18n = new VueI18n({
locale: 'en',
fallbackLocale: 'en',
silentFallbackWarn: true,
messages,
});
if (module.hot) {
module.hot.accept(context.id, () => {
const { messages: newMessages } = loadLocales();
Object.keys(newMessages)
.filter((locale) => messages[locale] !== extendMessages(newMessages[locale]))
.forEach((locale) => {
const msgs = extendMessages(newMessages[locale]);
messages[locale] = msgs;
i18n.setLocaleMessage(locale, msgs);
});
});
}
I added this request : ApiService.post. But I have the error TypeError: context is undefined droped at this line module.hot.accept(context.id.... Have you an idea how I can solve that ? My scope was to add this request in order to get translations from database and from .json files for now. I want to do a merge between both for now, in the feature I will get only from database but this will be done step by step.
The problem is, that you trying to declare multiple const in the wrong way, independently of trying to declaring them twice. This shows in:
const { context, messages } = loadLocales();
This would couse context and messages to be undefined. This won´t give an error, as I replicated in a small example:
const {first, second} = 'Testing'
console.log(first)
console.log(second)
Both, first and second, will be undefined. If you try to declare multiple const at once, you need to do it this way:
const context = loadLocales(), messages = loadLocales();

Async function overwrites array state when it should update only one item

I have a file upload component. The behavior is simple: I send one upload request to the back-end per file and as the upload progress increase, I have a bar that should increase with it.
I have a state that holds every selected file and their respective progress, as such:
interface IFiles {
file: File;
currentProgress: number;
}
const [selectedFiles, setSelectedFiles] = useState<IFiles[]>([]);
And when the user clicks the upload button, this function will be triggered and call uploadFile for each file in my array state.
const sendFilesHandler = async () => {
selectedFiles.map(async (file) => {
const fileType = file.file.type.split('/')[0];
const formData = new FormData();
formData.append(fileType, file.file);
formData.append('filename', file.file.name);
await uploadFile(formData, updateFileUploadProgress);
});
};
Here is what the uploadFile function looks like.
const uploadFile = async (body: FormData, setPercentage: (filename: string, progress: number) => void) => {
try {
const options = {
onUploadProgress: (progressEvent: ProgressEvent) => {
const { loaded, total } = progressEvent;
const percent = Math.floor((loaded * 100) / total);
const fileName = body.get('filename')!.toString();
if (percent <= 100) {
setPercentage(fileName, percent)
}
}
};
await axios.post(
"https://nestjs-upload.herokuapp.com/",
body,
options
);
} catch (error) {
console.log(error);
}
};
As you can see, when uploadProgress is triggered it should inform call setPercentage function, which is:
const updateFileUploadProgress = (fileName: string, progress: number) => {
console.log('Entrada', selectedFiles);
const currentObjectIndex = selectedFiles.findIndex((x) => fileName === x.file.name);
const newState = [...selectedFiles];
newState[currentObjectIndex] = {
...newState[currentObjectIndex],
currentProgress: progress,
};
setSelectedFiles(newState);
console.log('Saída', newState);
};
And this function should only update the object of my state array where the filenames match. However, it is overriding the whole thing. The behavior is as follows:
So it seems that everything is fine as long as I am updating the same object. But in the moment onUploadProgress is triggered to another object, selectedFiles states becomes its initial state again. What am I missing to make this work properly?
I am not sure what is the exact reason behind this behaviour but I came up with the below unexpectedly simple solution after spending 3 hours straight on it.
const updateFileUploadProgress = (fileName: string, progress: number) => {
setSelectedFiles(prevState => {
const currentObjectIndex = prevState.findIndex((x) => fileName === x.file.name);
const newState = [...prevState];
newState[currentObjectIndex] = {
...newState[currentObjectIndex],
currentProgress: progress,
};
return newState;
})
};
I was able to mimic your problem locally, and solved it with the above approach. I think it was because the function (for some reason) still referenced the initial state even after rerenders.

React : make backend API call when user is authenticated, use LocalStorage if not

I'm working on a small note taking app with React and Node.js (Express). If the user is authenticated I make API calls to the backend to fetch, create, update, delete notes persisted in a MongoDB database. If he's not, the notes are stored in localStorage. I have an AuthContext with login, logout and signup functions.
I can know if the user is loggedIn with my useAuth() custom hook in my AuthContext :
const { user } = useAuth();
And I have a separate file to make the API calls that I use in my components (getNotes, createNotes ...)
I fetch my notes in the useEffect hook
React.useEffect(() => {
const notes: Note[] = getNotes();
setNotes(notes);
}, []);
And I render my notes like this (simplified)
{notes.length > 0 && (
<ul>{notes.map(renderNote)}</ul>
)}
const renderNote = (note) => {
return (
<Note note={note} />
);
};
My question is what would be a good practice to implement the different behaviors (API calls or localStorage) ?
I can add a parameter isLoggedIn to the functions and add an if statement inside the function like this (simplified version) :
const getNotes = (isLoggedIn) => {
if (isLoggedIn) {
return notes = fetch("/notes")
} else {
return notes = localStorage.getItem("notes")
}
}
But this does not look like something clean to do if I have do to this in every function.
Thanks in advance for your help
Here's something you could do. I think I'd suggest you create the idea of some store that implements a simple getter/setter interface, then have your useAuth hook return the correct store depending on the auth state. If authenticated, then your hook returns the remote store. If not, then it returns the local storage store. But your store looks the same to your component no matter whether it's a local or remote store.
Now your code can just call get/set on the store and not care about where your info is stored or even whether the user is logged in. A main goal is to avoid having a lot of if (loggedIn) { ... } code all over your app.
Something like...
const useLocalStorageStore = () => {
const get = (key) => {
return localStorage.getItem(key);
};
const set = (key, value) => {
// I append 'local' here just to make it obvious the
// local store is in use in this example
localStorage.setItem(key, `${value} local`);
};
return { get, set };
};
// This contrived example uses localStorage too to make my example easier,
// but you'd add the fetch business to your get/set methods
// here in this remote store.
const useRemoteStore = () => {
const baseUrl = "http://localhost/foo/bar";
const get = async (key) => {
//return fetch(`${baseUrl}/${key}`);
// really should fetch here, but for this example use local
return localStorage.getItem(key);
};
const set = async (key, value) => {
// I append 'remote' here just to make it obvious the
// remote store is in use in this example
localStorage.setItem(key, `${value} remote`);
};
return { get, set };
};
const useAuth = () => {
// AuthContext is your source of loggedIn info,
// however you have it available in your app.
const { login, logout, loggedIn } = React.useContext(AuthContext);
const authedStore = useRemoteStore();
const unauthedStore = useLocalStorageStore();
const store = loggedIn ? authedStore : unauthedStore;
return { login, logout, loggedIn, store };
};
At this point, the store has all you need to get or set key values. Then you can use it in your components with...
const MyComponent = () => {
const { loggedIn, login, logout, store } = useAuth();
const setNotesValue = async (value) => {
// Your store handles communicating with the correct back end.
await store.set("notes", value);
};
const getNotesValue = async () => {
// Your store handles communicating with the correct back end.
const value = await store.get("notes");
};
return (
<div>Your UI...</div>
);
};
Here's a sandbox to demo this: https://codesandbox.io/s/gallant-gould-v1qe0

Loop from multiple airtable bases in a Next JS page

I think this more of a general async/await loop question, but I'm trying to do it within the bounds of an Airtable API request and within getStaticProps of Next.js so I thought that is important to share.
What I want to do is create an array of base IDs like ["appBaseId01", "appBaseId02", "appBaseId03"] and output the contents of a page. I have it working with 1 base, but am failing at getting it for multiple.
Below is the code for one static base, if anyone can help me grok how I'd want to loop over these. My gut says that I need to await each uniquely and then pop them into an array, but I'm not sure.
const records = await airtable
.base("appBaseId01")("Case Overview Information")
.select()
.firstPage();
const details = records.map((detail) => {
return {
city: detail.get("City") || null,
name: detail.get("Name") || null,
state: detail.get("State") || null,
};
});
return {
props: {
details,
},
};
EDIT
I've gotten closer to emulating it, but haven't figured out how to loop the initial requests yet.
This yields me an array of arrays that I can at least work with, but it's janky and unsustainable.
export async function getStaticProps() {
const caseOneRecords = await setOverviewBase("appBaseId01")
.select({})
.firstPage();
const caseTwoRecords = await setOverviewBase("appBaseId02")
.select({})
.firstPage();
const cases = [];
cases.push(minifyOverviewRecords(caseOneRecords));
cases.push(minifyOverviewRecords(caseTwoRecords));
return {
props: {
cases,
},
};
}
setOverviewBase is a helper that establishes the Airtable connection and sets the table name.
const setOverviewBase = (baseId) =>
base.base(baseId)("Case Overview Information");
You can map the array of base IDs and await with Promise.all. Assuming you have getFirstPage and minifyOverviewRecords defined as below, you could do the following:
const getFirstPage = (baseId) =>
airtable
.base(baseId)("Case Overview Information")
.select({})
.firstPage();
const minifyOverviewRecords = (records) =>
records.map((detail) => {
return {
city: detail.get("City") || null,
name: detail.get("Name") || null,
state: detail.get("State") || null,
};
});
export async function getStaticProps() {
const cases = await Promise.all(
["appBaseId01", "appBaseId02", "appBaseId03"].map(async (baseId) => {
const firstPage = await getFirstPage(baseId);
return minifyOverviewRecords(firstPage);
})
);
return {
props: {
cases
}
};
}

Categories

Resources