I have been searching for a while but can't seem to find a concrete answer to this problem. I have encountered this many times and overcame them by using "hacky" solutions. What is the recommended/standard way to get around child components re-rendering when setting parent state.
I have made a very simple example to show this problem where we have a left side child component with a count state and a right side child component which displays text when a button on the left side is clicked.
Example.js
import React, { useState } from "react";
import "../css/pages/Example.css";
function Example() {
const [rightText, setRightText] = useState();
const LeftSide = () => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
};
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
return (
<div className="wrapper">
<LeftSide />
<RightSide />
</div>
);
}
export default Example;
Example.css
.wrapper {
width: 300px;
height: 300px;
border: 1px solid red;
display: flex;
}
.egleft {
border: 1px solid green;
width: 50%;
}
.egleft p,
.egright p {
color: black;
}
.egright {
width: 50%;
border: 1px solid blue;
}
To replicate: click add button to increment value on left side.. then click update right. You will see due to the parent's setState, left side is re-rendered, causing its own count state to be reset to the initial value.
Here is a gif showing the problem
Thanks
The issue is you are creating the LeftSide (and it's state) and RightSide components on the fly each time the Example component re-renders.
It's not common practice to create child components inside the render function of the parent as it creates an unecessary overhead (creating a new function every render instead of using the same existing function). You can split up the components into multiple functions in the root of the file, or multiple files, and compose them like #dejan-sandic answer.
But if creating the child component inside the render function is a must. You can use the useMemo hook to stop react from recreating your child component:
import React, { useState, useMemo } from "react";
import "../css/pages/Example.css";
function Example() {
const [rightText, setRightText] = useState();
const LeftSide = useMemo(() => () => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
}, []);
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
return (
<div className="wrapper">
<LeftSide />
<RightSide />
</div>
);
}
export default Example;
But I would advise against it, as it can become unnecessarily complex, unless for something extremely dynamic.
You are defining both RightSide and the LeftSide components inside of Example. You can't do that because those two components will be created every time` Example component renders, which is happening after every statechange.
const RightSide = () => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
Do this in stead:
const LeftSide = ({ setRightText }) => {
const [count, setCount] = useState(0);
return (
<div className="egleft">
<p>{count}</p>
<button
onClick={() => {
setCount(count + 1);
}}
>
Add
</button>
<button
onClick={() => {
setRightText("hello world");
}}
>
Update right
</button>
</div>
);
};
const RightSide = ({ rightText }) => {
return <div className="egright">{rightText && <p>{rightText}</p>}</div>;
};
function Example() {
const [rightText, setRightText] = useState();
return (
<div className="wrapper">
<LeftSide rightText={rightText} />
<RightSide setRightText={setRightText} />
</div>
);
}
Related
I have a component which renders a react-modal, my problem is the react-modal does not open. modalFileNameOpen useState property does not set properly.
const [modalFileNameOpen, setmodalFileNameOpen] = useState(false)
const handleFileNameChangeClick = () => {
console.log(modalFileNameOpen) // set to false
setmodalFileNameOpen(true) // this does not set the `modalFileNameOpen` to true.
console.log(modalFileNameOpen) // is still false
}
return (
<div className="container-fluid">
<input type="button" className="btn" onClick={handleFileNameChangeClick} value="Rename File"></input>
<ModalFileName modalFileNameOpen={modalFileNameOpen} />
</div>
)
I have another component ModalFileName
import React, { useState } from 'react';
import Modal from 'react-modal';
Modal.setAppElement('#root');
const ModalFileName = ({ modalFileNameOpen }) => {
const [modalIsOpen, setModalIsOpen] = useState(modalFileNameOpen)
return (
<Modal isOpen={modalIsOpen} onRequestClose={() => setModalIsOpen(false)}>
<div>
<h1>File Name Change</h1>
<div>
<button onClick={() => setModalIsOpen(false)}>Close</button>
</div>
</div>
</Modal>
)
}
export default ModalFileName;
Here is a minimal example of what you seem to be trying to achieve.
It is a basic illustration of pulling state up to the parent and passing a handler function down to the child. In your code you were attempting to do this by declaring a new state in the child based on the parent's state which leads to unnecessary duplication and possible state conflicts down the road.
We declare our modal state and setter and a basic handler function:
const [openModal, setOpenModal] = useState(false);
const toggleModal = () => {
setOpenModal(!openModal);
}
We then conditionally render our modal component based on the current state value. If openModal is false we render the button to open it, otherwise we render the Modal component and pass our handler function toggleModal to it as a prop.
// if openModal is false render the button, else render our modal
{!openModal ?
<button type="button" onClick={toggleModal}>Open Modal</button>
: <Modal handler={toggleModal} />
}
If the modal is rendered it recieves the handler (here retrieved through destructuring function Modal({handler}) {...) and assign it to the onClick of the close button on our modal.
function Modal({handler}) {
return (
<div className="modal" >
<p>I'm a modal</p>
<button type="button" onClick={handler}>Close Modal</button>
</div>
)
}
Working Example
body {
padding: 0;
margin: 0;
}
.container {
position: relative;
height: 100vh;
width: 100vw;
background: gray;
}
.modal {
height: 50vh;
width: 50vw;
position: absolute;
top: 25%;
left: 50%;
transform: translateX(-50%);
background-color: aqua;
}
<script src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
<div id="App"></div>
<script type="text/babel">
const {useState} = React;
function App() {
const [openModal, setOpenModal] = useState(false);
const toggleModal = () => {
setOpenModal(!openModal);
}
return (
<div className="container">
{!openModal ?
<button type="button" onClick={toggleModal}>Open Modal</button>
: <Modal handler={toggleModal} />
}
</div>
)
}
function Modal({handler}) {
return (
<div className="modal" >
<p>I'm a modal</p>
<button type="button" onClick={handler}>Close Modal</button>
</div>
)
}
ReactDOM.render(<App />, document.getElementById('App'));
</script>
Some notes
If you console.log() before and after the setOpenModal() call you will get the same results that you did in your question; it will print the same value that it entered the render with. If it doesn't there is a problem and you have mutated the state. From the Docs
Never mutate this.state directly, as calling setState() afterwards may replace the mutation you made. Treat this.state as if it were immutable.
const [openModal, setOpenModal] = useState(false);
const toggleModal = () => {
console.log(openModal); // prints the value of openModal on entering render
setOpenModal(!openModal); // sets openModal to !openModal
console.log(openModal); // prints the value of openModal on entering render
}
Also, you may want to pass arguments to your passed handler (arguably the close button should always set openModal to false rather than hoping that toggling it will close it).
To do this you can simply accept an argument in your handler function.
const toggleModal = (newState) => {
setOpenModal(newState);
}
and pass it in your onClick calls.
<button type="button" onClick={() => toggleModal(true)}>Open Modal</button>
function Modal({handler}) {
return (
<div className="modal" >
<p>I'm a modal</p>
<button type="button" onClick={() => handler(false)}>Close Modal</button>
</div>
)
}
Those are the three components I’m using, excluding the component that displays them in the DOM, but that’s not needed.
Here I have a Parent and two Child components.
For some reason when the popup is active, and I click the Refresh Child 1 Component button, it changes the state back to Child1, but I lose the functionality within that component. So the popUpToggle function stops working.
It was working fine before. When I click the Refresh Child 1 Component again however, it starts working. Why is that?
import React, { useState, useEffect } from 'react';
import Child1 from './child1'
import Child2 from './child2'
const Parent = () => {
const [display, setDisplay] = useState('');
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(!popUp)
console.log('PopUp Toggle ran')
};
const reRenderComponent= () => {
setRenderCount(renderCount + 1);
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
popUpToggle();
console.log('reRenderComponent ran, and the key is ' + renderCount)
};
useEffect(() => {
setDisplay(
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
);
}, [])
return (
<div>
<button
style={{position: 'fixed', zIndex: '999', right: '0'}}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp ? <Child2 popUpToggle={popUpToggle}/> : null}
{display}
</div>
);
};
export default Parent;
Child 1:
import React from 'react';
const Child1 = ({ popUpToggle, renderCount }) => {
return (
<>
<button onClick={popUpToggle}>
Pop Up Toggle function
</button>
<h1>Child 1 is up, count is {renderCount}</h1>
</>
);
};
export default Child1;
Child 2:
import React, { useState } from 'react';
const Child2 = ({ popUpToggle }) => {
return (
<div
style={{
backgroundColor: 'rgba(0,0,0, .7)',
width: '100vw',
height: '100vh',
margin: '0',
}}
>
<h1>Child 2 is up</h1>
<h2>PopUp active</h2>
<button onClick={popUpToggle}>Toggle Pop Up</button>
</div>
);
};
export default Child2;
setDisplay(<Child1 /*etc*/ />);
Putting elements into state is usually not a good idea. It makes it very easy to cause bugs exactly like the one you're seeing. An element in state never gets updated, unless you explicitly do so, so it can easily refer to stale data. In your case, i think the issue is that the child component has a stale reference to popUpToggle, which in turn has an old instance of popUp in its closure.
The better approach, and the standard one, is for your state to contain just the minimal data. The elements get created when rendering, based on the data. That way, the elements are always in sync with the latest data.
In your case it looks like all the data already exists, so we don't need to add any new state variables:
const Parent = () => {
const [popUp, setPopUp] = useState(false);
const [renderCount, setRenderCount] = useState(0);
const popUpToggle = () => {
setPopUp(prev => !prev);
};
const reRenderComponent = () => {
setRenderCount(prev => prev + 1);
popUpToggle();
};
return (
<div>
<button
style={{ position: "fixed", zIndex: "999", right: "0" }}
onClick={reRenderComponent}
>
Refresh Child 1 Component
</button>
{popUp && <Child2 popUpToggle={popUpToggle} />}
<Child1
key={renderCount}
popUpToggle={popUpToggle}
renderCount={renderCount}
/>
</div>
);
};
i want to display a dialog to the bottom right corner of the page on clickin a button using react and typescript.
There is a button named "Add" in ListComponent. when clicking that button dialog which is the DialogComponent should be rendered.
Below is how the ListComponent and DialogComponent looks
function ListComponent() {
const onAddClicked = () => {
//what to be done
}
return (
<button onClick={onAddClicked}> Add</button>
);
}
function DialogComponent() {
return (
<Wrapper> Dialog </Wrapper>
)
}
Now i cannot call DailogComponent within ListComponent as it would push the layout of the page.
So i want to call this DailogComponent within MainComponent which is something like below
function Main () {
return (
<ListComponent>
//some props
</ListComponent>
)
}
I am new to using react with typescript. How can i do this. could someone provide some insights into this. thanks.
You will likely want to use fixed positioning (and possibly some increased z-index, if necessary) to display a dialog box at the bottom right of the screen. This will "break" it out of being rendered "inline" with your content.
At a minimum you should utilize the following CSS rules
position: fixed;
bottom: <M>; // M units from the bottom
right: <N>; // N units from the right
The linked codesandbox sets this Dialog class style and component
CSS
.dialog {
position: fixed;
bottom: 1rem;
right: 1rem;
padding: 1rem;
border-radius: 5px;
background-color: lightblue;
}
JSX
const Dialog = ({ showDialog, text }) =>
showDialog ? <div className="dialog">{text}</div> : null;
How you toggle the display state of the dialog is up to you.
EDIT: Full code with a demo ListComponent
const Dialog = ({ showDialog, text }) =>
showDialog ? <div className="dialog">{text}</div> : null;
const ListComponent = ({ data, toggleDialog }) =>
data.map((el, i) => (
<div key={i}>
<button type="button" onClick={toggleDialog}>
{el}
</button>
</div>
));
export default function App() {
const [showDialog, setShowDialog] = useState();
const toggleDialog = () => setShowDialog(s => !s);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<button type="button" onClick={toggleDialog}>
Toggle Dialog in Main
</button>
<h2>List Component</h2>
<ListComponent data={["A", "B", "C"]} toggleDialog={toggleDialog} />
<Dialog showDialog={showDialog} text="I'm a dialog" />
</div>
);
}
This is essentially applying Lifted State. State and mutators are lifted to a common ancestor of components interested in either displaying the state, or updating. State and state updaters are passed as props to children.
You need to have a callback in order to display the dialog box from Main function.
Below is an example:
import React, { useState } from "react";
import { render } from "react-dom";
import Modal from "react-modal";
const Main = () => {
const [isOpen, setIsOpen] = useState(false);
const handleAddClick = () => {
setIsOpen(true);
};
return (
<React.Fragment>
<ListComponent onAddClickedProps={handleAddClick} />
<DialogComponent isOpen={isOpen} />
</React.Fragment>
);
};
const ListComponent = ({ onAddClickedProps }) => {
const onAddClicked = () => {
onAddClickedProps();
};
return <button onClick={onAddClicked}> Add</button>;
};
const DialogComponent = ({ isOpen }) => {
return (
<Modal isOpen={isOpen}>
<div>I am a modal</div>
</Modal>
);
};
render(<Main />, document.getElementById("root"));
React-modal-example-on-sandbox
I wrote CSS. This works fine for blur.
document.addEventListener("click", () => {
document.querySelector(".transition").classList.toggle("blur");
})
.transition {
transition: 5s;
filter: blur(0px);
}
.blur {
filter: blur(5px);
}
<div class="transition">
TEXT
</div>
However, this will not work with React.js and Styled Components. I want to realize transition by filter without using add-on. Why doesn't this work, and how does it work?
CodeSandBox Demo
const Button = styled.button``;
let ToggleBlurText = styled.div`
transition: "5s"
`;
function App() {
const [blur, setBlur] = useState(false);
useEffect(() => {
ToggleBlurText = styled.div`
filter: blur(${blur ? "0px" : "5px"});
transition: "5s"
`;
}, [blur]);
return (
<div className="App">
<ToggleBlurText>
<h2>TEXT</h2>
</ToggleBlurText>
<Button onClick={() => setBlur(!blur)}>button</Button>
</div>
);
}
That approach will not work. React probably won't re-render the ToggleBlurText component and the styles inside the effect will not be applied.
Instead, you can create a styled component and toggle a css class that changes the blur depending on your state.
const Button = styled.button``;
const ToggleBlurText = styled.div`
transition: 5s;
filter: blur(0px);
&.blur {
filter: blur(5px);
}
`;
function App() {
const [blur, setBlur] = useState(false);
return (
<div className="App">
<ToggleBlurText className={blur ? 'blur' : ''}>
<h2>TEXT</h2>
</ToggleBlurText>
<Button onClick={() => setBlur(prev => !prev)}>button</Button>
</div>
);
}
I also fixed a typo in your styled component css and also changed the onClick handler slightly.
I'm testing out react-sweet-state as an alternative to Redux, but I feel like I'm missing something.
This is how my store is written :
import { createStore, createHook } from 'react-sweet-state'
const initialState = {
startHolidayButton: true,
}
const actions = {
setStartHolidayButton: value => ({ setState }) => {
setState({ startHolidayButton: value })
},
}
const ButtonsVisibleStore = createStore({
initialState,
actions,
name: 'ButtonsVisibleStore',
})
export const useButtonsVisible = createHook(ButtonsVisibleStore)
In my eyes it all seems pretty fine, the hooks works as long as I only need the initial state.
This is how I access and modify it :
const App = () => {
const [state, actions] = useButtonsVisible()
return (
<Styled>
<MainMenu />
<div className="l-opacity">
<WorldMap />
</div>
{state.startHolidayButton && (
<div
className="bottom-toolbar"
onClick={() => {
actions.setStartHolidayButton(false)
}}>
<StartSelectionButton />
</div>
)}
</Styled>
)
}
I can read the values from the state and I can trigger the actions but if an action updates a state value, my component don't re render, it's like he is unaware that something has been updated.
So am I doing it the right way or are actions not meant for this?
export default function App() {
const [state, actions] = useButtonsVisible();
return (
<div className="App">
{state.startHolidayButton ?
(
<div
className="bottom-toolbar"
onClick={() => {
actions.setStartHolidayButton(false);
}}
>
click me
</div>
)
:
(
<div
className="bottom-toolbar green"
onClick={() => {
actions.setStartHolidayButton(true);
}}
>
click me
</div>
)
}
</div>
);
}
With some changes your code works fine. Here is the css: .bottom-toolbar { display: inline-block; width: 100%; height: 50px; background: red; cursor: pointer; } .bottom-toolbar.green { background: green; }
https://codesandbox.io/s/elegant-mendeleev-o9zv7?file=/src/styles.css:58-218