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
Related
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;'
Hi i have parrent and child component. Parrent component is receiving data from server. This data are saved in state.data. Now, when i do action in child component, it should be call method from parrent controller. This is working now. Problem is inside this method which i am calling. I am receiving id as parameter. This data in parrent have list of items (packages) and every item has id. I need to update only one of them by id (or other way i don't know right way). Please how i can do it? I need to update isOpen state only that one item i open by clicking on button in child component
My method (but i am not sure if i started to do this right way), i stucked on this problem for while:
changeIsOpenState(typeOfPart: Number, id: Number) {
console.log(this.state.data.packages);
const selectedObject = this.state.data.packages.filter((obj) => {
const val= (obj.id === id) ? obj : false;
return val;
});
}
Array of data i want update (isOpen property).
what about immutably? I think u can use dot-prop-immutable package in this way:
const state = {
packages: [
{ isOpen: false, id: 1 },
{ isOpen: false, id: 2 },
{ isOpen: false, id: 3 }
]
};
const index = state.packages.findIndex(obj => obj.id === 3);
const newState = dotProp.set(state, `packages[${index}].isOpen`, true);
you could do it the ol' way :
changeIsOpenState(typeOfPart: Number, id: Number) {
// Copy the packages so you won't mumtate your state directly
const packages = Object.assign({}, ...this.StaticRange.data.packages);
// Get the package to edit and its index in the packages object
let packageIndex;
let packageToEdit;
for(let i = 0; i <= packages.length; i++){
if(packages[i].id === id){
packageIndex = i;
packageToEdit = packages[i];
packageToEdit.isOpen = true
}
}
packages[packageIndex] = packageToEdit;
setState({...this.state, data:{...this.state.data, packages}});
}
I did it like this:
1.I copy current data to another variable
2.Filter data by id
3.Save array key of item with same id
4. Change cloned value with negation
5. save new data to state
changeIsOpenState(typeOfPart: Number, id: Number) {
const subjectDataCopy = cloneDeep(this.state.data);
const keys = [];
subjectDataCopy.packages.filter((obj, key) => {
if (obj.id === id) {
keys.push(key);
}
});
subjectDataCopy.packages[keys[0]].isOpen = !subjectDataCopy.packages[keys[0]].isOpen;
this.setState({data: subjectDataCopy});
}
If there is better option to do this please let me know :)
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;
I have an array of 16 objects which I declare as a state in the constructor:
this.state = {
todos:[...Array(16)].map((_, idx) => {
return {active: false, idx}
}),
}
Their status will get updated through an ajax call in ComponentDidMount.
componentDidMount()
{
var newTodos = this.state.todos;
axios.get('my/url.html')
.then(function(res)
{
newTodos.map((t)=>{
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
}
this.setState({
todos:newTodos,
})
}
}
and then finally, I render it:
render(){
let todos = this.state.todos.map(t =>{
if(t.active === true){
console.log('true'}
else{
console.log('false')
}
})
return (
<div></div>
)
}
They all appear as active = false in the console, they never go into the if condition. When
I print out the entire state it appears not to be updated in the render method. In the console it says "value below was just updated now".
I thought changes to the state in ComponentWillMount will call the render function again?
How do I make that React will accept the new values of the state?
componentDidMount()
{
var newTodos = []; // <<<<<<<<<<<<<
axios.get('my/url.html')
.then(function(res)
{
newTodos = this.state.todos.map((t)=>{ //<<<<<<<<<<<<<<<
if (something something)
{
t.active = true;
}
else
{
t.active = false;
}
return t; //<<<<<<<<<<<<<<<<<<
} // <<<<< are you missing a semi-colon?
this.setState({
todos:newTodos,
})
}
}
The map() argument (in your code) is a function, not an expression, so an explicit return must be provided. I.E.:
xxx.map( t => ( "return t is implicit" ) );
xxx.map( t => { "return t must be explicit" } );
And, as #DanielKhoroshko points out, your new variable points to this.state. And of course never, never, ever alter this.state directly. Since map() returns a new array, not the original as altered, that's why we use map() and not forEach()
That is because you are actually not providing any new state, but mutating it instead.
React uses shallow comparison be default (where to objects are equal if they reference the same memory address). And that's exactly what's happening here:
var newTodos = this.state.todos; // newTodos === this.state.todos
this.setState({ todos:newTodos }) // replaces two equal addresses, so it simply doesn't change anything
The easiest solution, though probably not the most performant would be to clone your todos array:
var newTodos = [...this.state.todos]; // newTodos !== this.state.todos
So, first of all, what I'm trying to do is the following: I have a few divs that contain some text and an image. All the data for the divs is stored in a state array. You can also add divs and delete whichever div you desire. What I would like to implement now, is to change the picture when the user clicks on an image. There is a preset image library and whenever the user clicks on the image, the next image should be displayed.
Here is some relevant code:
let clicks = 0;
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
data : [
createData( someimage, "Image 1"),
createData( anotherimage, "Image 2"),
createData( thirdimage, "Image 3"),
createData( fourthimage, "Image 4"),
],
imgs : [imgsrc1,imgsrc2, imgsrc3, imgsrc4],
}
}
newIcon (n) {
let newStateArray = this.state.data.slice();
let newSubStateArray = newStateArray[n].slice();
if(clicks === 1) {
newSubStateArray[0] = this.state.imgs[0];
this.setState({imgsrc:newSubStateArray});
clicks++;
} else if (clicks === 2) {
newSubStateArray[0] = this.state.imgs[1];
this.setState({imgsrc:newSubStateArray});
clicks++;
} else if (clicks === 3) {
newSubStateArray[0] = this.state.imgs[2];
this.setState({imgsrc:newSubStateArray});
clicks++;
} else if (clicks === 4) {
newSubStateArray[0] = this.state.imgs[4];
this.setState({imgscr:newSubStateArray});
clicks++;
}
}
render () {
let { data }= this.state;
return(
<div>
{data.map((n) => {
return(
<Child imgsrc={n.imgsrc} key={n} newIcon={this.newIcon.bind(this, n)} header={n.header} />
);
})}
</div>
);
}
A few sidenotes: createArray is a function to create the sub-arrays and can probably be ignored for this question. What is important to know is, that the first element is called imgsrc, and the second element is called
So, something is going wrong here but I'm not sure what it is exactly. My guess is, that I'm not properly accessing the values within the arrays. Above, you can see that I tried to slice the arrays and to then allocate the new value. Another problem I've encountered, is that n comes up as undefined, when I try to call it from my newIcon()-function.
I'm kind of lost here, as I'm quite new to React so any sort of hints and suggestions are welcome.
I would do away with all that code in newIcon, and keep clicks as part of the state. If you have an array of images then you can use clicks as a pointer to the next image that should be shown.
In this example I've taken the liberty of adding in dummy images to help explain, and changed clicks to pointer as it makes more sense.
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
// clicks is called `pointer` here and initially
// is set to the first index of the imgs array
pointer: 0,
imgs: [
'https://dummyimage.com/100x100/000000/fff.png',
'https://dummyimage.com/100x100/41578a/fff.png',
'https://dummyimage.com/100x100/8a4242/fff.png',
'https://dummyimage.com/100x100/428a49/fff.png'
]
};
// Bind your class method in the constructor
this.handleClick = this.handleClick.bind(this);
}
// Here we get the length of the imgs array, and the current
// pointer position. If the pointer is at the end of the array
// set it back to zero, otherwise increase it by one.
handleClick() {
const { length } = this.state.imgs;
const { pointer } = this.state;
const newPointer = pointer === length - 1 ? 0 : pointer + 1;
this.setState({ pointer: newPointer });
}
render() {
const { pointer, imgs } = this.state;
// Have one image element to render. Every time the state is
// changed the src of the image will change too.
return (
<div>
<img src={imgs[pointer]} onClick={this.handleClick} />
</div>
);
}
}
DEMO
EDIT: Because you have more than one div with images the sources of which need to change, perhaps keep an array of images in a parent component and just pass a subset of those images down to each Image component as props which you then store in each component's state. That way you don't really need to change the Image component that much.
class Image extends React.Component {
constructor(props) {
super(props);
this.state = { pointer: 0, imgs: props.imgs };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const { length } = this.state.imgs;
const { pointer } = this.state;
const newPointer = pointer === length - 1 ? 0 : pointer + 1;
this.setState({ pointer: newPointer });
}
render() {
const { pointer, imgs } = this.state;
return (
<div>
<img src={imgs[pointer]} onClick={this.handleClick} />
</div>
);
}
}
class ImageSet extends React.Component {
constructor() {
super();
this.state = {
imgs: [
'https://dummyimage.com/100x100/000000/fff.png',
'https://dummyimage.com/100x100/41578a/fff.png',
'https://dummyimage.com/100x100/8a4242/fff.png',
'https://dummyimage.com/100x100/428a49/fff.png',
'https://dummyimage.com/100x100/bd86bd/fff.png',
'https://dummyimage.com/100x100/68b37c/fff.png',
'https://dummyimage.com/100x100/c9a7c8/000000.png',
'https://dummyimage.com/100x100/c7bfa7/000000.png'
]
}
}
render() {
const { imgs } = this.state;
return (
<div>
<Image imgs={imgs.slice(0, 4)} />
<Image imgs={imgs.slice(4, 8)} />
</div>
)
}
}
DEMO
Hope that helps.
Try to bind the newIcon() method in the constructor, like this :
this.newIcon = this.newIcon.bind(this);
and in the render method call it normally without any bind :
this.newIcon(n)