Add and remove functions in javascript array (React-Hooks) - javascript

I have a filtering system where I use an array to add or remove the filtering methods. But my state is not working properly or i've missed something.
const [filters, setFilters] = useState([]);
const [creatorFilter, setCreatorFilter] = useState(null);
const handleCreatorFilter =(filter) => {
setCreatorFilter(filter);
if (filter === 'structures' && filters.indexOf(byStructureFilter) === -1) {
setFilters([...filters, byStructureFilter]);
}
if (filter === 'members' && filters.indexOf(byMemberFilter) === -1) {
setFilters([...filters, byMemberFilter]);
}
if (filter === 'all') {
setFilters(filters.filter(el => el !== byStructureFilter || el !== byMemberFilter));
}
};
const byStructureFilter = (item) => {
return item.relationships.structure.data
};
const byMemberFilter = (item) => {
return item.relationships.user.data && !item.relationships.structure.data
};
Here the two buttons calling that handleCreatorFilter for filtering
<button onClick={() => creatorFilter === 'structures' ? handleCreatorFilter('all') : handleCreatorFilter('structures')}>Structures officielles</button>
<button onClick={() => creatorFilter === 'members' ? handleCreatorFilter('all') : handleCreatorFilter('members')} >Membres Wekomkom</button>
The things is for adding filtering methods to the array it works fine but not when the filter is set to all (removing the filtering methods). Do you see something wrong in my logic ?

Related

Replacing conditional chaining with an older method

I have some code that is using conditional chaining such as
productQuantity = purchases.filter((pObj) => pObj.elements[0]?.prodType?.id === purchase.elements[0]?.prodType?.id && pObj.sold === purchase.sold).length
It works fine but I need to convert the chained conditionals to an older method and I'm not sure how. Can anyone advise ?
Reason being that PM2 does not support chaining conditional operators.
Let's try this. Replacing all key?.key2 with key && key.key2
productQuantity = purchases.filter((pObj) => {
return pObj.elements[0] && pObj.elements[0].prodType && purchase.elements[0] && purchase.elements[0].prodType && pObj.elements[0].prodType.id === purchase.elements[0].prodType.id && pObj.sold === purchase.sold
}).length
I would probably do something like this.
First, we make the observation that the we are selecting an id from two similar objects: we can therefore refactor the logic required to select the id into a common function:
function selectId(item) {
if (item) {
const elements = item.elements;
if (elements) {
const element = elements[0];
if (element) {
const prodType = element.prodType;
if (prodType) {
const id = prodType.id;
return id;
}
}
}
}
}
You could also flatten the selection (which may or may not be more readable/maintainable):
function selectId(item) {
if (!item) return undefined;
const elements = item.elements;
if (!elements) return undefined;
const element = elements[0];
if (!element) return undefined;
const prodType = element.prodType;
if (!element) return undefined;
const id = prodType.id;
return id;
}
Once you have that, then your filter comes down to this:
productQuantity = purchases.filter( isMatch ).length;
function isMatch(obj) {
let itemId = selectId(obj);
let purchaseId = selectId(purchase);
const shouldKeep = itemId == purchaseId
&& obj.sold === purchase.sold;
return shouldKeep
}

How to pass correct state value into callback function inside useEffect hook?

