Dynamic rendering React components using data from multiple JSON objects and arrays - javascript

I am using React Native to dynamically render components. Currently the data is being taken by mapping over an array of objects which looks similar to:
const OBJECTS = [
{name: "Object 1", logo: require('../logos/object1.png')},
{name: "Object 2", logo: require('../logos/object2.png')},
{name: "Object 3", logo: require('../logos/object2.png')},
]
I then map over these objects to render a card for each object like so:
<View>
{object.map(item => {
return (
<View key={item.key}>
<Card>
<Text>{item.name}</Text>
<Image source={item.logo}/>
</Card>
</View>
);
})}
</View>
However now I would like to add additional items to each card which are taking information fetched from an API and retrieved in JSON form. These JSON objects look similar to the following:
Request 1:
fetchPriceActionCreator {
"items": [{
"objectId": "object1",
"price": 10
},
{
"objectId": "object2",
"price": 5
},
{
"objectId": "object3",
"price": 20
}
]
}
Request 2:
fetchBalanceActionCreator {
"items": [{
"objectId": "object1",
"balance": 2,
"account": 30022
},
{
"objectId": "object2",
"balance": 4,
"account": 40035
},
{
"objectId": "object3",
"balance": 6,
"account": 50021
}
]
}
As the application is growing in complexity, you may notice from the action creators used to make the requests that I have begun to use Redux. My challenge is now figuring out a way to associate the retrieved JSON information, with the initial OBJECTS object array so that I can include this information in the relevant dynamically rendered card displaying the name, logo, balance, and price of each object.
From what I understand, I need to take the static information from the OBJECTS object array, and move this to initialState in either the reducer or store - however I am completely lost as to how to go about doing this, and how to then associate the two retrieved JSON object arrays, with the OBJECT object array.
The end result should look something like this:
<View>
{object.map(item => {
return (
<View key={item.key}>
<Card>
<Text>{item.name}</Text>
<Image source={item.logo}/>
<Text>{item.price}</Text>
<Text>{item.balance}</Text>
</Card>
</View>
);
})}
</View>
Any help would be greatly appreciated.
EDIT:
Currently my redux reducers look like so:
function priceReducer(priceState = {}, action) {
switch (action.type) {
case REQUEST_PRICE:
return Object.assign({}, priceState, {
isFetching: true,
didInvalidate: false,
});
case RECEIVE_PRICE:
return Object.assign({}, priceState, {
isFetching: false,
didInvalidate: false,
lastUpdated: action.lastUpdated,
items: action.items,
});
default:
return Object.assign({}, priceState);
}
}

in your redux store you can update the state by through merging the initial state and the data coming from the api, like so
const priceState = {
// some other props
OBJECTS: [
{ name: "Object 1", logo: require('../logos/object1.png') },
{ name: "Object 2", logo: require('../logos/object2.png') },
{ name: "Object 3", logo: require('../logos/object2.png') },
] // your initial Data as you posted.
}
function priceReducer(priceState = {}, action) {
switch (action.type)
{
case REQUEST_PRICE: {
return {
...priceState,
didInvalidate: false,
isFetching: true,
OBJECTS: priceState.OBJECTS.map((obj) => {
const i = action.items.find((item) => item.objectId == obj.name) || {}; // using the conditional operator to avoid runtime error if no item was found for the given object
// Example: current object name is "object1", i will the item inside the payload that will have the its objectId equal to "object1"
return {
...obj,
...i /// i will be the item inside the payload that has the same object id as the current object name
}
})
}
}
case RECEIVE_PRICE: {
return {
...priceState,
didInvalidate: false,
lastUpdated: action.lastUpdated,
OBJECTS: priceState.OBJECTS.map((obj) => {
const i = action.items.find((item) => item.objectId == obj.name) || {}; // using the conditional operator to avoid runtime error if no item was found for the given object
// Example: current object name is "object1", i will the item inside the payload that will have the its objectId equal to "object1"
return {
...obj,
...i /// i will be the item inside the payload that has the same object id as the current object name
}
})
}
}
default: return priceState;
}
}
if you are not sure what the ... is its called the spread operator., which is equivalent to Object.assign that you were using

Related

How do I normalize/denormalize a tree?

