React element that counts across the component tree - javascript

What would be the best way to create an element that maintains an increasing counter across the tree? Example:
function SubElement(){
return <div><Counter/> <Counter/></div>
}
<CounterSection>
This should output 1: <Counter/>
<div>
This should output 2: <Counter/>
This should output 3 4: <SubElement/>
</div>
This should output 5: <Counter/>
This should output 6 7: <SubElement/>
</CounterSection>
The counter component can not leak any implementation details to the rest of the code, eg. having to pass some sort of counting prop into the <Counter/>.

Related

Accessing and modyfing slot's elements in Vue 3

I'm rewriting my MQTT based dashboard from Vue 2 to Vue 3 currently, and can't solve one problem.
The dashboard has many Vue components, which reacts to specific MQTT topics and values, to display current system state. One of them is the mqtt-multi-state component which is declared like below:
// status-page
<mqtt-multi-state subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
<div value="OPEN"><font-awesome-icon icon="door-open"/></div>
<div value="CLOSED"><font-awesome-icon icon="door-closed"/></div>
</mqtt-multi-state>
It contains dwo div elements, with two states that the system sensor (door) can has. These elements are passed to the default slot and are hidden by default via css.
What I want to achieve is to show one of them based on the equality of the value attr in each of them with the current MQTT value. So if the current value is OPEN then the first div show up, when value is CLOSED then the second one appears.
// mqtt-multi-state
<template>
<div class="mqtt-multi-state">
<div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
methods: {
messageArrived(value){
let states = this.$slots.default() // here are the two divs
for(let i = 0;i < states.length;i++ ){
if(states[i].props.value === value )
{
//states[i].elm.className = "state-active" <- Vue 2 solution using DOM with elm
//states[i].props.class = "state-active"; <- Vue 3, doesn't work, not reactive?
}
else
{
//states[i].props.class = "";
}
}
}
}
}
</script>
I know, this approach is a little bit different but I really like to describe dashboard element in this way in HTML. In the messsageArrive() method I'm iterating over the default slot children elements and if the value match the current value prop I want to show this item, by add a state-active class. But this solution does not work. The VNode is changed (checked in the console).
In Vue 2 I've simply get to the DOM element directly and change it class, but on Vue 3 I can't figure it out, how to get from VNode to the DOM Node. Or there are maybe an other/better way to do that?
Well, many evenings later I came to this solution. Instead of putting all the different states into a default slot, I've created dynamic named slots.
The different state elements (font-awesome icons in this case) go to each slot's template element. Unfortunately I can't pass the mqtt value to the template element itself, because it does not exists inside mqtt-multi-state parent component. Instead, I've passed the mqtt value into the template's first child element. It can be any element, div, p, font-awesome-icon.
// status-page
<mqtt-multi-state :state-count="2" subscribe-topic="home/env/sensor/wc/door/status" json-path="state">
<template #0><font-awesome-icon value="OPEN" icon="door-open"/></template>
<template #1><font-awesome-icon value="CLOSED" icon="door-closed"/></template>
</mqtt-multi-state>
There is also a prop state-count that defines the number of different states. Note the colon before prop name, so the string "2" is a Number type.
The mqtt-multi-state component becomes like this:
// mqtt-multi-state
<template>
<div class="mqtt-multi-state">
<template v-for="(slot, index) in slots">
<slot :name="index" v-if="slot" ></slot>
</template>
</div>
</template>
<script>
export default {
data: function(){
return {
slots: []
}
},
props: {
stateCount: {
default: 2,
type: Number
}
},
methods: {
messageArrived(value){
for(let i = 0; i < this.stateCount; i++ ) {
this.slots[i] = this.$slots[i]?.()[0]?.props?.value === value
}
}
}
}
</script>
In the messageArrived() function the code iterates through all the slots and compares the slot's first child element prop, named value with the current mqtt value received. If the values are equal then the this.slots[i] array value goes true, and the Vue's v-if directive makes corresponding slot visible, otherwise hides the slot.
Named slots names are created by the index value, so to get their content with this.$slot.slotNameRenderFunction() I call the function 0(), 1() etc using the this.$slot[index]() notation (accessing object member by name stored in the variable) with an optional chaining operator ?. - so the whole notation looks a little weird :).

React: Weird reconciliation result on list items whose content is served by array with same length as the list

