Dialog
export const layoutContext = React.createContext({});
layoutContext.displayName = "LayoutContext";
export class Layout extends React.Component {
constructor(props) {
super(props);
this.state = {
dialog: <Dialog></Dialog>,
};
}
render() {
const value = {
dialog: this.state.dialog,
};
return (
<layoutContext.Provider value={value}>
{this.props.children}
</layoutContext.Provider>
);
}
}
export const DialogContainer = () => (
<layoutContext.Consumer>
{({ dialog }) => dialog && <Backdrop>{dialog}</Backdrop>}
</layoutContext.Consumer>
);
DialogContainer는 Provider에서 제공한 Dialog를 조건부 rendering한다.
const Page = ({ header, children, footer }) => (
<div className="Page">
<header>{header}</header>
<main>{children}</main>
<footer>{footer}</footer>
<MyLayout.DialogContainer></MyLayout.DialogContainer>
</div>
);
이를 모든 Page rendering을 담당하는 Page 에 추가하였다. dialog (일종의 팝업)은 어떤 페이지에서든 나타날 수 있기 때문이다.
const App = () => (
<MyLayout.Layout>
<MyRouter.Router>
<MyRouter.Routes>
<MyRouter.Route path="/cart" element={<CartPage />} />
<MyRouter.Route path="/order" element={<OrderPage />} />
<MyRouter.Route path="/" element={<ProductPage />} />
</MyRouter.Routes>
</MyRouter.Router>
</MyLayout.Layout>
);
어떤 곳이든 Provider의 영향을 받아야하기 때문에 최상단 Component로 감싸져있다.
export const withLayout = (WrappedComponent) => {
const WithLayout = (props) => (
<layoutContext.Consumer>
{({ dialog, setDialog }) => {
const openDialog = setDialog;
const closeDialog = () => setDialog(null);
const enhancedProps = {
dialog,
openDialog,
closeDialog,
};
return <WrappedComponent {...props} {...enhancedProps} />;
}}
</layoutContext.Consumer>
);
WithLayout.displayName = `WithLayout(${getComponentName(WrappedComponent)})`;
return WithLayout;
};
export const DialogContainer = withLayout(
({ dialog }) => dialog && <Backdrop>{dialog}</Backdrop>
);
withRouter처럼 고차컴포넌트로 withLayout을 하나 만들엇다.
export const DialogContainer = withLayout(
({ dialog }) => dialog && <Backdrop>{dialog}</Backdrop>
);
기존과 동일하게 enhancedProps를 주입받아서 Consumer 같은 boilerplate없이 쉽게 사용가능 해졌다.
현재 padding-bottom 값이 따로 정의 되어 있지 않기 때문에 상위 element css로부터 영향을 받게 된다.
<style>
.red {
background-color: red;
}
.green div{
background-color: green;
}
</style>
const root = ReactDOM.createRoot(document.getElementById("root"));
const root2 = ReactDOM.createRoot(document.getElementById("dialog"));
// root.render(<App />);
const Red = () => <div className="red">Red</div>;
const Green = () => (
<div className="green">
<div>Green</div>
<Red></Red>
</div>
);
root.render(<Green></Green>);
root.render(<Red></Red>);
아예 부모를 다르게 Componet들을 render 하면 Red 가 green color로 override 되지 않지만 Green 의 하위 component로 Red Component로 해주고 싶을 수 있다. 이떄 사용할 수 있는게 리액트 포탈이다.
컴포넌트 트리는 마치 Red가 Green 하위 컴포넌트 인것처럼 동작하기 때문에 Red에서 발생한 이벤트가 bubbling되어서 상위 컴포넌트인 Green에게까지 전달이 될 수 있다.
const root = ReactDOM.createRoot(document.getElementById("root"));
const root2 = ReactDOM.createRoot(document.getElementById("dialog"));
// root.render(<App />);
import ReactDOM2 from "react-dom";
const Red = () =>
ReactDOM2.createPortal(
<div className="red">Red</div>,
document.getElementById("dialog")
);
const Green = () => (
<div className="green" onClick={(e) => console.log(e)} >
<div>Green</div>
<Red></Red>
</div>
);
root.render(<Green></Green>);
React Portal을 사용하니 실제 Dom tree는 서로 별도의 Dom을 가지는 것으로 나온다.
또한 Red 를 클릭했을때 발생하는 click event가 잘 bubbling 되는것을 확인할 수 있다.
export const DialogContainer = withLayout(
({ dialog }) =>
dialog &&
ReactDOM.createPortal(
<Backdrop>{dialog}</Backdrop>,
document.querySelector("#dialog")
)
);
portal을 사용하여 Dialog은 다른 DOM에 mount되게 하지만 여전히 Dialog를 쓰는 Page의 child component로 동작한다.
const PaymentSuccessDialog = ({ navigate, closeDialog }) => {
const handleClickNo = () => {
closeDialog();
navigate("/");
};
const handleClickYes = () => {
closeDialog();
navigate("/order");
};
return (
<Dialog
header={<>결제 완료</>}
footer={
<>
<Button style={{ marginRight: "8px" }} onClick={handleClickNo}>
아니오
</Button>
<Button styleType="brand" onClick={handleClickYes}>
네, 주문상태를 확인합니다.
</Button>
</>
}
>
결제가 완료되었습니다. 주문 상태를 보러 가시겠습니까?
</Dialog>
);
};
export default MyLayout.withLayout(MyRouter.withRouter(PaymentSuccessDialog));
cart page에서 주문을 하고나서 성공을 하면 호출되는 component이다. yes ,no로 어떤곳으로 navigate할지 결정하게 된다.
async handleSubmit(values) {
const { startLoading, finishLoading, openDialog } = this.props;
startLoading("결제중...");
try {
await OrderApi.createOrder(values);
} catch (e) {
openDialog(<ErrorDialog />);
return;
}
finishLoading();
openDialog(<PaymentSuccessDialog />);
}
cart page에서 주문 결제 api를 호출할 때 성공시 호출하는것을 확인할 수 있다.
Dialog 개선: Ref
굳이 사용자가 가르키키전에 Ref를 통해 원하는 element를 focus 할 수 있다.
class Dialog extends React.Component {
constructor(props) {
super(props);
this.footerRef = React.createRef();
}
componentDidMount() {
if (!this.footerRef.current) return;
const buttons = Array.from(
this.footerRef.current.querySelectorAll("button")
);
if (buttons.length === 0) return;
const activeButton = buttons[buttons.length - 1];
activeButton.focus();
}
render() {
const { header, children, footer } = this.props;
return (
<div className="Dialog">
{header && <header>{header}</header>}
<main>{children}</main>
{footer && <footer ref={this.footerRef}>{footer}</footer>}
</div>
);
}
}
export default Dialog;
Dialog를 Class Component 로 변경하여서 state에 footer에 Ref를 적용한다.
PaymentSucessDialog에서 footer props값으로 yes or no button을 넘겨줬기 때문이다.
componentDidMount 시점에 button element중 가장 마지막에 focus를 주도록한다.
'FrontEnd > [리액트 2부] 고급 주제와 훅' 카테고리의 다른 글
[3.2장 상태 훅] (0) | 2024.03.12 |
---|---|
[3.1장 클래스/함수 컴포넌트][3.2장 상태훅] (0) | 2024.03.11 |
[2.2장 라우터 1] [2.3장 라우터 2] (0) | 2024.03.04 |
[2.1장 컨텍스트] (0) | 2024.03.01 |
[1.4장 장바구니 화면] (0) | 2024.02.27 |