React useState setter wont update the value inside a context - javascript

So, I'm using the React Context API to manage some global state for my app
const blueprintList = [
{
id: new Date().toISOString(),
name: 'Getting started',
color: '#c04af6',
code: '<h1>Hello World!</h1>,
},
];
export const BlueprintsContext = createContext();
export const BlueprintsProvider = ({ children }) => {
const [blueprints, setBlueprints] = useState(blueprintList);
let [selected, setSelected] = useState(0);
const [count, setCount] = useState(blueprints.length);
useEffect(() => {
setCount(blueprints.length);
}, [blueprints]);
// just for debugging
useEffect(() => {
console.log(`DEBUG ==> ${selected}`);
}, [selected]);
const create = blueprint => {
blueprint.id = new Date().toISOString();
setBlueprints(blueprints => [...blueprints, blueprint]);
setSelected(count);
};
const remove = blueprint => {
if (count === 1) return;
setBlueprints(blueprints => blueprints.filter(b => b.id !== blueprint.id));
setSelected(-1);
};
const select = blueprint => {
const index = blueprints.indexOf(blueprint);
if (index !== -1) {
setSelected(index);
}
};
const value = {
blueprints,
count,
selected,
select,
create,
remove,
};
return (
<BlueprintsContext.Provider value={value}>
{children}
</BlueprintsContext.Provider>
);
};
Then I'm using the remove function inside the component like this.
const Blueprint = ({ blueprint, isSelected }) => {
const { select, remove } = useContext(BlueprintsContext);
return (
<div
className={classnames(`${CSS.blueprint}`, {
[`${CSS.blueprintActive}`]: isSelected,
})}
key={blueprint.id}
onClick={() => select(blueprint)}
>
<div
className={CSS.blueprintDot}
style={{ backgroundColor: blueprint.color }}
/>
<span
className={classnames(`${CSS.blueprintText}`, {
['text-gray-300']: isSelected,
['text-gray-600']: !isSelected,
})}
>
{blueprint.name}
</span>
{isSelected && (
<IoMdClose className={CSS.close} onClick={() => remove(blueprint)} />
)}
</div>
);
};
The parent component receives a list of items and renders a Blueprint component for each item. The problem is that when the IoMdClose icon is pressed the item will be removed from the list but the selected value won't update to -1. 🤷‍♂️

