What comparison process does the useEffect React hook use? - javascript

Let's start with my favorite JavaScript expression:
[]==[] // false
Now, let's say what the React doc says about skipping side effects:
You can tell React to skip applying an effect if certain values havenโ€™t changed between re-renders. To do so, pass an array as an optional second argument to useEffect:
useEffect(() => {/* only runs if 'count' changes */}, [count])
Now let's consider the following component which behavior had made me scratch my head:
const App = () => {
const [fruit, setFruit] = React.useState('');
React.useEffect(() => console.log(`Fruit changed to ${fruit}`), [fruit]);
const [fruits, setFruits] = React.useState([]);
React.useEffect(() => console.log(`Fruits changed to ${fruits}`), [fruits]);
return (<div>
<p>
New fruit:
<input value={fruit} onChange={evt => setFruit(evt.target.value)}/>
<button onClick={() => setFruits([...fruits, fruit])}>Add</button>
</p>
<p>
Fruits list:
</p>
<ul>
{fruits.map(f => (<li key={f}>{f}</li>))}
</ul>
</div>)
}
ReactDOM.render(<App/>, document.querySelector('#root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
When adding 'apple', this is what is being logged in the console:
// on first render
Fruit changed to
Fruits changed to
// after each keystroke of 'apple'
Fruit changed to a
Fruit changed to ap
Fruit changed to app
Fruit changed to appl
Fruit changed to apple
// ater clicking on 'add'
Fruits changed to apple
And I don't understand the middle part. After each keystroke, fruits goes from [] to [], which are not the same in JS if they refer to different objects. Therefore, I expected some Fruits changed to to be logged. So my question is:
What is the exact object comparison process used by React to decide on whether or not to skip the effect hook?

A function which is being used to compare objects is practically a polyfill of Object.is method. You can see it here in the source code:
https://github.com/facebook/react/blob/master/packages/shared/objectIs.js
And here's a function which compares prevDeps with nextDeps within useEffect implementation:
https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberHooks.new.js#L1427
By the way, Object.is is mentioned as a comparison algorhitm in the hooks API section of the docs, under useState.

After each keystroke, fruits goes from [] to []
It seems that you're under the impression that fruits is re-assigning to a new array after each key stroke which is not the case.
It is not comparing two new arrays, it is comparing the same label, which points to the same reference in the memory in this particular point of time.
Given:
var arr = [];
We can check if arr reference has changed over time (if no mutations took place).
simple example:
var arr = [];
var arr2 = arr;
console.log('arr === arr ', arr === arr)
console.log('arr === arr2 ', arr === arr2)
arr = [];
console.log('---- After the change ----');
console.log('arr === arr ', arr === arr)
console.log('arr === arr2 ', arr === arr2)

Related

What does Array(array.length).fill(0) mean?

I am learning and practicing hooks useState in react js. For example, when declaring a state variable like this,
const [votes, setVotes] = useState(0)
I know that the 0 value in useState() means that votes is initialized with starting value of 0. However, advancing a little bit, like this,
const [votes, setVotes] = useState(() => Array(fruits.length).fill(0))
Given an array,
const fruits = ["kiwi", "banana", "mango", "apple", "durian"]
I am a little confused with the Array in the second state variable wrapping fruits.length, why do we have to wrap it with Array ? fruits is already an array. And just to make sure that I understand, the .fill(0) means that we will initialize every element in the array fruit with 0? Am I right ? To give a context, I have an array fruits and two buttons, one is to vote, and the other is to select randomly the next fruit. Every time I vote, the vote will increase and when I click the other button, random fruits will be displayed and the vote will be 0 for fruits that haven't got voted. This is the vote button code,
const handleVotes = () => {
setVotes((votes) =>
votes.map((vote, index) => index === selected ? vote + 1 : vote)
)
}
Mapping over the entire array of fruits is rather inefficient. We can use a Map which allows us to create a mapping between fruits and their votes. This allows us to update a fruit directly, without having to evaluate a condition against each element. Run the demo below and vote for your favourite!
function App({ fruits }) {
const [selected, setSelected] = React.useState(fruits[0])
const [votes, setVotes] = React.useState(() => new Map)
const select = fruit => event => {
setSelected(fruit)
}
const vote = event => {
setVotes(vs =>
new Map(vs).set(selected, (vs.get(selected) || 0) + 1)
)
}
return <div>
{fruits.map(f =>
<button
onClick={select(f)}
className={selected == f ? "selected" : ""}
children={f}
/>
)}
<button onClick={vote} children="vote" />
<pre>{JSON.stringify(Object.fromEntries([...votes]))}</pre>
</div>
}
ReactDOM.render(<App fruits={["๐Ÿฅ","๐ŸŽ","๐Ÿ‘"]} />, document.body)
.selected { border-color: teal; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>
Array(array.length).fill(0) is a JavaScript expression that creates a new array filled with a specified value. The expression creates an array with the length equal to the length of the array variable passed as an argument. The .fill(0) method is then used to fill the entire array with the value 0.
Here's an example:
const array = [1, 2, 3, 4, 5];
const newArray = Array(array.length).fill(0);
console.log(newArray);
// Output: [0, 0, 0, 0, 0]
In this example, array is an array with 5 elements, so Array(array.length).fill(0) creates a new array with 5 elements, all filled with the value 0.
I hope this helps!

How do I apply a composed function to each object in a list using Ramda?

I'm building a simple app using RamdaJS that aims to take a list of objects that represent U.S. states, and for each state, it should calculate the number of electoral votes and add that value as a new property to each object, called electoralVotes. The basic gist of the calculation itself (as inaccurate as it may be) is to divide the population by 600000, round that number down, and if the rounded-down number is 0, round it up to 1.
For simplicity, the array of states only includes a state name and population for each state:
const states = [
{ state: 'Alabama', population: 4833722 },
{ state: 'Alaska', population: 735132 },
{ state: 'Arizona', population: 6626624 },
// ... etc.
];
I created a function called getElectoralVotesForState that is created with nested levels of composition (A composed function that is built using another composed function). This function takes a state object, examines its population property, then calculates and returns the corresponding number of electoral votes.
const R = require('ramda');
// This might not be factually accurate, but it's a ballpark anyway
const POP_PER_ELECTORAL_VOTE = 600000;
const populationLens = R.lensProp("population");
// Take a number (population) and calculate the number of electoral votes
// If the rounded-down calculation is 0, round it up to 1
const getElectoralVotes = R.pipe(
R.divide(R.__, POP_PER_ELECTORAL_VOTE),
Math.floor,
R.when(R.equals(0), R.always(1))
);
// Take a state object and return the electoral votes
const getElectoralVotesForState = R.pipe(
R.view(populationLens),
getElectoralVotes
);
If I want to pass in a single state to the getElectoralVotesForState function, it works fine:
const alabama = { state: 'Alabama', population: 4833722 };
const alabamaElectoralVotes = getElectoralVotesForState(alabama); // Resolves to 8
While this seems to work for a single object, I can't seem to get this to apply to an array of objects. My guess is that the solution might look something like this:
const statesWithElectoralVotes = R.map(
R.assoc("electoralVotes", getElectoralVotesForState)
)(states);
This does add an electoralVotes property to each state, but it's a function and not a resolved value. I'm sure it's just a silly thing I'm getting wrong here, but I can't figure it out.
What am I missing?
To apply a function to to an array of items use R.map. Since you want the value you don't need to R.assoc:
const POP_PER_ELECTORAL_VOTE = 600000;
const populationLens = R.lensProp("population");
const getElectoralVotes = R.pipe(
R.divide(R.__, POP_PER_ELECTORAL_VOTE),
Math.floor,
R.when(R.equals(0), R.always(1))
);
const getElectoralVotesForState = R.pipe(
R.view(populationLens),
getElectoralVotes
);
const mapStates = R.map(getElectoralVotesForState);
const states = [{"state":"Alabama","population":4833722},{"state":"Alaska","population":735132},{"state":"Arizona","population":6626624}];
const result = mapStates(states);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
In addition, the lens is a bit redundant here, take the value of population using R.prop. I would also replace R.when with R.max.
const POP_PER_ELECTORAL_VOTE = 600000;
const getElectoralVotesForState = R.pipe(
R.prop('population'),
R.divide(R.__, POP_PER_ELECTORAL_VOTE),
Math.floor,
R.max(1)
);
const mapStates = R.map(getElectoralVotesForState);
const states = [{"state":"Alabama","population":4833722},{"state":"Alaska","population":735132},{"state":"Arizona","population":6626624}];
const result = mapStates(states);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
To add the property to each object, you'll need to get the value from the object, and then add it as a property to the object. This means that we need to use 2 functions - f (R.assoc) & g (getElectoralVotesForState), and apply both of them to the object - x, but one of them (R.assoc) also need the result of the other function.
you'll need to apply getElectoralVotesForState on the object to get the number, (g(x)) and then take the result, and it to the object (
To add the electoralVotes you can use R.chain in conjunction with R.assoc. When R.chain is applied to function - R.chain(f, g)(x) is equivalent to f(g(x), x). In your case
f - assoc
g - getElectoralVotesForState
x - the object
The combined function - R.chain(R.assoc('electoralVotes'), getElectoralVotesForState) becomes assoc('electoralVotes')(getElectoralVotesForState(object), object).
Example:
const POP_PER_ELECTORAL_VOTE = 600000;
const getElectoralVotesForState = R.pipe(
R.prop('population'),
R.divide(R.__, POP_PER_ELECTORAL_VOTE),
Math.floor,
R.max(1)
);
const mapStates = R.map(
R.chain(R.assoc("electoralVotes"), getElectoralVotesForState)
);
const states = [{"state":"Alabama","population":4833722},{"state":"Alaska","population":735132},{"state":"Arizona","population":6626624}];
const result = mapStates(states);
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
I think Ori Drori answers your question well. I have no suggested improvements. But I want to show that it's not too hard to code the current apportionment method used for the U.S. Congress, the Huntington-Hill Method:
// Huntington-Hill apportionment method
const apportion = (total) => (pops) =>
huntingtonHill (total - pops.length, pops .map (pop => ({...pop, seats: 1})))
// method of equal proportions
const huntingtonHill = (toFill, seats, state = nextSeat (seats)) =>
toFill <= 0
? seats
: huntingtonHill (toFill - 1, seats .map (s => s.state == state ? {...s, seats: s.seats + 1} : s))
// find state to assign the next seat
const nextSeat = (seats) =>
seats
.map (({state, population, seats}) => [state, population * Math.sqrt(1 / (seats * (seats + 1)))])
.sort (([_, a], [_1, b]) => b - a)
[0] [0] // ideally, use a better max implementation that sort/head, but low priority
// convert census data to expected object format
const restructure = results =>
results
.slice (1) // remove header
.map (([population, state]) => ({state, population})) // make objects
.filter (({state}) => ! ['District of Columbia', 'Puerto Rico'] .includes (state)) // remove non-states
.sort (({state: s1}, {state: s2}) => s1 < s2 ? -1 : s1 > s2 ? 1 : 0) // alphabetize
fetch ('https://api.census.gov/data/2021/pep/population?get=POP_2021,NAME&for=state:*')
.then (res => res.json())
.then (restructure)
.then (apportion (435))
.then (console .log)
.catch (console .warn)
.as-console-wrapper {max-height: 100% !important; top: 0}
Here we call the U.S. Census API to fetch the populations of each state, remove Washington DC and Puerto Rico, reformat these results to your {state, population} input format, and then call apportion (435) with the array of values. (If you have the data already in that format, you can just call apportion (435)), and it will assign one seat to each state and then use the Huntington-Hill method to assign the remaining seats.
It does this by continually calling nextSeat, which divides each state's population by the geometric mean of its current number of seats and the next higher number, then choosing the state with the largest value.
This does not use Ramda for anything. Perhaps we would clean this up slightly with some Ramda functions (for example, replacing pop => ({...pop, seats: 1}) with assoc('seat', 1)), but it would not likely be a large gain. I saw this question because I pay attention to the Ramda tag. But the point here is that the actual current method of apportionment is not that difficult to implement, if you happen to be interested.
You can see how this technique is used to compare different sized houses in an old gist of mine.

useState creating multiple arrays

I am creating a dropdown filter to update the search results of a page using react hooks. Basically, I am passing an array with the options that the user chose from the dropdown menu. I am successfully updating the global state with the new arrays BUT my issue is useState creates a NEW array instead of merging the results with the previous state.
Above you can see, I made two calls with different filter options and the global state now holds 2 arrays. My goal is to have both arrays merged into one.
This is the function where the global state is being updated.
const Results = () => {
const [filterList, setFilterList] = useState([])
const setGlobalFilter = (newFilter) => {
let indexFilter = filterList.indexOf(newFilter);
// console.log("Top level index", indexFilter)
indexFilter ?
setFilterList([...new Set([...filterList, newFilter])]) :
setFilterList(filterList => filterList.filter((filter, i) => i !== indexFilter))
}
// console.log("TopFilterSelection:", filterList)
return (
<div>
<Filter setFilter={(filterList) => setGlobalFilter(filterList)}/>
</div>
)
}
I've been checking on using prevState like this:
...
setFilterList(prevState => [...new Set([...prevState, newFilter])]) :
...
But I don't know what I am doing wrong.
Any feedback would be much appreciated!
This happens because newFilteris an array, not a word.
Should be
setFilterList(previous => [...new Set([...previous, ...newFilter])])
Also this
let indexFilter = filterList.indexOf(newFilter);
always returns -1 if newFilteris an array (since you a sending brand new array each time), it's not a falsy value, be careful
Use the .concat method.
setFilterList(filterList.concat(newFilter))
Read more about it here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat

React/Javascript Having trouble implementing dictionary

I'm trying to implement a dictionary in my React project. I'm guessing it's basic javascript so if you're not familiar with React you might still be able to help.
My goal is to have a dict which contains questions, where each question has an array of answers. I want to be able to add a question first, and then add answers later.
I can add the record but then I'm having trouble modifying the answer array:
Initial
const [dict, setDict] = React.useState([])
Adding dictionary record
question = "question1"
const newDict = dict.concat({ key: question1, value: [] });
setDict(newDict);
Modifying dictionary value
const answer = "valueToAdd"
const newDict = dict;
newDict["test"].concat(answer);
setDict(newDict);
I'm getting the following error. Seems like newDict["test"] is undefined, even though I just added it. What am I doing wrong?
TypeError: Cannot read property 'concat' of undefined
Also, is this the correct way to append to a dictionary? I'm doing it like this so dictionary will re-render.
I believe you are setting the initial value of your dict state variable to an empty array rather than a true dictionary like you may be intending. Javascript objects behave very similarly to dictionaries in other languages, so you may want to use an object for this instead.
Declaring your state
const [dict,setDict] = useState({})
Here we are initializing the state (dict) to an empty javascript object. These objects behave similarly to the dictionaries you are familiar with in other languages.
Adding a key value pair
setDict(prevDict => ({...prevDict, newKey: []}))
Here we are using an arrow function to provide the previous state to the object we will be using to update the state. This is done to keep the previous state immutable. The spread/rest operator "..." is being used to collect all of the values of the previous state, then add the new key-value pair
Updating Values
setDict(prevDict => ({...prevDict, keyToUpdate: [...prevDict.keyToChange, "newValue"]}))
Similar to the above, the spread/rest operator is being used, this time in two places. First to retain the keys of the previous state, second to keep all of the values from the array that we are going to be adding to.
Your dict can be an object literal.
//function that creates a unique key every time
const key = ((key) => () => key++)(1);
//item is pure component, won't re render unless props change
const Item = React.memo(function Item({ addValue, item }) {
return (
<div>
<pre>{JSON.stringify(item, undefined, 2)}</pre>
<button
onClick={() => addValue(item.key, Date.now())}
>
add random value
</button>
</div>
);
});
const App = () => {
//dict local state
const [dict, setDict] = React.useState({});
//add item to dict using useCallback so function
// addDict is only created whe App mounts
const addDict = React.useCallback(
() =>
//callback to setDict prevent dict to be a dependency
// of useCallback
setDict((dict) => {
const k = key();
return {
...dict,//copy dict
[k]: { key: k, values: [] },//set k with new object
};
}),
[]//no dependency
);
//function to add value to dict use useCallback again
// so function is only created when App mounts
const addValue = React.useCallback(
(key, value) =>
//pass callback to setDict to prevent dependency
setDict((dict) => ({
...dict,//copy dict
[key]: {//set this key
...dict[key],//copy dict[key]
values: dict[key].values.concat(value),//add value
},
})),
[]
);
return (
<div>
<div>
<button onClick={addDict}>add dict</button>
</div>
<div>
<ul>
{Object.values(dict).map((dict) => (
<Item
key={dict.key}
addValue={addValue}
item={dict}
/>
))}
</ul>
</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
More info on updating immutable state can be found here

Displaying state list in JSX

I have some simple code:
class App extends React.Component {
state = {
letters: ["a", "b", "c"]
};
addLetter = () => {
const { letters } = this.state;
letters.push("d");
this.setState({ letters });
};
render() {
return (
<div>
<button onClick={this.addLetter}>Click to add a letter</button>
<h1>letters: {this.state.letters}</h1>
</div>
);
}
}
render(<App />, document.getElementById("root"));
Working example here.
When I click the button, the letter 'd' should be added to the letters in state. This works as intended. However, the letters do not update on the page as one would expect when state changes.
It's worth noting that:
<h1>letters: {this.state.letters.reduce((a, l) => a + l, "")}</h1>
or:
<h1>letters: {this.state.letters.toString()}</h1>
do update the page as expected.
Why is this?
You need to replace the letter's state (the array), instead of mutating it (working example). According to the react docs: Do Not Modify State Directly.
When you're converting the array to a string, the current string is different from the previous string because strings are immutable. The array however stays the same array even if you add another item to it.
Example:
const a = [1, 2, 3];
const strA = a.toString();
a.push(4);
const strB = a.toString();
console.log(a === a); // true
console.log(strA === strB); // false
Solution: create a new array from the old one.
addLetter = () => {
const { letters } = this.state;
this.setState({ letters: [...letters, 'd'] });
};
You have got an answer though. Would like to explain it a bit further.
You are basically not changing the state ever. And react only detects the changes in state. Now tricky part is that you might be thinking that you are updating the array object and hence changing the state but that's not true. The original array reference is never changed in state and hence no change. So, to solve this in es6 you could use ... operator as follows in your addLetter declaration.
addLetter = () => {
const { letters } = this.state;
letters.push("d");
this.setState({ letters:[...letters] });
};
... operator basically clones the original object and is a short hand operator from es6.
You could use clone method as well e.g. Object.assign.
Hope it helps!
I've forked and fixed your code here: https://codesandbox.io/s/ywnmr47q8z
1) You have to mutate the new state
2) you have to call .join("") to build string from array. now it is working

Categories