How to render custom elements for each item in an object (Map data structure) in React TS? - reactjs

I've been using this method to render multiple custom elements from an array but this is my first time doing it using a map data structure. It compiles fine but renders nothing. I've set up a codesandbox here.
import "./styles.css";
import React from "react";
import ImageGrid from "./ImageGrid";
interface OnlineImage {
id: string;
url: string;
}
export default function App() {
const [onlineImages, setOnlineImages] = React.useState<Map<string, OnlineImage[]>>();
React.useEffect(() => {
let onlineImageMap = new Map<string, OnlineImage[]>();
// fake api call
onlineImageMap.set("randomImageSet1", [
{ id: "1", url: "https://picsum.photos/200" },
{ id: "2", url: "https://picsum.photos/200" }
]);
onlineImageMap.set("randomImageSet2", [
{ id: "1", url: "https://picsum.photos/200" },
{ id: "2", url: "https://picsum.photos/200" }
]);
// set state
setOnlineImages(onlineImageMap);
}, []);
return (
<div className="App">
<div>Below should render my custom ImageGrid for each item in map...</div>
<>
{onlineImages?.forEach((imageSet, category) => {
return (
<>
<div>Image Category: {category}</div>
<ImageGrid images={imageSet} />
</>
);
})}
</>
</div>
);
}

