Rendering and best practices in React - reactjs

I am a beginner in React, I have a question regarding rendering, which consists of I use useEfect to render the change in a variable, which is decorated in a useState
Here are the statements:
const CardRepo: React.FC<DadosCardRepo> = ({ Nome }) => {
const [nomeRepo, setNomeRepo] = useState(Nome);
const [data, setData] = useState({});
const [languages, setLanguages] = useState<Languages[]>([]);
This is a component, which receives an object with a Name, and sets this value Name in nameRepo
Here are the two methods:
useEffect(() => {
getRepo(nomeRepo)
.then(data => {
setData(data);
});
}, [nomeRepo]);
useEffect(() => {
getLanguages(data['languages_url'])
.then(data => {
let total = 0;
const languagesRef: Languages[] = [];
Object.keys(data).map((key) => {
total += data[key];
languagesRef.push({ Nome: key, Porct: data[key] });
});
languagesRef.map((language, i, array) => {
setLanguages((oldValues) => [...oldValues, { Nome: language.Nome.toLowerCase(), Porct: +((language.Porct * 100) / total).toFixed(2) }]);
});
});
}, [data]);
Where the first useEfect I run when making a change to nomeRepo, and in this method, I make a request to the getRepo service
And the second one, when I change the date, and in this method, I make a request to the getLanguages ​​service, and do the processing of the response to make a list with it, where I assign languages.
Here is the listing:
{
languages.map((el, i) => {
return (
<div className="progress-bar" id={el.Nome} key={i} style={{ width: el.Porct + '%' }}></div>
);
})
}
My question is related to rendering, I know that I will only be able to list something, if I use useEfect in the variable "languages", where I will be assigning values ​​and with the changes in the variable to render again, otherwise I couldn't.
But when I make any changes to the code, it renders only where it was changed, but it assigns the same values ​​that were assigned, for example: [1,2,3] => [1,2,3,1,2,3].
I tried to put setLanguages ​​([]), but it has the same behavior
I was wondering how to fix this and if it's a good practice to make calls and list that way.

first, there is one bad practice where you create a derived state from props. nomeRepo comes from props Nome, you should avoid this derived states the most of you can. You better remove nomeRepo state and use only Nome.
your first use Effect becomes:
useEffect(() => {
getRepo(Nome)
.then(data => {
setData(data);
});
}, [Nome]);
you second useEffect has a setState triggered inside a map. That will trigger your setState multiple times, creating several rerenders. It's best to prepare your array of languages first, then call setState once.
Also, you should use forEach instead of map if you are not consuming the returned array, and only running some script on each element.
One thing to notice, is you are adding new languages to old ones. I assume that languages should be related data['languages_url'] value, this way you wouldn't include the old languages to the setLanguages.
One thing you could consider is to keep your total values instead of percentages. you can store total into a variable with useMemo, that will depend on languages. And declare a your percentage function into a utils folder and import it as need it.
refactored second useEffect:
// calculate total value that gets memoized
const totalLangCount = useMemo(() => languages.reduce((total, val) => total + val.count , 0), [languages])
useEffect(() => {
getLanguages(data['languages_url'])
.then(data => {
const languagesRef: Languages[] = [];
Object.keys(data).forEach((key) => {
// prepeare your languages properties
const nome = key.toLowerCase();
const count = data[key];
languagesRef.push({ nome, count });
});
// avoid multiple setLanguages calls
setLanguages(languagesRef)
});
}, [data]);
languages rendered:
{
languages.map((el, i) => {
return (
// here we are using array's index as key, but it's best to use some unique key overall instead
<div className="progress-bar" id={el.nome} key={i} style={{ width: percentage(el.count, total) }}></div>
);
})
}
percentage function reusable:
// utils percentage exported
export const percentage = (count, total) => ((count * 100) / total).toFixed(2) + '%'

Related

Proper on implementing incremental values

Now I know the title may be a bit vague so let me help you by explaining my current situation:
I have an array worth of 100 object, which in turn contain a number between 0 and 1. I want to loop through the array and calculate the total amount e.g (1 + 1 = 2).
Currently using .map to go through every object and calaculate the total. When I am counting up using the useState hook, it kinda works. My other approach was using a Let variabele and counting up like this. Although this is way to heavy for the browser.
I want to render the number in between the counts.
const[acousticness, setAcousticness] = useState(0);
let ids = [];
ids.length == 0 && tracks.items.map((track) => {
ids.push(track.track.id);
});
getAudioFeatures(ids).then((results) => {
results.map((item) => {
setAcousticness(acousticness + item.acousticness)
})
})
return (
<div>
Mood variabele: {acousticness}
</div>
)
What is the proper way on doing this?
I think this is roughly what you are after:
import {useMemo, useEffect, useState} from 'react';
const MiscComponent = ({ tracks }) => {
// Create state variable / setter to store acousticness
const [acousticness, setAcousticness] = useState(0);
// Get list of ids from tracks, use `useMemo` so that it does not recalculate the
// set of ids on every render and instead only updates when `tracks` reference
// changes.
const ids = useMemo(() => {
// map to list of ids or empty array if `tracks` or `tracks.items` is undefined
// or null.
return tracks?.items?.map(x => x.track.id) ?? [];
}, [tracks]);
// load audio features within a `useEffect` to ensure data is only retrieved when
// the reference of `ids` is changed (and not on every render).
useEffect(() => {
// create function to use async/await instead of promise syntax (preference)
const loadData = async () => {
// get data from async function (api call, etc).
const result = await getAudioFeatures(ids);
// calculate sum of acousticness and assign to state variable.
setAcousticness(result?.reduce((a, b) => a + (b?.acousticness ?? 0), 0) ?? 0)
};
// run async function.
loadData();
}, [ids, setAcousticness])
// render view.
return (
<div>
Mood variabele: {acousticness}
</div>
)
}

React JS & Recoil set/get on select/selectFamily for specific attribute

I use Recoil state management in ReactJS to preserve a keyboard letters data, for example
lettersAtom = atom(
key: 'Letters'
default: {
allowed : ['A','C','D']
pressedCounter : {'A':2, 'D':5}
}
)
lettersPressedSelect = selector({
key: 'LettersPressed',
get: ({ get }) => get(lettersAtom).pressedCounter, //Not work, returns undefined
set: () => ({ set }, pressedLetter) => {
let newState = {...lettersAtom};
newState.pressedCounter[pressedLetter]++;
set(lettersAtom, newState);
}
}),
In functional component i use
const [letters,setLetters] = useRecoilState(lettersAtom);
const [pressedCounter, setPressedCounter] = useRecoilState(lettersPressedSelect);
each time the a keyboard letter pressed the pressedCounter I want to increased for corresponded letter like that
setPressedCounter('A');
setPressedCounter('C'); ///etc...
How to achieve that ? Does recoil have a way to get/set a specific part/sub of json attribute ? (without make another atom? - I want to keep "Single source of truth")
Or do you have a suggetion better best practice to do that ?
There are some bugs in your code: no const, braces in atom call and no get inside the set. You also need spread the pressedCounter.
Overwise your solution works fine.
In Recoil you update the whole atom. So in this particular case you probably don't need the selector. Here is a working example with both approaches:
https://codesandbox.io/s/modest-wind-kosp7o?file=/src/App.js
It a best-practice to keep atom values rather simple.
You can update the state based on the existing state in a selector in a couple ways. You could use the get() callback from the setter or you could use the updater form of the setter where you pass a function as the new value which receives the current value as a parameter.
However, it's a good practice to have symmetry for the getter and setters of a selector. For example, here's a selector family which gets and sets the value of a counter:
const lettersPressedState = selectorFamily({
key: 'LettersPressed',
get: letter => ({ get }) => get(lettersAtom).pressedCounter[letter],
set: letter => ({ set }, newPressedValue) => {
set(lettersAtom, existingLetters => ({
...existingLetters,
pressedCounter: {
...existingLetters.pressedCounter,
[letter]: newPressedValue,
},
});
},
});
But note that the above will set the new value with a new counter value where you originally wanted the setter to increment the value. That's not really setting a new value and is more like an action. For that you don't really need a selector abstraction at all and can just use an updater when setting the atom:
const [letters, setLetters] = useRecoilState(lettersAtom);
const incrementCounter = pressedLetter =>
setLetters(existingLetters => ({
...existingLetters,
pressedCounter: {
...existingLetters.pressedCounter,
[pressedLetter]: (existingLetters.pressedCounter[pressedLetter] ?? 0) + 1,
},
});
Note that this uses the updater form of the selector to ensure it is incrementing based on the current value and not a potentially stale value as of the rendering.
Or, you can potentially simplify things more and use simpler values in the atoms by using an atom family for the pressed counter:
const pressedState = atomFamily({
key: 'LettersPressed',
default: 0,
});
And you can update in your component like the following:
const [counter, setCounter] = useRecoilState(pressedState(letter));
const incrementCounter = setCounter(x => x + 1);
Or create an general incrementor callback:
const incrementCounter = useRecoilCallback(({ set }) => pressedLetter => {
set(pressedState(pressedLetter)), x => x + 1 );
});
So the sort answer help by user4980215 is:
set: () => ({ get, set }, pressedLetter) => {
let newState = {...get(lettersAtom)};
newState.pressedCounter[pressedLetter]++;
set(lettersAtom, newState);
}

State array not updating when removing an array item in React

When I remove an array item from my state array, I'm also updating the prices after removing the array item. But prices are not updating. I have tried every thing, but didn't get any solution.
export default function CustomizerState(props) {
const initialTextItem = {
text: "Hello",
neonPrice: 0,
backplatePrice: 0,
neonPower: 0,
totalPrice: 0
}
const [settings, setSettings] = useState({
textItems: [initialTextItem],
libraryItems: [],
accessories: [...],
finalPrice: null
})
const removeItem = (id, itemType = "textItems") => {
const filteredItems = settings[itemType].filter((item) => {
return item.id !== id
})
setSettings((prevState) => (
{...prevState, [itemType]: filteredItems}
))
finalPrice()
}
const finalPrice = () => {
const textItemsPrice = getTotalPrice()
const libraryItemsPrice = getTotalPrice("libraryItems")
const accessoriesPrice = getTotalPrice("accessories", "unitPrice")
console.log(textItemsPrice, libraryItemsPrice, accessoriesPrice)
const finalPrice = textItemsPrice + libraryItemsPrice + parseInt(accessoriesPrice)
setSettings((prevState) => (
{...prevState, finalPrice}
))
}
const getTotalPrice = (itemType = "textItems", priceKey = "totalPrice") => {
let price = 0
settings[itemType].map((item) => (
price = price + (item[priceKey] * item.quantity)
))
return price
}
return (
<CustomizerContext.Provider value={{settings, addTextItem,
removeItem}}>
{props.children}
</CustomizerContext.Provider>
)
}
For now, it is behaving like when I remove any item, it doesn't update the finalPrice object item, but when I remove another item then it updates the prices for previous items. I don't know why it is behaving like this.
Can someone please have a look on my code and tell me what is wrong with it and how can I fix this?
You're calling finalPrice()right after you set your state
that triggers a re-rendering. You have to trigger the change using useEffect hook like this:
useEffect(() => {
finalPrice()
}, [settings]
You should probably consider separating your price from the rest of your settings.
Instead of calling a function right after updating the list, do the calculations before and update the state altogether. The problem with your approach is that when the calculus is being made, the state haven't updated yet, so when the function finalPrice() runs it takes the previous value.
I sincerely recommend you to use a Reducer instead, a single state with so many parameters may be troublesome.
Refer to useReducer, it will make your life easier.

Why do I get undefined value from async function?

I have been using Google firestore as a database for my projet.
In the collection "paths", I store all the paths I have in my app, which are composed of 2 fields : name, and coordinates (which is an array of objects with coordinates of points).
Anyway, i created a utility file in utils/firebase.js
In the file, i have this function which gets all the paths in my collection and return an array of all documents found :
export const fetchPaths = () => {
let pathsRef = db.collection('paths');
let pathsArray = []
pathsRef.get().then((response) => {
response.docs.forEach(path => {
const {nom, coordonnees } = path.data();
pathsArray.push({ nom: nom, coordonnees: coordonnees})
})
console.log(pathsArray)
return pathsArray;
});
};
In my react component, What i want to do is to load this function in useEffect to have all the data, and then display them. Here is the code I use :
import { addPath, fetchPaths } from './Utils/firebase';
//rest of the code
useEffect(() => {
let paths = fetchPaths()
setLoadedPaths(paths);
}, [loadedPaths])
//.......
The issue here is if I console log pathsArray in the function it's correct, but it never gets to the state.
When i console log paths in the component file, i get undefined.
I am quite new with react, i tried different things with await/async, etc. But I don't know what i am doing wrong here / what i misunderstand.
I know that because of my dependency, i would be supposed to have an infinite loop, but it's not even happening
Thank you for your help
Have a nice day
fetchPaths does not return any result. It should be:
export const fetchPaths = () => {
let pathsRef = db.collection('paths');
let pathsArray = []
return pathsRef.get().then((response) => {
response.docs.forEach(path => {
const {nom, coordonnees } = path.data();
pathsArray.push({ nom: nom, coordonnees: coordonnees})
})
console.log(pathsArray)
return pathsArray;
});
};
note the return statement.
Since the fetchPaths returns a promise, in the effect it should be like following:
useEffect(() => {
fetchPaths().then(paths =>
setLoadedPaths(paths));
}, [loadedPaths])

Why doesn't a useEffect hook trigger with an object in the dependency array?

I'm noticing something really strange while working with hooks, I've got the following:
import React, { useState, useEffect } from "react";
const [dependency1, setDependency1] = useState({});
const [dependency2, setDependency2] = useState([]);
useEffect(() => {
console.log("dependency 1 got an update");
}, [dependency1]);
useEffect(() => {
console.log("dependency 2 got an update");
}, [dependency2]);
setInterval(() => {
setDependency1(prevDep1 => {
const _key = "test_" + Math.random().toString();
if (prevDep1[_key] === undefined) prevDep1[_key] = [];
else prevDep1[key].push("foo");
return prevDep1;
})
setDependency2(prevDep2 => [...prevDep2, Math.random()]);
}, 1000);
for some reason only the useEffect with dependency2 (the array where items get added) triggers, the one with dependency1 (the object where keys get added) doesn't trigger..
Why is this happening, and how can I make it work?
setInterval(() => {
setDependency1(prevDep1 => {
const _key = "test_" + Math.random().toString();
return {...prevDep1, [_key]: [...(prevDep1[_key] || []), 'foo'] }
})
setDependency2(prevDep2 => [...prevDep2, Math.random()]);
}, 1000);
State should be updated in an immutable way.
React will only check for reference equality when deciding a dependency changed, so if the old and new values pass a === check, it considers it unchanged.
In your first dependency you simply added a key to the existing object, thus not changing the actual object. The second dependency actually gets replaced altogether when spreading the old values into a new array.
You're returning an assignment statement here:
setDependency1(prevDep1 => prevDep1["test_" + Math.random().toString()] = ["foo"]);
You should return an object. Maybe something like:
setDependency1(prevDep1 => ({ ...prevDep1, ["test_" + Math.random().toString()]: ["foo"] }));

Resources