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.
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'm trying to toggle the visibility of a component using another component like this:
const CompA = React.memo(({Vis})=>{
console.log('Rendered CompA');
return(<Pressable onPress={()=>{Vis[1](!Vis[0])}} style={...}></Pressable>);
})
const CompB = React.memo(({Vis})=>{
console.log('Rendered CompB');
return(<>{Vis[0]&&(<View style={...}></View>)}</>);
})
export default function App() {
const Vis=useState(()=>true);
return (<View style={styles.body}>
<CompA Vis={Vis}/>
<CompB Vis={Vis} />
</View>);
}
but every time I toggle the visibility even CompA gets re rendered
How do I only re render CompB everytime I change the visibility?
The array you receive from useState will never be === a previous array you received from useState; React creates a new array when you call useState each time. Just like [] === [] is always false, the check being done by memo on the props will always be false and it will re-render.
There are at least two ways to solve the problem:
Pass the component parts of that array instead, since the setter function is guaranteed to be stable, and only pass CompA the setter function since it doesn't need the flag (it can use the callback form of the setter).
If you really, really want to pass the array around instead, implement a custom memo callback for CompA that only looks at the second element in the array, and don't use the first element of the array in CompA. But I'd strongly recommend not doing that, the readability/semantics of it are very misleading/surprising.
Here's #1:
const CompA = React.memo(({setVisible}) => {
console.log("Rendered CompA");
return (<Pressable onPress={() => {setVisible(visible => !visible)}} style={...}></Pressable>);
});
const CompB = React.memo(({visible}) => {
console.log("Rendered CompB");
return (<>{visible && (<View style={...}></View>)}</>);
});
export default function App() {
const [visible, setVisible] = useState(true); // No need for the callback form here
return (<View style={styles.body}>
<CompA setVisible={setVisible} />
<CompB visible={visible} />
</View>);
}
Live Example:
const {useState} = React;
const CompA = React.memo(({setVisible}) => {
console.log("Rendered CompA");
return (<button onClick={() => {setVisible(visible => !visible)}}>pressable</button>);
});
const CompB = React.memo(({visible}) => {
console.log("Rendered CompB");
// Unfortunately, the version of Babel used by Stack
// Snippets is so old it doesn't understand shorthand
// fragment syntax
return (<React.Fragment>{visible && (<div>this is the View</div>)}</React.Fragment>);
});
function App() {
const [visible, setVisible] = useState(true); // No need for the callback form here
return (<div>
<CompA setVisible={setVisible} />
<CompB visible={visible} />
</div>);
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
You need to pass particular props in components. isVisible and the function which change the value of isVisible. When passing function as p props to components and using React.memo you need to use useCallback in function hooks to prevent re-render stuff.
Read more about memo and usecallback
import React from "react";
import ReactDOM from "react-dom";
const { useState, useCallback, memo } = React;
const CompA = memo(({ handleChange }) => {
console.log("Rendered CompA");
return <Pressable onPress={handleChange}>Comp A</Pressable>;
});
const CompB = memo(({ isVisible }) => {
console.log("Rendered CompB", isVisible);
return isVisible && <View> CompB</View>;
});
function App() {
const [isVisible, setVisible] = useState(true);
const handleChange = useCallback(() => setVisible((isVisible) => !isVisible),[]);
return (
<View style={styles.body}>
<CompA handleChange={handleChange} />
<CompB isVisible={isVisible} />
</View>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Live Example
const { useState, useCallback, memo } = React;
const CompA = memo(({ handleChange }) => {
console.log("Rendered CompA");
return <button onClick={handleChange}>Comp A</button>;
});
const CompB = memo(({ isVisible }) => {
console.log("Rendered CompB");
return isVisible && <div> CompB</div>;
});
function App() {
const [isVisible, setVisible] = useState(true);
const handleChange = useCallback(
() => setVisible((isVisible) => !isVisible),
[]
);
return (
<div>
<CompA handleChange={handleChange} />
<CompB isVisible={isVisible} />
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
<div id="root"></div>
I have a solution but it doesn't look pretty:
let UniVis;
const CompA = React.memo(({Vis})=>{
console.log('Rendered CompA');
return(<Pressable onPress={()=>{UniVis[1](!UniVis[0])}} style={...}></Pressable>);
});
const CompB = React.memo(({Vis})=>{
console.log('Rendered CompB');
return(<>{Vis[0]&&(<View style={...}></View>)}</>);
})
export default function App() {
const Vis=useState(()=>true);
UniVis=Vis;
return (<View style={styles.body}>
<CompA />
<CompB Vis={Vis} />
</View>);
}
but it does work
I have a variable that I don't want to bind to state in React. So I declared it as let with initial value as null. Later with event I set its value in parent and then pass it to child. But in child its value is getting null. Not sure what mistake I am making. Below is the code.
function Parent() {
const[showChild, setShowChild] = useState(false);
let data = null;
const setData = () => {
data = 'Test';
setShowChild(true);
console.log('function called');
};
return (
<>
<button onClick={setData}>
Click Me
</button>
{showChild && <Child data={data} />}
</>
);
}
function Child({data}) {
console.log('data ' + data);
return (
<>
<h2>
{data}
</h2>
</>
);
}
ReactDOM.render(
<Parent />,
document.getElementById('mountNode'),
);
If you want to persist state you have to use useState as you did in the first line. so instead of manually let data ... and const setData you should have something like the following:
const [data, setData] = useState(null)
You sould use useState, whenever the state changes in parent, child will be render again.
I changed your example a little bit to show you what happen exactly, child just shows the value that parent has sent.
const {useState} = React;
const Parent=()=> {
const[showChild, setShowChild] = useState(false);
const[data, setData] = useState(0);
//let data = null;
const handleClick = () => {
setData((prev=>prev+1));
setShowChild(true);
console.log('function called');
};
return (
<div>
<button onClick={handleClick}>
Click Me
</button>
{showChild && <Child data={data} />}
</div>
);
}
const Child=({data})=> {
console.log('data ' + data);
return (
<div>
<h2>
{data}
</h2>
</div>
);
}
ReactDOM.render( <Parent/> ,
document.getElementById("mountNode")
);
<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="mountNode"></div>
In my code below, how to call executeBeforeToggle function before executing parent's function togglePage?
import * as React from "react";
import { useState } from "react";
import { render } from "react-dom";
const Page1 = (props) => {
//how to call this function before toggling?
const executeBeforeToggle = () => {
alert('HOORAY')
}
return (
<div>
<h1>I am Page 1</h1>
<button onClick={() => props.togglePage()}>Toggle</button>
</div>
)
};
const Page2 = (props) => {
return (
<div>
<h1>I am Page 2</h1>
<button onClick={() => props.togglePage()}>Toggle</button>
</div>
)
}
const App = () => {
const [page, setPage] = useState(false)
const togglePage = () => {
setPage(!page)
}
if(page === false) return <Page1 togglePage={togglePage} />
else return <Page2 togglePage={togglePage} />
}
render(<App />, document.getElementById("root"));
Not sure if I understood your question correctly, but actually that is simple:
const Page1 = (props) => {
const executeBeforeToggle = () => {
alert('HOORAY')
}
const handleClick = () => { // click handler which exec both funcs
executeBeforeToggle();
props.togglePage();
}
return (
<div>
<h1>I am Page 1</h1>
<button onClick={handleClick}>Toggle</button>
</div>
)
};
So the main idea is that your click handler should be a function which executes both executeBefore and togglePage functions in the order you need. You may write that right inside the buttons onClick, or (as above) create separate function and pass it into the onClick what makes code a bit more readable.