I have a json tree structure that I want to normalize into something like a hashmap and then denormalize it back to a tree if needed.
I have a very dynamic tree that I want to use as state in my react-redux project, but for that I somehow need to transform the data so that I can access it without having to search elements recursively in the tree each time I want to update/access the state.
const actual = {
id: "1",
type: 'Container',
data: {},
items: [
{
id: "2",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
},
{
id: "3",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
}
]
};
Now I want to transform it into something like:
const expected = {
1: {
id: "1",
type: 'Container',
data: {},
items: [1, 2]
},
2: {
id: "2",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
},
3: {
id: "3",
type: "Subcontainer",
data: {
title: "A custom title",
text: "A random Headline"
},
items: []
}
};
I found a JS lib called Normalizr, but I absolutely don't get how to create the schemas for it to work.
That was my last try, and it returns only the inner two items and also directly the data object inside without id, items around:
const data = new schema.Entity("data");
const item = new schema.Object({ data });
item.define({ items: new schema.Array(item) });
const items = new schema.Array(item);
const normalizedData = normalize(mock, items);
I'm not going to worry too much about the types, since you can alter those to meet your needs. Going off you're example, I will define
interface Tree {
id: string;
type: string;
data: {
title?: string;
text?: string;
items: Tree[];
}
}
interface NormalizedTree {
[k: string]: {
id: string;
type: string;
data: {
title?: string;
text?: string;
items: string[]
}
}
}
and we want to implement function normalize(tree: Tree): NormalizedTree and function denormalize(norm: NormalizedTree): Tree.
The normalize() function is fairly straightforward since you can recursively walk the tree and collect the normalized trees into one big normalized tree:
function normalize(tree: Tree): NormalizedTree {
return Object.assign({
[tree.id]: {
...tree,
data: {
...tree.data,
items: tree.data.items.map(v => v.id)
}
},
}, ...tree.data.items.map(normalize));
}
In English, we are making a single normalized tree with a property with key tree.id and a value that's the same as tree except the data.items property is mapped to just the ids. And then we are mapping each element of data.items with normalize to get a list of normalized trees that we spread into that normalized tree via the Object.assign() method. Let's make sure it works:
const normalizedMock = normalize(mock);
console.log(normalizedMock);
/* {
"1": {
"id": "1",
"type": "Container",
"data": {
"items": [
"2",
"3"
]
}
},
"2": {
"id": "2",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
},
"3": {
"id": "3",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
}
} */
Looks good.
The denormalize() function is a little trickier, because we need to trust that the normalized tree is valid and actually represents a tree with a single root and no cycles. And we need to find and return that root. Here's one approach:
function denormalize(norm: NormalizedTree): Tree {
// make Trees with no children
const treeHash: Record<string, Tree> =
Object.fromEntries(Object.entries(norm).
map(([k, v]) => [k, { ...v, data: { ...v.data, items: [] } }])
);
// keep track of trees with no parents
const parentlessTrees =
Object.fromEntries(Object.entries(norm).map(([k, v]) => [k, true]));
Object.values(norm).forEach(v => {
// hook up children
treeHash[v.id].data.items = v.data.items.map(k => treeHash[k]);
// trees that are children do have parents, remove from parentlessTrees
v.data.items.forEach(k => delete parentlessTrees[k]);
})
const parentlessTreeIds = Object.keys(parentlessTrees);
if (parentlessTreeIds.length !== 1)
throw new Error("uh oh, there are " +
parentlessTreeIds.length +
" parentless trees, but there should be exactly 1");
return treeHash[parentlessTreeIds[0]];
}
In English... first we copy the normalized tree into a new treeHash object where all the data.items are empty. This will eventually hold our denormalized trees, but right now there are no children.
Then, in order to help us find the root, we make a set of all the ids of the trees, from which we will remove any ids corresponding to trees with parents. When we're all done, there should hopefully be a single id left, that of the root.
Then we start populating the children of treeHash's properties, by mapping the corresponding data.items array from the normalized tree to an array of properties of treeHash. And we remove all of these child ids from parentlessTreeIds.
Finally, we should have exactly one property in parentlessTreeIds. If not, we have some kind of forest, or cycle, and we throw an error. But assuming we do have a single parentless tree, we return it.
Let's test it out:
const reconsitutedMock = denormalize(normalizedMock);
console.log(reconsitutedMock);
/* {
"id": "1",
"type": "Container",
"data": {
"items": [
{
"id": "2",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
},
{
"id": "3",
"type": "Subcontainer",
"data": {
"title": "A custom title",
"text": "A random Headline",
"items": []
}
}
]
}
} */
Also looks good.
Playground link to code
I would recommend .flatMap for this kind of transformations:
const flattenTree = element => [
element,
...element.data.items.flatMap(normalizeTree)
]
This move you from this shape:
{
id: 1,
data: { items: [
{
id: 2,
data: { items: [
{ id: 3, data: { items: [] } },
] }
] }
}
to this one:
[
{ id: 1, data: {...}},
{ id: 2, data: {...}},
{ id: 3, data: {...}},
]
Then once you have a flat array, you can transform it further to remove the references and create an object from entries:
const normalizedTree = element => {
let flat = flattenTree(element)
// only keep the id of each items:
// [{ id: 1, data:{...}}] -> [1]
// for (const el of flat) {
// el.data.items = el.data.items.map(child => child.id)
// }
// note that the for loop will change the initial data
// to preserve it you can achieve the same result with
// a map that will copy every elements:
const noRefs = flat.map(el => ({
...el,
data: {
...el.data,
items: el.data.items.map(child => child.id),
},
}))
// then if you need an object, 2 steps, get entries, [id, elem]:
const entries = noRefs.map(({ id, ...element }) => [id, element])
// then the built-in `Object.fromEntries` do all the work for you
// using the first part of the entry as key and last as value:
return Object.fromEntries(entries)
// if you have multiple objects with the same id's, only the last one
// will be in your returned object
}

Formatting Arrays of Objects into Objects (ES6 Way)

I am trying to create a pretty Objects of arrays but this come instead.
{
"label": [
"Instagram"
],
"value": [
"#username"
]
}
So, how can I change it? What I want is to be like this
{
"label": "instagram",
"value": "#username"
},
I don't know how it happened, but my guess is it was the result of me using formik to define initialValues of a complex nested array from strapi. I was using array.map to map the objects. Hence perhaps thats why it was so messy.
So what is the solution for this? Formatting Arrays of Arrays into Objects? Merging? Converting? I have no idea what it was called. Thanks in advance for anybody replying this.
(updated) The initialValues:
const formik = useFormik({
enableReinitialize: true,
initialValues: {
name: vendor?.name || '',
description: vendor?.description || '',
company: {
social_media: [
{
label: vendor.company?.social_media.map((x) => x.label) || '',
value: vendor.company?.social_media.map((x) => x.value) || ''
}
]
}
},
You can use for..in and array join method
const data = {
"label": [
"Instagram"
],
"value": [
"#username"
]
};
for (let keys in data) {
data[keys] = data[keys].join(',')
};
console.log(data)
Array.map returns an array if you don't want any object use Array.foreach
Or
Use [...Array.map()]
label: [...vendor.company?.social_media.map((x) => x.label)]
value: [...vendor.company?.social_media.map((x) => x.value)

I can't get this array to filter out on click in React

React newb here. Basically I am simply trying to remove an array item with the handleRemove function onClick. Seems like standard procedure and most of what I've read is doing something near identical in my eyes. I've messed around with it a ton and at one point had it deleting the item, but then it wouldn't rerender, and I eventually ended up where I started. Also tried putting the handler on both li and span, but neither worked. Any help would be great, my brain is done.
const guard = [
{
closedguard: [
{
name: "Armbar",
link: "",
x: "X",
id: "1",
},
{
name: "Triangle",
link: "",
x: "X",
id: "2",
},
{
name: "Omaplata",
link: "",
x: "X",
id: "3",
},
],
const [list, setList] = React.useState(guard);
function handleRemove(id) {
setList(list.filter((move) => move.id !== id));
console.log(list);
}
return (
<div className="guard">
<span onClick={(e) => toggleSubMenu(e, 0)}><AddCircleIcon
style={{ position: "relative", top: "26" }} />
</span>{" "}
<ul className="techUl">Closed Guard
{list.map((pos) =>pos.closedguard.map((move, index) => (
<li
onClick={() => handleRemove(move.id)}
key={move.id}
className="techLi"
style={subMenuState[0] ? { display: "block" } : { display: "none" }}
>{move.name}<span onClick={() => handleRemove(move.id)}> {move.x}</span>
</li>
))
)}
</ul>
```
There's a couple of issues with this.
Firstly, the brackets seem to be out of order for the guard constant
Secondly, the function setList is called asynchronously. This means that the setList function takes longer as compared to the other lines of code below it which will execute first. Thus, the list is logged first. If you want the console log to appear properly use the following snippet
useEffect(()=>{
console.log(list)},
[list]);
Thirdly, while this is fine for one update, it's recommended to use the functional process when updating state as in:
setList(list=>list.filter((move) => move.id !== id))
The difference is that this will ensure you always get the current value of state.(else it might give an older value of state which will be instantaneous but may not be the updated one)
const guard = [
{
closedguard: [
{
name: "Armbar",
link: "",
x: "X",
id: "1",
},
{
name: "Triangle",
link: "",
x: "X",
id: "2",
},
{
name: "Omaplata",
link: "",
x: "X",
id: "3",
},
],
You're setting guard as the initial state, which has one object (closedguard).
Initialize your state to guard.closedguard. Not sure what the point of enclosing the data is though.

Best way to to pass the output of a function to child components in Vue

I have a parent component with a data object config as below:
data() {
return {
config: {
Groups: [
{
name: "A",
Types: [
{ mask: 1234, name: "Alice", type: 1},
{ mask: 5678, name "Bob", type: 1},
]
},
{
name: "B",
Types: [
{ mask: 9876, name: "Charlie", type: 2},
{ mask: 5432, name "Drake", type: 2},
]
}
],
},
Defaults: {
dummyBoolean: false,
dummyNumber: 1
}
}
}
}
There are also 2 child components that for each of them, I want to pass the Types array (within each elements of the Groups object) if each_element.name == child component's name.
What I've done so far is having a computed function for each of the components as follows (which is highly inefficient):
computed: {
dataSender_A() {
let array= []
this.config.Groups.forEach( element => {
if (element.name === "A") array = element.Types
});
return array
},
dataSender_B() {
let array= []
this.config.Groups.forEach( element => {
if (element.name === "B") array = element.Types
});
return array
},
}
I'm looking for a better alternative to make this happen (as I might have more child components) and two approaches I tried so far have failed.
Having only one computed function that takes the component's name as argument and can be passed like <child-component-A :types="dataSender('A')" /> <child-component-B :types="dataSender('B')" /> (As it throws error dataSender is not a function)
computed: {
dataSender: function(groupName) {
let array= []
this.config.Groups.forEach( element => {
if (element.name === groupName) array = element.Types
});
return array
},
}
Having the above function in methods and pass that as props to child components (As it passes the function itself, not the outputted array)
I'd appreciate any help on this.
The computed properties don't accept parameters that are involved in the calculation, In this case you could just use a method like :
methods: {
dataSender: function(groupName) {
let array= []
this.config.Groups.forEach( element => {
if (element.name === groupName) array = element.Types
});
return array
},
}

JavaScript | Spread operator update nested value

I am trying to update a nested value of an object using the spread operator. This is my first time using this and I believe I am pretty close to achieving my end goal but I just can't seem to figure out what I actually need to do next.
I have an array which is structured like this:
[
{
name: "Category 1",
posts: [
{
id: 1,
published: false,
category: "Category 1"
},
{
id: 2,
published: true,
category: "Category 1"
}
]
},
{
name: "Category 2",
posts: [
{
id: 3,
published: true,
category: "Category 2"
},
{
id: 4,
published: true,
category: "Category 2"
}
]
}
]
On the click of a button I am trying to update the published value, and as I am using React I need to set the state. So it got recommended to me that I update using the spread operator.
onPostClick(post) {
post.pubished = !post.published;
this.setState({...this.state.posts[post.category], post})
}
If I log out the result of {...this.state.posts[post.category], post} I can see that the published is getting added to the parent which forms:
{
name: "Category 1",
published: false,
posts: [
...
]
}
Obviously this isn't the intended result, I want it to update the actual object within the posts object.
I have tried to do something like this.setState({...this.state.posts[post.category].posts, post}) but I get a message that it is undefined.
You can't access your data with this.state.posts[post.category]. posts data in the objects of the array.
You can make a filter to find your category object in array and change its posts value.
onPostClick(post) {
//CLONE YOUR DATA
var postArray = this.state.posts;
//FIND YOUR CATEGORY OBJECT IN ARRAY
var categoryIndex = postArray.findIndex(function(obj){
return obj.name === post.category;
});
//FIND YOUR POST AND CHANGE PUBLISHED VALUE
postArray[categoryIndex].posts.forEach(function(item){
if (item.id === post.id) {
item.published = !item.published;
}
});
//SET TO STATE TO RERENDER
this.setState({ posts: postArray});
}
This should work if your name of the state is true.
just adding, we know there are many ways to succeed, maybe you also want to try this way too..
onPostClick = post => {
let published = this.state.data.map((item, i) => {
item.posts.map((item_post, i) => {
if (item_post.category === post.category) {
item_post.published = !post.published;
}
});
});
this.setState({ ...this.state.data, published });
};

Categories

Resources