UPDATE: Problem is not solved but it turned out to be something else other than what it looked like when I first posted this. I modified the title to better describe what it is really about. Some comments in the code are updated to save some read time. React does reuse element when it should, result is weird when two arrays are of same length.
Codesandbox for quick check.
I came into this frustrating yet interesting issue, and have been struggled with it for quite a while, with no luck. Not sure if it is actually a bug of React.
According to the official documentation here, if I'm understanding this right, by giving a correct key (unique within siblings) to an element, React should try to update and reuse it instead of destroying and recreating it on a re-render.
Check out this example on codepen, I've simplified it to just enough for showing the problem. Raw code is also included below for a quick reference.
The app is working fine if it is just as simple as is, so increase/decrease state cur (by clicking the two buttons) rolls numbers back and forth.
If you check it out in browser dev tools, to see what elements are destroyed and recreated on each button click, it's clear that every clicking on + destroys the first item and creates the last item, every clicking on - destroys the last item and create the first item, other <li>s in-between are just updated and reused. This is correct and expected.
The problem:
If num and loop happen to be of same length, e.g. change num to [3, 4, 5, 6] (remove any two items would be fine) so it has 4 items same as loop does, I'll expect to see all items being reused on button click and no item should be destroyed and recreated, as all items are in the same DOM tree with keys set to be easily identified.
However, if you check in browser dev tool again, it works quite differently indeed although nothing seems to be wrong on rendered result. Clicking on + destroys the first item and recreates it as the last item, while reusing all others. Clicking on - reuses first item (the last item from last render) and destroys/recreates all others.
Although the app still works without problem (because it's simple as just display some numbers), but all class driven transitions will mess up and all children elements under <li> reinitialize, e.g. <img> reload source images.
Questions
What's so special when two arrays are of same length in this case? What is really happening here?
What can or should be done to fix it? Meaning try to reuse existing items as much as possible. A quick dirty method I could think of is to insert some dummy item into num and add some logic in map() iteration to skip the dummy item. This works but I don't feel it as a 'correct' way.
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
const num = [1, 2, 3, 4, 5, 6]; // array to serve content to list items
const loop = [1, 2, 3, 4]; // array to be mapped to generate the list
const RollingNums = () => {
const [cur, setCur] = React.useState(0); // control number rolling
return (
<div className="problem">
<ul>
{loop.map((value, index) => { // list items count is fixed
// simple CS to get continued content from array num
let idx = index + cur;
idx =
idx >= 0
? idx % num.length
: ((idx % num.length) + num.length) % num.length;
const target = num[idx].toString(); // content from array num for current element
return (
<li key={target} className={target}>
{target}
</li>
);
})}
</ul>
<div className="control">
<button onClick={() => setCur((prev) => prev - 1)}>-</button>
<span>{cur}</span>
<button onClick={() => setCur((prev) => prev + 1)}>+</button>
</div>
</div>
);
};
ReactDOM.render(<RollingNums />, document.querySelector('#root'));
Seems to be some issue with React, it does somehow remove and then recreate those items. Check this, and particularly the codesandbox provided by Kunuk Nykjær.
In my work, I just took a quick dirty detour by extending the first array (num) to make sure it is longer than the second (loop), in which case React reconciliation does work as expected. This requires many extra check in my case, but that's all I could do for now.

How to loop over an Array and find part that is included in the Item not the whole item?

I am building a weather app so I need to grab the icon that is matching the call from API. I'm using parcel and parcel dumps all the images in dist folder. I import all the images and that gives me an object which I converted in a one dimensional Array. Because Parcel gives some extra text I can't grab the proper image.
This is the array that I can see:
[0: "/01d.b9bbb2b9.svg" 1: "/01n.2290e7c6.svg" 2: "/02d.ac486e56.svg" 3: "/02n.259589cf.svg"]
<img src="${result.weather.icon}" alt="" class="weather--icon" />//I like to render it like this.
Is there a way I can loop over an array and get only the icon that is matching result.weather.icon ??? I need to find only the 01d or 02d from the array.
It isn't really clear what you are asking - because [0: "/01d.b9bbb2b9.svg" 1: "/01n.2290e7c6.svg" 2: "/02d.ac486e56.svg" 3: "/02n.259589cf.svg"] isn't a valid array.
However, presuming your array is simply the text elements then you can just use find. i.e.
let data = ["/01d.b9bbb2b9.svg", "/01n.2290e7c6.svg", "/02d.ac486e56.svg", "/02n.259589cf.svg"];
let result = data.find(x => x.startsWith('/01d'));
console.log(result);
Here find is better than filter as find returns the first value that matches from the collection. Once it matches the value in findings, it will not check the remaining values.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
Array.prototype.filter allows you to loop through an array and test its values against a condition. It will then only return the values that have passed that test.
const array = [ "/01d.b9bbb2b9.svg", "/01n.2290e7c6.svg", "/02d.ac486e56.svg", "/02n.259589cf.svg"
];
const targetVal = '01d';
const filteredArray = array.filter(image => image.includes(targetVal)
);

React / Javascript Out Of Order Code Execution

In regards to my previous question (code is still giving me trouble): React: Javascript assignment not updating object
Code Here https://codesandbox.io/s/github/nieroda/js_err
You can see the object I have on line 2. There is no mutation that occurs between 2 and 5 although the print output is different (as seen below) leading me to believe that the code is being executed out of order.
codeBreaker.js:108
1. console.log("BEFORE")
2. console.log(gameBoardCopy[currentRow])
3. console.log("END")
let copy = this.state.gameBoard[currentRow].slice()
4. console.log("Copy Start")
5. console.log(copy)
6. console.log("Before Assignment")
copy[4] = { numColorMatch: 2, numExactMatch: 2 }
7. console.log("After Assignment")
8. console.log(copy)
9. console.log("copy end")
Looking at the output
BEFORE
2.
0: "BlueViolet"
1: "BlueViolet"
2: "BlueViolet"
3: "BlueViolet"
4: {numColorMatch: 0, numExactMatch: 0}
END
Copy Start
5.
0: "BlueViolet"
1: "BlueViolet"
2: "BlueViolet"
3: "BlueViolet"
4: {numColorMatch: 2, numExactMatch: 2}
Before Assignment
After Assignment
0: "BlueViolet"
1: "BlueViolet"
2: "BlueViolet"
3: "BlueViolet"
4: {numColorMatch: 2, numExactMatch: 2}
copy end
I cant figure out what is causing this, tips appreciated. Thanks
console.log is actually an async method and that is most likely why you are seeing the execution "appear" out of order. Whenever you console.log an object, make sure to console.log(JSON.stringify(JSON.parse(value)));.
A better way to see execution order and its values is to add debugger statement. Try adding debugger; right above step 5 and walk through the code to see what the values actually are. I would imagine the values would be as you expect them to be. If not, stepping through the process using the debugger will tell you why.
Looks like you are unintentionally mutating the state of your component. You are not copying the object here. Javascript objects are passed by reference, which means when you directly assign an object like this to another variable, they both will modify the same data.
Instead of:
let copy = this.state.gameBoard[currentRow].slice()
call:
let copy = Object.assign({}, this.state.gameBoard[currentRow]);
If it is your intention to update the state of your component you should call the.setState({obj}).
If you have to deep clone an object, I would suggest deep copy functions from either lodash or underscore (or create your own: Objects in JS: deep copy).
Hope this helps,

How do I perform a merge sort based on the property of an object (rather than an Array)?

Background
Using JavaScript, I need to sort a large JSON object based on a given property of that object. I am assuming that a merge sort is the fastest approach. If this is not the fastest approach, please tell me what is. There are myriad examples online of a merge sort against an array, but very little with objects. Here is a sample object:
fruitForSale = {
1: {"type":"orange","UnitPrice":0.20},
2: {"type":"banana","UnitPrice":0.30},
3: {"type":"pear","UnitPrice":0.10},
4: {"type":"apple","UnitPrice":0.50},
5: {"type":"peach","UnitPrice":0.70}
}
Question
Using a merge sort (or faster algorithm), how would I sort the fruitForSale object so that I end up with an object sorted by 'type':
fruitForSale = {
4: {"type":"apple","UnitPrice":0.50},
2: {"type":"banana","UnitPrice":0.30},
1: {"type":"orange","UnitPrice":0.20},
5: {"type":"peach","UnitPrice":0.70},
3: {"type":"pear","UnitPrice":0.10}
}
NOTE: The original keys (1,2,3,4 & 5) would need to stay assigned to their respective object, so a key of 1 should always match with {"type":"orange","UnitPrice":0.20} and a key of 2 will always match with {"type":"banana","UnitPrice":0.30} and so on.
Thanks!
You can't sort the keys on the object, but you can keep your own array of sorted keys.
var fruitForSale = {
1: {"type":"orange","UnitPrice":0.20},
2: {"type":"banana","UnitPrice":0.30},
3: {"type":"pear","UnitPrice":0.10},
4: {"type":"apple","UnitPrice":0.50},
5: {"type":"peach","UnitPrice":0.70}
},
sortedKeys = Object.keys(fruitForSale).sort(function (i,j) {
return fruitForSale[i]["type"] > fruitForSale[j]["type"];
});
Example: http://jsfiddle.net/X2hFt/ (Output displayed on the console)
Object.keys is not supported everywhere but you can polyfill if you need to easily. See:
https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Object/keys
Oh, and if you are curious about the underlying implementation of sort see:
Javascript Array.sort implementation?

Categories

Resources