Prevent modal from closing after re-render in react - javascript

Inside a Component, I have a Modal from which the user can do an update to some data through an API and then change the state of that main Component. Because there is a change in state, everything will re-render.
I would like to keep the modal open, so that I can show a success message.
The code would be something like this.
const Main = () => {
const [state, setState()] = useState();
return (
<Component state={state}>
<Modal onButtonClick={() => {
updateThroughApi().then(() => setState())} />
</Component>
)
}
When user clicks on modal's button, the state changes, and Component is re-rendered. Modal is rendered too, as it's inside.
I have thought of two possible solutions:
Move the modal outside of the component. This is a problem, as my actual code is not as simple as the example I posted. In my code, the modal opens on the click of a button B, which is deep inside Component. So, if I move the modal out from Component, I would have to pass the status and the action to change status (e.g. [open, setOpen]) through several components until button B (prop drilling).
Another solution: On the action onButtonClick I just do the API update, and use a new state updated = true; then, onModalClose, only if updated is true, I run setState so Component is rendered just after the modal is closed. But this solution seems a hack to me. There must be a better way.
Is there any better solution?

Something is obviously going very wrong here, the Modal should not close. As a workaround you could do something like this:
const Main = () => {
const [state, setState()] = useState();
const modal = useMemo(() => (
<Modal onButtonClick={() => {
updateThroughApi().then(() => setState())} />
), [])
return (
<Component state={state}>{modal}</Component>
)
}

Your Modal is re-rendering because your function passed as onButtonClick is redefined at every render.
So you have 2 options here:
1/ Keep your Modal inside your Component and use useMemo
import { useMemo } from 'react'
const Main = () => {
const [state, setState] = useState();
const modal = useMemo(() => (
<Modal onButtonClick={() => (
updateThroughApi().then(() => setState())
)}
/>
), [])
return (
<Component state={state}>
{modal}
</Component>
)
}
Or 2/ Move your Modal outside your component and use combination of memo and useCallback
import { memo, useCallback } from 'react'
const Main = () => {
const [state, setState] = useState();
const onButtonClick = useCallback(() => updateThroughApi().then(() => setState()), []);
return (
<Component state={state}>
<Modal onButtonClick={onButtonClick} />
</Component>
)
}
const Modal = memo(({onButtonClick}) => {
})
So in this case, at every render, memo will compare if all Modal props are === from previous render, which is now the case, because memoization of onButtonClick with useCallback, and so your Modal component will not re-render
https://reactjs.org/docs/hooks-reference.html#usememo

Related

Variable passed trough state in Link can't update

I am updating my theme in my App per useState. This is passed to Topbar-Component per prop. console.log() gets triggered every time it changes. From Topbar theme is passed into a link to AboutMe-Copmponent as state, which works, but when i now change the state of theme it only updates in Topbar. I even tried Useeffect. Only when I refresh the site the change is noticed. I read hours about this but I cant solve it somehow.
AppComponent (not all code just the necessary):
function App() {
const [theme, setTheme] = useState('dark')
return (
<Topbar theme={theme}></Topbar>
<ToggleButton variant='light' onClick={() => setTheme('light')}>Light</ToggleButton>
<ToggleButton variant='dark' onClick={() => setTheme('dark')}>Dark</ToggleButton>
TopbarComponent:
export default function Topbar({theme}) {
console.log('Topbar',theme)
React.useEffect(()=>{
console.log('changed')
},[theme])
Output when I press the buttons:
Topbar light
changed
Topbar dark
changed
AboutMeComponent:
export default function AboutMe() {
const location = useLocation()
console.log(location.state)
React.useEffect(() => {
console.log('About-Me',location.state)
},[location])
Initial output:
dark
About-Me dark
When I now press the other Button I only get the Topbar Output
Only when refreshing I get the AboutMe Outputs again.
PS
The theme is changed anyway from dark to light but i need this state to change fonts etc.
I would suggest sticking with documentation's recommendation which is to use useContext for very this example of setting theme using context.
Check out: https://beta.reactjs.org/apis/react/useContext
Usage : Passing data deeply into the tree
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
useContext returns the context value for the context you passed. To determine the context value, React searches the component tree and finds the closest context provider above for that particular context.
To pass context to a Button, wrap it or one of its parent components into the corresponding context provider:
function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
// ... renders buttons inside ...
}
It doesn’t matter how many layers of components there are between the provider and the Button. When a Button anywhere inside of Form calls useContext(ThemeContext), it will receive "dark" as the value.
I have it working now with the useContext hook. Thank you i somehow forgot about it.
App:
export const ThemeContext = React.createContext()
function App() {
const [theme, setTheme] = useState('black')
console.log(theme)
return (
<ThemeContext.Provider value={{backgroundColor:theme}}>
<BrowserRouter>
<div className='App' id={theme}>
<Topbar/>
<div className="position-absolute top-0 start-0">
<ToggleButton variant='light' onClick={() => setTheme('white')}>Light</ToggleButton>
<ToggleButton variant='dark' onClick={() => setTheme('black')}>Dark</ToggleButton>
</div>
Topbar:
export default function Topbar() {
const {user,logout} = UserAuth()
const [error, setError] = useState('')
const navigate = useNavigate()
const style = useContext(ThemeContext)
console.log(style)
AboutMe:
export default function AboutMe() {
const style = useContext(ThemeContext)
console.log(style)
return (
<>
<div className='d-flex' style={style}>
I had to move my Routing from Index.js to App.js because it had to be wrapped in the Context provider, but now my theme gets passed into every single component.

Can't update parent component state with React UseState

im facing this weird behavior when trying to update the parent component with an set function to the child with props
this hook is to open and close the modal to edit an element
//PARENT FILE
//hook
const [isEditModalOpen, setEditModalOpen] = useState(false)
//more code...
//modal
{isEditModalOpen && <EditExcerciseModal setEditModalOpen={setEditModalOpen} isEditModalOpen={isEditModalOpen} />}
and this is the child code
//CHILD FILE
export const EditExcerciseModal = ({setEditModalOpen, excerciseInfo,fetchExcercisesFromRoutine})
//more code etc etc
<div className="addExcerciseModalContainer">
<span onClick={() =>{ setEditModalOpen(false) }} className="xModal">X</span>
i checked and the onClick is working. if i change the parent state manually the Modal works fine and closes.
the weird case when it its working is when instead of calling the set function i create a function with a setTimeout without time like this:
function closeModal(){
setTimeout(() => { setEditModalOpen(false)}, 0);
}
any ideas?
thanks for the help
You need to create a separation of concern. A Modal consists of three parts
The Modal of its Self.
The Content of the Modal.
And the container of the two.
You should be using the useState() hook and calling setEditModalOpen in the same containing component.
You need to make sure that you're declaring and setting state inside the same component.
// children would be the content of the modal
const Modal = ({ children, selector, open, setOpen }) => {
// we need the useEffect hook so that when we change open to false
// the modal component will re-render and the portal will not be created
useEffect(() => {
setOpen(false);
//provide useEffect hook with clean up.
return () => setOpen(true);
}, [selector]);
return open ? createPortal(children, selector) : null;
};
export const EditExerciseModal = ({ close }) => {
return (
<div>
{/* Instead of creating a handler inside this component we can create it in it's parent element */}
<span onClick={close}>X</span>
{/* Content */}
</div>
);
};
export const ModalBtn = () => {
const [isEditModalOpen, setEditModalOpen] = useState(false);
// this is where it all comes together,
// our button element will keep track of the isEditModalOpen variable,
// which in turn will update both child elements
// when true useEffect hook will re-render Modal Component only now it "will" createPortal()
// when our EditExerciseModal alls close it will set change the isEditModalOpen to false
// which will be passed to the Modal component which
// will then cause the component to re-render and not call createPortal()
return (
<>
<button onClick={() => setEditModalOpen(true)}>EditExerciseModal</button>
{setEditModalOpen && (
<Modal
open={isEditModalOpen}
setOpen={setEditModalOpen}
selector={'#portal'}>
<div className='overlay'>
<EditExerciseModal close={() => setEditModalOpen(false)} />
</div>
</Modal>
)}
</>
);
};

How to get updated state from react parent component

I have the following structure
=Parent Component (with a list as a state)
== Sub Comp list={list})
=== Sub Comp2 list={list}
==== Node list={list}
export const Node = (props) => {
// some state setup
const [checked, setChecked] = useState(false)
const handleCheckbox = () => {
if(!checked){
//Checkbox checked, add this Node to list in Parent Component
props.updateList(someLabel)
}
else{
props.removeFromList(someLabel)
}
}
return(
<TreeItem
icon = {<Checkbox checked={checked} onChange={handleCheckbox}}
>
{expanded && !fetching ? childNodes : <TreeItem label="reached end" />
</TreeItem>
)
}
Now this work the way that I intended, but the problem is since it's a TreeView if I collapse and expand one of the parent nodes, I lose the checked value.
To fix this I put
useEffect(() => {
var inList = props.list.find(function(item) { return item.name === label} ) !=== undefined
if(inList){ setChecked(true)} else{setChecked(false)}
}, [])
Now the above works as intended, but only if I go to the next page on my form and then come back. The problem is that when the list is updated, the useEffect is still using the old state of the list.
How do I force useEffect to use the most updated state, or force the state to update since it's asynchronous?
You have a few options depending on your circumstance. If you just want the latest parent state in the child component you can pass the parent's state as well as the updater as a prop to the child component as below.
Parent:
const ParentComponent = () => {
const [checked, setChecked] = React.useState(false);
return (
<ChildComponent checked={checked} setChecked={setChecked} />
)
}
Child:
const ChildComponent = ({checked, setChecked}) => {
return (
<>
<p>{checked}</p>
<button onClick={() =>setChecked(!checked)}>Button!</button>
</>
)
}
If you are trying to use the state information in parallel components or if you are passing state more than one or two levels down, consider using a context.

How to avoid re-renders at the App level when I make a change to a child component (like a searchbar)?

I'm trying to make a searchbar React component that doesn't trigger an App-wide re-render when I type, yet allows me to use the query in other components/to make an API call.
Background:
I learned that stateless input components are good for reusability and creating controlled components. So state stays at parent (or App) level and the component's value gets passed in via props.
On the other hand, tracking the query's state at the App level causes ALL components to re-render (when the input's handleChange calls setQuery) and feels like a needless use of resources.
What am I missing here? Do I leave the query piece of state at the SearchBar level instead? Should I use React.memo or useCallback?
SearchBar component:
import React from 'react';
const Searchbar = ({ query, handleQueryChange }) => {
return (
<div className="field">
<label>Enter search term</label>
<input type="text" onChange={handleQueryChange} value={query}></input>
</div>
);
};
export default Searchbar;
And the App component
const App = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleQueryChange = (e) => {
setQuery(e.currentTarget.value);
};
useEffect(() => {
function search() {
...makeAPIcallwith(query).then((result) => {setResults(result)})
};
if (query) {
const timer = setTimeout(() => {
search()}, 1000);
return () => clearTimeout(timer);
}
}, [query]);
return (
<div className="content-container">
<SearchBar query={query} handleQueryChange={handleQueryChange} />
<...Other React component not needing to re-render... />
</div>
);
};
export default App;
The tiniest optimization that you could make here is this:
const handleQueryChange = useCallback((e) => {
setQuery(e.currentTarget.value);
},[]);
It's not worth making. What you've shown is good idomatic react code.
I guess the other thing that you could do, if you haven't already because you haven't shown the code, is to help React out by encapsulating the results in a component like this:
return (
<div className="content-container">
<SearchBar query={query} handleQueryChange={handleQueryChange} />
<ListOfThings results={results}/>
</div>
);
Super tiny components, so tiny they seem almost trivially simple, is the name of the game in React. If your components are over 30-40 lines long, then 👎

What is going wrong when I pass a boolean from a child to its parent component using React Hooks?

I'm currently trying to revise the dropdown menu component on my Gatsby site so that it reports a boolean to its parent component, a navbar. I plan on using that boolean to trigger some conditional CSS in Emotion.
The boolean isOpen reports if the dropdown menu is open or not, so true means it's open, and false means it's not.
As of now, I'm using React Hooks to pass that data from the child to the parent component. It seems like I'm successfully passing data, but when I click the dropdown menu, it sends both a true and a false boolean value in rapid succession, even as the menu remains open.
How do I revise this code so that isOpen in the child component is correctly reported to the parent component?
import React, { useState, useEffect } from "react"
const Child = ({ isExpanded }) => {
const [expandState, setExpandState] = useState(false)
useEffect(() => {
setExpandState(isOpen)
isExpanded(expandState)
})
return(
<dropdownWrapper>
<button
{...isExpanded}
/>
{isOpen && (
<Menu>
//menu items go here
</Menu>
)}
</dropdownWrapper>
)
}
const Parent = () => {
const [expandState, setExpandState] = useState(false)
const onExpand = (checkExpand) => {
setExpandState(checkExpand)
}
return(
<Dropdown
isExpanded={onExpand}
onClick={console.log(expandState)}
/>
)
}
Figured this one out myself. Parent needed a useEffect to register the incoming boolean.
Fixed code for the parent:
const Parent = () => {
const [expandState, setExpandState] = useState(false)
const onExpand = (checkExpand) => {
setExpandState(checkExpand)
}
useEffect(() => {
onExpand(expandState)
})
return(
<Dropdown
isExpanded={onExpand}
onClick={console.log(expandState)}
/>
)
}

Categories

Resources