Hooks and webworkers - javascript

Is there a way for a webworker to use a react hook?
I am using Apollo Client to perform useLazyQuery which is a custom hook.
But the actual operation takes quite long and times out most often!
I want to run this on another thread to not disrupt the main application.

Although I'm not sure whether your timeout problem would be fixed by executing the query in a webworker, the easiest way to achieve that (without considering pending/error states) would be something like this:
worker.js
self.addEventListener('message', async event => {
const {apolloClient, query} = e.data;
const result = await apolloClient.query({query});
self.postMessage(result);
self.close();
});
App.jsx
const query = gql`Some query here`;
const useWebWorkerQuery = query => {
const apolloClient = useApolloClient();
const [result, setResult] = useState(null);
useEffect(() => {
const worker = new Worker('worker.js');
worker.postMessage({apolloClient, query});
worker.onmessage = event => setResult(event.data);
}, []);
};
const App = () => {
const result = useWebWorkerQuery(query);
useEffect(() => {
if(result) {
// Do something once query completes
}
}, [result]);
return null;
};
export default App;

Related

How to make sure a JavaScript code would be executed only once in React components?

I'm trying to use file upload with preview and this is the code of my component:
const [uploadField, setUploadFiled] = useState()
useEffect(() => {
const temp = new FileUploadWithPreview('fileUpload', {
multiple: multiple,
});
window.addEventListener(Events.IMAGE_ADDED, (e) => {
const { detail } = e;
console.log('detail', detail);
});
}, [])
The problem is that since I have <React.StrictMode> I see two file upload controls in my page. And whenever I save the file, because of HMR another control would be created.
I want to only run that initialization code once.
How can I achieve that?
You can use the useRef hook to store a reference to the FileUploadWithPreview instance.
This will ensure that the code is only executed once, even when HMR is enabled.
const uploadFieldRef = useRef();
useEffect(() => {
const temp = new FileUploadWithPreview('fileUpload', {
multiple: multiple,
});
uploadFieldRef.current = temp;
window.addEventListener(Events.IMAGE_ADDED, (e) => {
const { detail } = e;
console.log('detail', detail);
});
}, []);
try useRef hook
const effectCalled = useRef(false);
useEffect(() => {
if (effectCalled.current) return;
console.log("app rendered");
effectCalled.current = true;
}, []);

How to handle refresh for data filtering in React useEffect?

