I want to create a React component for a Dialog.
I want to use Bootstrap Modal component.
I use TypeScript.
This is the usage I plan to use:
<Dialog icon="edit" title="Edit the item" buttons={...} onSave={...} >
... put here the content of the dialog
</Dialog>
Now, I started to write the code to make the Modal visible (pseudo-code):
interface Props {
// open by:
buttonText: OpenIcons,
title: string,
isOpen?: boolean
}
export const Dialog:FC<Props> = (props) => {
let {buttonText, title, isOpen, children} = props
return <>
{button && <button className="btn btn-sm">{buttonText}}</button> }
{ isOpen && <div>
{title}
<div>{children}</div>
<buttons>...</buttons>
</div>
}
</>
}
The problem is: how can I manage to change the isOpen property to true and made the Dialog component reflect this state?
Yes, I can use another component in the page (button text, icon, link...) that manage to change that state and pass it to the Dialog component, but I don't want to.
I want the Dialog component to self-manage its icon/button/link trigger.
The only solution I can think about is create another component inside the Dialog component, a sort of wrapper.
It has to manage the DialogButton (or whatever) and a new DialogInternal component that are not exported, so that the developer just see the exorted Dialog component.
I hope this idea is clear.
There is any better way (simpler, cleaner) to achieve my goal?
Here is your code using a useState hook
export const Dialog:FC<Props> = (props) => {
const [open, setOpen] = React.useState(false);
let {buttonText, title, children} = props
const handleClick = () => setOpen(!open);
return <>
{button && <button className="btn btn-sm" onClick={handleClick}>{buttonText}}</button> }
{ open && <div>
{title}
<div>{children}</div>
<button onClick={handleClick}>Close</button>
</div>
}
</>
}
You can use react hooks like useState and useEffect for maintaining a state in functional components
Ex:
for Usestate
const [isOpen, setIsOpen] = React.useState(initial value);
for useEffect
React.useEffect(()=>{},[dependencies]);
Related
Hi I am a beginner in React, I am using Fluent UI in my project .
I am planning to use Panel control from Fluent UI and make that as common component so that I can reuse it.I use bellow code
import * as React from 'react';
import { DefaultButton } from '#fluentui/react/lib/Button';
import { Panel } from '#fluentui/react/lib/Panel';
import { useBoolean } from '#fluentui/react-hooks';
export const PanelBasicExample: React.FunctionComponent = () => {
const [isOpen, { setTrue: openPanel, setFalse: dismissPanel }] = useBoolean(false);
return (
<div>
<Panel
headerText="Sample panel"
isOpen={isOpen}
onDismiss={dismissPanel}
// You MUST provide this prop! Otherwise screen readers will just say "button" with no label.
closeButtonAriaLabel="Close"
>
<p>Content goes here.</p>
</Panel>
</div>
);
};
https://developer.microsoft.com/en-us/fluentui#/controls/web/panel#best-practices
I remove <DefaultButton text="Open panel" onClick={openPanel} /> from the example .
So my question is how can I open or close this panel from any other component ?
I would use React useState hook for this.
Make a state in the component that you want render the Panel like
const [openPanel, setOpenPanel] = useState({
isOpen: false,
headerText: ''
})
Lets say for example you will open it from button
<Button onClick={() => setOpenPanel({
isOpen: true,
headerText: 'Panel-1'
})
}> Open me ! </Button>
Then pass the state as props to the Panel component
<PanelBasicExample openPanel={openPanel} setOpenPanel={setOpenPanel} />
in PanelBasicExample component you can extract the props and use it.
export const PanelBasicExample(props) => {
const {openPanel, setOpenPanel} = props
const handleClose = () => {setOpenPanel({isOpen: false})}
return (
<div>
<Panel
headerText={openPanel.headerText}
isOpen={openPanel.isOpen}
onDismiss={() => handleClose}
// You MUST provide this prop! Otherwise screen readers will just say "button" with no label.
closeButtonAriaLabel="Close"
>
<p>Content goes here.</p>
</Panel>
</div>
);
}
I have a React button component with onClick handler and its state - stating whether it was opened on click. If I render two of such components in a wrapper container, and I click on one of them, both buttons update the state. How should I handle the state, so that only one of the buttons updates without using ids?
import React, {useState} from 'react';
const Button = (props) => {
const [open, setOpen] = useState(false);
const text = open ? 'Open' : 'Closed';
const toggleButton = () => { setOpen(!open) };
return (
<button onClick={toggleButton}>{text}</button>
)
}
// parent component
import React from 'react';
import Button from './Button'
const ButtonsWrapper = () => {
return (
<div>
<Button />
<Button />
</div>
)
}
I also tried reversing the logic and putting the state in a wrapper component, and then passing the onClick handler as a props to a button, but the sitation is the same. They both change the state at the same time when one of them is clicked.
I am using React Hooks.
My understanding is that you are saying that when you click one button both buttons seems to have their state updated, but you only want the clicked button to update its state.
(i.e. if you click Button A only that button will show 'Open' as its text, Button B will continue to show closed)
If the above is right, then your code should already do the correct thing. If it doesn't then you might have a bug elsewhere that would cause this.
If however you want to click one button and BOTH should switch state then you could achieve this by keeping track of the state in the parent component:
import React, {useState} from 'react';
const Button = (props) => {
const text = props.isOpen ? 'Open' : 'Closed';
const handleClick = () => {
// do some stuff you want each button to do individually
.....
props.onClick()
}
return (
<button onClick={handleClick}>{text}</button>
)
}
// parent component
import React from 'react';
import Button from './Button'
const ButtonsWrapper = () => {
const [buttonState, setButtonState] = useState(false)
return (
<div>
<Button onClick={() => setButtonState(!buttonState)} isOpen={buttonState} />
<Button onClick={() => setButtonState(!buttonState)} isOpen={buttonState} />
</div>
)
}
I have the following component which makes up my modal:
import React from 'react';
import { ModalBody, Button, Alert } from 'bootstrap';
import { AppModalHeader } from '../../common/AppModalHeader';
import ModalWrapper from './ModalWrapper';
const QuestionModal= ({
title,
noText = 'No',
yesText = 'Yes',
questionText,
onYesAction
children
}) => {
const { toggle, isOpen, openModal } = useModalForm();
return (
<React.Fragment>
<ModalWrapper className={className} isOpen={isOpen} toggle={toggle}>
<AppModalHeader toggle={toggle}>{modalTitle}</AppModalHeader>
{isOpen ? (
<ModalBody>
<p>{questionText}</p>
<Button
className="float-right"
color="primary"
onClick={() => {
if (onYesAction !== undefined) {
onYesAction(toggle);
}
}}
>
{yesText != null ? yesText : 'Yes'}
</Button>
</ModalBody>
) : null}
</ModalWrapper>
{children({
triggerModal: () => openModal({ id: undefined }),
toggle
})}
</React.Fragment>
);
};
export default QuestionModal;
I want to use it as such, where I can dynamically choose the name of the trigger that opens the modal:
In use e.g. (note: the inner question modal would be repeated, used 4 or 5 times in my application):
....
<QuestionModal
//....params that match up with above
>
{({ triggerModal }) => (
<QuestionModal
//....params that match up with the component
>
{({ triggerModal2 }) => (
<>
<Button onClick={()=>triggerModal();}>Trigger Modal 1</Button>
<div>
<Button onClick={()=>triggerModal2();}>Trigger Modal 2</Button>
</div>
</>
</>
)}
</QuestionModal>
....
How could I achieve this, by extending the question modal to pass a dynamic function? Just because I keep getting stuck in having to think about duplicating the original component, I want to make this component as reusable as I can. Any help would be greatly appreciated.
Thanks in advance
I think you're overcomplicating things. The problem is you're trying to control whether or not the modal is rendered from inside the modal itself. If you really want to have reusable components, it's good to decouple presentation from logic. In your case, you want to have a modal component with all the presentation/layout/styling stuff and pass in via props the actual content.
For example:
import React from 'react';
import { ModalBody, Button, Alert } from 'bootstrap';
import { AppModalHeader } from '../../common/AppModalHeader';
import ModalWrapper from './ModalWrapper';
const QuestionModal= ({
title,
noText = 'No',
yesText = 'Yes',
questionText,
onYesAction
children
}) => {
return (
<React.Fragment>
<ModalWrapper>
<AppModalHeader toggle={toggle}>{title}</AppModalHeader>
<ModalBody>
<p>{questionText}</p>
<Button
className="float-right"
color="primary"
onClick={onYesAction}
>
{yesText}
</Button>
</ModalBody>
</ModalWrapper>
</React.Fragment>
);
};
export default QuestionModal;
Now this is a purely presentational component, it creates a skeleton in which you put the actual content. And for using it, you'll control whether or not the modal is rendered from where it is actually used, like so:
import React, {useState} from 'react';
import QuestionModal from './QuestionModal'
const SomeComponent = (props) => {
const [showModal, setShowModal] = useState(false);
const toggleModal = () => {
setShowModal(!showModal);
}
const yesActionLogic = () => {
// Your yes-action logic...
}
return (
<div>
{showModal ? (
<QuestionModal
title="Sample title",
questionText="Question?"
onYesAction={yesActionLogic}
/>
) : null}
<Button onClick={toggleModal}>Toggle Modal</Button>
{/* The rest of your stuff... */}
</div>
);
}
If you want to create reusable components, it's good practice to not put any business logic on it. Use props to pass in functions that will be triggered from inside the components, and lift all the work to the components that actually hold your business logic.
One of the SOLID principles of software engineering is called Single-responsibility principle, and you can apply it to your React components:
Your Modal component is responsible for displaying data in its correct layout and triggering some set of functions from outside, regardless of what data/logic you pass.
This Modal component will be used by some other component whose responsibility is to show the user a modal with some specific data, at the right time.
So it makes sense that you should toggle your modal from outside.
On a personal note, I like to structure a React app in components that hold only presentational logic, and are used by containers, which are more logic-dense (generally having async requests).
Why does using a custom hook to render a component rerenders the App Component. If I toggle <Comp> or <AnotherComp> the entire App component gets rerendered same for the other component.
I tried React.memo and wrap the component again in App component but has no effect.
I read that its the way react compares and treats that functions are different betwn renders. But is there an advanced pattern people use for this purpose??
export const useCard = (params)=>{
const { ToBeWrappedComponent } = params;
const [isOpen,setIsOpen] = useState(true);
const toggle= ()=> setIsOpen(!isOpen);
const WrappedComponent = ()=>{
return (
<Collapse isOpen={isOpen} >
<button onClick= {toggle}> </button>
<ToBeWrappedComponent />
</Collapse>
)
}
return [WrappedComponent,toggle]
};
const App = ()=>{
const [Comp, toggleComp] = useCard({ToBeWrappedComponent: ()=> (<h1>Wrapped Item <h1>) });
const [AnotherComp, toggleAnotherComp] = useCard({ToBeWrappedComponent: ()=> (<h1>Another Wrapped Item <h1>) })
return (
<AnotherComp > </AnotherComp>
<Comp> </Comp>
)
}
Please note this code is just an exampple I have created to demonstrate what I am facing, I do more complex things with this apporach and just want to know about Advanced patterns to achieve it and the reason for rendering. Thanks
Because this invocation of useState is actually called within the App component's function:
const [isOpen,setIsOpen] = useState(true);
So it becomes a part of App's state, and not the wrapped component's state.
The component rerenders because every time the App is rerendered, the WrappedComponent function is created anew. It is a different function with a different address in memory, and therefore the component tree is rerendered from scratch.
This is a really weird pattern you are using. You are really overcomplicating things. Why not simply pass the render function to the wrapper component?
const TogglableComponent = ({ renderWrappedComponent, isOpen, onClick }) => {
return (
<Collapse isOpen={isOpen} >
<button onClick={onClick} />
{ renderWrappedComponent() }
</Collapse>
)
};
You then control the toggle state of each component from its parent through props. Or, if you don't care about passing the toggle state to the parent component, just store it in the wrapper:
const TogglableComponent = ({ renderWrappedComponent }) => {
const [isOpen, setIsOpen] = React.useState(false);
return (
<Collapse isOpen={isOpen} >
<button onClick={() => setIsOpen(!isOpen)} />
{ renderWrappedComponent() }
</Collapse>
)
};
I'm trying to store a button element globally using react context
const QuitLesson = ({goToLessons}) => {
// this is where I'm trying to create a new context
const closeBtnContext = React.createContext();
return (
<div>
<div styleName='close'>
<button
id='closeLessonBtn'
type='button'
styleName='closeIcon'
onClick={() => setQuitOverlayOpen(true)}
disabled={quitOverlayOpen}>
<img src='/resources/img/btn_close.svg'/>
</button>
</div>
{quitOverlayOpen &&
<Overlay
onClose={() => setQuitOverlayOpen(false)}
onConfirm={goToLessons}
/>
}
</div>
);
};
QuitLesson.propTypes = {
goToLessons: PropTypes.func.isRequired
};
export default CSSModules(QuitLesson, styles);
How do I add this button to context so I can use it in another component later?
You can't createContext inside a functional component.
// Declair this outside of the QuitLesson functional component.
const closeBtnContext = React.createContext();
const QuitLesson = ({goToLessons}) => {
return (
<div>
<div styleName='close'>
<button
id='closeLessonBtn'
type='button'
styleName='closeIcon'
onClick={() => setQuitOverlayOpen(true)}
disabled={quitOverlayOpen}>
<img src='/resources/img/btn_close.svg'/>
</button>
</div>
{quitOverlayOpen &&
<Overlay
onClose={() => setQuitOverlayOpen(false)}
onConfirm={goToLessons}
/>
}
</div>
);
};
QuitLesson.propTypes = {
goToLessons: PropTypes.func.isRequired
};
export default CSSModules(QuitLesson, styles);
I also think you maybe trying to use context in a way that it wasn't intended. the context is for passing data (state) and functions primarily to update that state.
from https://reactjs.org/docs/context.html
Context is designed to share data that can be considered “global” for
a tree of React components, such as the current authenticated user,
theme, or preferred language.
It sounds like you want to store the actual component in state to then reuse later. A better way of doing this that is 'React' friendly would be to create the closeBtnContext.context and wrap the app in the closeBtnContext.provider and then wrap the QuitLesson component with the closeBtnContext.consumer and then import the QuitLesson component wherever it is needed in your app.