I have a class
class Booper{
constructor(){
this.boops = 0;
}
}
and a react component using this class to maintain some state
import React, {useState} from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [booper, setBooper] = useState(new Booper());
const handleClick = () => {
booper.boops = booper.boops+1;
setBooper(booper);
}
return (
<div>
{booper.boops}<br/>
<button onClick={handleClick}>
Boop? :(
</button><br/>
</div>
)
}
ReactDOM.render(<App />, document.querySelector("#root"))
However react doesn't seem to trigger a re-draw when i click the button.
If i add the following it does work.
const App = () => {
...
const clone = (original) => {
return Object.assign(Object.create(Object.getPrototypeOf(original)), original)
}
const handleClickWithClone = () => {
const cloned = clone(booper)
cloned.boops = cloned.boops+1
setBooper(cloned);
}
...
return (
<div>
...
<button onClick={handleClickWithClone}>
Boop! :)
</button><br/>
</div>
)
}
However, i'm not entirely sure that this is the way to go. I don't know if the garbage collector will clean up all the cloned object. Or if there are other performance issues with this solution.
Is there a better way of using class instances in react states?
(full fiddle: https://jsfiddle.net/ho6fka1x/2/)
You are mutating the state directly which will not cause re-render.
This should work
const handleClick = () => {
// booper.boops = booper.boops + 1; //<---remove this
setBooper(prev => ({...prev, boops: prev.boops + 1}));
};
Related
I have found this error while trying to build another React app. So I am only asking the main issue here in a demo app, I might not be able to change any rendering methods here since it is not the actual project.
Issue in simplified form -> I was building a app where two count will be shown and a + button will be there next to that count value. When the button is clicked the count should be increased by 1. Unfortunately when I try to click on the button the value is increasing only the first time. After that the value is not even changing. But when I am implementing the same using Class component its working as expected.
Functional Component
import React, { useState } from "react";
function Page(props) {
const [count, setCount] = useState(0);
const [content, setContent] = useState({
button: (value) => {
return <button onClick={() => handlePlus(value)}>+</button>;
},
});
function handlePlus(value) {
console.log("value=", value);
const data = count + 1;
setCount((count) => data);
}
return (
<div>
<span>Functional Component Count = {count}</span>
{content.button(10)} // 10 will be replaced with another variable
</div>
);
}
export default Page;
Class Component
import React, { Component } from "react";
class PageClass extends Component {
state = {
count: 0,
content: {
button: (value) => {
return (
<button onClick={() => this.handlePlus(value)}>+</button>
);
},
},
};
handlePlus = (value) => {
console.log("value=", value);
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<span>Class Component Count = {this.state.count}</span>
{this.state.content.button(10)} // 10 will be replaced with another variable
</div>
);
}
}
export default PageClass;
App.js
import "./App.css";
import Page from "./components/Page";
import PageClass from "./components/PageClass";
function App() {
return (
<div className="App">
<Page />
<PageClass />
</div>
);
}
export default App;
However, If I replace that content state variable with normal const variable type and it is working as expected.
Below is working when I am not using any hooks to render the button.
But this is not helpful for my case.
const content = {
content: () => {
console.log(count);
return <button onClick={() => handlePlus(value)}>+</button>;
},
};
I was trying to create some re-usable components and hence I wanted to have that function in state variable which return button tag, so that I can implements some other logic there.
The value will be missing since you're passing a hard-coded 10.
I'd recommend simplifying the handlePlus to just:
setCount(c => c + 1);
Then set the onclick like so:
<button onClick={handlePlus}>+</button>
And your code will work as expected as you can see in this snippet:
const { useState } = React;
const Example = () => {
const [count, setCount] = useState(0);
const [content, setContent] = useState({
content: (value) => {
return <button onClick={handlePlus}>+</button>;
},
});
function handlePlus(value) {
setCount(c => c + 1);
}
return (
<div>
<span>{count}</span>
{content.content(10)}
</div>
);
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
That said, I'd recommend removing the button from the hook, and just render it yourself:
const { useState } = React;
const Example = () => {
const [count, setCount] = useState(0);
function handlePlus(value) {
setCount(c => c + 1);
}
return (
<div>
<span>{count}</span>
<button onClick={handlePlus}>+</button>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>
See React documentation about the c => c + 1 syntax
I have a simple class, something like this:
class ReallyHugeClass {
constructor() {
this.counter = 0;
}
increment = () => {
this.counter += 1
}
}
If I use it in the code in a straightforward way it won't keep its state. The class will be recreated every time on render and it's not reactive at all.
const Component = () => {
const instance = new ReallyHugeClass();
return (
<button onClick={instance.increment}>
{instance.counter}
</button>
)
}
Don't rush to say: you don't need the class! Write this:
const Component = () => {
const [counter, setCounter] = useState(0);
return (
<button onClick={() => { setCounter(value => value + 1) }}>
{counter}
</button>
)
}
I used the ridiculously small class example, but the real one is complicated. Very complicated. I can't just split it into the set of useState calls.
Let's go forward. I can wrap the instance to useRef to save its value.
const Component = () => {
const instance = useRef(new ReallyHugeClass());
return (
<button onClick={instance.current.increment}>
{instance.current.counter}
</button>
)
}
The value is saved, but it's still not reactive. I can somehow force the component to rerender by passing the corresponding callback to class, but it looks awkwardly.
What's the right pattern to solve such task in React? It looks that it's quite likely situation.
One solution would be to use useRef and force rendering with a useState. Here an example:
const { useRef, useState } = React;
class ReallyHugeClass {
constructor() {
this.counter = 0;
}
increment() {
this.counter += 1;
console.log(this.counter);
}
}
function App() {
const instance = useRef(new ReallyHugeClass());
const [forceRender, setForceRender] = useState(true);
return (
<button
onClick={() => {
instance.current.increment();
setForceRender(!forceRender);
}}
>
{instance.current.counter}
</button>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<>
<App />
<App />
</>
);
<script src="https://unpkg.com/#babel/standalone/babel.min.js"></script>
<script
crossorigin
src="https://unpkg.com/react#18/umd/react.production.min.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"
></script>
<div id="root"></div>
I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.
I am unsure what I am doing wrong..If I switch the state to a counter it works, but I need it to be an array of objects to store different values for each div. Not sure why the dom never updates.
import "./styles.css";
import { useState } from "react";
// The parent component
const App = () => {
const [textBoxDivs, setTextBoxDivs] = useState([]);
const addNewTextBox = () => {
const textboxes = textBoxDivs;
const numOfTextBoxDivs = textBoxDivs.length;
const newTextBox = { id: `div${numOfTextBoxDivs + 1}` };
textboxes.push(newTextBox);
setTextBoxDivs(textboxes);
console.log(textboxes, "boxes");
};
return (
<div>
<button onClick={() => addNewTextBox()}>Click me</button>
{textBoxDivs.length > 0
? textBoxDivs.map((item, index) => (
<div key={index} id={item.id}>
text
</div>
))
: null}
</div>
);
};
export default App;
codeSandbox: https://codesandbox.io/s/friendly-water-wf4f5?file=/src/App.js:0-749
Because you are attempting to mutate existing state by pushing a new item to textBoxDivs, rather than providing a new state, breaking one of the fundamental rules of React.
Instead of pushing to your textBoxDivs array, provide a new copy of your state, with the new item inserted:
import "./styles.css";
import { useState } from "react";
// The parent component
const App = () => {
const [textBoxDivs, setTextBoxDivs] = useState([]);
const addNewTextBox = () => {
const numOfTextBoxDivs = textBoxDivs.length;
const newTextBox = { id: `div${numOfTextBoxDivs + 1}` };
setTextBoxDivs((textBoxDivs) => ([
...textBoxDivs,
newTextBox
]));
};
return (
<div>
<button onClick={() => addNewTextBox()}>Click me</button>
{textBoxDivs.length}
{textBoxDivs.length > 0
? textBoxDivs.map((item, index) => (
<div key={index} id={item.id}>
text
</div>
))
: null}
</div>
);
};
export default App;
Reading:
Array spreading
I should note that there's nothing inherently wrong with using push in this scenario, the issue is that you are pushing to existing state. You could just as easily do
// Still creating a new state from the current state
const newTxtBoxDivs = [...textBoxDivs];
newTxtBoxDivs.push(newTextBox);
setTextBoxDivs(newTxtBoxDivs);
For the same effect. The key take-away is that newTxtBoxDivs does not refer to the same object as textBoxDivs, it refers to a copy of it.
There are two components, I want to implement an element array using the useContext hook, but when the button is clicked, the element is not removed, but on the contrary, there are more of them. Tell me what is wrong here. I would be very grateful!
First component:
import React from 'react';
import CartItem from './CartItem';
import Context from '../Context';
function Cart() {
let sum = 0;
let arrPrice = [];
let [products, setProducts] = React.useState([]);
let loacalProsucts = JSON.parse(localStorage.getItem('products'));
if(loacalProsucts === null) {
return(
<div className="EmptyCart">
<h1>Cart is empty</h1>
</div>
)
} else {
{loacalProsucts.map(item => products.push(item))}
{loacalProsucts.map(item => arrPrice.push(JSON.parse(item.total)))}
}
for(let i in arrPrice) {
sum += arrPrice[i];
}
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
return(
<Context.Provider value={{removeItem}}>
<div className="Cart">
<h1>Your purchases:</h1>
<CartItem products = {products} />
<h1>Total: {sum}$</h1>
</div>
</Context.Provider>
)
}
Second component:
import React, { useContext } from 'react';
import Context from '../Context';
function CartList({products}) {
const {removeItem} = useContext(Context);
return(
<div className="CartList">
<img src={products.image} />
<h2>{products.name}</h2>
<h3 className="CartInfo">{products.kg}kg.</h3>
<h2 className="CartInfo">{products.total}$</h2>
<button className="CartInfo" onClick={() => removeItem(products.id)}>×</button>
</div>
);
}
export default CartList;
Component with a context:
import React from 'react';
const Context = React.createContext();
export default Context;
Adding to the comment above ^^
It's almost always a mistake to have initialization expressions inside your render loop (ie, outside of hooks). You'll also want to avoid mutating your local state, that's why useState returns a setter.
Totally untested:
function Cart() {
let [sum, setSum] = React.useState();
const loacalProsucts = useMemo(() => JSON.parse(localStorage.getItem('products')));
// Init products with local products if they exist
let [products, setProducts] = React.useState(loacalProsucts || []);
useEffect(() => {
// This is actually derived state so the whole thing
// could be replaced with
// const sum = products.reduce((a, c) => a + c?.total, 0);
setSum(products.reduce((a, c) => a + c?.total, 0));
}, [products]);
function removeItem(id) {
setProducts(
products.filter(item => item.id !== id)
)
}
...