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>
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 want to call the increment function of the CounterText from the CounterButton.
Note: This is not the particular case I'm trying to solve, but it's very similar. I have a stipulation: I can’t change the order of the items, they have to stay at one level, they have to be separate.
I have one root component looks like this:
function App() {
return (
<div>
<CounterText/>
<CounterButton/>
</div>
);
}
CounterText looks like this:
const count = useRef(0);
function CounterText() {
function Increment() {
count.current++;
}
return(
<h2>{count.current}</h2>
);
}
CounterButton looks like this:
function CounterButton() {
return (
<button>Increment</button>
);
}
PS.: Sorry for my English.
The usual solution is to lift state up into the parent component, and pass it down to child components as props or context, along with a function they can use to update the state if needed, something like this:
function App() {
const [counter, setCounter] = useState(0);
const increment = useCallback(
() => setCounter(c => c + 1),
[]
);
return (
<div>
<CounterText counter={counter} />
<CounterButton increment={increment} />
</div>
);
}
function CounterText({counter}) {
return(
<h2>{counter}</h2>
);
}
function CounterButton({increment}) {
return (
<button onClick={increment}>Increment</button>
);
}
Live Example:
const {useState, useCallback} = React;
function App() {
const [counter, setCounter] = useState(0);
const increment = useCallback(
() => setCounter(c => c + 1),
[]
);
return (
<div>
<CounterText counter={counter} />
<CounterButton increment={increment} />
</div>
);
}
function CounterText({counter}) {
return(
<h2>{counter}</h2>
);
}
function CounterButton({increment}) {
return (
<button onClick={increment}>Increment</button>
);
}
ReactDOM.render(<App/>, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
The useCallback call there isn't required, but it makes the increment function stable, which prevents unnecessarily re-rendering CounterButton when counter changes.
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}));
};
I wrote a React-App and now I want to implement some Tab-Navigation.
The problem is, my react app is integrated in a Symfony-Project that uses Twig templates.
So my React-App lives in templates/dashboard/index.html.twig:
{% block content %}
<div id="react-app"></div>
{% endblock %}
And the tab-menu lives in templates/navigation/navbar.html.twig :
<div class="flex-1 px-4 flex justify-between">
<div id="react-tabs"></div>
</div>
react-App.js renders both of the apps.
import React from 'react';
import {render} from 'react-dom';
import {ready} from '../../helper/vanilla-helper'
import ReactTabs from "./ReactTabs";
function ReactApp() {
return (
<div className="App p-20 bg-blue-300">
Blubber
</div>
);
}
export default ReactApp;
ready(() => {
render(
<ReactApp/>,
document.getElementById('react-app')
);
render(
<ReactTabs/>,
document.getElementById('react-tabs')
);
});
I did quite a lot of research in the internet about sharing state.
But it seems all of this is only related to sharing state inside of ONE ReactJs App and between their components.
In my case I need to share state between two apps.
Is this even possible or should I take a complete different approach to this?
You can make a simple store (as in state management) to share the state between components. If you need a more serious solution, I suggest you should look into Redux / MobX or some other state management tool.
In the snippet only a very basic counter is shared - it's easier to follow through like this:
const { useState, useEffect, useMemo } = React
// observer pattern
const makeObservable = (target) => {
let listeners = []
let value = target
function get() {
return value
}
function set(newValue) {
if (value === newValue) return
value = newValue
listeners.forEach((l) => l(value))
}
function subscribe(listenerFunc) {
listeners.push(listenerFunc)
return () => unsubscribe(listenerFunc) // can be used in the useEffect
}
function unsubscribe(listenerFunc) {
listeners = listeners.filter((l) => l !== listenerFunc)
}
return {
get,
set,
subscribe,
}
}
const simpleStore = makeObservable({ count: 0 })
const useStore = () => {
const [counter, setCounter] = useState(simpleStore.get())
useEffect(() => {
return simpleStore.subscribe(setCounter)
}, [])
const actions = useMemo(() => {
return {
incrementCount: () => simpleStore.set({ ...counter, count: counter.count + 1 }),
decrementCount: () => simpleStore.set({ ...counter, count: counter.count - 1 }),
}
}, [counter])
return {
state: counter,
actions
}
}
const App = () => {
const { state, actions } = useStore()
return (
<div>
{state.count}
<button
onClick={() => actions.incrementCount()}
>
INCREMENT
</button>
<button
onClick={() => actions.decrementCount()}
>
DECREMENT
</button>
</div>
)
}
const Tabs = () => {
const { state, actions } = useStore()
return (
<div>
{state.count}
<button
onClick={() => actions.incrementCount()}
>
INCREMENT
</button>
<button
onClick={() => actions.decrementCount()}
>
DECREMENT
</button>
</div>
)
}
ReactDOM.render(
<App />,
document.getElementById('app')
);
ReactDOM.render(
<Tabs />,
document.getElementById('tabs')
);
<script src="https://unpkg.com/react#17/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.development.js" crossorigin></script>
<div id="app"></div>
<div id="tabs"></div>
Thanks streletss for the great example on the makeObservable & the custom hook!
I have following components .
this component accept props which is function that can be called.
deleteIcon = () => {
console.log("deleting the document");
}
export const Parent = (props) => {
return (
<button onclick={() => {deleteIcon()}}
)
}
Now, I have some another component which uses this component. which has its own implementation of the deleteIcon method.
deletechildIcon = () => {
}
export const child = () => {
return (
<Parent deleteIcon={() => {deletechildIcon()}} />
)
}
So, from child still it is calling the parent method and not the child one. can any one help me with this ?
Some notice points:
onClick rather than onclick
no need to use arrow function inside props, which may cause performance loss and it's not the best practice
write your functions inside the component
Child component is the one which been called inside the Parent, you made it the opposite
Try the demo in-text:
const Parent = () => {
const deleteIcon = () => {
console.log("deleting the document");
};
return <Child deleteIcon={deleteIcon} />;
};
const Child = props => {
return <button onClick={props.deleteIcon}>XXX</button>;
};
ReactDOM.render(<Parent />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
You are calling the deleteIcon method in your Parent component, not the props.deleteIcon
It sounds like you want Parent to have a default deleteIcon prop that can be optionally overridden in specific implementations. You could do that by editing Parent like so:
deleteIcon = () => {
console.log("deleting the document");
}
export const Parent = (props) => {
const buttonOnClickHandler = props.deleteIcon || deleteIcon;
return (
<button onClick={() => {buttonOnClickHandler()}}
)
}
Or you could use default arguments:
deleteIconDefault = () => {
console.log("deleting the document");
}
export const Parent = ({
deleteIcon = deleteIconDefault
}) => {;
return (
<button onClick={() => {this.props.deleteIcon()}}
)
}
Hope this points you in the right direction!