Hi Samuel: I think you should first convert the map to an Array and then use the Array.prototype.map method which actually returns a copy of the original array with your function applied to it.
As you probably already figured out the return statement does nothing within a forEach function (to be more exact it only stops the execution of the code but does not bring back anything into the outer context).
If you really want to use forEach you'll have to use an array or Object to catch it then use Object.entries/.map to iterate over it
const myMap = new Map(Object.entries({a: 1, b: 2}))
const myMapCaptureList = []
myMap.forEach((value, key) => {
myMapCaptureList.push([key, value])
}
// then you can use myMapCaptureList
myMapCaptureList.map(([key, value]) => <div>{key}: <span>{value}</span></div>);
But I would suggest that it is much easier to use the very helpful Array.from method that can help convert a Map into an array of key/value pair entries: [[key1, val1], [key2, val2]]. It is essentially equivalent to running Object.entries(someObject).
{Array.from(onlineImages || []).map(([category, imageSet]) => {
return (
<>
<div>Image Category: {category}</div>
<ImageGrid images={imageSet} />
</>
);
})}

You are using the .forEach method - which returns nothing, instead use .map that is identical but does return things
{onlineImages?.map((imageSet, category) =>
<>
<div>Image Category: {category}</div>
<ImageGrid images={imageSet} />
</>
)}

Related

SolidJS: input field loses focus when typing

I have a newbie question on SolidJS. I have an array with objects, like a to-do list. I render this as a list with input fields to edit one of the properties in these objects. When typing in one of the input fields, the input directly loses focus though.
How can I prevent the inputs to lose focus when typing?
Here is a CodeSandbox example demonstrating the issue: https://codesandbox.io/s/6s8y2x?file=/src/main.tsx
Here is the source code demonstrating the issue:
import { render } from "solid-js/web";
import { createSignal, For } from 'solid-js'
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<For each={todos()}>
{(todo, index) => {
console.log('render', index(), todo)
return <div>
<input
value={todo.text}
onInput={event => {
setTodos(todos => {
return replace(todos, index(), {
...todo,
text: event.target.value
})
})
}}
/>
</div>
}}
</For>
Data: {JSON.stringify(todos())}
</div>
</div>
);
}
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T) : Array<T> {
const clone = array.slice(0)
clone[index] = newItem
return clone
}
render(() => <App />, document.getElementById("app")!);
UPDATE: I've worked out a CodeSandbox example with the problem and the three proposed solutions (based on two answers): https://codesandbox.io/s/solidjs-input-field-loses-focus-when-typing-itttzy?file=/src/App.tsx
<For> components keys items of the input array by the reference.
When you are updating a todo item inside todos with replace, you are creating a brand new object. Solid then treats the new object as a completely unrelated item, and creates a fresh HTML element for it.
You can use createStore instead, and update only the single property of your todo object, without changing the reference to it.
const [todos, setTodos] = createStore([
{ id: 1, text: 'cleanup' },
{ id: 2, text: 'groceries' },
])
const updateTodo = (id, text) => {
setTodos(o => o.id === id, "text", text)
}
Or use an alternative Control Flow component for mapping the input array, that takes an explicit key property:
https://github.com/solidjs-community/solid-primitives/tree/main/packages/keyed#Key
<Key each={todos()} by="id">
...
</Key>
While #thetarnav solutions work, I want to propose my own.
I would solve it by using <Index>
import { render } from "solid-js/web";
import { createSignal, Index } from "solid-js";
/*
* Returns a cloned array where the item at the provided index is replaced
*/
function replace<T>(array: Array<T>, index: number, newItem: T): Array<T> {
const clone = array.slice(0);
clone[index] = newItem;
return clone;
}
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<div>
<div>
<h2>Todos</h2>
<p>
Problem: whilst typing in one of the input fields, they lose focus
</p>
<Index each={todos()}>
{(todo, index) => {
console.log("render", index, todo());
return (
<div>
<input
value={todo().text}
onInput={(event) => {
setTodos((todos) => {
return replace(todos, index, {
...todo(),
text: event.target.value
});
});
}}
/>
</div>
);
}}
</Index>
Dat: {JSON.stringify(todos())}
</div>
</div>
);
}
render(() => <App />, document.getElementById("app")!);
As you can see, instead of the index being a function/signal, now the object is. This allows the framework to replace the value of the textbox inline.
To remember how it works: For remembers your objects by reference. If your objects switch places then the same object can be reused. Index remembers your values by index. If the value at a certain index is changed then that is reflected in the signal.
This solution is not more or less correct than the other one proposed, but I feel this is more in line and closer to the core of Solid.
With For, whole element will be re-created when the item updates. You lose focus when you update the item because the element (input) with the focus gets destroyed, along with its parent (li), and a new element is created.
You have two options. You can either manually take focus when the new element is created or have a finer reactivity where element is kept while the property is updated. The indexArray provides the latter out of the box.
The indexArray keeps the element references while updating the item. The Index component uses indexArray under the hood.
function App() {
const [todos, setTodos] = createSignal([
{ id: 1, text: "cleanup" },
{ id: 2, text: "groceries" }
]);
return (
<ul>
{indexArray(todos, (todo, index) => (
<li>
<input
value={todo().text}
onInput={(event) => {
const text = event.target.value;
setTodos(todos().map((v, i) => i === index ? { ...v, text } : v))
}}
/>
</li>
))}
</ul>
);
}
Note: For component caches the items internally to avoid unnecessary re-renders. Unchanged items will be re-used but updated ones will be re-created.

how to add unique key using uuid in react js?

I read a comment from someone here in StockOverflow who talks about React keys and said that
'React expects STABLE keys, meaning you should assign the keys once and every item on your list should receive the same key every time, that way React can optimize around your data changes when it is reconciling the virtual DOM and decides which components need to re-render. So, if you are using UUID you need to do it at the data level, not at the UI level',
and I want to ask if anyone know how to apply this in a real code where we have for example a context component that have an array of objects and another component that maps through this array, how can we apply this using uuid() or any other package.
Although it is not a common requirement for you to generate id on FE, it happens some times, so using uuid is a really good way of doing that. It is easy and implementation is quick.
I made an example for you here how to do it:
import { useEffect, useState } from "react";
import { v1 } from "uuid";
import "./styles.css";
const items: { name: string; id?: string }[] = [
{
name: "Name1"
},
{
name: "Name2"
},
{
name: "Name3"
},
{
name: "Name4"
},
{
name: "Name5"
},
{
name: "Name6"
}
];
export default function App() {
const [itemList, setItemList] = useState<{ name: string; id?: string }[]>([]);
useEffect(() => {
setItemList(items.map((item) => ({ ...item, id: v1() })));
}, []);
return (
<div className="App">
{itemList.map((item) => (
<div key={item.id}>
{item.name} - {item.id}
</div>
))}
</div>
);
}
In this example your array is empty at the beginning and after first useEffect gets populated by items with uuid generated ids:
And code sandbox code
You can install react-uuid
import uuid from 'react-uuid'
const array = ['one', 'two', 'three']
export const LineItem = item => <li key={uuid()}>{item}</li>
export const List = () => array.map(item => <LineItem item={item} />)
or you can use crypto.randomUUID() directly without pckgs

