Looping through a Map in React - javascript

I have a Map object:
let dateJobMap = new Map();
for (let jobInArray of this.state.jobs) {
let deliveryDate: Date = new Date(jobInArray.DeliveryDate);
let deliveryDateString: string = deliveryDate.toLocaleDateString("en-US");
if (dateJobMap.has(deliveryDateString)) {
let jobsForDate: IDeliveryJob[] = dateJobMap.get(deliveryDateString);
jobsForDate.push(jobInArray);
}
else {
let jobsForDate: IDeliveryJob[] = [jobInArray];
dateJobMap.set(deliveryDateString, jobsForDate);
}
}
In my render method, I want to call a TruckJobComp object for each delivery job in the value's array to display it:
<div className={ styles.column }>
<p className={ styles.description }>{escape(this.props.description)}</p>
{
dateJobMap.forEach(function(jobsForDate, dateString) {
jobsForDate.map(job => (
<TruckJobComp job = { job } />
))
})
}
</div>
This seems like it should work but doesn't. It never creates a TruckJobComp. I do a .forEach iteration on my Map, and for each value's array, I use .map to get the individual job object to send to TruckJobComp object.
When I create a temp array to grab the jobs from the last loop:
let tempJobs: IDeliveryJob[];
and in the loop add in:
if (dateJobMap.has(deliveryDateString)) {
let jobsForDate: IDeliveryJob[] = dateJobMap.get(deliveryDateString);
jobsForDate.push(jobInArray);
tempJobs = jobsForDate;
}
and then use that array in the render:
<div className={ styles.column }>
<p className={ styles.description }>{escape(this.props.description)}</p>
{
tempJobs.map(job => (
<TruckJobComp job = { job }/>
))
}
</div>
It displays as expected.
I do have a warnings in Visual Studio Code:
Warning - tslint - ...\TruckDeliverySchedule.tsx(104,38): error no-function-expression: Use arrow function instead of function expression
I don't know enough to understand. Line 104 corresponds with:
dateJobMap.forEach(function(jobsForDate, dateString) {
I am very new to this so I'm not 100% sure how most of this works. Just trying to put pieces I've learned together to get things to work.
Second Edit:
{escape(this.props.description)}
{
[...dateJobMap.keys()].map(jobsForDate => // line 154
jobsForDate.map(job => (
<TruckJobComp job = { job } />
))
)
}
Produces error:
[09:06:56] Error - typescript - src\...\TruckDeliverySchedule.tsx(154,27): error TS2461: Type 'IterableIterator<any>' is not an array type.

dateJobMap.forEach(...) returns undefined, so it cannot be mapped to a collection of elements.
ES6 maps have forEach method for compatibility purposes (generally for..of is preferred to iterate over iterables) and don't have map method. A map should be converted to array first, then it could be mapped to an element. Since values aren't used, only keys need to be retrieved:
{
[...dateJobMap.keys()].map(jobsForDate =>
jobsForDate.map(job => (
<TruckJobComp job = { job } />
))
)
}

All this warning is saying is that instead of using the syntax function(jobsForDate, dateString) {} you should use the syntax (jobsForDate, dateString) => {}.
The reason could be the way this is scoped in arrow functions versus function expressions. See this post.
My guess as to the reason your first approach didn't work but your second one did is that forEach doesn't actually return an array, and if it did, calling map within forEach would return an array of arrays (but, again, it doesn't). Not sure how React would handle that, but React does know how to handle a single array, which is what your last approach returns.

Related

Why is the first element getting removed too?

So I am new to javascript and I tried making a todo list. This works well with adding elements. The issue is when I am removing some item, the first one gets removed too, why is it so? I know I am missing a small thing and this may be really basic but I am not able to find out what that is.
const App1 = () => {
const [item, updatedItem]=useState('');
const [Items, setItems]=useState([]);
function inputEvent(event) {
updatedItem(event.target.value);
}
const addItem = (event) => {
event.preventDefault();
setItems((prev) => {
return[
...prev,
item
]
});
updatedItem('');
}
let key=0;
return(<>
<div className='back'>
<div className='list'>
<header>ToDo List</header>
<form onSubmit={addItem}>
<input type='text' placeholder='Add an item' value={item} onChange={inputEvent}/>
<button type='submit'>+</button>
</form>
<div className='items'>
<ol>
{Items.map((val) => <li><button id={key++} onClick={(event) => {
setItems((Items) => {
return Items.filter((val, index) => {
if(index!==Number(event.target.id)){
return index;
}
}
);
});
key=0;
}}>x</button>{val}</li>)}
</ol>
</div>
</div>
</div>
</>);
You return the index in your filter, expecting this to always be true, yet 0 (the index of the first element) is a falsy value.
Try this instead:
return Items.filter((_val, index) => index !== Number(event.target.id));
Some unrelated code-quality notes:
In React, you should always set a key prop on each element when looping through them, rather than id.
map has a second argument, index, which it passes into the callback --- you don't have to keep track of this yourself with e.g. key++ etc.
If you use map's index parameter, then you can pass that directly into your filter rather than using Number(event.target.id), which is not very idiomatic in React.
If you don't use an argument of a callback, it's a good idea to prefix it with a _ (like I've done with _val here), to make it explicit that you're not using it.
Your filter callback should return a flag. index is a number. When treated as a flag, 0 is false (more on MDN). Instead:
return Items.filter((val, index) => index !== Number(event.target.id));
However, your code is returning an array of li elements without setting key on them (see: keys), which React needs in order to manage that list properly (you should be seeing a warning about it in devtools if you're using the development version of the libs, which is best in development). You can't use the mechanism you're using now for keys when doing that, it will not work reliably (see this article linked by the React documentation). Instead, assign each Todo item a unique ID when you create it that doesn't change, and use that as the key (and as the value to look for when removing the item):
// Outside the component:
let lastId = 0;
// Inside the component:
const addItem = (event) => {
event.preventDefault();
setItems((prev) => {
return [
...prev,
{text: item, id: ++lastId}
];
});
updatedItem("");
};
// Add a remove function:
const removeItem = ({currentTarget}) => {
const id = +currentTarget.getAttribute("data-id"); // Get the ID, convert string to number
setItems(items => items.filter(item => item.id !== id));
};
// When rendering:
{Items.map((item) => <li key={item.id}><button data-id={item.id} onClick={removeItem}>x</button>{item.text}</li>)}
In some cases it may be useful to use useCallback to memoize removeItem to avoid unnecessary rendering, but often that's overkill.

Re-factoring map function so that a string is returned instead

I have a React component that consumes the value objects - which gives us access to an array, but currently, using the map function is going to return an array of labels/ assistants as opposed to just one string value. Can anyone help out I would best refactor this code to reflect this change? The current implementation does return the correct values, but I'm just not sure its the right use of map.
const FirstRepeatAttributeLabelAssistant = ({ objects, className }: Props) => {
if (objects.length === 0) {
return null;
}
let label = objects[0].attributeCollection.questions.map((question) => {
return question._contributions.label;
});
let assistant = objects[0].attributeCollection.questions.map((question) => {
return question._contributions.assistant;
});
return (
<StyledGroup as={Row}>
<StyledLabelWrapper>{label}</StyledLabelWrapper>
<Column size={12} />
<Column>
<StyledAssistantWrapper>{assistant}</StyledAssistantWrapper>
</Column>
</StyledGroup>
);
};
This is the array in question:
So, I really just want to assign my values label & assistant to those in this array.
Current output when I console.log(labely);:

ReactJS: Join map output with concatenating value

In my ReactJS application I am getting the mobile numbers as a string which I need to break and generate a link for them to be clickable on the mobile devices. But, instead I am getting [object Object], [object Object] as an output, whereas it should be xxxxx, xxxxx, ....
Also, I need to move this mobileNumbers function to a separate location where it can be accessed via multiple components.
For example: Currently this code is located in the Footer component and this code is also need on the Contact Us component.
...
function isEmpty(value) {
return ((value === undefined) || (value === null))
? ''
: value;
};
function mobileNumbers(value) {
const returning = [];
if(isEmpty(value))
{
var data = value.split(',');
data.map((number, index) => {
var trimed = number.trim();
returning.push(<NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>);
});
return returning.join(', ');
}
return '';
};
...
What am I doing wrong here?
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
First question:
What am I doing wrong here?
The issue what you have is happening because of Array.prototype.join(). If creates a string at the end of the day. From the documentation:
The join() method creates and returns a new string by concatenating all of the elements in an array (or an array-like object), separated by commas or a specified separator string. If the array has only one item, then that item will be returned without using the separator.
Think about the following:
const navLinks = [{link:'randomlink'}, {link:'randomlink2'}];
console.log(navLinks.join(','))
If you would like to use concatenate with , then you can do similarly like this:
function mobileNumbers(value) {
if(isEmpty(value)) {
const data = value.split(',');
return data.map((number, index) => {
const trimed = number.trim();
return <NavLink to={`tel:${trimed}`} key={index}>{trimed}</NavLink>;
}).reduce((prev, curr) => [prev, ', ', curr]);
}
return [];
};
Then you need to use map() in JSX to make it work.
Second question:
Is there any way to create a separate file for the common constants / functions like this to be accessed when needed?
Usually what I do for constants is that I create in the src folder a file called Consts.js and put there as the following:
export default {
AppLogo: 'assets/logo_large.jpg',
AppTitle: 'Some app name',
RunFunction: function() { console.log(`I'm running`) }
}
Then simply import in a component when something is needed like:
import Consts from './Consts';
And using in render for example:
return <>
<h1>{Consts.AppTitle}</h1>
</>
Similarly you can call functions as well.
+1 suggestion:
Array.prototype.map() returns an array so you don't need to create one as you did earlier. From the documentation:
The map() method creates a new array populated with the results of calling a provided function on every element in the calling array.
I hope this helps!

Is it a good pattern to use !! notation to listen for ReactJS changes?

I've been adopting ReactJS + Redux in my projects for a couple of years. I often end up in asynchronous situations where I need my component to wait for the state to be updated to render. Normally the simple logic !this.props.isFetching ? <Component /> : "Loading..." is enough.
However there are cases where I need to check for the state of an array that is embedded in the state object. In these cases, most of my components end up looking like this:
renderPostAuthor = () => {
if (!!this.props.postDetails.author) {
return this.props.postDetails.author[0].name;
} else {
return (
<div>
<StyledTitle variant="subheading" gutterBottom color="primary">
Loading...
</StyledTitle>
</div>
);
}
};
Is this use of the !! notation a good pattern / practice in ReactJS?
UPDATE: Thanks for the responses, and they are all valid. Perhaps, to clarify my question further, note that this.props.postDetails is a state itself that contains a number of objects and arrays. Therefore the problem is that if I omit the !! and this.props.postDetails isn't instantiated yet, and hence contains no arrays such as author[], I get the undefined error.
This has much more to do with just JavaScript in general than with React.
No, that use of !! isn't particularly useful. This:
if (!!this.props.postDetails.author) {
is the same as this:
if (this.props.postDetails.author) {
Neither of them means that author contains an array with at least one entry, which your next line of code is relying on. To do that, add .length or, with your particular example, probably [0] instead (in case author had an entry, but that entry was a falsy value):
if (this.props.postDetails.author[0]) {
If author may be null or undefined, we need to do two checks:
if (this.props.postDetails.author && this.props.postDetails.author[0]) {
Since we're going to use the result, it may be best to save the result to a variable or constant:
const firstAuthor = this.props.postDetails.author && this.props.postDetails.author[0];
if (firstAuthor) {
return firstAuthor.name;
}
Example of the current code throwing an error:
console.log("Running");
const author = [];
if (!!author) {
console.log(author[0].name);
} else {
console.log("No Author");
}
Example of checking [0] when we know author won't be null/falsy:
console.log("Running");
const author = [];
if (author[0]) {
console.log(author[0].name);
} else {
console.log("No Author");
}
Example of the double-check when author may be null/falsy:
console.log("Running");
const author = null;
if (author && author[0]) {
console.log(author[0].name);
} else {
console.log("No Author");
}
Example of saving and using the result:
function example(author) {
const firstAuthor = author && author[0];
if (firstAuthor) {
return firstAuthor.name;
} else {
return "Loading...";
}
}
console.log(example(null)); // Loading...
console.log(example([])); // Loading...
console.log(example([{name:"Harlan Ellison"}])); // "Harlan Ellison" (RIP)
There are times in react when using the !! is particularly helpful, but this is not the instance as stated above. The most common case I've found is when evaluating whether you're going to render array items or not. Often people will use the length of the array to decide whether to work with it or not since 0 length is a falsey boolean:
render () {
return this.props.array.length && <MyList items={this.props.array} />
}
Unfortunately this will return the 0 which will be rendered on the page. Since false will not render on the page a good alternative would be to use the double bang so that false is returned.
render () {
return !!this.props.array.length && <MyList items={this.props.array} />
}

ReactJS: using object ref as {key}?

How can I use an object reference as {key} in ReactJS?
I've tried this:
let ruleList = _.map(this.state.rules, function(rule) {
return <RuleList rule={rule} key={rule} />
});
but this ends up being printed in the console:
Warning: flattenChildren(...): Encountered two children with the same
key, .0:$[object Object]. Child keys must be unique; when two
children share a key, only the first child will be used.
Any way to get around this without hacks such as generating IDs for each item?
I had a similar situation where the object has no unique id.
I ended up generating ids for the items based on object references:
let curId = 1;
const ids = new WeakMap();
function getObjectId(object) {
if (ids.has(object)) {
return ids.get(object);
} else {
const id = String(curId++);
ids.set(object, id);
return id;
}
}
// Usage
<RuleList rule={rule} key={getObjectId(rule)} />
I know you mentioned that you don't want to generate ids, but I thought I'd share this since it is generic and doesn't depend on any properties in your object.
I created a lib intended to solve this problem. It's based on the idea suggested by #amann (using a weakMap). It provides a hook, so that the weakMap is created and destroyed with the component.
Have a look to https://www.npmjs.com/package/react-key-from-object
import { useKeyGen } from 'react-key-from-object'
const DogList = () => {
const keyGen = useKeyGen();
return (
<ul>
{dogs.map((dog) => (
<li key={keyGen.getKey(dog)}>
{dog.name}
-
{dog.age}
</li>
))
</ul>
);
}
Update
Objects cannot be used as keys. React js requires a key to be a string or a number and should be unique.
IMO there are two ways to solve this problem (open to suggestions)
Option 1
Iterate through the array and create a unique index
var rules = data.rules;
for(var i=0;i<rules.length;i++){
data.rules[i].key = i;
}
Use this key in _.map
let ruleList = _.map(this.state.rules, function(rule) {
return <RuleList rule={rule} key={rule.key} />
});
Option 2
Maintain a array of indices of rule objects which are not deleted.
var N = rules.length;
var arrayOfRules = Array.apply(null, {length: N}).map(Number.call, Number);
When you delete an item remove it using .splice.
The component should look like this
let ruleList = _.map(this.state.rules, function(rule, index) {
return <RuleList rule={rule} key={arrayOfRules[index]} />
});
----
Since rule object has no property which is unique and key needs to be unique, add the index parameter that comes in the map method.
let ruleList = _.map(this.state.rules, function(rule, index) { // <--- notice index parameter
return <RuleList rule={rule} key={index} />
});
Why do you insist on using the rule object as the key?
The easiest solution would be to use an index, returned from the underscore map function, instead. Like this:
let ruleList = _.map(this.state.rules, function(rule, index) {
return <RuleList rule={rule} key={index} />
});
Here is a similar topic related to the map function.

Categories