I have the following code in my React component:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
}
}, [])
Where:
[data, SetData] is the state that is used to handle populating a container
facilityData is data accessed from my app context
id is accessed from the URL
What seems to happen is that the data loads the first time without fault, but it errors out when hosted on the actual site (on localhost, it waits and eventually loads). To try to get a better idea of what was happening, I tried the following code:
const { id } = useParams();
const { tripData, facilityData } = useContext(AppContext);
const [data, setData] = useState([]);
useEffect(() => {
const idResults = facilityData.filter(facility => facility.id === id);
if (idResults.length > 0) {
setData(idResults[0]);
} else if (idResults.length === 0) {
console.log(`id: ${id}`)
console.log(`len: ${idResults}`)
}, [])
On localhost, on refresh, it console logs the actual id but then console logs the empty array before finally loading the data.
What I'm wondering is why this is the observed behavior. The "id" value seems to be constantly available, but the filter doesn't seem to run prior to the site loading. Is there a way to prevent this?
EDIT:
This is how I get the data (from Firebase)
App.js
import { collection, getDocs } from "firebase/firestore";
import { db } from "./firebase";
const [truckData, setTruckData] = useState([]);
const [facilityData, setFacilityData] = useState([]);
const [tripData, setTripData] = useState([]);
useEffect(() => {
const fetchData = async (resource, setter) => {
let list = [];
try {
const querySnapshot = await getDocs(collection(db, resource));
querySnapshot.forEach((doc) => {
let docData = doc.data();
if (resource === "trips") {
docData.startDate = docData.startDate.toDate();
docData.endDate = docData.endDate.toDate();
}
list.push({ id: doc.id, ...docData });
});
setter(list);
} catch (error) {
console.log(error);
}
};
fetchData("trucks", setTruckData);
fetchData("facilities", setFacilityData);
fetchData("trips", setTripData);
}, []);
The app is at logi-dashboard, if that helps any.
EDIT Turns out the issue was with my hosting service, not the project. Go figure.
Based on my understanding, it seems like the facilityData on which you are trying to apply filter and which is coming from AppContext(Context hook variable) is found to be empty array when the useEffect code is getting executed, this might be scene if you are hitting any API to get the data into facility but the API response is not coming till the time useEffect is getting executed or any other source which is not populating the facilityData until useEffect runs.
In that case, you can add facilityData in the dependency array of useEffect, which will help the useEffect execute again once the facilityData is populated(updated)

React - Refactoring logic with hooks

INTRODUCTION
I have a screen which makes some queries to my server:
Fetch user by username (when the user types something in the search input)
Fetch premium users (when screen mounts + pull to refresh)
Fetch young users (when screen mounts + pull to refresh)
I am thinking about moving this to a hook.
This is my current code:
function MyScreen() {
// Fetch by username
const [isSearching, setIsSearching] = useState([]);
const [searchedUsers, setSearchedUsers] = useState([]);
// Premium
const [isLoading, setIsLoading] = useState(true); <--- NOTE THIS
const [premiumUsers, setPremiumUsers] = useState([]);
// Young users
const [youngUsers, setYoungUsers] = useState([]);
/*
FIRST METHOD
*/
const searchUsersByUsername = async (limit = 20) => {
setIsSearching(true);
try {
const result = await api.users.searchUsersByUsername(username, limit);
setSearchedUsers(result);
} catch(err) {
console.log(err);
}
setIsSearching(false);
}
/*
SECOND METHOD
*/
const getPremiumUsers = async (limit = 10) => {
// Note: No loading here
try {
const result = await api.users.getPremiumUsers(limit);
setPremiumUsers(result);
} catch(err) {
console.log(err);
}
}
/*
THIRD METHOD
*/
const getYoungUsers = async (limit = 10) => {
// Note: No loading here
try {
const result = await api.users.getYoungUsers(limit);
setYoungUsersUsers(result);
} catch(err) {
console.log(err);
}
}
// Effects and rendering...
useEffect(() => {
(async () => {
const promises = [getPremiumUsers(), getYoungUsers()];
await Promise.all(promises).catch((err) => {
console.log(err))
});
setIsLoading(false); // <---- NOTE THIS
})();
}, []);
}
PROBLEM
I cannot use the typical useQuery hook, as I am using Firestore Callable Functions. So, the only way to make requests, is to call my api methods (api.users.searchUser...)
As you can see in the code, I have two types of loading indicators (two states):
isSearching (for the searching by username functionality)
isLoading (for fetching premium and young users in parallel)
How can I implement a reusable hook for this logic?
I mean, I need to abstract all this stuff, in order to be able to:
Search users by username in other screens
Search premium users (without fetching young users in parallel)
Search young users (without fetching premium users in parallel)
And, also, to get the loading status of the three queries.
Note: In my current screen, as I have said before, I am using "setIsLoading" for the young and premium users parallel fetching, but maybe (to be more flexible) in other screens I will need the loading status for each logic independently.
Any help or ideas?
You could use a React.useEffect and React.useCallback by each fetch method.
Also, save the loading status individually.
Check this out:
import { useEffect, useCallback } from 'react';
const defaultParams = {
fetchPremiumUsersOnMount: false,
fetchYoungUsersOnMount: false,
searchOnMount: false,
username: '',
limit: 20,
}
function useUsersApi(params = defaultParams) {
const [isSearching, setIsSearching] = useState(false);
const [searchedUsers, setSearchedUsers] = useState([]);
const [premiumUsers, setPremiumUsers] = useState([]);
const [premiumLoading, setPremiumLoading] = useState(false);
const [youngUsers, setYoungUsers] = useState([]);
const [youngLoading, setYoungLoading] = useState(false);
const fetchPremiumUsers = useCallback(async () => {
try {
setPremiumLoading(true);
const result = api.users.getPremiumUsers(params.limit);
setPremiumUsers(result);
} catch (err) {
console.log(err)
} finally {
setPremiumLoading(false);
}
}, [params.limit]);
const fetchYoungUsers = useCallback(async () => {
/* similar logic to `fetchPremiumUsers` */
}, [params.limit, params.]);
const fetchSearchUsers = useCallback(async (username) => {
/* fetch logic here */
}, [params.limit]);
useEffect(() => {
if(params.fetchPremiumUsersOnMount) {
fetchPremiumUsers();
}
}, [params.fetchPremiumUsersOnMount, params.limit]);
useEffect(() => {
if(params.fetchYoungUsersOnMount) {
fetchYoungUsers();
}
}, [params.fetchYoungUsersOnMount, params.limit]);
useEffect(() => {
if(params.fetchSearchUsers) {
fetchSearchUser(params.username);
}
}, [params.searchOnMount, params.limit, params.username]);
return {
isSearching,
searchedUsers,
isLoading: premiumLoading || youngLoading,
premiumUsers,
premiumUsersLoading: premiumLoading,
refreshPremiumUsers: fetchPremiumUsers,
youngUsers,
youngUsersLoading: youngLoading,
refreshYoungUsers: fetchYoungUsers,
}
}

Test firestore trigger locally