I'm trying to change a state of a variable that holds photo ID when a user presses arrow keys or clicks on an image, then I render my image depending on that photo ID.
CODE:
const Lightbox = ({ filteredPhotos }) => {
const [currentPhotoId, setCurrentPhotoId] = useState(null);
const currentPhoto = filteredPhotos.filter((photo) => photo.strapiId === currentPhotoId)[0];
let lightbox = "";
const getLastPhotoId = (filteredPhotos) => {
const ids = filteredPhotos.map((item) => item.strapiId).sort((a, b) => a - b);
const result = ids.slice(-1)[0];
return result;
};
//Select image on click
const selectImage = useCallback(
(e) => {
const divId = parseInt(e.target.parentElement.parentElement.className.split(" ")[0]);
const imgSelected = e.target.parentElement.parentElement.className.includes("gatsby-image-wrapper");
imgSelected && divId !== NaN ? setCurrentPhotoId(divId) : setCurrentPhotoId(null);
},
[setCurrentPhotoId]
);
//Change image on keypress
const changeImage = useCallback(
(e, currentPhotoId) => {
if (document.location.pathname !== "/portfolio" || currentPhotoId === null) return;
const key = e.keyCode;
console.log("changeImage start: ", currentPhotoId);
if (key === 27) {
setCurrentPhotoId(null);
} else if (key === 39) {
setCurrentPhotoId(currentPhotoId + 1);
} else if (key === 37) {
if (currentPhotoId - 1 <= 0) {
setCurrentPhotoId(getLastPhotoId(filteredPhotos))
} else {
setCurrentPhotoId(currentPhotoId - 1)
}
}
},
[setCurrentPhotoId]
);
useEffect(() => {
const gallery = document.getElementById("portfolio-gallery");
gallery.addEventListener("click", (e) => selectImage(e));
document.addEventListener("keydown", (e) => changeImage(e, currentPhotoId));
}, [selectImage, changeImage]);
useEffect(() => {
console.log(currentPhotoId);
}, [currentPhotoId]);
return currentPhotoId === null ? (
<div id="lightbox" className="lightbox">
<h4>Nothing to display</h4>
</div>
) : (
<div id="lightbox" className="lightbox lightbox-active">
<img src={currentPhoto.photo.childImageSharp.fluid.src} alt={currentPhoto.categories[0].name} />
</div>
);
};
export default Lightbox;
Setting up a state by clicking/un-clicking on an image works without a problem, the state is set to a correct number.
But my function that handles keydown events is returned because my state currentPhotoId is null, and I don't get it why when I've set my state by selecting an image.
If I add currentPhotoId in useEffect dependency array
useEffect(() => {
const gallery = document.getElementById("portfolio-gallery");
gallery.addEventListener("click", (e) => selectImage(e));
document.addEventListener("keydown", (e) => changeImage(e, currentPhotoId));
}, [selectImage, changeImage, currentPhotoId]); //here
it breaks my selectImage (click) function. And also the more times user presses right arrow key, the more times it updates the state, resulting into so many updates it crashes down site eventually.
What am I doing wrong? Why my state isn't updating correctly?
FIX:
useEffect(() => {
document.addEventListener("keydown", changeImage); //invoking not calling
return () => {
document.removeEventListener("keydown", changeImage);
};
}, [changeImage, currentPhotoId]);
const changeImage = useCallback(
(e) => {
if (document.location.pathname !== "/portfolio" || currentPhotoId === null) return;
const key = e.keyCode;
if (key === 27) {
setCurrentPhotoId(null);
} else if (key === 39) {
setCurrentPhotoId(currentPhotoId + 1);
} else if (key === 37) {
if (currentPhotoId - 1 <= 0) {
setCurrentPhotoId(getLastPhotoId(filteredPhotos));
} else {
setCurrentPhotoId(currentPhotoId - 1);
}
}
},
[setCurrentPhotoId, currentPhotoId] //added currentPhotoId as dependency
);
So in useEffect I was making a mistake with calling my function, instead of invoking it and this was just adding event listeners to infinite.
And in my callback function instead of passing the state as an argument, I've added id as a dependency.
Also, I've separated selectImage and changeImage into two useEffects, for selectImage useEffect I don't have currentPhotoId as a dependency.
If somebody wants to elaborate more on the details, feel free to do so.

Filtering an array: how to order the filters