how to render array in object in array? (react)

const checked = [{
food:['apple', 'banana']
drink:['wine', 'beer']
}];
render (
<>
{checked.map((value) => {
value.food.forEach((each) => (
<div>{each}</div>
)
)}
</>
)
I tried this way and noting is shown in browser..
what would be the best way to approach?
Need to Return Your data like below!!
import React from "react";
export default function App() {
let checked = [{
food:['apple', 'banana'],
drink:['wine', 'beer']
}];
return (
<div className="App">
{
checked.map((item) => {
return item.food.map((fruit)=>{
return <h1>{fruit}</h1>
})
})
}
</div>
);
}
Your code has multiple errors.
It should be render instead of rander
While defining object, multiple properties should be separated using a comma. So put comma after the food array.
forEach doesn't return any thing. It just iterates over an array. So, if you want to return something (in this case a div element), use map.
Also, you should use key for each div element otherwise react would give you a warning in the console. This is done so that while re-rendering, based on the keys, react would understand which component to re-render and which to skip. Otherwise all the div would be re-rendered which is a costly operation.
const checked = [
{
food: ["apple", "banana"],
drink: ["wine", "beer"]
}
]
return (
<>
{checked.map((value) => {
return value.food.map((each, index) => {
return <div key={index}>{each}</div>;
});
})}
</>
);
There is a couple of improvements that require to be implemented to make the list displayed.
First, the map method does not return anything.
Two solutions:
Remove the curly brackets checked.map((value) => value...
Add a return keyword: checked.map((value) => { return value...}
The other issue is that the second loop is iterated using the forEach method.
The difference between the two (forEach and map) from MDN:
The forEach() method executes a provided function once for each array
element.
MDN
The map() method creates a new array populated with the results of
calling a provided function on every element in the calling array.
MDN
Basically, it means that forEach does not return anything and that why you need to use map
checked.map((value) => {
return value.food.map((each) => (<div>{each}</div>))
})}
or
checked.map((value) =>
value.food.map((each) => (<div>{each}</div>))
)}
You are iterating over the checked array items using forEach which won't induce any results since the forEach method
executes a provided function once for each array element.
which won't result in a transformed array.
What you are looking for is the map method which
creates a new array populated with the results of calling a provided function on every element in the calling array.
hence returning your transformed items so that they can be rendered (transformed at compilation time to ReactElement using the JSX syntax).
Note that you need to use an HTML tag instead of a React.Fragment the empty tag <> syntax:
const checked = [{
food:['apple', 'banana'], // there is a missing comma here
drink:['wine', 'beer']
}];
render ( // render and not rander
<div> // div instead of empty tag
{checked.map((item) => item.food.map((each) => <div>{each}</div>))}
</div>
)
Can check this approach. if you want to print just food values, below code should work. If you want to print all the values (both food and drink), then uncomment the commented code below.
export default function App() {
const checked = [
{
food: ["apple", "banana"],
drink: ["wine", "beer"]
},
{
food: ["grapes", "oranges"],
drink: ["coke", "sprite"]
}
];
// to display values of all arrays.
// return (
// <>
// {checked.map((value) => {
// const keys = Object.keys(value);
// return keys.map((eachKey) => {
// return value[eachKey].map((individualValue) =>
// (
// <div>{individualValue}</div>
// )
// )
// });
// })
// }
// </>
// );
// To display just food values.
return (
<>
{checked.map((value) => {
return value.food.map((each) => <div>{each}</div>);
})}
</>
);
}

can not set data from back end use React.js

This is from back-end data format:
{data: Array(15)}
data[0]:
option: Array(1)
0: "String"
_id: "String"
col1: "String"
col2: "String"
col3: "String"
....data[14]
and this is front end code:
const Component1 = () => {
const [dbvalue, setDbvalue] = useState(null)
//option, _id, col1, col2, col3
const getAlldbValue = async () => {
try {
const resp = await services.getAlldbValue();
console.log(resp) //enter F12 can get total data from back end.
setDbvalue(resp.data.data)
console.log(dbvalue) //setDbvalue() not work , this print null
} catch (error) {
console.log(error)
alert('Failed!')
}
}
useEffect(() => {
getAlldbValue()
}, [])
if(!dbvalue) {
return (
<p>Loading Component...</p> //not appear
)
}
return (
<p>{dbvalue}</p> //not appear
);
};
export default Component1;
How can I set this nested object json into this dbvalue?
I want use dbvalue.keyname to display.
Thanks for your help
To display JavaScript objects in your jsx you can do the following:
<pre>{JSON.stringify(dbvalue,undefined,2)}</pre>
If your data[x].option is an object where every property is a string then you can do the following to list them:
const ListProps = props => (
<ul>
{Object.entries(props).map(([key, value]) => (
<li key={key}>{value}</li>
))}
</ul>
)
//here is how you would render listProps
<listProps {...data[0].option} />
First, your console.log is printing null because when you use setState, the variable you just setted will only be setted in the next render. Try printing resp.data.data instead and then put console.log(dbvalue) outside the getAlldbValue function, so you can understand better.
Also,
`return (
<p>{dbvalue}</p> //not appear
);`
your trying to put an object into a HTML Element, try dbvalue[0].col1 or whatever you want to show instead.
Hope it helps you.

React - Iterating over State to dynamically generic element

This is a pretty basic question but I've been struggling; Likely due to my 'not quite understanding' how .map() works, and when to make an Object an array etc.
import React, { Component } from 'react';
import Post from './Post/Post';
class Posts extends Component {
state= {
posts: [
{
title: "Test",
content: "Some content"
},
{
title: "Test2",
content: "Some Additional Content"
}
]
};
render() {
let post = null;
post = Object.keys(this.state.posts).map(function (item, key) {
return <Post title={this.state.posts[item].title} content={this.state.posts[item].content} />
})
return (
<div>
<div>List of Posts</div>
{post}
</div>
);
}
}
export default Posts;
This is where I settled, since I got my JSBin version to work. However I'm getting a warning on my 'anonymous callback' function that creates the <Post> elements. <Post> is just expecting those two props and rendering them. I'm getting a warning on line 26 that mentions state being undefined. Am I not initializing state properly?
post = this.state.posts.map(function (item, key) {
return <Post title={item.title} content={item.content} key={key}/>
})
Your state.posts is an array and you should map over it.
post = Object.keys(this.state.posts).map((item, key) => {
return (
<Post
title={this.state.posts[item].title}
content={this.state.posts[item].content}
/>
)
})
as Subin says, what you need to do in that case is just map over the posts array.
But, to fix your code is very simple. just use arrow function instead of a regular function.
Do it like this:
post = this.state.posts.map(post => {
return <Post title={post.title} content={post.content} key={post.title}/>
})
When you map over array, you dont need to acces that state again, because post will be assigned to post inside map function. And then just acces title and content. and dont forget to pass key,there i've passed key={post.title} but that is not a good solution, because it needs to be unique.

Resources