I am writing a test which tests a firebase trigger. The problem, however, is that I cannot make it work.
I want to use the local firestore emulator and Jest in order to simulate a change in the firestore and see if the trigger does what it needs to do.
I require the cloud function in my test and I initialize my app
Setup.js:
const firebase = require('#firebase/testing');
const PROJECT_ID = 'project';
let admin;
let db;
const setupAdmin = async () => {
admin = firebase.initializeAdminApp({
projectId: PROJECT_ID
});
db = admin.firestore();
};
const getAdmin = () => {
return admin;
};
const getDb = () => {
return db;
};
module.exports.setupAdmin = setupAdmin;
module.exports.getAdmin = getAdmin;
module.exports.getDb = getDb;
Test.js
describe('Billing', () => {
let dbRef;
beforeAll(async () => {
const {db, admin} = require('../../../functions/helpers/setup');
dbRef = db;
});
afterAll(async () => {
await Promise.all(firebase.apps().map(app => app.delete()));
console.log(`View rule coverage information at ${COVERAGE_URL}\n`);
});
it('test', async () => {
const mockData = {
'Users/user1': {
uid: 'user1'
},
['Users/user1/Taxes/' + new Date().getFullYear().toString()]: {
totalExpenseEuro: 0
}
};
for (const key in mockData) {
const ref = dbRef.doc(key);
await ref.set(mockData[key]);
}
// Create mockup data
await dbRef.collection('Users').doc('user1').collection('Expenses').doc('expense1').set({
amountEuroInclVAT: 100
});
// Make snapshot for state of database beforehand
const beforeSnap = test.firestore.makeDocumentSnapshot({amountEuroInclVAT: 0}, 'Users/user1/Expenses/expense1');
// Make snapshot for state of database after the change
const afterSnap = test.firestore.makeDocumentSnapshot(
{amountEuroInclVAT: 100},
'Users/user1/Expenses/expense1'
);
const change = test.makeChange(beforeSnap, afterSnap);
// Call wrapped function with the Change object
const wrapped = test.wrap(calculateTaxesOnExpenseUpdate);
wrapped(change, {
params: {
uid: 'test1'
}
});
});
});
Now the main problem comes when I try to access this db object in my trigger
const calculateTaxesOnExpenseUpdate = functions.firestore
.document('Users/{uid}/Expenses/{expenseId}')
.onWrite(async (change, context) => {
const {getDb} = require('../helpers/setup'); // This setup is the same as above
let db = getDb();
...
For some reason when I perform an action like (await db.collection('Users').get()).get('totalExpenseEuro'), Jest stops executing my code. When I set a debugger right after that line, it never gets printed. That piece of code crashes, and I have no idea why. I think the DB instance if not properly configured in my cloud trigger function.
Question: What is a good way of sharing the DB instance (admin.firestore()) between the test and the cloud trigger functions?

Unnecessary parameter in useEffect dependency array

I'm creating an application where users can create and share notes.
To share each other's notes users have to send requests to specific users.
The requests are fetched whenever home is loaded.
However, requests is a context since it is also consumed in the toolbar and requests page to show the presence of the requests
When I'm using setRequsts method of the context to set all the requests after home loads, the fetch goes into an infinite loop of /noteand /me URLs, since the setRequests method is also provided in the dependency array of useEffect
When removed, useEffect show missing dependencies. What's the work around?
const {setRequests } = useContext(RequestsContext)
const [notes, setNotes] = useState([])
const [fetched, setFetched] = useState('')
const { isAuthenticated } = props
const {page}=useContext(PageContext)
const [sortBy,setSortBy]=useState('latest')
useEffect(() => {
const fetch = async () => {
try {
let url = 'http://192.168.56.1:5000/api/v1/note', p, sort
if (page) p = `?page=${page}&limit=12`
if (sortBy === 'latest') {
sort=''
} else if (sortBy === 'most_liked') {
sort='&sort=likes'
}
const res = await Axios.get(url+p+sort)
setNotes(res.data.data)
if (res.data.data.length > 0) {
setFetched('Y')
} else {
setFetched('N')
}
} catch (err) {
console.log(err)
} finally {
if (isAuthenticated) {
const fetch = async () => {
const res = await axios.get(`user/me`)
if (res.data.data.createdPosts.length > 0) {
const arr = res.data.data.createdPosts.map(el => el.request)
console.log(arr)
setRequests(arr)
}
}
fetch()
}
}
}
fetch()
}, [isAuthenticated, /* setRequests, */ page, sortBy])
The problem is that the context provides a technically different setRequests function on each render (that have a different address). This causes useEffect to fire on each render.
To work around this, you could wrap setRequests in a useCallback() hook, like so:
// ...
const wrappedSetRequests = useCallback(setRequests, []);
// ...
useEffect(() => {
// do your stuff using 'wrappedSetRequests' instead of setRequests.
}, [ wrappedSetRequests /*...*/ ]);

Categories

Resources