I have state set as follow
const [stories, setStories] = useState([]);
I fetch Data from API in array, and i map the array and set the using setStories as:
setStories(prevState => prevState.concat({user: {name: 'XYZ', profile: 'ABC', stories: [{id: 1, image: 'testing'}];
The above codes are working fine, but i am stuck, when i have to concat the latest story if the id did not matched with fetched data. I have tried below solution but it didnot help:
stories.map(story => {
if(story && story.hasOwnProperty(key)){
//where above key is the user key fetched from the another API, i.e., user key
story?.[key].stories.map(storedStory =>
id(storedStory.id !== fetchedStory.id){
story?.[key].stories.concat({story})}
but the above code did not work, as it only mutate the state and is avoiding re-rendering.
Looking for a clean and efficient method to overcome this. THanks
It's hard to tell what you're trying to accomplish without seeing a full example. But I think your main problem is that you're not using the returned value from map, and from the naming it looks like you're appending the wrong element.
It will help to simplify first.
const newState = stories.map(story => {
if (story?.hasOwnProperty(key)) {
const found = story[key].stories.find(s => s.id === fetchedStory.id);
if (found) {
return story;
} else {
// Let's make a new object with the fetchedStory
// appended into THIS user's stories
return {
...story,
[key]: {
...story[key],
stories: [
...story[key].stories,
// This is supposed to be fetchedStory
// not `story` right??
fetchedStory,
]
}
}
}
} else {
return story;
}
});
setStory(newState);
Edit: You're having a hard time expressing your business logic, and the complexity of the data structure is not helping. So keep simplifying, encapsulate the complex syntax into functions then express your business logic plainly. Ie,
const appendStory = (originalObject, userId, storyToAppend) => {
return {
...originalObject,
[userId]: {
...originalObject[userId],
stories: [
...originalObject[userId].stories,
storyToAppend,
]
}
}
};
const userExistsInList = (users, user) => {
return users?.hasOwnProperty(user);
}
const newStoryAlreadyInStories = (stories, newStory) => {
return stories.find(s => s.id === newStory.id);
}
const newState = stories.map(story => {
if (userExistsInList(story, key)) {
const found = newStoryAlreadyInStories(story[key].stories, fetchedStory);
if (found) {
// User is already in state and the new story is already in the list
// Add business logic here
} else {
// User is already in state and the new story
// is not in their list
// Add business logic here
}
} else {
// User is not in the list yet
// Add business logic here
}
});
Related
I'm trying to work around the fact that Datocms doesn't support a where filter in their GraphQL schema. Since there isn't that much data, I figured I could query all of it, and do the find on my end, but ... I'm not succeeding, at least not using "modern" methods.
What I get back when I query all of the data looks like this:
"foo": {
"data": {
"allGiveawayLandingPages": [
{
"lpSection": [
{},
{},
{},
{},
{},
{},
{},
{
"id": "34525949",
"products": [
{
"__typename": "PurchaseCardRecord",
"discountAmount": 50,
"discountAmountPct": null,
"discountEndDate": "2022-11-01T23:00:00+00:00",
"id": "44144096"
},
{
"__typename": "PurchaseCardRecord",
"discountAmount": null,
"discountAmountPct": null,
"discountEndDate": null,
"id": "44144097"
}
]
}
]
}
]
}
}
I need to find the object down in the "products" array by "id". This general question has been asked and answered lots of times, but the only answer I can get to work is from way back in 2013, and it seems to me there aught to be a more modern way to do it.
I'm doing this inside of a try/catch block, which I mention because it seems to be making this hard to debug (I'll come back to this):
export default async function createPaymentIntentHandler(req, res) {
const body = JSON.parse(req.body);
const {
productId,
productType
} = body;
let data;
if ('POST' === req.method) {
try {
switch (productType) {
case 'SeminarRecord':
data = await request({ query: singleSeminarQuery(productId) });
productObjName = 'seminar';
break;
default:
data = await request({ query: singleProductQuery(productId) });
productObjName = 'product';
}
/**
* Here's where I want to do my query / filtering
*/
// ... do more stuff and create Stripe paymentIntent
res.status(200).send({clientSecret: paymentIntent.client_secret})
} catch (error) {
logger.error({error}, 'Create Payment Intent error');
return res.status(400).end(`Create Payment Intent error: ${error.message}`);
}
} else {
res.status(405).end('Method not allowed');
}
}
My first, naive attempt was
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = foo.data.allGiveawayLandingPages.find((page) => {
return page.lpSection.find((section) => {
return section?.products.find((record) => record.id === parentId)
})
});
logger.debug({card}, 'Got card');
In the abstract, aside from the fact that the above is fairly brittle because it relies on the schema not changing, I'd expect some similar sort of ES6 construction to work. This particular one, however, throws, but not in a particularly useful way:
[08:09:18.690] ERROR: Create Payment Intent error
env: "development"
error: {}
That's what I meant by it being hard to debug — I don't know why the error object is empty. But, in any case, that's when I started searching StackOverflow. The first answer which looked promising was this one, which I implemented as
...
const {
productId,
productType,
parentId
} = body;
...
function findCard(parent, id) {
logger.debug({parent}, 'searching in parent')
for (const item of parent) {
if ('PurchaseCardRecord' === item.__typename && item.id === id) return item;
if (item.children?.length) {
const innerResult = findCard(item.children, id);
if (innerResult) return innerResult;
}
}
}
if ('POST' === req.method) {
try {
...
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = findCard(foo, parentId);
logger.debug({card}, 'Got card');
This similarly throws unhelpfully, but my guess is it doesn't work because in the structure, not all children are iterables. Then I found this answer, which uses reduce instead of my original attempt at find, so I took a pass at it:
const card = foo.data.allGiveawayLandingPages.reduce((item) => {
item?.lpSection.reduce((section) => {
section?.products.reduce((record) => {
if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
})
})
})
This is actually the closest I've gotten using ES6 functionality. It doesn't throw an error; however, it's also not returning the matching child object, it's returning the first parent object that contains the match (i.e., it's returning the whole "lpSection" object). Also, it has the same brittleness problem of requiring knowledge of the schema. I'm relatively certain something like this is the right way to go, but I'm just not understanding his original construction:
arr.reduce((a, item) => {
if (a) return a;
if (item.id === id) return item;
I've tried to understand the MDN documentation for Array.reduce, but, I don't know, I must be undercaffeinated or something. The syntax is described as
reduce((previousValue, currentValue) => { /* … */ } )
and then several variations on the theme. I thought it would return all the way up the stack in my construction, but it doesn't. I also tried
const card = foo.data.allGiveawayLandingPages.reduce((accumulator, item) => {
return item?.lpSection.reduce((section) => {
return section?.products.reduce((record) => {
if ('PurchaseCardRecord' === record.__typename && record.id === parentId) return record;
})
})
})
but the result was the same. Finally, not understanding what I'm doing, I went back to an older answer that doesn't use the ES6 methods but relies on recursing the object.
...
function filterCards(object) {
if (object.hasOwnProperty('__typename') && object.hasOwnProperty('id') && ('PurchaseCardRecord' === object.__typename && parentId === object.id)) return object;
for (let i=0; i<Object.keys(object).length; i++) {
if (typeof object[Object.keys(object)[i]] == 'object') {
const o = filterCards(object[Object.keys(object)[i]]);
if (o != null) return o;
}
}
return null;
}
if ('POST' === req.method) {
try {
...
const foo = await request({ query: ALL_PURCHASE_CARDS_QUERY });
const card = filterCards(foo);
logger.debug({card}, 'Got card');
This actually works, but ISTM there should be a more elegant way to solve the problem with modern Javascript. I'm thinking it's some combination of .find, .some, and .reduce. Or maybe just for ... in.
I'll keep poking at this, but if anyone has an elegant/modern answer, I'd appreciate it!
I have 2 states product and variations I call an API and set the values of both state to the API response.
I want the product state to stay as it is and not update
const [product, setProduct] = useState({} as any);
const [variations, setVariations] = useState([] as any);
useEffect(() => {
const getProduct = async () => {
const data = await axios.get("/products?id=4533843820679");
console.log(data);
setProduct(data.data);
// #ts-ignore
setVariations([data.data]);
};
getProduct();
}, []);
In return I map the variations array and return inputs for title, and price and a button to add variations. Adding variations will add another product to variations array. So it just pushes product to variations.
Then I have inputs for title in variation and prices in variation.variants. The problem is with onChange.
When I change the price of one element in variants it changes for all and also changes it for PRODUCT state.
The code can be found here: https://codesandbox.io/s/smoosh-firefly-6n747?file=/src/App.js
Add variations, change prices add another variations and you'll see all issues I'm facing.
It is because of this:
variant.price = e.target.value; // same issue with title
the variant object reference is shared among variations and you are modifying it directly. It is shared because you you made a shallow copy of a variation using ... when adding it.
Here is the solution:
You should update the specific variant object in immutable way (in react you should always update state in immutable way). For that you need to use this as onChange for price:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
variants: x.variants.map((y) => {
if (y.id === variant.id) {
return {
...y,
price: e.target.value
};
}
return y;
})
};
}
return x;
});
setVariations(updated);
}
}
This for onChange for title:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
title: e.target.value
};
}
return x;
});
setVariations(updated);
}
}
NOTE but ids of variations must be different. For testing purposes you can use this as click handler when adding a new variation:
onClick = {
() => {
setVariations((prev) => [...prev, {
...product,
id: Math.floor(Math.random() * 1000) // for testing
}]);
}
}
First, you are not pushing the product to variations. You are overwriting it.
To push a value to array with useState,
setVariations([...variations, product])
But, if you change the product object, variations also gonna be change because it's the same object. (Maybe, react not gonna re-render it but trust me, it is changed.) If you want to keep it same you need to create new object.
So,
setProduct(data.data);
setVariations([...variations, {...data.data}]);
Now, you can change product. variations not gonna change.
This was because you did a shallow copy of an object.
Try to do like this:
setVariations([...variations, data.data,]);
I'm testing a function to see if, when called, it will return the proper created list.
To start, I create the elements, using the createDesign.execute() functions. It's tested on another file and working.
Then, I call the function I want to test: listAllDesigns.execute() and store it's value in a variable.
If I console.log(list), it returns the full list properly.
In pseudocode, what I'd like to do is: Expect list array to have an element with the design object and, within it, a design_id that equals "payload3".
How should I write this test?
Is there a better way to do this? (other than checking if list !== empty, please)
it('should return a list of all designs', async () => {
// Create fake payloads
const payload1 = {
...defaultPayload,
...{ design: { ...defaultPayload.design, design_id: 'payload1' } },
};
const payload2 = {
...defaultPayload,
...{ design: { ...defaultPayload.design, design_id: 'payload2' } },
};
const payload3 = {
...defaultPayload,
...{ design: { ...defaultPayload.design, design_id: 'payload3' } },
};
await createDesign.execute(payload1);
await createDesign.execute(payload2);
await createDesign.execute(payload3);
const list = await listAllDesigns.execute();
// expect(list). ????
});
The easiest method would be a combination of expect.arrayContaining and expect.objectContaining like so:
expect(list).toEqual(
expect.arrayContaining([
expect.objectContaining({
design: expect.objectContaining({
design_id: "payload3"
})
})
])
);
I'm new to react and as well to the terms of functional, imperative, declarative. And I get to know that pure function is easy to test. I am self taught to program with Javascript. So far, it is working but my goal is to learn to write clean and maintainable code.
my question is the method addProductToSaleList below is bad and untestable because it is imperative? and how can I do it differently.
class SaleComponent extends React.Component {
addProductToSaleList = (values, dispatch, props) => {
//filter product from productList
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
if (productFound) {
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.filter(detail => {
if (productFound.name === detail.product) {
return detail
}
return undefined
})[0]
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
} else {
alert('The product code you add is not exist in product list');
}
}
render() {
// Render saleList
}
}
I belive this question should go to Code Review, but I will give it a shot. Part of the code can be improved
const productFound = props.productList.filter(product => {
if (values.productCode === product.code.toString()) {
return product
}
return undefined
})[0]
First, filter function receives a callback and for each item that callback will be executed. If the callback returns a value interpreted as true, it will return the item in the new array the function will build. Otherwise, it will skip that item. Assuming you're trying to find one item in the code, you could use the function find which will return you that element directly (no need for [0]), or undefined if that item is not found. So your code could be rewrite to
const productFound = props.productList.find(product => values.productCode === product.code.toString());
Note: No IE support.
Then, if the value was not found, you could just alert and do an early return. (You might also want to handle errors differently, with a better format than plain alert).
The code would look like
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// rest of the function
in order to find details, you can use find method as well
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
and then just call the rest of the code
// if it is exist just increment the qty
if (detailFound) {
const { sub_total, ...rest } = detailFound
props.dispatcher('UPDATE_SALEDETAIL_ASYNC', {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
})
// if it is not exist add new one
} else {
props.dispatcher('ADD_SALEDETAIL_ASYNC', {
product: productFound.id,
price: productFound.price,
qty: 1
})
}
Another improvement:
You're receiving a dispatch function as a parameter, but you're not using it. So you could remove it from function's declaration
(values, props) => { ... }
And you could split the last part into two different functions, something like
const getAction = details => `${detailFound ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = detailFound;
return {
...rest,
qty: parseInt(detailFound.qty, 10) + 1
};
}
and then just call
props.dispatcher(getAction(details), getObject(details, productFound));
The end result would look like
addProductToSaleList = (values, props) => {
//filter product from productList
const productFound = props.productList.find(product => values.productCode === product.code.toString());
if (!productFound) {
alert('The product code you add is not exist in product list');
return;
}
// filter sale list to check if there is already product in the list.
const detailFound = props.saleItem.details.find(detail => productFound.name === detail.product);
const getAction = details => `${details ? 'UPDATE' : 'ADD'}_SALEDETAIL_ASYNC`;
const getObject = (details, productFound) => {
if (!details) {
return {
product: productFound.id,
price: productFound.price,
qty: 1
};
}
const { sub_total, ...rest } = details;
return {
...rest,
qty: parseInt(details.qty, 10) + 1
};
}
props.dispatcher(getAction(details), getObject(details, productFound));
}
my question is the method addProductToSaleList below is bad and
untestable because it is imperative
Well your code is testable, there are no external dependencies. So you could pass mocked values and props and add unit tests to that. That means, passing a fake values and props (they are just plain js object) and make assertions over that.
For instance:
You could mock dispatcher function and given the fake values in productList and saleItem.details you could see if dispatcher is called with the proper values. You should test different combinations of that
Mock alert function (Again, I would use another UI approach) and verify it is called, and that no other code is called (asserting that your fake dispatcher is not called). Something like this:
let actionToAssert;
let objectToAssert;
let values = { productCode: 'somecode' };
let props = {
productList: // your item listm with id and price, name, etc,
saleItem: {
details: // your details array here
}
dispatcher: (action, newObject) => {
actionToAssert = action;
objectToAssert = newObject;
}
}
addProductToSaleList(values, props); // make here assertions over actionToAssert and objectToAssert
Suppose I have a redux store with this state structure:
{
items: {
"id1" : {
foo: "foo1",
bar: "bar1"
},
"id2": {
foo: "foo2",
bar: "bar2"
}
}
}
This store evolves by receiving full new values of items:
const reduceItems = function(items = {}, action) {
if (action.type === 'RECEIVE_ITEM') {
return {
...items,
[action.payload.id]: action.payload,
};
}
return items;
};
I want to display a Root view that renders a list of SubItem views, that only extract a part of the state.
For example the SubItem view only cares about the foos, and should get it:
function SubItem({ id, foo }) {
return <div key={id}>{foo}</div>
}
Since I only care about "subpart" of the states, that's what I want to pass to a "dumb" Root view:
const Root = function({ subitems }) {
// subitems[0] => { id: 'id1', foo: "foo1" }
// subitems[1] => { id; 'id2', foo : "foo2" }
const children = subitems.map(SubItem);
return <div>{children}</div>;
};
I can easily connect this component to subscribe to changes in the state:
function mapStatesToProps(state) {
return {
subitems: xxxSelectSubItems(state)
}
}
return connect(mapStatesToProps)(Root)
My fundamental problem is what happens when the part of the state that I don't care about (bar) changes.
Or even, when I receive a new value of an item, where neither foo nor bar has changed:
setInterval(() => {
store.dispatch({
type: 'RECEIVE_ITEM',
payload: {
id: 'id1',
foo: 'foo1',
bar: 'bar1',
},
});
}, 1000);
If I use the "naive" selector implementation:
// naive version
function toSubItem(id, item) {
const foo = item.foo;
return { id, foo };
}
function dumbSelectSubItems(state) {
const ids = Object.keys(state.items);
return ids.map(id => {
const item = state.items[id];
return toSubItem(id, item);
});
}
Then the list is a completely new object at every called, and my component gets rendered everytime, for nothing.
Of course, if I use a 'constant' selector, that always return the same list, since the connected component is pure, it is re-renderered (but that's just to illustrate connected components are pure):
// fully pure implementation
const SUBITEMS = [
{
id: 'id0',
foo: 'foo0',
},
];
function constSelectSubItems(state) {
return SUBITEMS;
}
Now this gets a bit tricky if I use an "almostConst" version where the List changes, but contains the same element.
const SUBITEM = {
id: 'id0',
foo: 'foo0',
};
function almostConstSelectSubItems(state) {
return [SUBITEM];
}
Now, predictably, since the list is different, even though the item inside is the same, the component gets rerendered every second.
This is where I though 'reselect' could help, but I'm wondering if I am not missing the point entirely. I can get reselect to behave using this:
const reselectSelectIds = (state, props) => Object.keys(state.items);
const reselectSelectItems = (state, props) => state.items;
const reselectSelectSubItems = createSelector([reSelectIds, reSelectItems], (ids, items) => {
return ids.map(id => toSubItem(id, items));
});
But then it behaves exactly like the naive version.
So:
is it pointless to try to memoize an array ?
can reselect handle this ?
should I change the organisation of the state ?
should I just implement shouldComponentUpdate on the Root, using a "deepEqual" test ?
should I give up on Root being a connected component, and make each LeafItems be connected components themselves ?
could immutable.js help ?
is it actually not an issue, because React is smart and will not repaint anything once the virtual-dom is computed ?
It's possible what I'm trying to do his meaningless, and hides an issue in my redux store, so feel free to state obvious errors.
You're definitely right about the new array references causing re-renders, and sort of on the right track with your selectors, but you do need to change your approach some.
Rather than having a selector that immediately returns Object.keys(state.item), you need to deal with the object itself:
const selectItems = state => state.items;
const selectSubItems = createSelector(
selectItems,
(items) => {
const ids = Object.keys(items);
return ids.map(id => toSubItem(id, items));
}
);
That way, the array will only get recalculated when the state.items object is replaced.
Beyond that, yes, you may also want to look at connecting your individual list item components so that each one looks up its own data by ID. See my blog post Practical Redux, Part 6: Connected Lists, Forms, and Performance for examples. I also have a bunch of related articles in the Redux Techniques#Selectors and Normalization and Performance#Redux Performance sections of my React/Redux links list.