Ciao, I found your problem: in function select you already set selected:
const select = blueprint => {
const index = blueprints.indexOf(blueprint);
if (index !== -1) {
setSelected(index); // here you set selected
}
};
When you click on IoMdClose icon, also select will be fired. So the result is that this useEffect:
useEffect(() => {
console.log(`DEBUG ==> ${selected}`);
}, [selected]);
doesn't log anything.
I tried to remove setSelected(index); from select function and when I click on IoMdClose icon, selected will be setted to -1 and useEffect logs DEBUG ==> -1.
But now you have another problem: if you remove setSelected(index); from select and you try to select one blueprint from left treeview, blueprint will be not selected. So I re-added setSelected(index); in select function. Removed setSelected(-1); from remove function and now useEffect doesn't log anything!
I think this happens because you are trying to set selected to an index that doesn't exists (because you removed blueprint on icon click). To verify this, I modified setSelected(index); in select function to setSelected(0); and infact now if I remove one blueprint, useEffect will be triggered and logs DEBUG ==> 0.
If the idea behind setSelected(-1); is to deselect all the blueprints in treeview, you could do something like:
export const BlueprintsProvider = ({ children }) => {
....
const removeSelection = useRef(0);
useEffect(() => {
if (blueprints.length < removeSelection.current) setSelected(-1); // if I removed a blueprint, then fire setSelected(-1);
setCount(blueprints.length);
removeSelection.current = blueprints.length; //removeSelection.current setted as blueprints.length
}, [blueprints]);
And of course remove setSelected(-1); from remove function.

Related

React: Unable to filter list of components

I am trying to have a list of table fields which can be added and removed at any time.
Issue
As soon as I click delete any element in the middle(except the last one), it deletes all items underneath it
Code is here
I am not sure what is it that's doing wrong, it removes all the elements, even if I try to delete a specific one.
Adding code here also
import React, { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
const ListComponent = ({ remove, id }) => {
return (
<tr>
<td>
<input />
</td>
<td><button onClick={() => remove(id)}>Delete</button></td>
</tr>
)
}
const App = () => {
const [array, setArray] = useState([]);
const remove = (itemId) => {
setArray(array.filter(each => each.id !== itemId))
}
const addListComponent = () => {
let id = uuidv4();
setArray(oldArray => [...oldArray, {
id,
jsx: <ListComponent key={id} id={id} remove={remove}/>
}])
}
return (
<div>
<button onClick={addListComponent}>Add ListComponent</button>
{array.length > 0 && array.map(each => each.jsx)}
</div>
)
}
export default App;
the issue is everytime you create a new element a new remove function is created with reference to that current state in time. When you create an element, you create a jsx with a remove pointing to that array reference specific in time.
You could fix that changing your remove function to something like:
const remove = (itemId) => {
setArray(prevArr => prevArr.filter(each => each.id !== itemId))
}
which would ensure that you will always have the correct reference to the current state.
Although that solves the issue, I would suggest to follow the guidelines and only store the necessary information at your state, and pass it to JSX at render when you map:
const App = () => {
const [array, setArray] = useState([]);
const remove = (itemId) => {
setArray(array.filter(each => each.id !== itemId))
}
const addListComponent = () => {
let id = uuidv4();
setArray(oldArray => [...oldArray, { id }])
}
return (
<div>
<button onClick={addListComponent}>Add ListComponent</button>
{array.length > 0 && array.map(({ id }) => <ListComponent key={id} id={id} remove={remove}/>)}
</div>
)
}

How do I call a function within another function when the data I need is in the global scope?

I have a set of card objects that I map over.
When I click on a card it adds the selected class which in turn gives it a border to show the user it is selected, it also adds the id of the card to the selectedCards useState array.
WHAT I WANT TO HAPPEN:
Each card object has a creditAvailable key state which is equal to a figure.
On selection (click) of the card, in addition to selecting the card I would also like to add up the creditAvailable and display it on the screen. and when I unselect the card I would like the figure to go down.
WHAT I HAVE TRIED:
I thought it would be as simple as calling the function to add up the credit inside the first function which selects the card, however when console logging inside the first function I see that the state has not yet updated. (scope).
I then tried to call the function outside of the first function but it gave me an infinite loop. Here is my code.
Any ideas? Thanks
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const [total, setTotal] = useState();
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
// addUpCreditAvailable(); // nothing happens
console.log(selectedCards); // []
};
console.log(selectedCards) // [1] for example. This is in the global scope
const addUpCreditAvailable = () => {
console.log("inside add up credit");
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
setTotal(result);
return result;
};
return (
<div className="Container">
<UserInputForm submitData={handleSubmitData} />
<h1> Cards available to you displayed are below!</h1>
{cards.map(
({
id,
name,
number,
apr,
balanceTransfer,
purchaseDuration,
creditAvailable,
expiry,
}) => (
<CreditCard
key={id}
name={name}
number={number}
apr={apr}
balanceTransferDuration={balanceTransfer}
purchaseOfferDuration={purchaseDuration}
creditAvailable={creditAvailable}
expiry={expiry}
onClickCard={() => handleSelectCard(id)}
selected={selectedCards.includes(id)}
/>
)
)}
<span> £{total}</span>
)}
I figured it out with the help from above. As Wilms said i had to return the result of the handleSelectCard function and return the result of the addUpCredit function. Then I called the addUpCreditAvailable with the selectedCards state and stored the result in a variable which i then displayed in my render method.
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
return selectedCards;
};
const addUpCreditAvailable = (selectedCards) => {
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
return result;
};
const totalCredit = addUpCreditAvailable(selectedCards);
render method:
render (
[...]
{selectedCards.length && (
<div className={bem(baseClass, "total-credit")}>
Total Credit available: £{totalCredit}
</div>
)}
[...]
)

React with a handler function that returns a function (a curried) - what's the benefit of using it?

I'm writing a component that renders Checkboxes of Categories as part of a large system, here is the code:
import React, { useState, useEffect } from 'react';
const Checkbox = ({ categories, handleFilter }) => {
const [checked, setChecked] = useState([]);
// get all the categories that user clicked and send them to the Backend
const handleToggle = c => () => {
const currentCategoryId = checked.indexOf(c); // return the first index with cateogry 'c
const newCheckedCategoryId = [...checked];
// if currently checked wasn't already in Checked state ==> Push
// else Pull it
// User want to Check
if (currentCategoryId === -1) {
// not in the state
newCheckedCategoryId.push(c);
}
else {
// User wants to Uncheck
newCheckedCategoryId.splice(currentCategoryId, 1); // remove it from the array
}
// console.log(newCheckedCategoryId);
setChecked(newCheckedCategoryId);
handleFilter(newCheckedCategoryId, 'category');
}
return (
<>
{
categories.map((cat, index) => (
<li key={index} className='list-unstyled'>
<input
onChange={handleToggle(cat._id)}
value={checked.indexOf(cat._id) === -1}
type='checkbox'
className='form-check-input' />
<label className='form-check-label'>{cat.name}</label>
</li>
))
}
</>
)
};
export default Checkbox;
Whenever a user clicks one of the Checkboxes the handler handleToggle is invoked.
What I don't understand is why I need to use a curried function?
When I try to use:
const handleToggle = c => { ... }
Instead of:
const handleToggle = c => () => { ... }
React throws:
Unhandled Rejection (Error): Too many re-renders. React limits the number of renders to prevent an infinite loop.
What's the deal here and why is it required to use a curried function ?
The main problem is handleToggle has been evaluated and assigned the result of that function to onChange instead of passing as a callback function.
Based on that you need to change how you call the function handleToggle as:
onChange={() => handleToggle(cat._id)}
From your example:
onChange={handleToggle(cat._id)}
And at the end const handleToggle = c => { ... } will be just fine.
According to your onChange function you are calling function directly instead of assigning it:
onChange={handleToggle(cat._id)} <- Call as soon as this sees
instead you have to do this:
onChange={() => handleToggle(cat._id)}
And define handleToggle like this:
// get all the categories that user clicked and send them to the Backend
const handleToggle = c => {
const currentCategoryId = checked.indexOf(c); // return the first index with cateogry 'c
const newCheckedCategoryId = [...checked];
// if currently checked wasn't already in Checked state ==> Push
// else Pull it
// User want to Check
if (currentCategoryId === -1) {
// not in the state
newCheckedCategoryId.push(c);
}
else {
// User wants to Uncheck
newCheckedCategoryId.splice(currentCategoryId, 1); // remove it from the array
}
// console.log(newCheckedCategoryId);
setChecked(newCheckedCategoryId);
handleFilter(newCheckedCategoryId, 'category');
}

