Summary: PERN app needs to download XLSX that represents the same query that was run to populate a data grid on the client side. For example a user has defined a series of product family prefixes and they're sent to the server endpoint:
const getItemsByFamily = async (like) => {
const items = await fetch('/item/like/' + like);
const result = await items.json();
return result;
};
Where the endpoint uses a call to the associated database function:
app.get('/item/like/:like', async (req, res) => {
const { like } = req.params;
const { rows } = await dbo.getByItemPrefix(like);
res.json(rows);
});
And, the function runs the actual query against the PG Db:
getByItemPrefix: async (like) => await pool.query(`select * from myschema.items where ${like}`);
All is well... But, now stakeholders on this project have requested the ability to download a representative XLSX for the same data set. My idea is to use PG's COPY command to write a unique XLSX on the server and link to it somehow.
I start with a proof of concept, hard-coded address, just to simply download a sample xlsx:
// TODO: Add some sort of reference to represent data-grid query.
const DownloadXLSX = ({/* query or query ref */}) => {
return (
<a href='http://localhost:1234/download?fileName=sample.xlsx' target='_blank' rel='noopener noreferrer' download>
<Button
variant='contained'
size='small'
>
Download
</Button>
</a>
);
};
But, now I'm on SO because I am stuck in an architecture question. What is possible?
Is it time to audit every query and reference it somehow that way? Currently this product has no auditing, but perhaps it is time to spend time to do that? Possibly make an audit entry and send the id down with the results, passing the audit.id down do the download button for its query param?
Or, is there some more simple way to track a data grid's representative query on the client?
I like to do whatever is optimal and I'm hoping to find opinions or some insight. Thanks in advance for any help in getting over this architecture hurdle (missing having a team!).
I thought I should follow up here.
To access the actual text of a query within a PG stored function that could be used with a trigger you can use the function current_query().
However, my whole idea of using triggers went down the drain when I found this in the PG docs:
SELECT does not modify any rows so you cannot create SELECT triggers. Rules and views are more appropriate in such cases.
While the suggested approach might work in some cases it really would not for me as the results of some queries are huge and I really needed a reference.
Instead I wrap pool.query with a function that just takes the passed-in SQL and stores it in a simple, but sufficient for my purposes, "audit" table.
CREATE TABLE myschema.audit
(
id serial,
user_id varchar(50) not null,
full_query text not null,
x_time timestamp not null default now(),
CONSTRAINT audit_pkey PRIMARY KEY (id)
)
TABLESPACE pg_default;
ALTER TABLE myschema.audit
OWNER to postgres;
pool.queryWithAudit = async query => {
const { rows } = await pool.query(SQL`insert into iol.trac_audit (user_id, full_query) values ($1, $2) returning id`, ['dev-trac', query]);
const auditID = rows[0].id;
const queryResult = await pool.query(query);
queryResult.auditID = auditID;
return queryResult;
};
And, finally, the retun from the server changed just a little bit to include the auditID, and the download component had to change (to CSV for now, instead of XLSX):
const DownloadXLSX = ({ queryId, style = {} }) => {
return (
<a style={{ textDecoration: 'none', ...style }} href={`http://localhost:1234/download?queryId=${queryId}`} target='_blank' rel='noopener noreferrer' download>
<Button
endIcon={<GetAppIcon />}
size='small'
>
Download
</Button>
</a>
);
};
Along with corresponding route for lookup of query, then execution:
app.get('/download', async (req, res) => {
const { queryId } = req.query;
// Now grab query from audit and run that.
const fileName = await dbo.makeXLSX(queryId);
res.download(fileName, fileName.split('/').pop(), (err) => {
console.log(err ? 'Error attempting to download: ' + err.message : 'No server-side error in attempt to download.');
if (!err) fs.unlink(fileName, (unlinkErr) => console.log(unlinkErr ? 'Temp folder cleanup error: ' + unlinkErr.message : `File at: [${fileName}] removed from system temp folder.`));
});
});
dbo.js updates:
makeXLSX: async auditId => {
const { rows } = await pool.query(`select full_query from myschema.audit where id = ${auditId}`);
const query = rows[0].full_query;
const fileName = path.join('C:/tmp/', Date.now().toString() + '.csv');
const copyCommand = `COPY (${query}) TO '${fileName}' CSV HEADER;`;
await pool.query(copyCommand);
return fileName;
}
Anyway, this works for me. Maybe somebody will submit improvements? I just include the button if there is a queryId, which I null-out with the associated useState setter function:
const [queryId, setAuditIdRef] = useState(null);
{queryId ? <DownloadXLSX queryId={queryId} /> : null}
Good day.
Related
I have setup knex migration to create two tables
export async function up(knex: Knex): Promise<void> {
const table1 = await knex.schema.hasTable('users');
if (!table1) {
await knex.schema.createTable('users', (table) => {
table.increments("id").primary();
table.string("username");
table.string("email");
table.string("password");
table.string("role");
table.timestamp("created_at");
table.timestamp("updated_at");
table.timestamp("logged_at");
});
}
const table2 = await knex.schema.hasTable('usersloc');
if (!table2) {
await knex.schema.createTable('usersloc', (table) => {
table.increments("locid");
table.string("lat");
table.string("lng")
table.foreign("userid");
});
}
}
However, when I did a select * from usersloc;, userid in usersloc is nowhere to be found.
My intention is to have it referred to the id at the "users" table.
Any idea what happened?
Solution found - looks like the UUID needs to have a .unique() reference on both tables. My guess is that even though UUID may take billions of years to have 1% chance of collision, it is still not a unique ID, thus a .unique() reference is needed.
I have an application that triggers many update and I would like to know more about the best way to update the app properly.
In my app, I have 5 slots to fill with books (can be managed by drag and drop). When the app launches, the filled book for the user are loaded and are stored in the state.
Problem : when I update a book, like if I switch the position of 2 books in my list, I must do some operations to say "this book belongs here now and the other one belongs here now, switch!"
I feel like I'm doing some tedious actions because if I just return the whole data (get, after updating) from my API call and call the "load" function (as I do when I launch the app) I will not have to handle the update of the operation.
Plus, it could create bug If I'm loading correctly, but not updating correctly (if I miss position of a book for example)
The benefit I see in a functional update is that I only update the 2 books I need, instead of reload all of them again and again.
What way would be better? Should I get rid of those updates functions and just reload the data entirely? I think there could be also some libraries that cache it to only re-render modified books
Thanks you
Without code it is difficult to fully understand the problem but getting the data from the server has 2 advantages.
You are sure the ui shows the data as it is on the server
Your client code does not need to contain the logic of what needs to happen, the server has this logic. When the logic is refactored in some way they don't go out of sync.
Because of this I usually choose to get the data as is on the server.
One problem with fetching data based on user interaction is that fetching is async so the following can happen:
User does action A, request made for A, user Does action B, request made for B, B request resolves and UI is set to result of request B, request made for A resolves and UI is set to result of A.
So the order the user does the actions does not guarantee the order in which the requests are resolved.
To solve this you can use a helper that resolves only if it was last requested, in the example above when A request resolves the UI does not need to be set with anything because it has already been replaced with another request.
In the example below you can type search value, when the value is 1 character long it'll take 2 seconds to resolve so when you type ab the ab request will resolve before the a request. but because the function making the request is wrapped with the last helper when a resolves it'll will be rejected because it has been replaced with the newer request ab.
//constant to reject with when request is replaced with a
// more recent request
const REPLACED = {
message: 'replaced by more recent request',
};
//helper to resolve only last requested promise
const last = (fn) => {
const check = {};
return (...args) => {
const current = {};
check.current = current;
return Promise.resolve()
.then(() => fn(...args))
.then((result) => {
//see if current request is last request
if (check.current === current) {
return result;
}
//was not last request so reject
return Promise.reject(REPLACED);
});
};
};
const later = (howLong, value) =>
new Promise((resolve) =>
setTimeout(() => resolve(value), howLong)
);
const request = (value) =>
later(value.length === 1 ? 2000 : 10, value).then(
(result) => {
console.log('request resolved:', result);
return result;
}
);
const lastRequest = last(request);
const App = () => {
const [search, setSearch] = React.useState('');
const [result, setResult] = React.useState('');
React.useEffect(() => {
//if you use request instead of lastRequest here
// you see it will break, UI is updated as requests
// resolve without checking if it was the last request
lastRequest(search)
.then((result) => setResult(`result:${result}`))
.catch((err) => {
console.log(
'rejected with:',
err,
'for search:',
search
);
if (err !== REPLACED) {
//if the reject reason is not caused because request was
// replaced by a newer then reject this promise
return Promise.reject(err);
}
});
}, [search]);
return (
<div>
<label>
search
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
></input>
</label>
<div>{result}</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm trying to perform real-time Named Entity Recognition highlighting in a WYSIWYG editor, which requires me to make a request to my back-end in between each keystroke.
After spending about a week on ProseMirror I gave up on it and decided to try DraftJS. I have searched the repository and docs and haven't found any asynchronous examples using Decorations. (There are some examples with Entities, but they seem like a bad fit for my problem.)
Here is the stripped down Codepen of what I'd like to solve.
It boils down to me wanting to do something like this:
const handleStrategy = (contentBlock, callback, contentState) => {
const text = contentBlock.getText();
let matchArr, start;
while ((matchArr = properNouns.exec(text)) !== null) {
start = matchArr.index;
setTimeout(() => {
// THROWS ERROR: Cannot read property '0' of null
callback(start, start + matchArr[0].length);
}, 200) // to simulate API request
}
};
I expected it to asynchronously call the callback once the timeout resolved but instead matchArr is empty, which just confuses me.
Any help is appreciated!
ok, one possible solution, a example, simple version (may not be 100% solid) :
write a function take editor's string, send it to server, and resolve the data get from server, you need to figure out send the whole editor string or just one word
getServerResult = data => new Promise((resolve, reject) => {
...
fetch(link, {
method: 'POST',
headers: {
...
},
// figure what to send here
body: this.state.editorState.getCurrentContent().getPlainText(),
})
.then(res => resolve(res))
.catch(reject);
});
determine when to call the getServerResult function(i.e when to send string to server and get entity data), from what I understand from your comment, when user hit spacebar key, send the word before to server, this can done by draftjs Key Bindings or react SyntheticEvent. You will need to handle case what if user hit spacebar many times continuously.
function myKeyBindingFn(e: SyntheticKeyboardEvent): string {
if (e.keyCode === 32) {
return 'send-server';
}
return getDefaultKeyBinding(e);
}
async handleKeyCommand(command: string): DraftHandleValue {
if (command === 'send-server') {
// you need to manually add a space char to the editorState
// and get result from server
...
// entity data get from server
const result = await getServerResult()
return 'handled';
}
return 'not-handled';
}
add entity data get from server to specific word using ContentState.createEntity()
async handleKeyCommand(command: string): DraftHandleValue {
if (command === 'send-server') {
// you need to manually add a space char to the editorState
// and get result from server
...
// entity data get from server
const result = await getServerResult()
const newContentState = ContentState.createEntity(
type: 'string',
mutability: ...
data: result
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
// you need to figure out the selectionState, selectionState mean add
// the entity data to where
const contentStateWithEntity = Modifier.applyEntity(
newContentState,
selectionState,
entityKey
);
// create a new EditorState and use this.setState()
const newEditorState = EditorState.push(
...
contentState: contentStateWithEntity
)
this.setState({
editorState: newEditorState
})
return 'handled';
}
return 'not-handled';
}
create different decorators find words with specific entity data, and return different style or whatever you need to return
...
const compositeDecorator = new CompositeDecorator([
strategy: findSubjStrategy,
component: HandleSubjSpan,
])
function findSubjStrategy(contentBlock, callback, contentState) {
// search whole editor content find words with subj entity data
// if the word's entity data === 'Subj'
// pass the start index & end index of the word to callback
...
if(...) {
...
callback(startIndex, endIndex);
}
}
// this function handle what if findSubjStrategy() find any word with subj
// entity data
const HandleSubjSpan = (props) => {
// if the word with subj entity data, it font color become red
return <span {...props} style={{ color: 'red' }}>{props.children}</span>;
};
This question already has answers here:
Cloud Firestore collection count
(29 answers)
Closed 10 months ago.
In Firestore, how can I get the total number of documents in a collection?
For instance if I have
/people
/123456
/name - 'John'
/456789
/name - 'Jane'
I want to query how many people I have and get 2.
I could do a query on /people and then get the length of the returned results, but that seems a waste, especially because I will be doing this on larger datasets.
You currently have 3 options:
Option 1: Client side
This is basically the approach you mentioned. Select all from collection and count on the client side. This works well enough for small datasets but obviously doesn't work if the dataset is larger.
Option 2: Write-time best-effort
With this approach, you can use Cloud Functions to update a counter for each addition and deletion from the collection.
This works well for any dataset size, as long as additions/deletions only occur at the rate less than or equal to 1 per second. This gives you a single document to read to give you the almost current count immediately.
If need need to exceed 1 per second, you need to implement distributed counters per our documentation.
Option 3: Write-time exact
Rather than using Cloud Functions, in your client you can update the counter at the same time as you add or delete a document. This means the counter will also be current, but you'll need to make sure to include this logic anywhere you add or delete documents.
Like option 2, you'll need to implement distributed counters if you want to exceed per second
Aggregations are the way to go (firebase functions looks like the recommended way to update these aggregations as client side exposes info to the user you may not want exposed) https://firebase.google.com/docs/firestore/solutions/aggregation
Another way (NOT recommended) which is not good for large lists and involves downloading the whole list: res.size like this example:
db.collection("logs")
.get()
.then((res) => console.log(res.size));
If you use AngulareFire2, you can do (assuming private afs: AngularFirestore is injected in your constructor):
this.afs.collection(myCollection).valueChanges().subscribe( values => console.log(values.length));
Here, values is an array of all items in myCollection. You don't need metadata so you can use valueChanges() method directly.
Be careful counting number of documents for large collections with a cloud function. It is a little bit complex with firestore database if you want to have a precalculated counter for every collection.
Code like this doesn't work in this case:
export const customerCounterListener =
functions.firestore.document('customers/{customerId}')
.onWrite((change, context) => {
// on create
if (!change.before.exists && change.after.exists) {
return firestore
.collection('metadatas')
.doc('customers')
.get()
.then(docSnap =>
docSnap.ref.set({
count: docSnap.data().count + 1
}))
// on delete
} else if (change.before.exists && !change.after.exists) {
return firestore
.collection('metadatas')
.doc('customers')
.get()
.then(docSnap =>
docSnap.ref.set({
count: docSnap.data().count - 1
}))
}
return null;
});
The reason is because every cloud firestore trigger has to be idempotent, as firestore documentation say: https://firebase.google.com/docs/functions/firestore-events#limitations_and_guarantees
Solution
So, in order to prevent multiple executions of your code, you need to manage with events and transactions. This is my particular way to handle large collection counters:
const executeOnce = (change, context, task) => {
const eventRef = firestore.collection('events').doc(context.eventId);
return firestore.runTransaction(t =>
t
.get(eventRef)
.then(docSnap => (docSnap.exists ? null : task(t)))
.then(() => t.set(eventRef, { processed: true }))
);
};
const documentCounter = collectionName => (change, context) =>
executeOnce(change, context, t => {
// on create
if (!change.before.exists && change.after.exists) {
return t
.get(firestore.collection('metadatas')
.doc(collectionName))
.then(docSnap =>
t.set(docSnap.ref, {
count: ((docSnap.data() && docSnap.data().count) || 0) + 1
}));
// on delete
} else if (change.before.exists && !change.after.exists) {
return t
.get(firestore.collection('metadatas')
.doc(collectionName))
.then(docSnap =>
t.set(docSnap.ref, {
count: docSnap.data().count - 1
}));
}
return null;
});
Use cases here:
/**
* Count documents in articles collection.
*/
exports.articlesCounter = functions.firestore
.document('articles/{id}')
.onWrite(documentCounter('articles'));
/**
* Count documents in customers collection.
*/
exports.customersCounter = functions.firestore
.document('customers/{id}')
.onWrite(documentCounter('customers'));
As you can see, the key to prevent multiple execution is the property called eventId in the context object. If the function has been handled many times for the same event, the event id will be the same in all cases. Unfortunately, you must have "events" collection in your database.
Please check below answer I found on another thread. Your count should be atomic. Its required to use FieldValue.increment() function in such case.
https://stackoverflow.com/a/49407570/3337028
firebase-admin offers select(fields) which allows you to only fetch specific fields for documents within your collection. Using select is more performant than fetching all fields. However, it is only available for firebase-admin and firebase-admin is typically only used server side.
select can be used as follows:
select('age', 'name') // fetch the age and name fields
select() // select no fields, which is perfect if you just want a count
select is available for Node.js servers but I am not sure about other languages:
https://googleapis.dev/nodejs/firestore/latest/Query.html#select
https://googleapis.dev/nodejs/firestore/latest/CollectionReference.html#select
Here's a server side cloud function written in Node.js which uses select to count a filtered collection and to get the IDs of all resulting documents. Its written in TS but easily converted to JS.
import admin from 'firebase-admin'
// https://stackoverflow.com/questions/46554091/cloud-firestore-collection-count
// we need to use admin SDK here as select() is only available for admin
export const videoIds = async (req: any): Promise<any> => {
const id: string = req.query.id || null
const group: string = req.query.group || null
let processed: boolean = null
if (req.query.processed === 'true') processed = true
if (req.query.processed === 'false') processed = false
let q: admin.firestore.Query<admin.firestore.DocumentData> = admin.firestore().collection('videos')
if (group != null) q = q.where('group', '==', group)
if (processed != null) q = q.where('flowPlayerProcessed', '==', processed)
// select restricts returned fields such as ... select('id', 'name')
const query: admin.firestore.QuerySnapshot<admin.firestore.DocumentData> = await q.orderBy('timeCreated').select().get()
const ids: string[] = query.docs.map((doc: admin.firestore.QueryDocumentSnapshot<admin.firestore.DocumentData>) => doc.id) // ({ id: doc.id, ...doc.data() })
return {
id,
group,
processed,
idx: id == null ? null : ids.indexOf(id),
count: ids.length,
ids
}
}
The cloud function HTTP request completes within 1 second for a collection of 500 docs where each doc contains a lot of data. Not amazingly performant but much better than not using select. Performance could be improved by introducing client side caching (or even server side caching).
The cloud function entry point looks like this:
exports.videoIds = functions.https.onRequest(async (req, res) => {
const response: any = await videoIds(req)
res.json(response)
})
The HTTP request URL would be:
https://SERVER/videoIds?group=my-group&processed=true
Firebase functions detail where the server is located on deployment.
Following Dan Answer: You can have a separated counter in your database and use Cloud Functions to maintain it. (Write-time best-effort)
// Example of performing an increment when item is added
module.exports.incrementIncomesCounter = collectionRef.onCreate(event => {
const counterRef = event.data.ref.firestore.doc('counters/incomes')
counterRef.get()
.then(documentSnapshot => {
const currentCount = documentSnapshot.exists ? documentSnapshot.data().count : 0
counterRef.set({
count: Number(currentCount) + 1
})
.then(() => {
console.log('counter has increased!')
})
})
})
This code shows you the complete example of how to do it:
https://gist.github.com/saintplay/3f965e0aea933a1129cc2c9a823e74d7
Get a new write batch
WriteBatch batch = db.batch();
Add a New Value to Collection "NYC"
DocumentReference nycRef = db.collection("cities").document();
batch.set(nycRef, new City());
Maintain a Document with Id as Count and initial Value as total=0
During Add Operation perform like below
DocumentReference countRef= db.collection("cities").document("count");
batch.update(countRef, "total", FieldValue.increment(1));
During Delete Operation perform like below
DocumentReference countRef= db.collection("cities").document("count");
batch.update(countRef, "total", FieldValue.increment(-1));
Always get Document count from
DocumentReference nycRef = db.collection("cities").document("count");
I created an NPM package to handle all counters:
First install the module in your functions directory:
npm i adv-firestore-functions
then use it like so:
import { eventExists, colCounter } from 'adv-firestore-functions';
functions.firestore
.document('posts/{docId}')
.onWrite(async (change: any, context: any) => {
// don't run if repeated function
if (await eventExists(context)) {
return null;
}
await colCounter(change, context);
}
It handles events, and everything else.
If you want to make it a universal counter for all functions:
import { eventExists, colCounter } from 'adv-firestore-functions';
functions.firestore
.document('{colId}/{docId}')
.onWrite(async (change: any, context: any) => {
const colId = context.params.colId;
// don't run if repeated function
if (await eventExists(context) || colId.startsWith('_')) {
return null;
}
await colCounter(change, context);
}
And don't forget your rules:
match /_counters/{document} {
allow read;
allow write: if false;
}
And of course access it this way:
const collectionPath = 'path/to/collection';
const colSnap = await db.doc('_counters/' + collectionPath).get();
const count = colSnap.get('count');
Read more: https://code.build/p/9DicAmrnRoK4uk62Hw1bEV/firestore-counters
GitHub: https://github.com/jdgamble555/adv-firestore-functions
Use Transaction to update the count inside the success listener of your database write.
FirebaseFirestore.getInstance().runTransaction(new Transaction.Function<Long>() {
#Nullable
#Override
public Long apply(#NonNull Transaction transaction) throws FirebaseFirestoreException {
DocumentSnapshot snapshot = transaction
.get(pRefs.postRef(forumHelper.getPost_id()));
long newCount;
if (b) {
newCount = snapshot.getLong(kMap.like_count) + 1;
} else {
newCount = snapshot.getLong(kMap.like_count) - 1;
}
transaction.update(pRefs.postRef(forumHelper.getPost_id()),
kMap.like_count, newCount);
return newCount;
}
});
In my react App I have a input element. The search query should be memoized, which means that if the user has previously searched for 'John' and the API has provided me valid results for that query, then next time when the user types 'Joh', there should be suggestion for the user with the previously memoized values(in this case 'John' would be suggested).
I am new to react and am trying caching for the first time.I read a few articles but couldn't implement the desired functionality.
You don't clarify which API you're using nor which stack; the solution would vary somewhat depending on if you are using XHR requests or something over GraphQL.
For an asynchronous XHR request to some backend API, I would do something like the example below.
Query the API for the search term
_queryUserXHR = (searchTxt) => {
jQuery.ajax({
type: "GET",
url: url,
data: searchTxt,
success: (data) => {
this.setState({previousQueries: this.state.previousQueries.concat([searchTxt])
}
});
}
You would run this function whenever you want to do the check against your API. If the API can find the search string you query, then insert that data into a local state array variable (previousQueries in my example).
You can either return the data to be inserted from the database if there are unknowns to your view (e.g database id). Above I just insert the searchTxt which is what we send in to the function based on what the user typed in the input-field. The choice is yours here.
Get suggestions for previously searched terms
I would start by adding an input field that runs a function on the onKeyPress event:
<input type="text" onKeyPress={this._getSuggestions} />
then the function would be something like:
_getSuggestions = (e) => {
let inputValue = e.target.value;
let {previousQueries} = this.state;
let results = [];
previousQueries.forEach((q) => {
if (q.toString().indexOf(inputValue)>-1) {
result.push(a);
}
}
this.setState({suggestions: results});
}
Then you can output this.state.suggestions somewhere and add behavior there. Perhaps some keyboard navigation or something. There are many different ways to implement how the results are displayed and how you would select one.
Note: I haven't tested the code above
I guess you have somewhere a function that queries the server, such as
const queryServer = function(queryString) {
/* access the server */
}
The trick would be to memorize this core function only, so that your UI thinks its actually accessing the server.
In javascript it is very easy to implement your own memorization decorator, but you could use existing ones. For example, lru-memoize looks popular on npm. You use it this way:
const memoize = require('lru-memoize')
const queryServer_memoized = memoize(100)(queryServer)
This code keeps in memory the last 100 request results. Next, in your code, you call queryServer_memoized instead of queryServer.
You can create a memoization function:
const memo = (callback) => {
// We will save the key-value pairs in the following variable. It will be our cache storage
const cache = new Map();
return (...args) => {
// The key will be used to identify the different arguments combination. Same arguments means same key
const key = JSON.stringify(args);
// If the cache storage has the key we are looking for, return the previously stored value
if (cache.has(key)) return cache.get(key);
// If the key is new, call the function (in this case fetch)
const value = callback(...args);
// And save the new key-value pair to the cache
cache.set(key, value);
return value;
};
};
const memoizedFetch = memo(fetch);
This memo function will act like a key-value cache. If the params (in our case the URL) of the function (fetch) are the same, the function will not be executed. Instead, the previous result will be returned.
So you can just use this memoized version memoizedFetch in your useEffect to make sure network request are not repeated for that particular petition.
For example you can do:
// Place this outside your react element
const memoizedFetchJson = memo((...args) => fetch(...args).then(res => res.json()));
useEffect(() => {
memoizedFetchJson(`https://pokeapi.co/api/v2/pokemon/${pokemon}/`)
.then(response => {
setPokemonData(response);
})
.catch(error => {
console.error(error);
});
}, [pokemon]);
Demo integrated in React