There are 2 components:
a filter component that sets the state
an output component that renders items based on the filters
There are 2 arrays:
an array of all items
an array of the selected filtered options.
let itemsFiltered;
if (this.state.continent !== "") { itemsFiltered = items.filter( (item) => item.continent == this.state.continent ); }
if (this.state.country !== "") { itemsFiltered = items.filter( (item) => item.country == this.state.country ); }
if (this.state.region !== "") { itemsFiltered = items.filter((item) => item.region == this.state.region); }
if (this.state.activity !== "") { itemsFiltered = items.filter((item) => item.activity == this.state.activity); }
if (this.state.skill !== "") { itemsFiltered = items.filter((item) => item.skill == this.state.skill); }
PROBLEM: it does not work for skill and region. for skill it works if it is selected first, but not if other selections have already been made. For regions it simply doesn't apply the filters at all. Hence it simply shows all items independent of the filter set. It works for all the other filters and combinations of them.
this is what the data looks like (dummy):
{
title: "Item 1",
description: "Description of the item",
image: imageItem1,
continent: "europe",
country: "portugal",
region: "norte",
activity: "kite",
skill: "proLocal",
},
How would you write this to make it work? Should the filters be ordered differently or is there another approach I'm missing?
Extension (code to comments below): Trying to iterate thru the filters fails because you cannot iterate thru the state object:
let itemsFiltered = items.slice();
const filtersSet = ["continent", "country", "region", "activity", "skill"]
for (let i = 0; i < filtersSet.length; i++) {
if (this.state.filtersSet[i] !== "") {itemsFiltered = itemsFiltered.filter( item => item.filtersSet[i] == this.state.filtersSet[i] );}
}
Issue
Doesn't seem like filtering by more than any single filter should work as each filter operation completely overwrites any previous filtering results.
let itemsFiltered;
if (this.state.continent !== "") {
itemsFiltered = items.filter(
(item) => item.continent == this.state.continent
);
}
if (this.state.country !== "") {
itemsFiltered = items.filter((item) => item.country == this.state.country);
}
if (this.state.region !== "") {
itemsFiltered = items.filter((item) => item.region == this.state.region);
}
if (this.state.activity !== "") {
itemsFiltered = items.filter((item) => item.activity == this.state.activity);
}
if (this.state.skill !== "") {
itemsFiltered = items.filter((item) => item.skill == this.state.skill);
}
Solution
You can filter each subsequent filter from the result of the previous filter operation.
let itemsFiltered = items.slice();
if (this.state.continent !== "") {
itemsFiltered = itemsFiltered.filter(item => item.continent === this.state.continent);
}
if (this.state.country !== "") {
itemsFiltered = itemsFiltered.filter(item => item.country === this.state.country);
}
if (this.state.region !== "") {
itemsFiltered = itemsFiltered.filter(item => item.region === this.state.region);
}
if (this.state.activity !== "") {
itemsFiltered = itemsFiltered.filter(item => item.activity === this.state.activity);
}
if (this.state.skill !== "") {
itemsFiltered = itemsFiltered.filter(item => item.skill == this.state.skill);
}
or to save a lot of array iterations do it all in a single filter function.
const itemsFiltered = items.filter((item) => {
if (this.state.continent) return item.continent === this.state.continent;
if (this.state.country) return item.country === this.state.country;
if (this.state.region) return item.region === this.state.region;
if (this.state.activity) return item.activity === this.state.activity;
if (this.state.skill) return item.skill === this.state.skill;
return true;
});

Is it possible in JEST to test a specific part of a method to check on a specific result