React-Native Item Selection

I want to select an item from a list and to highlight it. I got a function on how to delete a particular item from the array, i want to reuse this function to highlight it.
Data is an array of objects. I want to use hooks to manage state. id is asigned to every item in the array. I want to presshandler function as property for onPress.
const [selected, setSelect] = useState(Data);
const presshandler = (id) => {
setSelect((prev) => {
return prev.filter((list) => list.id !== id);
});
};
If I get it right, you want to set highlighted to true after you press an image:
const toggleSelect = id => {
setSelect(prev => {
return prev.map(item => {
if(item.id !== id) return item
return {
...item,
highlighted: !item.highlighted
}
})
})
}
// Usage:
<Image onPress={() => toggleSelect(image.id)} {...rest} />
After reading your comment, I see you want a toggle. So I edited my example.

ReactJS Collapsible element breaks when setting new title and body in hooks

This is my first React project and I would like to use hooks, but I seem to have an issue with an element
function Document(props) {
const [id] = useState(props.match.params.id);
const [document, setDocument] = useState(0);
///////////////////////////////////////
useEffect(function getTheDocument() {
getDocument({
id,
}).then(document => {
setDocument(document.data);
}).catch(error => {
console.log(error);
});
}, [id]);
////////////////////////////////////////////
const [body, setBody] = useState(0);
const [title, setTitle] = useState(0);
////////////////////////
useEffect(function setBodyAndTitle() {
if (document) {
setTitle(document.title);
setBody(document.description);
}
}, [document]);
//////////////////////////
const changeBody = (data) => {
...
const module = ...
setTitle(module[0].title);
setBody(module[0].body);
}
}
So that is how I handle the body and title. When a button is clicked, changeBody is called which finds an object based on some values and sets a new title and body. But that component which is a simple collapsible menu like this. https://www.npmjs.com/package/react-collapse. The button is in the collapsible data. Am I handling this wrong? The menu is no longer collapsible all the hidden button can be seen. I expected the title and body to change ... that's all.
you don't need to handle states of title and body if these are a properties of document object. The same for id, but create an effect to do the request when id change and use state for document by setting a value for this state inside the effect, and create a state for collapse state.
Here is my code to handle this logic
import React from 'react'
function Document({ id = 1 }) {
const [document, setDocument] = React.useState({})
const [innerId, setId] = React.useState(() => id)
const [collapsed, setCollapsed] = React.useState(false)
React.useEffect(() => {
// Call my API request when my ID changes
fetch("https://jsonplaceholder.typicode.com/posts/"+innerId)
.then(response => response.json())
.then(setDocument)
console.log("rerender")
}, [innerId])
// For demo only, increment the ID every 5 seconds
React.useEffect(() => {
const intervalId = setInterval(() =>setId(i => i+1) , 5000)
return () => clearTimeout(intervalId)
}, [])
const toggle = React.useCallback(() => setCollapsed(c => !c))
// You can use useMemo hooks to get title and body if you want
const [title, body] => React.useMemo(
() => [document.title, document.body]
, [document]
)
return (<article>
<header>{document.title}</header>
<hr></hr>
<p style={{height: collapsed ? 0: "100%", overflow: "hidden"}}>
{document.body}
</p>
<button onClick={toggle}>Collapse</button>
</article>
);
}
ReactDOM.render(<Document id={1}/>, document.querySelector("#app"))
This is really cool. I'm beginning to like React. Seems that you can create a
memoized functional component https://logrocket.com/blog/pure-functional-components/ which doesn't update if the props don't change. In my case the props don't change at all after the initial render.
import React, { memo } from 'react';
function PercentageStat({ label, score = 0, total = Math.max(1, score) }) {
return (
<div>
<h6>{ label }</h6>
<span>{ Math.round(score / total * 100) }%</span>
</div>
)
}
function arePropsEqual(prevProps, nextProps) {
return prevProps.label === nextProps.label;
}
// Wrap component using `React.memo()` and pass `arePropsEqual`
export default memo(PercentageStat, arePropsEqual);

Categories

Resources