I'm currently trying to implement some kind of modal (I'm aware that there is a bunch of libraries for that). The real code is much more complex because of a bunch of animation stuff, but it boils down to this (also see this Stackblitz):
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} />
</div>
);
}
The first time the parent component sets the visible property it works without a problem. But when I close the "modal" and want to set the property again it does not show up again, because the property from the point of view of the "modal" didn't actually change.
Is there a way to always rerender a FunctionComponent when a property gets touched even if the value didn't change?
Have you try this:
const Modal: React.FunctionComponent<{ visible?: boolean }> = ({
visible,
setIsVisible
}) => {
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => setIsVisible(false)}>Close</button>
</div>
);
};
const App: React.FunctionComponent = () => {
const [showModal, setShowModal] = React.useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>Show modal</button>
<Modal visible={showModal} setIsVisible={setShowModal} />
</div>
);
}
It will then re-render also your parent component, because they share the same state
you're trying changing the value in the child element, this does not get reflected in the parent
My suggestion is that to close the modal from parent itself
which reduces the code complexity and there is only single source of data here
export const Modal: React.FunctionComponent<{ visible?: boolean , onClose }> = ({
visible,onClose
}) => {
const [isVisible, setIsVisible] = React.useState(visible);
React.useEffect(() => setIsVisible(visible), [visible]);
if (!isVisible) {
return null;
}
return (
<div>
I'm visible <button onClick={() => onClose()}>Close</button>
</div>
);
};
<Modal visible={showModal} onClose={()=>setShowModal(false)} />
working example https://stackblitz.com/edit/react-ts-heiqak?file=Modal.tsx,App.tsx,index.html
Related
I have 2 popup's(I reuse CloseButton(component) and Modal(component) in 2 popup's) and need to do focus trap at all. I lf answer 4 better way.
1 popup Screen, components: ModalLogin-Modal-CloseButton.
I read about some hooks: useRef() and forwardRef(props, ref)
but i don't undestand why it's not work in my case. I am trying to find a solution. I need help :)
In ModalLogin, I try to do a focus trap. To do this, I mark what should happen with focus when moving to 1 and the last element. I need to pass my ref hook obtained via Modal-CloseButton. I read that you can't just transfer refs to functional components. I try to use the forwardref hook in the necessary components where I transfer it, here's what I do:
All links without focus-trap and hook's!.
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/form-login/modal-login.jsx [Modal-login full]
const ModalLogin = () => {
const topTabTrap* = useRef();
const bottomTabTrap* = useRef();
const firstFocusableElement = useRef();
const lastFocusableElement = useRef();
useEffect(() => {
const trapFocus = (event) => {
if (event.target === topTabTrap.current) {
lastFocusableElement.current.focus()
}
if (event.target === bottomTabTrap.current) {
firstFocusableElement.current.focus()
}
}
document.addEventListener('focusin', trapFocus)
return () => document.removeEventListener('focusin', trapFocus)
}, [firstFocusableElement, lastFocusableElement])
return (
<Modal onCloseModal={() => onCloseForm()} ref={lastFocusableElement}>
<form >
<span ref={topTabTrap} tabIndex="0" />
<Logo />
<Input id="email" ref={firstFocusableElement} />
<Input id="password" />
<Button type="submit" />
<span ref={bottomTabTrap} tabIndex="0"/>
</form>
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/modal/modal.jsx [Modal full]
const Modal = forwardRef(({ props, ref }) => {
const { children, onCloseModal, ...props } = props;
const overlayRef = useRef();
useEffect(() => {
const preventWheelScroll = (evt) => evt.preventDefault();
document.addEventListener('keydown', onEscClick);
window.addEventListener('wheel', preventWheelScroll, { passive: false });
return () => {
document.removeEventListener('keydown', onEscClick);
window.removeEventListener('wheel', preventWheelScroll);
};
});
const onCloseModalButtonClick = () => {
onCloseModal();
};
return (
<div className="overlay" ref={overlayRef}
onClick={(evt) => onOverlayClick(evt)}>
<div className="modal">
<CloseButton
ref={ref}
onClick={() => onCloseModalButtonClick()}
{...props}
/>
{children}
</div>
</div>
);
});
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/close-button/close-button.jsx [CloseButton full]
const CloseButton = forwardRef(({ props, ref }) => {
const {className, onClick, ...props} = props;
return (
<button className={`${className} close-button`}
onClick={(evt) => onClick(evt)}
tabIndex="0"
ref={ref}
{...props}
>Close</button>
);
});
And now i have a lot of errors just like: 1 - Cannot read properties of undefined (reading 'children') - Modal, 2 - ... className undefined in CloseButton etc.
2 popup Screen, components: Modal(reuse in 1 popup) - InfoSuccess- CloseButton(reuse in 1 popup)
I have only 1 interactive element - button (tabindex) and no more. Now i don't have any idea about 2 popup with focus-trap ((
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/success-modal/success-modal.jsx [SuccessModal full]
const SuccessModal = ({ className, onChangeVisibleSuccess }) => {
return (
<Modal onCloseModal={() => onChangeVisibleSuccess(false)}>
<InfoSuccess className={className} />
</Modal>
);
};
https://github.com/j3n4r3v/ligabank-credit/blob/master/src/components/info-block/info-block.jsx [Infoblock full]
const InfoBlock = ({ className, title, desc, type }) => {
return (
<section className={`info-block ${className} info-block--${type}`}>
<h3 className="info-block__title">{title}</h3>
<p className="info-block__desc">{desc}</p>
</section>
);
};
const InfoSuccess = ({ className }) => (
<InfoBlock
title="Спасибо за обращение в наш банк."
desc="Наш менеджер скоро свяжется с вами по указанному номеру телефона."
type="center"
className={className}
/>
);
I know about 3 in 1 = 1 component and no problem in popup with Focus-Trap. But i want understand about my case, it's real to life or not and what best practice.
I have 2 components. In the parent component I have this:
const Demo = () => {
const [state, setState] = useState(true);
const onChange = useCallback(
(value: string) => {
console.log(value);
},
[],
);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<Bar searchHandler={onChangeSearchHandler} />
</div>
</div>
);
};
In the Bar component I have this:
const Bar = ({ searchHandler }) => {
console.log('bar');
return (
<div>
<input type="text" onChange={(value) => searchHandler(value.target.value)} />
</div>
);
};
Wrapping onChange with useCallback I expect to cache the function and when I click on <button onClick={() => setState(false)}>sds</button> I don't want to render Bar component, but it is triggered. Why Bar component is triggered and how to prevent this with useCallback?
This has nothing to do with the onChange function you're wrapping with useCallback. Bar gets re-rendered because you're changing the state through setState in its parent component. When you change the state in a component all its child components get re-rendered.
You can verify it yourself by trying this:
const Demo = () => {
const [state, setState] = useState(true);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<Bar />
</div>
</div>
);
};
const Bar = ({ searchHandler }) => {
console.log('bar');
return (
<div></div>
);
};
You'll see that the Bar gets re-rerender anyway.
If you want to skip re-rerendring any of the child components, you should memoize them using React.memo when applicable.
Also, you should familiarize yourself with how state in react works and how does it affect the nested components as this is a main concept.
The issue is that you haven't used React.memo on Bar component. The function useCallback works only if you use HOC from memo.
try this, in Bar component create this wrapped component:
const WrappedBar = React.memo(Bar);
and in parent component use this wrapped bar:
const Demo = () => {
const [state, setState] = useState(true);
const onChange = useCallback(
(value: string) => {
console.log(value);
},
[],
);
return (
<div className="a">
<button onClick={() => setState(!state)}>sds</button>
<div className="123">
<WrappedBar searchHandler={onChangeSearchHandler} />
</div>
</div>
);
};
I am trying to create a function close modal when click outside but I am keep getting this error:
TypeError: node.current is not a function
Here is my following code in MemberCard.js:
const [modalStatus, setModalStatus] = useState(false);
const node = useRef(null);
const openModal = () => {
setModalStatus(!modalStatus);
};
const handleClick = (e) => {
if (node.current(e.target)) {
return;
}
// outside click
setModalStatus(false);
};
useEffect(() => {
document.addEventListener("mousedown", handleClick);
return () => {
document.removeEventListener("mousedown", handleClick);
};
}, []);
return (
<div className="member-card">
<div className="member-edit" onClick={openModal}>
<Symlink />
</div>
{modalStatus && (
<TeamStatusModal
active={modalStatus}
ref={node}
tab={tab}
member={member}
/>
)}
...
}
Here is my modal that I open after click:
const TeamStatusModal = (props) => {
const { active, tab, member, ref } = props;
console.log(ref);
return (
<div
className={`team-status-modal-container ${active ? "ACTIVE_CLASS" : ""}`}
>
<button className="status">
<ProfileIcon /> <span>View Profile</span>
</button>
<hr />
<button className="status">
<MessageIcon /> <span>Message Me</span>
</button>
</div>
);
};
How can I implement this feature?
In react, there are some good libraries that can help you with modals, one of them is called react-modal, you can give it a check.
If you want to implement a modal by yourself, we can follow some steps.
First we need to define a context, because the modal state needs to be accesed by more than one component or page in your app.
In the context, you could store the modal in a isModalOpen state, and add functions to manipulate it, such as openModal and closeModal. It really depends on the amount of features you want to add to this implementation.
Finally, you make the context globally accessible wrapping your app around a provider.
an example implementation
const ModalContext = createContext({})
export const ModalContextProvider = ({children}) => {
const [isModalOpen, setIsModalOpen] = useState(false)
const toggleModalState = () => {
setIsModalOpen(state => !state)
}
return <ModalContext.Provider value={{isModalOpen, toggleModalState}}>{children}<ModalContext.Provider>
}
export const useModal = () => {
return useContext(ModalContext)
}
Now the modal will be available globally
Let's say we have a component Accordion that has an internal state isOpen, so you can close and open this component.
We now want to have a parent component that also has a state isOpen and has button. In this component, we have 2 times Accordion and we are passing to Accordion isOpen and we want that if the parent changes state isOpen Accordion accept this.
All component are functional components
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
In this case above Accordion will take on first render as the initial state parent isOpen prop. In case we press the button toggle isOpen parent we will change the parent state but children will not be updated.
To fix this we can use useEffect
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
in this case, children will be properly updated when a parent changes isOpen state.
There is one issue with this:
"React Hook useEffect has a missing dependency: 'isOpen'. Either include it or remove the dependency array react-hooks/exhaustive-deps"
So how to remove this issue that esLint is complaining and we do not want to put isOpen in this since it will cause bug.
in case we add isOpen into the array like this:
useEffect(() => {
if (parentIsOpen !== isOpen) {
setIsOpen(parentIsOpen);
}
}, [parentIsOpen, isOpen]);
We will have then a situation where we will click on the internal button in accordion and update the internal state then useEffect will run and see that parent has a different state than the child and will immediately set the old state.
So basically you have a loop where the accordion will never be open then.
The question is what is the best way to update the child state based on the parent state?
Please do not suggest to put all-state in parent and pass props without child state. this will not work since both Accordions in this example have to have their own state and be able to open and close in an independent way, but yet if parent says close or open it should listen to that.
Thank you!
Actually I would say this is way to do it
const Accordion = ({ isOpen: parentIsOpen = false }) => {
const [isOpen, setIsOpen] = useState(parentIsOpen);
const handleSetIsOpen = () => setIsOpen(!isOpen);
useEffect(() => {
setIsOpen(parentIsOpen);
}, [parentIsOpen]);
return (
<div>
I'm open: {isOpen}
<button onClick={handleSetIsOpen}>toggle isOpen child</button>
</div>
);
};
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const handleSetIsOpen = () => setIsOpen(!isOpen);
return (
<div>
<button onClick={handleSetIsOpen}>toggle isOpen parent</button>
<Accordion isOpen={isOpen} />
<Accordion isOpen={isOpen} />
</div>
);
};
So just remove state check in a child component, let him update the state but since is updated with the same value it will not rerender or do some expensive behavior.
Tested it today and with a check, if states are different or without is the same, react takes care to not trigger rerender if the state that is updated is the same as before.
What you’re saying not to suggest is in fact the solution I would offer… You’ll need state to control isOpen for the parent component. Also, you should have separate methods in the parent that control state for each accordion, passed along to each accordion in props…
Not sure why you want separate state for the child components. I believe something like this would suffice.
const MasterComponent = () => {
const [isOpen, setIsOpen] = useState(false);
const [isOpen1, setIsOpen1] = useState(false);
const [isOpen2, setIsOpen2] = useState(false);
const handleParentClose = () => {
setIsOpen(false);
setIsOpen1(false);
setIsOpen2(false);
}
return (
<div>
<button onClick={handleParentClose}>toggle isOpen parent</button>
<Accordion isOpen={isOpen1} setIsOpen={setIsOpen1} />
<Accordion isOpen={isOpen2} setIsOpen={setIsOpen2} />
</div>
);
};
const Accordion = props => {
return (
<div>
I'm open: {props.isOpen}
<button onClick={props.setIsOpen}>toggle isOpen child</button>
</div>
);
}
This doesn't include code for actual visibility toggle, but the state is there, including state that closes accordions on parent close.
I'm looking for the easiest solution to pass data from a child component to his parent.
I've heard about using Context, pass trough properties or update props, but I don't know which one is the best solution.
I'm building an admin interface, with a PageComponent that contains a ChildComponent with a table where I can select multiple line. I want to send to my parent PageComponent the number of line I've selected in my ChildComponent.
Something like that :
PageComponent :
<div className="App">
<EnhancedTable />
<h2>count 0</h2>
(count should be updated from child)
</div>
ChildComponent :
const EnhancedTable = () => {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Click me {count}
</button>
)
};
I'm sure it's a pretty simple thing to do, I don't want to use redux for that.
A common technique for these situations is to lift the state up to the first common ancestor of all the components that needs to use the state (i.e. the PageComponent in this case) and pass down the state and state-altering functions to the child components as props.
Example
const { useState } = React;
function PageComponent() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1)
}
return (
<div className="App">
<ChildComponent onClick={increment} count={count} />
<h2>count {count}</h2>
(count should be updated from child)
</div>
);
}
const ChildComponent = ({ onClick, count }) => {
return (
<button onClick={onClick}>
Click me {count}
</button>
)
};
ReactDOM.render(<PageComponent />, document.getElementById("root"));
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
You can create a method in your parent component, pass it to child component and call it from props every time child's state changes, keeping the state in child component.
const EnhancedTable = ({ parentCallback }) => {
const [count, setCount] = useState(0);
return (
<button onClick={() => {
const newValue = count + 1;
setCount(newValue);
parentCallback(newValue);
}}>
Click me {count}
</button>
)
};
class PageComponent extends React.Component {
callback = (count) => {
// do something with value in parent component, like save to state
}
render() {
return (
<div className="App">
<EnhancedTable parentCallback={this.callback} />
<h2>count 0</h2>
(count should be updated from child)
</div>
)
}
}
To make things super simple you can actually share state setters to children and now they have the access to set the state of its parent.
example:
Assume there are 4 components as below,
function App() {
return (
<div className="App">
<GrandParent />
</div>
);
}
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
return (
<>
<div>{name}</div>
<Parent setName={setName} />
</>
);
};
const Parent = params => {
return (
<>
<button onClick={() => params.setName("i'm from Parent")}>
from Parent
</button>
<Child setName={params.setName} />
</>
);
};
const Child = params => {
return (
<>
<button onClick={() => params.setName("i'm from Child")}>
from Child
</button>
</>
);
};
so grandparent component has the actual state and by sharing the setter method (setName) to parent and child, they get the access to change the state of the grandparent.
you can find the working code in below sandbox,
https://codesandbox.io/embed/async-fire-kl197
IF we Have Parent Class Component and Child function component this is how we going to access child component useStates hooks value :--
class parent extends Component() {
constructor(props){
super(props)
this.ChildComponentRef = React.createRef()
}
render(){
console.log(' check child stateValue: ',
this.ChildComponentRef.current.info);
return (<> <ChildComponent ref={this.ChildComponentRef} /> </>)
}
}
Child Component we would create using
React.forwardRef((props, ref) => (<></>))
. and
useImperativeHandle(ref, createHandle, [deps])
to customizes the instance value that is exposed to parent components
const childComponent = React.forwardRef((props, ref) => {
const [info, setInfo] = useState("")
useEffect(() => {
axios.get("someUrl").then((data)=>setInfo(data))
})
useImperativeHandle(ref, () => {
return {
info: info
}
})
return (<> <h2> Child Component <h2> </>)
})
I had to do this in type script. The object-oriented aspect would need the dev to add this callback method as a field in the interface after inheriting from parent and the type of this prop would be Function. I found this cool!
Here's an another example of how we can pass state directly to the parent.
I modified a component example from react-select library which is a CreatableSelect component. The component was originally developed as class based component, I turned it into a functional component and changed state manipulation algorithm.
import React, {KeyboardEventHandler} from 'react';
import CreatableSelect from 'react-select/creatable';
import { ActionMeta, OnChangeValue } from 'react-select';
const MultiSelectTextInput = (props) => {
const components = {
DropdownIndicator: null,
};
interface Option {
readonly label: string;
readonly value: string;
}
const createOption = (label: string) => ({
label,
value: label,
});
const handleChange = (value: OnChangeValue<Option, true>, actionMeta: ActionMeta<Option>) => {
console.group('Value Changed');
console.log(value);
console.log(`action: ${actionMeta.action}`);
console.groupEnd();
props.setValue(value);
};
const handleInputChange = (inputValue: string) => {
props.setInputValue(inputValue);
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!props.inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
console.group('Value Added');
console.log(props.value);
console.groupEnd();
props.setInputValue('');
props.setValue([...props.value, createOption(props.inputValue)])
event.preventDefault();
}
};
return (
<CreatableSelect
id={props.id}
instanceId={props.id}
className="w-100"
components={components}
inputValue={props.inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder="Type something and press enter..."
value={props.value}
/>
);
};
export default MultiSelectTextInput;
I call it from the pages of my next js project like this
import MultiSelectTextInput from "../components/Form/MultiSelect/MultiSelectTextInput";
const NcciLite = () => {
const [value, setValue] = useState<any>([]);
const [inputValue, setInputValue] = useState<any>('');
return (
<React.Fragment>
....
<div className="d-inline-flex col-md-9">
<MultiSelectTextInput
id="codes"
value={value}
setValue={setValue}
inputValue={inputValue}
setInputValue={setInputValue}
/>
</div>
...
</React.Fragment>
);
};
As seen, the component modifies the page's (parent page's) state in which it is called.
I've had to deal with a similar issue, and found another approach, using an object to reference the states between different functions, and in the same file.
import React, { useState } from "react";
let myState = {};
const GrandParent = () => {
const [name, setName] = useState("i'm Grand Parent");
myState.name=name;
myState.setName=setName;
return (
<>
<div>{name}</div>
<Parent />
</>
);
};
export default GrandParent;
const Parent = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Parent")}>
from Parent
</button>
<Child />
</>
);
};
const Child = () => {
return (
<>
<button onClick={() => myState.setName("i'm from Child")}>
from Child
</button>
</>
);
};