I have a class with a method for which I'm building a test with JEST.
The below snippet shows what I want to test.
I would like to test for example c5 and I would like to check there what will happen if the length === 0 or length === 2 as the specific result should happen only with length === 1
I already built tests for other cases as follow
test('C1 - is default, same country, active', async () => {
try {
const service = serviceWithDAO({ sites: Data.mock});
const defaultSite = await service.getDefaultSite({ studyId: 'DEFAULT1', locale: 'us_ES'});
console.log('C1', defaultSite);
expect(defaultSite.default).toBeTruthy();
expect(defaultSite.countryCode).toEqual('es');
expect(defaultSite.status).toEqual('ACTIVE');
expect(defaultSite.siteId).toEqual('site 1');
} catch (e) {
console.error(e.message);
}
});
The above test passes as expected but I have doubts about how can I test what result to expect in case 5 as described that case should have length === 1 and I want to test a fail case.
below is the method and I'm interested in how should I do a test for C5 case
class SiteService extends ServiceBase {
getDefaultSite({ studyId, locale }) {
return this.withDAO(async ({ sites }) => {
const sitesStudy = await sites.select({ studyId });
if (!sitesStudy || !sitesStudy.length) return undefined;
const countryCode = locale.substr(3, 4).toLowerCase();
const c1 = sitesStudy.filter(
site =>
site.default &&
site.countryCode === countryCode &&
site.status === 'ACTIVE'
);
console.log('C1', c1);
if (c1[0]) return c1[0];
const c2 = sitesStudy.filter(
site =>
site.default &&
site.countryCode === countryCode &&
site.status === 'INACTIVE'
);
console.log('C2', c2);
if (c2[0]) return c2[0];
console.log(
sitesStudy.filter(
site =>
site.default &&
site.countryCode !== countryCode &&
site.status === 'ACTIVE'
),
countryCode
);
const c3 = sitesStudy.filter(
site =>
site.default &&
site.countryCode !== countryCode &&
site.status === 'ACTIVE'
);
console.log('C3', c3);
if (c3[0]) return c3[0];
const c4 = sitesStudy.filter(
site =>
site.default &&
site.countryCode !== countryCode &&
site.status === 'INACTIVE'
);
console.log('C4', c4);
if (c4[0]) return c4[0];
const c5 = sitesStudy.filter(site => site.status === 'ACTIVE');
console.log('C5', c5);
if (c5.length === 1) return c5[0];
return null;
});
}
}
Service base
function ServiceBase(ctx) {
let key;
let value;
for ([key, value] of Object.entries(ctx)) {
this[key] = value;
}
}
ServiceBase.prototype.constructor = ServiceBase;
module.exports = ServiceBase;

How to setState only when "map" loop is finished?

I send some http requests and after the Promises have solved I need to loop through the array and to do some async things. I want to leave the function asynchronous but I need to execute setState only after "map" loop is finished:
fetchData = () => {
let data = [];
let tickets = http.get('/api/zendesk/tickets'),
users = http.get('/api/zendesk/users'),
items = http.get('/api/orders/items'),
reqTypes = http.get("/api/support/requests");
Promise.all([users,tickets,items,reqTypes])
.then(values => {
console.log(values);
let usersArr = values[0].data.users,
ticketsArr = values[1].data.tickets,
itemsArr = values[2].data,
requestArr = values[3].data;
data = ticketsArr.map((ticket,key) => {
let id = ticket.custom_fields.find(field => field.id === productions_fields.order_id || field.id === develop_fields.order_id).value;
id === null ? ticket.Items = [] : ticket.Items = itemsArr.filter(item => item.order_id === parseInt(id));
ticket.Requests = requestArr.filter(request => request.id === ticket.id);
})
this.setState({tickets:data})
}).catch(err => console.log(err))
}
Is it possible to do or I need to make sync loop like "for"?
I get an array of "undefined objects", Why?
Because you are not returning anything inside map body, and by default it returns undefined.
Check this snippet:
let a = [1,2,3,4].map(el => {
console.log('el = ', el);
})
console.log('a = ', a); //array of undefined values
If you wants to modify the existing array, then use forEach instead of map.
Like this:
ticketsArr.forEach((ticket,key) => {
let id = ticket.custom_fields.find(field => field.id === productions_fields.order_id || field.id === develop_fields.order_id).value;
ticket.Items = (id === null) ? [] : itemsArr.filter(item => item.order_id === parseInt(id));
ticket.Requests = requestArr.filter(request => request.id === ticket.id);
})
this.setState({ tickets: ticketsArr })
Array.prototype.map expects you to return how each element should be in its final form. Right now you are not returning anything, hence every element becoming undefined.
data = ticketsArr.map((ticket,key) => {
let id = ticket.custom_fields.find(field => field.id === productions_fields.order_id || field.id === develop_fields.order_id).value;
id === null ? ticket.Items = [] : ticket.Items = itemsArr.filter(item => item.order_id === parseInt(id));
ticket.Requests = requestArr.filter(request => request.id === ticket.id);
return ticket // <- this
})

Categories