I'm a newbie in React. I have 6 divs and whenever I call foo() I want to add a number to the first div that's empty.
For example, let's say that the values of the six divs are 1,2,0,0,0,0 and when I call foo(), I want to have 1,2,3,0,0,0.
Here is what I've tried:
var index = 1;
function foo() {
let var x = document.getElementsByClassName("square") // square is the class of my div
x[index-1].innerHTML = index.toString()
index++;
}
I don't know when I should call foo(), and I don't know how should I write foo().
The "React way" is to think about this is:
What should the UI look like for the given data?
How to update the data?
Converting your problem description to this kind of thinking, we would start with an array with six values. For each of these values we are going to render a div:
const data = [0,0,0,0,0,0];
function MyComponent() {
return (
<div>
{data.map((value, i) => <div key={i}>{value}</div>)}
</div>
);
}
ReactDOM.render(<MyComponent />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Now that we can render the data, how are we going to change it? From your description it sounds like every time a function is called, you want change the first 0 value in the array to another value. This can easily be done with:
// Find the index of the first 0 value
const index = data.indexOf(0);
if (index > -1) {
// if it exists, update the value
data[index] = index + 1;
}
To make this work properly with React we have to do two things: Keep track of the updated data in state, so that React rerenders the component when it changes, and update the data in a way that creates a new array instead of mutating the existing array.
You are not explaining how/when the function is called, so I'm going to add a button that would trigger such a function. If the function is triggered differently then the component needs to be adjusted accordingly of course.
function update(data) {
const index = data.indexOf(0);
if (index > -1) {
data = Array.from(data); // create a copy of the array
data[index] = index + 1;
return data;
}
return data;
}
function MyComponent() {
var [data, setData] = React.useState([0,0,0,0,0,0]);
return (
<div>
{data.map((value, i) => <div key={i}>{value}</div>)}
<button onClick={() => setData(update(data))}>Update</button>
</div>
);
}
ReactDOM.render(<MyComponent />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
You would use state to hold the value and then display the value of that variable.
If you're using functional components:
const App = () => {
const [values, setValues] = React.useState([0, 0, 0, 0, 0, 0]);
const [index, setIndex] = React.useState(0);
const foo = () => {
const tempValues = [...values];
tempValues[index] = index;
setValues(tempValues);
setIndex((index + 1) % values.length);
}
return (
<div>
{ values.map((value) => <div key={`square-${value}`}>{value}</div>) }
<button onClick={ foo }>Click me</button>
</div>
);
};
In class-based components:
constructor(props) {
super(props);
this.state = {
values: [0, 0, 0, 0, 0, 0],
index: 0
};
this.foo = this.foo.bind(this);
}
foo() {
const tempValues = [...values];
const newIndex = index + 1;
tempValues[newIndex] = newIndex;
this.setState({
values: tempValues,
index: newIndex
});
}
render() {
return (
<div>
{ values.map((value) => <div key={`square-${value}`>value</div>) }
<button onClick={ this.foo}>Click me</button>
</div>
);
}
If you need to set the innerHTML of a React component, you can try this:
return <div dangerouslySetInnerHTML={foo()} />;
the foo() here returns the value you want to post in the div.
But in my opinion, your way of thinking on this problem is wrong.
React is cool, but the logic is a bit different of common programming :D
The ideal approach would be to have the divs created by React (using its render method). Then you can pass a variable from array, which is stored in your state. You then just need to change this array within the state and it'll reflect in your view. If you need a working example, just let me know.
However, if you want to update the divs that are not created using react, then you need to use a dirty approach. I would suggest not to use react if you can't generate the view from react.
React is good to separate the concerns between the view and the data.
So the concept of state for this example is useful to store the data.
And the JSX, the React "template" language, to display the view.
I propose this solution:
import React from "react";
class Boxes extends React.Component {
state = {
divs: [1, 2, 3, 0, 0, 0]
};
add() {
// get the index of the first element equals to the condition
const index = this.state.divs.findIndex(elt => elt === 0);
// clone the array (best practice)
const newArray = [...this.state.divs];
// splice, i.e. remove the element at index AND add the new character
newArray.splice(index, 1, "X");
// update the state
// this is responsible, under the hood, to call the render method
this.setState({ divs: newArray });
}
render() {
return (
<div>
<h1>Boxes</h1>
{/* iterate over the state.divs array */}
{this.state.divs.map(function(elt, index) {
return (
<div
key={index}
style={{ border: "1px solid gray", marginBottom: 10 }}
>
{elt}
</div>
);
})}
<button onClick={() => this.add()}>Add a value</button>
</div>
);
}
}
export default Boxes;
Related
Solved - wasn't aware of the useRef hook which helped me track each individual mapped item.
I have a set of results mapped out within a card element. I want to keep a click count for each of those elements, but with a global JS variable, it counts the clicks of all elements if I call that variable on more than one clickable element per session. I have tried to do id.index, adding (id) + index etc but am stumped. How do I properly use the unique id's to track the index for each card? Thanks
function onClick(id) {
let index = 0;
index++;
if (index >= 1) {
dosomething
} else if (index === 0) {
dosomethingelse
}
}
It's not clear what and how you want to count and onclick events.
Assuming that you need to keep track of clicks on each element/id:
You can use the useRef hook and keep it a global object to track the number of clicks per id.
const clicksPerId = useRef({});
function onClick(id) {
if (!clicksPerId.current[id]) {
clicksPerId.current[id] = 0;
}
clicksPerId.current[id]++;
// whatever you want to do with the clicks count
}
I'm kinda confused by your question to be honest however for working with arrays in javascript / React maybe you'll find some of these helpful
Getting the array length
const MyComponent = () => {
const [myArray, setMyArray] = useState([1, 2]);
// find the length of the array
const getArrayLength = () => {
return myArray.length;
}
return (
<p>hello there</p>
)
}
Doing something with the index of a maped component:
const MyComponent = () => {
const [myArray, setMyArray] = useState([1, 2]);
const handleClick = (index) => {
// do somthing with the index of the el
};
return (
<>
{ myArray.map((el, index) => {
return (
<p
key={index}
onClick={() => handleClick(index)}
>
el number { el }
</p>
)
})
}
</>
)
}
I am getting different behaviour depending on whether I am using a boolvalue on with useState, or whether I am using a bool value inside an object with useState.
This first bit of code will show the hidden text when the button is pressed. It uses contextMenuIsOpen which is a bool directly on the state, to control the visibility of the hidden text.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
setContextMenuIsOpen(true);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuIsOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
This next bit of code uses a property on an object which is on the state. It doesn't show the hidden text when I do it this way.
const Parent = () => {
const [contextMenuState, setContextMenuState] = useState({ isOpen: false, x: 0, y: 0, clipboard:null });
const [contextMenuIsOpen, setContextMenuIsOpen] = useState(false);
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
return <div><h1>Hello</h1>
<button onClick={openChild}>Open Child</button>
{contextMenuState.isOpen &&
<h1>hidden</h1> }
</div>
}
export default Parent;
React checks objects for equality by checking their reference.
Simply, look at the below example.
const x = { a : 1, b : 2};
x.a = 3;
console.log(x===x);
So when you do the below,
const openChild = ()=>{
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
You are not changing the reference of contextMenuState. Hence, there is no real change of state and setContextMenuState does not lead to any rerender.
Solution:
Create a new reference.
One example, is using spread operator:
const openChild = ()=>{
setContextMenuState({ ...contextMenuState , isOpen : true });
}
The problem with your second approach is that React will not identify that the value has changed.
const openChild = () => {
contextMenuState.isOpen = true;
setContextMenuState(contextMenuState);
}
In this code, you refer to the object's field, but the object reference itself does not change. React is only detecting that the contextMenuState refers to the same address as before and from its point of view nothing has changed, so there is no need to rerender anything.
If you change your code like this, a new object will be created and old contextMenuState is not equal with the new contextMenuState as Javascript has created a new object with a new address to the memory (ie. oldContextMenuState !== newContextMenuState).:
const openChild = () => {
setContextMenuState({
...contextMenuState,
isOpen: true
});
}
This way React will identify the state change and will rerender.
State is immutable in react.
you have to use setContextMenuState() to update the state value.
Because you want to update state according to the previous state, it's better to pass in an arrow function in setContextMenuState where prev is the previous state.
const openChild = () =>{
setContextMenuState((prev) => ({...prev, isOpen: true }))
}
Try change
contextMenuState.isOpen = true;
to:
setContextMenuState((i) => ({...i, isOpen: true}) )
never change state like this 'contextMenuState.isOpen = true;'
I'm implementing a shopping cart for a ecommerce website. The shopping cart is a state variable shopCart represented by an array of objects. Each object contains information about a product, such as title and price. I am trying to implement a remove button, which is actually doing what is intended from it, which is to remove items from the shopCart state, but the changes are not represented on the screen render. I can empty the cart, but the screen still shows the products. Here is the main code of the shopping cart page:
return (
<div class={styles.container}>
<h1>Product</h1><h1>Quantity</h1><h1>Unit price</h1><h1>Total price</h1><div></div>
{
shopCart.map((product, i, array) => <CartItem key={product.id} product={product} index={i} array={array}/>)
}
</div>
)
And here is the implementation of CartItem.js
const CartItem = (props) => {
let { shopCart, setShopCart } = useContext(Context);
let product = props.product;
// takes the identification of a shopping cart product and removes it from the cart
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart;
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={() => {
decrease(product.id);
}}>Remove</button>
</div>
)
}
Why isn't it rendering the cart correctly, even though the cart items are being removed after each click of the remove button ?
Issue
You are mutating state. You save a reference to state, mutate it, then save it back into state, so the array reference never changes. React uses shallow equality when checking if state or props update.
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart; // <-- Saved state ref
aux.splice(i, 1); // <-- mutation
setShopCart(aux); // <-- Saved ref back to state
}
}
})
}
Solution
The correct way to update arrays in react state is to copy the array elements into a new array reference. This can be easily accomplished by filtering the current cart by item id. I also suggest changing the argument name so it is clearer what it represents.
const decrease = (id) => {
setShopCart(shopCart => shopCart.filter(item => item.id !== id));
}
You're modifying the shopCart (aux is a reference) directly which is both the context and the collection that you're iterating over. You need to make sure you're updating a copy of the shopping cart and resetting the context. Minimally, you can do the following:
const decrease = (element) => {
shopCart.forEach((el, i) => {
if (el.hasOwnProperty('id')) {
if (el.id === element) {
let aux = shopCart.slice(); // makes a copy
aux.splice(i, 1);
setShopCart(aux);
}
}
})
}
However, I suggest using the approach Drew recommended. The current approach isn't ideal.
The solution is much simpler than you think. You can use array.filter to remove the matching product by id.
const CartItem = (props) => {
const { product} = props;
let { shopCart, setShopCart } = useContext(Context);
// takes the identification of a shopping cart product and removes it from the cart
const handleClick = (e) => {
const filteredShopCart = shopCart.filter(item => item.id !== product.id);
setShopCart(filteredShopCart);
};
return (
<div>
<img src={product.image}></img>
<h1>{product.quantity}</h1>
<h1>{product.price}</h1>
<h1>{product.price * product.quantity}</h1>
<button onClick={handleClick}>Remove</button>
</div>
);
};
I have a block of code like this:
randomArray(arrayOfImage){
//algorithm goes here
return shuffledArray;
}
shuffle(){
this.randomArray(images);
this.forceUpdate();
}
render(){
let array = this.randomArray(images); //images is a declared array
let squares = array.map((item,index) => <Squares/> )
return(
<div>
<div id='square-container'>
{squares}
</div>
<button className='btn' onClick = {this.shuffle.bind(this)}>Shuffle</button>
</div>
)
}
Basically, I have an array images declared. The function randomArray() return a shuffled version of images. And then, for each of the item inside the shuffled array array the browser will render a div with the given props.
Now I have a button so that the user can shuffle the array themselves. Here, I use forceupdate() because even though the array is shuffled when the button is clicked, the DOM won't update because there is no changes in the state.
It works! But since using forceupdate() is not encouraged, how should I make this less... say, amateur?
Sure, React will render an element again as soon as it's state gets changed, or it receives new props from it's parent component (or a state library).
You can read more about how react handles the rendering in their documentation
So, to handle this, just create a new array based on your original one, and then set it to the state again.
In the olden days, you would require a class for setting the state on component level
const target = document.getElementById('container');
class ArrayOfImages extends React.Component {
constructor() {
// don't forget to call super
super();
// set the initial state
this.state = {
images: [
'https://c402277.ssl.cf1.rackcdn.com/photos/18357/images/featured_story/Medium_WW252652.jpg?1576698319',
'https://c402277.ssl.cf1.rackcdn.com/photos/882/images/circle/African_Elephant_7.27.2012_hero_and_circle_HI_53941.jpg?1345532748',
'https://c402277.ssl.cf1.rackcdn.com/photos/1732/images/circle/Asian_Elephant_8.13.2012_Hero_And_Circle_HI_247511.jpg?1345551842'
]
};
// bind the function we are going to call from the render function
this.shuffle = this.shuffle.bind( this );
}
shuffle() {
// create a shallow copy of the state object
const copyOfState = this.state.images.slice();
// shuffle it a bit (Durstenfeld shuffle: http://en.wikipedia.org/wiki/Fisher-Yates_shuffle#The_modern_algorithm)
for (let i = copyOfState.length; i--;) {
let target = Math.floor( Math.random() * (i+1) );
[copyOfState[i], copyOfState[target]] = [copyOfState[target], copyOfState[i]];
}
// update the existing state with the new array
this.setState({ images: copyOfState });
}
render() {
const { images } = this.state;
return (
<div>
<h1>Some elephants</h1>
{ images.map( img => <img key={ img } alt={ img } src={ img } /> ) }
<div><button type="button" onClick={ this.shuffle }>Shuffle images</button></div>
</div>
);
}
}
ReactDOM.render( <ArrayOfImages />, target );
<script id="react" src="https://cdnjs.cloudflare.com/ajax/libs/react/15.6.2/react.js"></script>
<script id="react-dom" src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/15.6.2/react-dom.js"></script>
<div id="container"></div>
But now, you could do it by using the useState helper for that
// this would be an import in normal code
const { useState } = React;
const target = document.getElementById('container');
const shuffle = arr => {
// create a shallow copy of the array
const copy = arr.slice();
// shuffle it a bit (Durstenfeld shuffle: http://en.wikipedia.org/wiki/Fisher-Yates_shuffle#The_modern_algorithm)
for (let i = copy.length; i--;) {
let target = Math.floor( Math.random() * (i+1) );
[copy[i], copy[target]] = [copy[target], copy[i]];
}
return copy;
}
const ArrayOfImages = () => {
const [images, updateImages] = useState([
'https://c402277.ssl.cf1.rackcdn.com/photos/18357/images/featured_story/Medium_WW252652.jpg?1576698319',
'https://c402277.ssl.cf1.rackcdn.com/photos/882/images/circle/African_Elephant_7.27.2012_hero_and_circle_HI_53941.jpg?1345532748',
'https://c402277.ssl.cf1.rackcdn.com/photos/1732/images/circle/Asian_Elephant_8.13.2012_Hero_And_Circle_HI_247511.jpg?1345551842'
]);
return (
<div>
<h1>Some elephants</h1>
{ images.map( img => <img key={ img } alt={ img } src={ img } /> ) }
<div><button type="button" onClick={ () => updateImages( shuffle( images ) ) }>Shuffle images</button></div>
</div>
);
}
ReactDOM.render( <ArrayOfImages />, target );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="container"></div>
The shuffle algorithm comes from this answer
I use a dictionary to store the likes of posts in every card.
constructor(props) {
super(props);
this.state = {
like_dict: {}
};
this.likeClicked = this.likeClicked.bind(this)
}
I have a like button on every card and the text denotes the number of likes.
<Button transparent onPress={() => {this.likeClicked(poid, 0)}}>
<Icon name="ios-heart-outline" style={{ color: 'black' }} />
</Button>
<Text>{this.state.like_dict[poid]}</Text>
The likedClicked function looks like this. I set the state of like_dict, but the text above won't rerender.
likeClicked(poid, liked){
var like_dict_tmp = this.state.like_dict
if (liked){
like_dict_tmp[poid] -= 1
}else{
like_dict_tmp[poid] += 1
}
this.setState({
like_dict: like_dict_tmp
})
}
One of the key principles of React is never mutate the state directly.
When you do var like_dict_tmp = this.state.like_dict, like_dict_tmp is referencing the same object in the state so altering like_dict_tmp will mutate the state directly, so the subsequent setState will not update the component.
You can create a shallow copy with var like_dict_tmp = { ...this.state.like_dict } or replace the whole function with:
this.setState((prevState) => ({
like_dict: {
...prevState,
[poid]: (prevState[poid] || 0) + (liked ? 1 : -1),
},
}));
You need to copy the state before modifying it. What you are doing is just creating a link to the state in like_dict_tmp (either use what I am doing below, or use concat with an empty array):
var like_dict_tmp = this.state.like_dict.concat([])
likeClicked(poid, liked) {
var like_dict_tmp = this.state.like_dict.slice()
if (liked) {
like_dict_tmp[poid] -= 1
} else {
like_dict_tmp[poid] += 1
}
this.setState({
like_dict: like_dict_tmp
})
}
Also, {} is an object in JS, and is not typically called a dictionary