I'm new to immutablejs and have managed to update a property of an object stored in an array of objects. My goal is to simplify my development but I feel like I'v made it more complicated. Clearly, I'm missing something to make this simpler.
I created to stackblitz projects, one with the code without immutablejs https://stackblitz.com/edit/react-before-immutable , and one with immutablejs https://stackblitz.com/edit/react-after-immutable
(code below also).
I've seen some examples here where people use the second parameter of findIndex, but that function never got called for me. It also is not in the docs so I'm guessing it's not supported any more.
With Immutable.js
import React, { useState } from 'react';
import { List } from 'immutable';
export default () => {
const init = [{
id: 101,
interestLevel: 1
},
{
id: 102,
interestLevel: 0
}];
const [myArray, setMyArray] = useState(init);
const updateRow = (e) => {
const id = parseInt(e.target.attributes['data-id'].value);
const immutableMyArray = List(myArray);
const index = List(myArray).findIndex((item) => {
return item.id === id;
});
const myRow = immutableMyArray.get(index);
myRow.interestLevel++;
const newArray = immutableMyArray.set(index, myRow);
setMyArray(newArray);
};
return (
<ul>
{
myArray.map(function (val) {
return (
<li key={val.id}>
<button onClick={updateRow} data-id={val.id} >Update Title</button>
{val.id} : {val.interestLevel}
</li>
)
})
}
</ul>
)
}
Without Immutable.js
import React, { useState } from 'react';
export default () => {
const init = [{
id: 101,
interestLevel: 1
},
{
id: 102,
interestLevel: 0
}];
const [myArray, setMyArray] = useState(init);
const updateRow = (e) => {
const id = e.target.attributes['data-id'].value;
const newMyArray = [...myArray];
var index = newMyArray.findIndex(a=>a.id==id);
newMyArray[index].interestLevel = myArray[index].interestLevel + 1;
setMyArray(newMyArray);
}
return (
<ul>
{
myArray.map(function (val) {
return (
<li key={val.id}>
<button onClick={updateRow} data-id={val.id} >Update Title</button>
{val.id} : {val.interestLevel}
</li>
)
})
}
</ul>
)
}
Have you considered the purpose of immutablejs?
In your example, you are only adding needless complexity to your code, without leveraging the gains provided by the library.
The purpose of immutable is to provide immutable collections, inspired from scala. In other words, you create your collection, then you pass it to another component, and you can be certain that no element was appended or removed. The individual elements, however, are not under such guarantee, partly due to the constraints (or lack thereof) brought by JS.
As it stands in your code, there are very few reasons to do something like this. I've taken the liberty of changing your quote quite a bit in order to showcase how to do so:
class Comp extends React.Component {
constructor(props) {
super(constructor);
if (props.interests) {
this.state = {
interests: props.interests
}
} else {
this.state = {
interests: Immutable.Set([])
}
}
}
updateRow(e) {
return function() {
this.setState({
interests: this.state.interests.update((elements) => {
for (var element of elements) {
if (element.id == e) {
element.interestLevel++;
}
}
return elements;
})
});
}
}
render() {
var interests = this.state.interests;
var updateRow = this.updateRow;
var list = this;
//return (<div>Test</div>);
return ( <
ul > {
interests.map(function(val) {
return ( <
li key = {
val.id
} >
<
button onClick = {
updateRow(val.id).bind(list)
}
data-id = {
val.id
} > Update Title < /button> {
val.id
}: {
val.interestLevel
} <
/li>
)
})
} <
/ul>
)
}
}
var interests = Immutable.Set([{
id: 1,
interestLevel: 0
},
{
id: 2,
interestLevel: 0
}
])
ReactDOM.render( < Comp interests = {
interests
}
/>, document.getElementById("app"));
<script src="https://cdn.jsdelivr.net/npm/immutable#4.0.0-rc.12/dist/immutable.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0/umd/react-dom.production.min.js"></script>
<div id='app'></div>
The changes:
Rewrote your component as a class. This is purely to highlight the rest of the changes
Your component now takes an external prop containing interests. This means you can pass a Set and be sure that it won't have elements added out of the blue within the component
The component is in charge of the interest levels. As such, whenever you click on one of the buttons, the update function is called on the Set, which is used to update the items inside the collection
The entire thing is rendered as an array through render()
This is both more readable and easier to manage.
As you mentioned in this comment, you're looking for easier ways of updating objects in a deep object hierarchy using Immutable.js.
updateIn should do the trick.
const immutableObject = Immutable.fromJS({ outerProp: { innerCount: 1 } });
immutableObject.updateIn(['outerProp', 'innerCount'], count => count + 1);
It's also worth noting that you probably want to call Immutable.fromJS() instead of using Immutable.List() since the latter won't deeply convert your JavaScript object into an Immutable one, which can lead to bugs if you're assuming the data structure to be deeply Immutable. Switching the code above to use Immutable.fromJS() and updateIn we get:
// In updateRow
const immutableMyArray = fromJS(myArray);
const index = immutableMyArray.findIndex((item) => {
return item.id === id;
});
const newArray = immutableMyArray.updateIn([index, 'interestLevel'], interestLevel => interestLevel + 1);
setMyArray(newArray);
Related
I am completing a technical challenge and came across a scenario I never had to deal with before.
I am asked to code up a shopping cart that has a UI that represents basic checkout data like order total, current items in cart, etc.
One of the requirements states I need to implement a Checkout class that can be instantiated:
const checkout = new Checkout();
And I should be able to obtain basic info from it like:
const total = checkout.total();
And add items to the cart through it:
checkout.add(product.id);
What makes this a tricky thing to solve is that I can't think of a clean "DRY" way of implementing it into the UI. This is mainly because any updates in the checkout class will not trigger any re-renders since it's not part of the state. I would usually use state variables for this.
I tried binding state variables to parameters in the checkout class like:
const [total, setTotal] = useState();
useEffect(()=>{
setTotal(checkout.total)
}, [checkout.total])
But checkout.total is only the reference to the method, so it never changes so I do not get the binding I want.
Trying out other stuff I managed to put together a "solution" but I question whether it is a good pattern.
I basically pass a callback to the checkout class which is called whenever the cart is updated. The callback is a state variable's setter, so:
const [cart, setCart] = useState<string[]>(checkout.cart);
checkout.callback = setCart;
Then inside the add method:
add(productId) {
// Some code...
this.callback([...this.cart]);
}
What this grants is that the cart state variable is updated whenever the checkout class has changes in its parameters. So it fires a rerender on the Cart component and all of its children that have props being passed down. Therefore I get a synced UI.
The thing is I kind of don't need the cart variable other than for forcing re-renders. I can get the cart info directly from the checkout class which is what I do. But for it to be reflected in the UI I need some state variable to be updated. It could even be a counter, I only went for cart instead of a counter to make it more coherent I guess.
Am I overcomplicating things here? Is there a pattern I am missing that is used for this scenarios? How does one usually interact with an instantiated class and ensures the UI is somehow updated from changes to the class?
EDIT (adding missing info):
The Checkout class needs to implement the following interface:
interface Checkout {
// ...
// Some non relevant properties methods
// ...
add(id: number): this;
}
So it is explicitly asked that the add method returns this (in order to allow function chaining).
mixing of patterns
Using OOP instances with methods that mutate internal state will prevent observation of a state change -
const a = new Checkout()
const b = a // b is *same* state
console.log(a.count) // 0
a.add(item)
console.log(a.count) // 1
console.log(a == b) // true
console.log(a.count == b.count) // true
React is a functional-oriented pattern and uses complimentary ideas like immutability. Immutable object methods will create new data instead of mutating existing state -
const a = new Checkout()
const b = a.add(item) // b is *new* state
console.log(a.count) // 0
console.log(b.count) // 1
console.log(a == b) // false
console.log(a.count == b.count) // false
In this way, a == b is false which effectively sends the signal to redraw this component. So we need a immutable Checkout class, where methods return new state instead of mutating existing state -
// Checkout.js
class Checkout {
constructor(items = []) {
this.items = items
}
add(item) {
return new Checkout([...this.items, item]) // new state, no mutation
}
get count() {
return this.items.length // computed state, no mutation
}
get total() {
return this.items.reduce((t, i) => t + i.price, 0) // computed, no mutation
}
}
export default Checkout
demo app
Let's make a quick app. You can click the 🍐 and 🥨 buttons to add items to the cart. The app will show the correct count and total as well as the individual items -
App component preview
Now "syncing" the class to the component is just using ordinary React pattern. Use your class and methods directly in your componenets -
import Checkout from "./Checkout.js"
import Cart from "./Cart.js"
function App({ products = [] }) {
const [checkout, setCheckout] = React.useState(new Checkout)
const addItem = item => event =>
setCheckout(checkout.add(item))
return <div>
{products.map(p =>
<button key={p.name} onClick={addItem(p)}>{p.name}</button>
)}
<b>{checkout.count} items for {money(checkout.total)}</b>
<Cart checkout={checkout} />
</div>
}
const data =
[{name: "🍐", price: 5}, {name: "🥨", price: 3}]
const money = f =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f)
A simple Cart component uses JSON.stringify to quickly visualize each item -
// Cart.js
function Cart({ checkout }) {
return <pre>{JSON.stringify(checkout, null, 2)}</pre>
}
export default Cart
Run the demo below to verify the result in your browser -
class Checkout {
constructor(items = []) {
this.items = items
}
add(item) {
return new Checkout([...this.items, item])
}
get count() {
return this.items.length
}
get total() {
return this.items.reduce((t, i) => t + i.price, 0)
}
}
function App({ products = [] }) {
const [checkout, setCheckout] = React.useState(new Checkout)
const addItem = item => event =>
setCheckout(checkout.add(item))
return <div>
{products.map(p =>
<button key={p.name} onClick={addItem(p)}>{p.name}</button>
)}
<b>{checkout.count} items for {money(checkout.total)}</b>
<Cart checkout={checkout} />
</div>
}
const money = f =>
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(f)
function Cart({ checkout }) {
return <pre>{JSON.stringify(checkout, null, 2)}</pre>
}
const data = [{name: "🍐", price: 5}, {name: "🥨", price: 3}]
ReactDOM.render(<App products={data} />, document.body)
<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>
Hmm, looks like you need to share the state. The first solution that came to my mind is just to use the Class component. You can use force rerender while you need and write more custom logic without useEffect hacks.
The second solution is more clear IMO. It uses an Observer pattern. You need to add a subscription to your Checkout class. So basically.
useEffect(() => {
const subscription = (newState) => setState(newState)
const instance = new Checkout()
instance.subcribe(subscription)
return instance.unsubcribe(subscription)
}, [setState])
Since setState is immutable, this hook will be run only once.
Your idea is correct, you need somehow to start re-render to sync state of checkout object and state of a component.
E.g. you may do it by context and force update (in case if you do not want to duplicate data in object and state):
const CheckoutContext = React.createContext();
const checkout = new Checkout();
const CheckoutProvider = ({ children }) => {
// init force update, just to start re-render
const [ignored, forceUpdate] = React.useReducer((x) => x + 1, 0);
const add = (a) => {
checkout.add(a);
forceUpdate();
};
const total = checkout.total();
const value = { add, total };
return (
<CheckoutContext.Provider value={value}>
{children}
</CheckoutContext.Provider>
);
};
const Child = () => {
const v = React.useContext(CheckoutContext);
console.log(v.total);
return <button onClick={() => v.add(100)}>Click</button>;
};
export default function App() {
return (
<div className="App">
<CheckoutProvider>
<Child />
</CheckoutProvider>
</div>
);
}
You can make a Cart class that allows for observers to be notified when something important happens. To make it available for the react components, provide an instance of it with a context, and use a stateful hook to notify components by setting the state through the observer function.
Here we go:
First, we need a Cart class that notifies observers when something happens
export class Cart {
constructor() {
this.products = [];
this.subscribers = new Set();
}
subscribe = (notifyMe) => {
this.subscribers.add(notifyMe);
};
unSubscribe = (notifyMe) => {
this.subscribers.delete(notifyMe);
};
addToCart = (product) => {
this.products = [...this.products, product];
this.notify();
};
removeFromCart = (product) => {
this.products = this.products.filter(product);
this.notify();
};
notify = () => {
this.subscribers.forEach((n) => n(this.products));
};
}
We will expose this through the react tree with a context, so lets make one
const CartContext = React.createContext();
export const CartProvider = ({ children, cart }) => {
return <CartContext.Provider value={cart}>{children}</CartContext.Provider>;
};
Now for the trick! A hook that will update its state using the carts observer pattern, thereby notifying the component that uses it.
export const useCart = () => {
const cart = React.useContext(CartContext);
const [content, r] = React.useState();
React.useEffect(() => {
const notify = (productsInCart) => r(productsInCart);
cart.subscribe(notify);
cart.notify();
return () => cart.unSubscribe(notify);
}, [cart, r]);
return {
addToCart: cart.addToCart,
removeFromCart: cart.removeFromCart,
content
};
};
Note that it can be worth to update after subscribing.
Now we have our library set up, we can make some components. So here's where we instantiate the Cart class. We make a new Cart, and let the provider provide that instance
const cart = new Cart();
export default function App() {
return (
<div className="App">
<CartProvider cart={cart}>
<CartCounter />
<h1>Welcome to the shop</h1>
<h2>start putting stuff in the cart!</h2>
<Catalog />
<button
onClick={() => {
// this will still notify components
cart.addToCart({ foo: "bar" });
}}
>
add product by directly manipulating class instance
</button>
</CartProvider>
</div>
);
}
Here are the other components
const Catalog = () => {
const getProducts = async () =>
await fetch(
"https://random-data-api.com/api/commerce/random_commerce?size=6"
).then((r) => r.json());
const [products, setProducts] = React.useState();
React.useEffect(() => {
getProducts().then(setProducts);
}, []);
if (!products) {
return null;
}
return (
<ul
style={{
listStyle: "none",
display: "grid",
gridTemplateColumns: "50% 50%"
}}
>
{products.map((product) => (
<Item key={product.uid} product={product} />
))}
</ul>
);
};
const Item = ({ product }) => {
const { addToCart } = useCart();
const addProductToCart = () => addToCart(product);
return (
<li>
<article
style={{
maxWidth: 200,
border: "1px solid black",
margin: 10,
padding: 10
}}
>
<h4>{product.product_name}</h4>
<div>
<div>$ {product.price}</div>
<button onClick={addProductToCart}>add to cart</button>
</div>
</article>
</li>
);
};
const CartCounter = () => {
const { content } = useCart();
return <div>items in cart: {content?.length || 0}</div>;
};
This can be a pretty handy pattern, and can be taken pretty far (e.g. React Query works like this).
CodeSandbox link
I read with interest most of the answers, and I found them pretty explicative and comprehensive, especially the extensive example of the observer pattern.
I used the same approach to handle a similar need, without having to implement all the pattern and it makes use of the 'EventEmitter' class.
This way you can subscribe your React UI to several different type of events, olle's example would become something like this:
export class Cart extends EventEmitter {
constructor() {
super();
this.products = [];
}
addToCart = (product) => {
this.products = [...this.products, product];
this.emit("CART_UPDATE", this.products)
};
removeFromCart = (product) => {
this.products = this.products.filter(product);
this.emit("CART_UPDATE", this.products)
};
}
And in React you'd just need a custom hook or just an effect placed on top where you can place your event listeners:
export default function App() {
const cartRef = useRef(new Cart())
const [items, setItems] = useState([])
useEffect(()=>{
const cart = cartRef.current
cart.on("CART_UPDATE", setItems)
return () => cart.removeListener("CART_UPDATE", setItems)
}, []) //
return (
<div className="App">
<div>{items.map(it => item.id)}</div>
<button
onClick={() => {
// this will still notify components
cart.addToCart({ id: "bar" });
}}
>
add product by directly manipulating class instance
</button>
</div>
);
}
I think it is perfectly reasonable to send a callback to the object and then call that callback when it is needed. If you don't want to add any unnecessary data, then don't:
add(productId) {
// Some code...
this.callback();
}
checkout.callback = () => {
setTotal(checkout.total);
}
I'm trying to make an update on an Object that is in the state using React Hooks, but when I try to update the Objet the function .map stop working.
First I have this handleCheckedFilterChange function and it change the state Object but the component was not re-render:
const handleCheckedFilterChange = (name) => {
let prevFilters = filters;
for (let filter in prevFilters) {
if (prevFilters[filter].name === name) {
prevFilters[filter].checked = !prevFilters[filter].checked;
prevFilters[filter].filters.map((value) => {
value.disabled = !value.disabled;
});
setFilters(prevFilters);
break;
}
}
};
So then I change it to this one to make the component render again:
const handleCheckedFilterChange = (name) => {
let prevFilters = { ...filters };
for (let filter in prevFilters) {
if (prevFilters[filter].name === name) {
prevFilters[filter].checked = !prevFilters[filter].checked;
prevFilters[filter].filters.map((value) => {
value.disabled = !value.disabled;
});
setFilters(prevFilters);
break;
}
}
};
But this second one generates me an error:
TypeError: filters.map is not a function
The error is when I call the following:
const renderCards = filters.map((filter) => (
<div className="card text-center">
...
</div>
));
In the first option you are trying to modify the state directly which is not allowed in React's useState and that's why it is not rendering your expected change.
The problem with the second option is your are trying to use {} instead of []. Try as:
let prevFilters = [ ...filters ];
The reason behind is .map() is a function on arrays and not on objects.
I'm new to react and only understand the basics. I got this project from someone to look at, but I'm scratching my head since morning with this problem:
Uncaught TypeError: this.state.persons.map is not a function.
Please, if you can try to try to go over it in easy but in under the hood way. Thank You!
import React, { useState, Component } from 'react';
import './App.css';
import Person from './Person/Person';
import person from './Person/Person';
import { render } from '#testing-library/react';
class App extends Component {
state =
{
persons:[
{id: '123', name:'Max', age: 28 },
{id: '124',name:'Mari', age: 26 },
{id: '125',name: 'laly', age: 20 }
],
showPersons: false
}
nameChangeHandler=( event,id ) =>
{ const personIndex = this.state.persons.findIndex(p=>{
return p.id === id;
});
const person = {...this.state.persons[personIndex]
};
person.name=event.target.value;
const persons=[ ...this.state.persons];
persons[personIndex]=person;
this.setState(
{
persons:person
}
)
}
togglePersonHandler = ()=>
{
const doesShow = this.state.showPersons;
this.setState ({showPersons: !doesShow});
}
deletePersonHandler= (personIndex)=> {
//const persons = this.state.persons;
const persons = [...this.state.persons]
persons.splice(personIndex,1);
this.setState({persons:persons});
}
render()
{
const style ={
backgroundColor:'yellow',
font:'inherit',
border:'1px solid blue',
padding:'8px',
cursor:'pointer'
};
let persons=null;
if (this.state.showPersons){
persons= (
<div>
{this.state.persons.map((person,index)=> {return <Person click={() => this.deletePersonHandler(index)}
name = {person.name}
age = {person.age}
key={person.id}
change ={(event) => this.nameChangeHandler(event,person.id)}
/>
})};
</div>)
};
return (
<div className="App">
<h1>Hi This is react App</h1>
<button style={style} onClick={this.togglePersonHandler}> Toggle Persons</button>
{persons}
</div>
);
}
}
export default App;
Error lies here :
nameChangeHandler=( event,id ) =>
{ const personIndex = this.state.persons.findIndex(p=>{
return p.id === id;
});
const person = {...this.state.persons[personIndex]
};
person.name=event.target.value;
const persons=[ ...this.state.persons];
persons[personIndex]=person;
this.setState(
{
persons:person ------------ > You are assigning a single object to a list in your state, so your map is giving an error, it must be {persons: persons}
}
)
}
Whenever you see ___ is not a function, try looking at whatever the function is referring to.
In this case you're using map, which is an Array function. So you need to verify if the array (the thing at the left of the dot) is actually an array.
The function call:
this.state.persons.map
The array you need to pay attention to is persons. So try to look for the place in the code where persons is not getting recognized an array.
You have a typo here, you are assigning and object to state instead of an array. in the funcion nameChangeHandler.
this.setState(
{
persons:person
}
)
With a simple console inside the render or just watching it inside the Components Menu in the browser you can notice it bro.
It probably happens here, where you replace the array of persons with a single person:
this.setState(
{
persons: person
}
)
You probably want to do this instead:
this.setState({persons});
EDIT: since the code snip does not reproduce the bug - here is a link to the github repo: (code is far FAR from complete)
https://github.com/altruios/clicker-game
I have run it on two computers now - both with the same behavior the code snip doesn't show.
//interestingly enough, this works just fine, where the same code I run locally has the doubling.
//when I comment out ALL other code except for this code I STILL get the error locally
//at this point the only difference is import export of components... here they are in one file.
//below is original code from file (
/*
FILE::::Clicker.js
import React from 'react';
function Clicker(props)
{
return(
<div>
{props.name}
<button
name={props.name}
onClick={props.HandleClick}
data-target={props.subjectsOfIncrease}>
{props.name} {props.value}
</button>
</div>
)
}
export default Clicker;
FILE:: Resouce.js
import React from 'react';
function Resource(props)
{
return(
<div>
{props.name} and {props.amount || 0}
</div>
)
}
export default Resource;
*/
//besides the import/export and seprate files - code is the same. it works in here, does not work locally on my machine.
const gameData = {
clickerData: [{
name: "grey",
subjectsOfIncrease: ["grey"],
isUnlocked: true,
value: 1
}],
resourceData: [{
name: "grey",
resouceMax: 100,
isUnlocked: true,
changePerTick: 0,
counterTillStopped: 100,
amount: 0
}]
}
class App extends React.Component {
constructor() {
super();
this.state = {
resources: gameData.resourceData,
clickers: gameData.clickerData
};
this.gainResource = this.gainResource.bind(this);
}
gainResource(event) {
console.count("gain button");
const name = event.target.name;
this.setState((prevState) => {
const newResources = prevState.resources.map(resource => {
if (resource.name === name) {
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
console.log(prevState.resources.find(item => item.name === name).amount, "old");
console.log(newResources.find(item => item.name === name).amount, "new");
return {
resources: newResources
}
});
}
render() {
const resources = this.state.resources.map(resourceData => {
return (
<Resource
name = {resourceData.name}
resouceMax = {resourceData.resourceMax}
isUnlocked = {resourceData.isUnlocked}
changePerTick = {resourceData.changePerTick}
counterTillStopped = {resourceData.countTillStopped}
amount = {resourceData.amount}
key = {resourceData.name}
/>
)
})
const clickers = this.state.clickers.map(clickerData => {
return (
<Clicker
name = {clickerData.name}
HandleClick = {this.gainResource}
value = {clickerData.amount}
key = {clickerData.name}
/>
)
})
return (
<div className = "App" >
{resources}
{clickers}
</div>
)
}
}
function Resource(props) {
return <div > {props.name} and {props.amount || 0} </div>
}
function Clicker(props) {
return (
<div > {props.name}
<button name = {props.name} onClick = {props.HandleClick}>
{props.name} {props.value}
</button>
</div>
)
}
const root = document.getElementById('root');
ReactDOM.render( <App / >,root );
<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>
<div id="root"></div>
so I'm building a clicker game to learn react, and something I don't understand why this code is behaving the way it does:
in the main app I have this function:
gainResource(event)
{
console.count("gain button");
const name = event.target.name;
this.setState( (prevState)=>
{
const newResources = prevState.resources.map(resource=>
{
if(resource.name === name)
{
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
console.log(prevState.resources.find(item=>item.name===name).amount, "old");
console.log(newResources.find(item=>item.name===name).amount, "new");
return {resources: newResources}
});
}
that console.count runs a single time... but I get 2 'old and new' pairs. as if setState is running twice in this function which only runs once?
the console.output is:
App.js:64 gain button: 1
App.js:76 1 "old"
App.js:77 1 "new"
App.js:76 2 "old"
App.js:77 2 "new"
so it looks like the function is running once. but the set state is being run twice?
the symptoms are that it counts up by 2. but also the initial state of amount is 0, not 1, as seen in the gamedata.json
resourceData:
[
{
name:"grey",
resouceMax:100,
isUnlocked:true,
changePerTick:0,
counterTillStopped:100,
amount:0
},{etc},{},{}],
clickerData:
[
{
name:"grey",
subjectsOfIncrease:["grey"],
isUnlocked:true,
value:1
},{etc},{},{}]
i don't think the rest of the code I'm about to most is relevant to this behavior, but I don't know react yet, so I don't know what I'm missing: but this is how I'm generating the clicker button:
const clickers = this.state.clickers.map(clickerData=>
{
return(
<Clicker
name={clickerData.name}
HandleClick = {this.gainResource}
value = {clickerData.amount}
key={clickerData.name}
/>
)
})
and in the clicker.js functional component I'm just returning this:
<div>
{props.name}
<button name={props.name} onClick={props.HandleClick}>
{props.name} {props.value}
</button>
</div>
the function is bound to this in the constructor... I don't understand why this is running setState twice inside a function that's called once.
I've also tried:
<div>
{props.name}
<button name={props.name} onClick={()=>props.HandleClick}> //anon function results in no output
{props.name} {props.value}
</button>
</div>
This is an intended behavior of a setState(callback) method wrapped in a <React.Strict> component.
The callback is executed twice to make sure it doesn't mutate state directly.
as per: https://github.com/facebook/react/issues/12856#issuecomment-390206425
In the snippet, you create a new array, but the objects inside of it are still the same:
const newResources = lastResources.map(resource => {
if(resource.name === name){
resource.amount = Number(resource.amount) + 1
}
return resource;
}
You have to duplicate each object individually:
const newResources = lastResources.map(resource => {
const newObject = Object.assign({}, resource)
if(resource.name === name){
newObject.amount = Number(newObject.amount) + 1
}
return newObject;
}
BEST ANSWER:
I was using create-react-app. and my App Component was wrapped in Strict mode... which fires setState twice... which perfectly explains why this was not reproducible on the code snip, and why the function was being called once, yet setState was called twice.
removing strict mode fixed the issue completely.
As long as you didn't provide us a runnable example I've one doubt about what could be happened and let's see if it works.
What I can see is in the gainResource function and specially in this line resource.amount = Number(resource.amount) + 1 you're trying to update the state without using setState which is not recommended by React Documentation
Please instead try first to assign a const myRessource = ressource then return myRessource instead.
gainResource(event)
{
console.count("gain button");
const name = event.target.name;
this.setState( (prevState)=>
{
const newResources = prevState.resources.map(resource=>
{
const myRessource = ressource;
if(myRessource.name === name)
{
myRessource.amount = Number(myRessource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return myRessource;
});
console.log(prevState.resources.find(item=>item.name===name).amount, "old");
console.log(newResources.find(item=>item.name===name).amount, "new");
return {resources: newResources}
});
}
okay... so after some hair pulling... I found out a way that works... but I DON'T think this is 'best practice' but it now works for me when I write this:
gainResource(event)
{
const name = event.target.name;
const lastResources = this.state.resources.slice();
const newResources = lastResources.map(resource=>
{
if(resource.name === name)
{
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
this.setState({resources: newResources});
}
vs
gainResource(event)
{
console.count("gain button");
const name = event.target.name;
this.setState( (prevState)=>
{
const newResources = prevState.resources.map(resource=>
{
if(resource.name === name)
{
resource.amount = Number(resource.amount) + 1 //Number(prevState.clickers.find(x=>x.name===name).value)
}
return resource;
});
console.log(prevState.resources.find(item=>item.name===name).amount, "old");
console.log(newResources.find(item=>item.name===name).amount, "new");
return {resources: newResources}
});
}
that setState without the function of prevState is called once... whereas with the prevState it's called twice... why?
so I still don't understand why setState using a function with prevState is causing two function calls within a function that's called only once... I have read that I should be using prev state as this.state.resources.slice(); just takes an 'untimed snapshot' and could be unreliable. is this true... or is this methodology acceptable?
this is AN answer to anyone else struggling with this. hopefully a better answer can be posted after this enlightenment to what might be happening.
Ok took me a bit of time to figure this out. As others have mentioned your call back needs to be idempotent. the thing to realise here is that react passes the same state object instance into your callback each time it calls it. hence if you change the state object on the first call it will be different on the second call and your callback function will not be idempotent.
this.setState((state) =>
{
//state.counter will have the same value on the first and second
//time your callback is called
return { counter: state.counter + 1};
});
this.setState((state) =>
{
//state.counter will have a value of n+1 the second time it is called
//because you are changing the sate object. This will have the net effect
//of incrementing state.counter by 2 each time you call this.setState!!
state.counter = state.counter + 1;
return { counter: state.counter};
});
The above is probably obvious but this situation becomes less obvious when dealing with arrays. for eg
this.setState((state) =>
{
//even though we are creating a new array, the
//objects in the array have just been copied
//so changing them is probelmatic
newArray = [...state.someArray];
//this is ok as we are replacing the object at newArray[1]
newArray[1] = {objectField : 1};
//this is not ok
newArray[1].objectField = 1;
return { someArray: newArray};
});
im sorry for not-specific question, but Im confused a little bit. Im started learning redux, and now i need to implement it in Completely working project: https://github.com/CodeNinja1395/Test-task-for-inCode and there is a branch for redux-version (which is obviously isn`t working).
The main logic of my app located in ContactsApp.js file:
class ContactsApp extends Component {
constructor(){
super();
this.state = {
searchResults: [],
displayedUsers: [],
displayedInfo: [],
selectedUser: null,
searchValue: ''
}
}
componentWillMount() {
this.props.fetchData();
}
handleSearch(event){
let CONTACTS = this.props.items;
let inputValue = event.target.value;
this.setState({searchValue: inputValue});
let iterate = function(obj, callback) {
for (var property in obj) {
if (obj.hasOwnProperty(property)) {
if (typeof obj[property] == "object") {
iterate(obj[property], callback);
} else {
callback(obj[property]);
}
}
}
}
let searchResults = [];
CONTACTS.forEach(el => { //this finds all matches (except avatar)
let arr = [];
iterate(el, function (e) {
if (e!=el.general.avatar)
arr.push(e);
});
for (var i = 0; i < arr.length; i++) {
if(arr[i].toLowerCase().indexOf(inputValue) !== -1){
searchResults.push(el.foundValue = arr[i]);
}
}
});
var displayedUsers = CONTACTS.filter(el => { //this finds element by first match (except avatar)
let arr = [];
iterate(el, function (e) {
if (e!=el.general.avatar)
arr.push(e);
});
for (var i = 0; i < arr.length; i++) {
if(arr[i].toLowerCase().indexOf(inputValue) !== -1){
el.foundValue = arr[i];
return arr[i];
}
}
});
this.setState({searchResults: searchResults});
this.setState({displayedUsers: displayedUsers});
}
handleSelectedUser(user, color){
this.setState({selectedUser: user}, function (){
});
}
render() {
let users;
let selectElem = this.selectElement;
if (this.state.displayedUsers) {
users = this.state.displayedUsers.map(user => {
return (
<User
key={user.contact.phone}
user={user}
color={(user==this.state.selectedUser) ? 'LightSkyBlue ' : 'white'}
selectUser={this.handleSelectedUser.bind(this)}
searchValue={this.state.searchValue}
searchResults={this.state.searchResults}
/>
);
});
}
return (
<div>
<div className="left-column">
<div className="users">
<SearchUser handleEvent= {this.handleSearch.bind(this)} />
<ul className="usersList"> {users} </ul>
</div>
</div>
<div className="right-column">
<ContactDetail selectedUser={this.state.selectedUser} />
</div>
</div>
);
}
}
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {fetchData})(ContactsApp);
I know that the question is too abstract and probably when i learn redux better it will be obvious, but for i get stucked with this and dont know where to start.
I don't exactly get what your app is doing so my answer will be also quite generic.
Start with setting up the project with redux. You need redux and react-redux packages to get it up. Use a tutorial from redux official page. Install chrome redux dev tools extensions to be able to debug and look-up redux state.
Then go on with refactoring...
Start with isolating your data models. Probably you'll have a user and some kind of displayFilter. Create reducers for both of them.
Keep all users in user reducer and current state of filter in displayFilter. Set some mocked data as default state.
Combine filtering settings from displayFilter and list of users from user on the fly in mapStateToProps. Do your filtering of currently visible users in mapStateToProps so your component will get only the users that are intended to be visible.
Then add some dynamic stuff
Every time a user changes some filtering settings, dispatch an action with new settings and handle it in displayFilter, apply changes to the store. React-redux will automatically call your mapStateToProps and apply new list of filtered users to the component and trigger render.
Further steps
This is just couple of hints how to start. If you want your app to be performant and stable, you should familiarize with memoization concepts, reselect and immutable.js libraries.