상황에 따라 확인 대화 상자를 표시 해야하는 앱을 만들고 있습니다.
무언가를 제거하고 싶다고 말하면, deleteSomething(id)
어떤 리듀서가 그 이벤트를 잡아서 그것을 보여주기 위해 대화 상자 리듀서를 채울 것 같은 액션을 전달할 것입니다.
이 대화가 제출되면 의심의 여지가 있습니다.
- 이 컴포넌트는 첫 번째로 전달 된 조치에 따라 올바른 조치를 어떻게 전달할 수 있습니까?
- 액션 제작자가이 논리를 처리해야합니까?
- 리듀서 내부에 액션을 추가 할 수 있습니까?
편집하다:
더 명확하게하기 위해 :
deleteThingA(id) => show dialog with Questions => deleteThingARemotely(id)
createThingB(id) => Show dialog with Questions => createThingBRemotely(id)
그래서 대화 상자 구성 요소를 재사용하려고합니다. 대화 상자를 표시하거나 숨기는 것은 감속기에서 쉽게 수행 할 수 있으므로 문제가되지 않습니다. 내가 지정하려고하는 것은 왼쪽에서 흐름을 시작하는 동작에 따라 오른쪽에서 동작을 전달하는 방법입니다.
답변
내가 제안하는 접근법은 약간 장황하지만 복잡한 앱으로 확장하는 것이 좋습니다. 모달을 표시 하려면보고자 하는 모달을 설명하는 동작 을 시작하십시오.
모달을 보여주기 위해 액션 전달
this.props.dispatch({
type: 'SHOW_MODAL',
modalType: 'DELETE_POST',
modalProps: {
postId: 42
}
})
(물론 문자열은 상수 일 수 있습니다. 단순화를 위해 인라인 문자열을 사용하고 있습니다.)
모달 상태를 관리하기위한 감속기 작성
그런 다음 다음 값만 허용하는 감속기가 있는지 확인하십시오.
const initialState = {
modalType: null,
modalProps: {}
}
function modal(state = initialState, action) {
switch (action.type) {
case 'SHOW_MODAL':
return {
modalType: action.modalType,
modalProps: action.modalProps
}
case 'HIDE_MODAL':
return initialState
default:
return state
}
}
/* .... */
const rootReducer = combineReducers({
modal,
/* other reducers */
})
큰! 이제 액션을 디스패치 state.modal
하면 현재 보이는 모달 창에 대한 정보가 포함되도록 업데이트됩니다.
루트 모달 컴포넌트 작성
컴포넌트 계층 구조의 루트 <ModalRoot>
에서 Redux 저장소에 연결된 컴포넌트를 추가하십시오 . state.modal
적절한 모달 컴포넌트 를 듣고 표시하여의 소품을 전달합니다 state.modal.modalProps
.
// These are regular React components we will write soon
import DeletePostModal from './DeletePostModal'
import ConfirmLogoutModal from './ConfirmLogoutModal'
const MODAL_COMPONENTS = {
'DELETE_POST': DeletePostModal,
'CONFIRM_LOGOUT': ConfirmLogoutModal,
/* other modals */
}
const ModalRoot = ({ modalType, modalProps }) => {
if (!modalType) {
return <span /> // after React v15 you can return null here
}
const SpecificModal = MODAL_COMPONENTS[modalType]
return <SpecificModal {...modalProps} />
}
export default connect(
state => state.modal
)(ModalRoot)
우리는 여기서 무엇을 했습니까? ModalRoot
전류를 판독 modalType
하고 modalProps
에서 state.modal
되는 것이 접속되고, 대응하는 요소와 같은 렌더링 DeletePostModal
하거나 ConfirmLogoutModal
. 모든 모달은 컴포넌트입니다!
특정 모달 구성 요소 작성
여기에는 일반적인 규칙이 없습니다. 그것들은 액션을 디스패치하고, 스토어 상태에서 무언가를 읽고, 모달이 될 수있는 React 컴포넌트 일뿐 입니다.
예를 들어 DeletePostModal
다음과 같습니다.
import { deletePost, hideModal } from '../actions'
const DeletePostModal = ({ post, dispatch }) => (
<div>
<p>Delete post {post.name}?</p>
<button onClick={() => {
dispatch(deletePost(post.id)).then(() => {
dispatch(hideModal())
})
}}>
Yes
</button>
<button onClick={() => dispatch(hideModal())}>
Nope
</button>
</div>
)
export default connect(
(state, ownProps) => ({
post: state.postsById[ownProps.postId]
})
)(DeletePostModal)
는 DeletePostModal
이 연결된 모든 구성 요소와 같은 게시물 제목과 작품을 표시 할 수 있도록 저장소에 연결되어 : 그것은 포함하여, 작업을 전달할 수 있습니다 hideModal
그 자체를 숨길 필요가있을 때.
프리젠 테이션 컴포넌트 추출
모든“특정”모달에 대해 동일한 레이아웃 논리를 복사하여 붙여 넣기하는 것은 어색합니다. 하지만 구성 요소가 있습니까? 따라서 특정 모달의 기능을 모르지만 모양을 처리하는 프레젠테이션 <Modal>
구성 요소를 추출 할 수 있습니다 .
그런 다음과 같은 특정 모달 DeletePostModal
은 렌더링에 사용할 수 있습니다.
import { deletePost, hideModal } from '../actions'
import Modal from './Modal'
const DeletePostModal = ({ post, dispatch }) => (
<Modal
dangerText={`Delete post ${post.name}?`}
onDangerClick={() =>
dispatch(deletePost(post.id)).then(() => {
dispatch(hideModal())
})
})
/>
)
export default connect(
(state, ownProps) => ({
post: state.postsById[ownProps.postId]
})
)(DeletePostModal)
<Modal>
응용 프로그램에서 수락 할 수 있는 소품 세트를 생각해내는 것은 당신 에게 달려 있지만, 여러 종류의 모달 (예 : 정보 모달, 확인 모달 등) 및 여러 가지 스타일이있을 수 있다고 생각합니다.
접근성 및 외부 클릭 또는 이스케이프 키 숨기기
모달의 마지막 중요한 부분은 일반적으로 사용자가 외부를 클릭하거나 이스케이프를 누를 때 숨기려고한다는 것입니다.
이 구현에 대한 조언을 제공하는 대신 직접 구현하지 않는 것이 좋습니다. 접근성을 고려하여 올바르게 얻기가 어렵습니다.
대신, 와 같은 접근 가능한 상용 모달 구성 요소 를 사용하는 것이 좋습니다 react-modal
. 그것은 완전히 사용자 정의가 가능하며, 원하는 것을 무엇이든 넣을 수 있지만 시각 장애인이 여전히 모달을 사용할 수 있도록 접근성을 올바르게 처리합니다.
응용 프로그램에 맞는 소품을 받아들이고 자식 버튼이나 다른 내용을 생성하는 react-modal
자신 만의 랩핑도 가능 <Modal>
합니다. 그것은 단지 구성 요소입니다!
다른 접근법
여러 가지 방법이 있습니다.
어떤 사람들은이 접근 방식의 장황함을 좋아하지 않으며 “포털”이라는 기술로 컴포넌트 내부에서<Modal>
렌더링 할 수 있는 컴포넌트를 선호합니다 . 포털을 사용하면 컴포넌트를 렌더링 할 수 있으며 실제로 는 DOM의 미리 결정된 위치에서 렌더링되므로 모달에 매우 편리합니다.
사실 react-modal
이전에 이미 링크를 했으므로 기술적으로 내부적으로 그렇게 할 수 있으므로 맨 위에서 렌더링 할 필요조차 없습니다. 나는 여전히 그것을 보여주는 구성 요소에서 보여주고 싶은 모달을 분리하는 것이 좋지만, 당신은 또한 react-modal
당신의 구성 요소에서 직접 사용할 수 있으며 위에서 쓴 것을 대부분 건너 뛸 수 있습니다 .
두 가지 접근 방식을 모두 고려하고 실험하고 앱과 팀에 가장 적합한 것을 선택하는 것이 좋습니다.
답변
업데이트 : React 16.0 소개 링크를 통한 포털ReactDOM.createPortal
업데이트 : 반작용의 다음 버전 (섬유 : 아마 16 또는 17) 포털을 생성하는 방법이 포함됩니다 : ReactDOM.unstable_createPortal()
링크를
포털 사용
Dan Abramov의 대답은 첫 번째 부분은 훌륭하지만 많은 상용구가 필요합니다. 그가 말했듯이 포털을 사용할 수도 있습니다. 나는 그 아이디어를 조금 확장 할 것이다.
포털의 장점은 props를 사용하여 매우 간단한 부모 / 자식 통신을 통해 팝업 및 버튼이 React 트리에 매우 가깝게 유지된다는 것입니다. 포털로 비동기 작업을 쉽게 처리하거나 부모가 포털을 사용자 정의 할 수 있습니다.
포털이란 무엇입니까?
포털을 사용하면 document.body
React 트리에 깊이 중첩 된 요소 내 에서 직접 렌더링 할 수 있습니다 .
아이디어는 예를 들어 다음 반응 트리를 본문으로 렌더링한다는 것입니다.
<div className="layout">
<div className="outside-portal">
<Portal>
<div className="inside-portal">
PortalContent
</div>
</Portal>
</div>
</div>
그리고 당신은 출력으로 얻는다 :
<body>
<div class="layout">
<div class="outside-portal">
</div>
</div>
<div class="inside-portal">
PortalContent
</div>
</body>
inside-portal
노드 안에 번역 한 <body>
대신 정상적인 깊게 중첩 장소를.
포털을 사용하는 경우
포털은 특히 기존 React 구성 요소 위에 있어야하는 요소 (팝업, 드롭 다운, 제안, 핫스팟)를 표시하는 데 도움이됩니다.
포털을 사용하는 이유
더 이상 z-index 문제가 없습니다 . 포털에서 렌더링 할 수 <body>
있습니다. 팝업 또는 드롭 다운을 표시하려면 z- 색인 문제와 싸우지 않으려는 경우에 좋습니다. 포탈 요소 document.body
는 마운트 순서대로 추가됩니다. 즉,를 사용하지 않는 한 z-index
기본 동작은 포탈 순서대로 포털을 서로 쌓아 올리는 것입니다. 실제로, 그것은 다른 팝업 내부에서 팝업을 안전하게 열 수 있고, 생각하지 않아도 첫 번째 팝업 위에 두 번째 팝업이 표시되도록 z-index
합니다.
실제로
가장 간단한 : 로컬 React 상태 사용 : 간단한 삭제 확인 팝업을 위해 Redux 상용구를 사용할 가치가 없다고 생각하면 포털을 사용하면 코드가 크게 단순화됩니다. 상호 작용이 매우 로컬이고 실제로 구현 세부 사항 인 유스 케이스의 경우 핫 리로드, 시간 여행, 작업 로깅 및 Redux가 제공하는 모든 이점에 대해 정말로 관심이 있습니까? 개인적으로, 나는이 경우 로컬 상태를 사용하지 않습니다. 코드는 다음과 같이 간단 해집니다.
class DeleteButton extends React.Component {
static propTypes = {
onDelete: PropTypes.func.isRequired,
};
state = { confirmationPopup: false };
open = () => {
this.setState({ confirmationPopup: true });
};
close = () => {
this.setState({ confirmationPopup: false });
};
render() {
return (
<div className="delete-button">
<div onClick={() => this.open()}>Delete</div>
{this.state.confirmationPopup && (
<Portal>
<DeleteConfirmationPopup
onCancel={() => this.close()}
onConfirm={() => {
this.close();
this.props.onDelete();
}}
/>
</Portal>
)}
</div>
);
}
}
단순 : 여전히 Redux 상태를 사용할 수 있습니다 : 정말로 원한다면, 표시 connect
여부를 선택할 수 있습니다 DeleteConfirmationPopup
. 포털은 React 트리에 깊이 중첩되어 있기 때문에 부모가 소품을 포털에 전달할 수 있기 때문에이 포털의 동작을 사용자 정의하는 것이 매우 간단합니다. 포털을 사용하지 않는 경우 일반적으로 React 트리의 맨 위에 팝업을 렌더링해야합니다.z-index
이유는 일반적으로 “사용 사례에 따라 구축 한 일반 DeleteConfirmationPopup을 사용자 지정하는 방법”과 같은 사항을 고려해야합니다. 그리고 일반적으로 중첩 된 확인 / 취소 작업, 변환 번들 키 또는 더 나쁜 렌더링 함수 (또는 직렬화 할 수없는 것)가 포함 된 작업을 디스패치하는 등이 문제에 대한 해키 솔루션을 찾을 수 있습니다. 포털에서는 그렇게 할 필요가 없으며 일반 소품 만 전달할 수 있습니다 DeleteConfirmationPopup
.DeleteButton
결론
포털은 코드를 단순화하는 데 매우 유용합니다. 더 이상 그들 없이는 할 수 없었습니다.
포털 구현은 다음과 같은 다른 유용한 기능으로도 도움이 될 수 있습니다.
- 접근성
- 포털을 닫는 Espace 단축키
- 외부 클릭 처리 (포털 닫기 또는 닫기)
- 링크 클릭 처리 (포털 닫기 또는 닫기)
- 포털 트리에서 사용 가능한 반응 컨텍스트
반응-포탈 또는 반응-모달 은 전체 화면이어야하는 팝업, 모달 및 오버레이에 적합합니다. 일반적으로 화면 중앙에 중앙에 위치합니다.
react-tether 는 대부분의 React 개발자에게 알려지지 않았지만 여기서 찾을 수있는 가장 유용한 도구 중 하나입니다. 테더 를 사용하면 포털을 만들 수 있지만 지정된 대상을 기준으로 포털을 자동으로 배치합니다. 툴팁, 드롭 다운, 핫스팟, 헬프 박스에 이상적입니다. 위치 absolute
/ relative
및에 문제가 z-index
있거나 드롭 다운이 뷰포트 외부로 나가는 경우 Tether가 모든 문제를 해결합니다.
예를 들어, 온보드 핫스팟을 쉽게 구현하면 클릭하면 툴팁으로 확장됩니다.
실제 생산 코드는 여기에 있습니다. 더 간단 할 수 없습니다 🙂
<MenuHotspots.contacts>
<ContactButton/>
</MenuHotspots.contacts>
편집 : 방금 포털을 원하는 노드로 렌더링 할 수있는 반응 게이트 를 발견했습니다 (본문은 아님).
편집 : 반응 팝퍼 는 반응 테더에 대한 적절한 대안이 될 수 있습니다. PopperJS 는 DOM을 직접 건드리지 않고 요소에 대한 적절한 위치 만 계산하는 라이브러리로, 사용자가 DOM 노드를 배치 할 위치와시기를 선택할 수 있도록하며 Tether는 본문에 직접 추가합니다.
편집 : 반응 슬롯 채우기 가있어 흥미롭고 트리의 원하는 위치에 요소를 예약 된 요소 슬롯에 렌더링하여 유사한 문제를 해결할 수 있습니다
답변
이 주제에 대한 JS 커뮤니티의 알려진 전문가들이 제공하는 많은 훌륭한 솔루션과 귀중한 논평이 여기에 있습니다. 그것은 보일 수있는 사소한 문제가 아니라는 것을 나타내는 지표 일 수 있습니다. 이것이 이것이 문제에 대한 의심과 불확실성의 원인이 될 수 있다고 생각합니다.
여기서 근본적인 문제는 React에서 컴포넌트를 부모에게만 마운트 할 수 있다는 것입니다. 항상 원하는 동작은 아닙니다. 그러나이 문제를 해결하는 방법은 무엇입니까?
이 문제를 해결하기 위해 해결 된 해결책을 제안합니다. 자세한 문제 정의, src 및 예제는 https://github.com/fckt/react-layer-stack#rationale 에서 찾을 수 있습니다.
이론적 해석
react
/react-dom
2 가지 기본 가정 / 아이디어가 제공됩니다.
- 모든 UI는 자연스럽게 계층 적입니다. 이것이 우리가
components
서로를 포장react-dom
기본적으로 (물리적으로) 자식 컴포넌트를 부모 DOM 노드에 마운트문제는 때로는 두 번째 속성이 귀하의 경우에 원하는 것이 아니라는 것입니다. 때로는 컴포넌트를 다른 물리적 DOM 노드에 마운트하고 부모와 자식 사이의 논리적 연결을 동시에 유지하려고합니다.
정식 예제는 툴팁과 같은 구성 요소입니다. 어떤 개발 프로세스 시점에서 설명을 추가해야한다는 것을 알 수 있습니다
UI element
. 고정 된 레이어로 렌더링되고 좌표 (UI element
마우스 좌표 또는 좌표)를 알고 있어야합니다 . 동시에 정보를 표시해야하는지 여부, 내용 및 상위 구성 요소의 일부 컨텍스트에 대한 정보가 필요합니다. 이 예는 때때로 논리적 계층이 물리적 DOM 계층과 일치하지 않음을 보여줍니다.
한 번 봐 가지고 https://github.com/fckt/react-layer-stack/blob/master/README.md#real-world-usage-example를 귀하의 질문에 대한 대답은 구체적인 예를 볼 수 :
import { Layer, LayerContext } from 'react-layer-stack'
// ... for each `object` in array of `objects`
const modalId = 'DeleteObjectConfirmation' + objects[rowIndex].id
return (
<Cell {...props}>
// the layer definition. The content will show up in the LayerStackMountPoint when `show(modalId)` be fired in LayerContext
<Layer use={[objects[rowIndex], rowIndex]} id={modalId}> {({
hideMe, // alias for `hide(modalId)`
index } // useful to know to set zIndex, for example
, e) => // access to the arguments (click event data in this example)
<Modal onClick={ hideMe } zIndex={(index + 1) * 1000}>
<ConfirmationDialog
title={ 'Delete' }
message={ "You're about to delete to " + '"' + objects[rowIndex].name + '"' }
confirmButton={ <Button type="primary">DELETE</Button> }
onConfirm={ this.handleDeleteObject.bind(this, objects[rowIndex].name, hideMe) } // hide after confirmation
close={ hideMe } />
</Modal> }
</Layer>
// this is the toggle for Layer with `id === modalId` can be defined everywhere in the components tree
<LayerContext id={ modalId }> {({showMe}) => // showMe is alias for `show(modalId)`
<div style={styles.iconOverlay} onClick={ (e) => showMe(e) }> // additional arguments can be passed (like event)
<Icon type="trash" />
</div> }
</LayerContext>
</Cell>)
// ...
답변
제 생각에는 최소한의 구현에는 두 가지 요구 사항이 있습니다. 모달이 열려 있는지 여부를 추적하는 상태 및 모달을 표준 반응 트리 외부로 렌더링하는 포털입니다.
아래의 ModalContainer 구성 요소는 모달 및 트리거에 대한 해당 렌더 기능과 함께 해당 요구 사항을 구현하며, 이는 모달을 열기 위해 콜백을 실행하는 역할을합니다.
import React from 'react';
import PropTypes from 'prop-types';
import Portal from 'react-portal';
class ModalContainer extends React.Component {
state = {
isOpen: false,
};
openModal = () => {
this.setState(() => ({ isOpen: true }));
}
closeModal = () => {
this.setState(() => ({ isOpen: false }));
}
renderModal() {
return (
this.props.renderModal({
isOpen: this.state.isOpen,
closeModal: this.closeModal,
})
);
}
renderTrigger() {
return (
this.props.renderTrigger({
openModal: this.openModal
})
)
}
render() {
return (
<React.Fragment>
<Portal>
{this.renderModal()}
</Portal>
{this.renderTrigger()}
</React.Fragment>
);
}
}
ModalContainer.propTypes = {
renderModal: PropTypes.func.isRequired,
renderTrigger: PropTypes.func.isRequired,
};
export default ModalContainer;
간단한 사용 사례는 다음과 같습니다.
import React from 'react';
import Modal from 'react-modal';
import Fade from 'components/Animations/Fade';
import ModalContainer from 'components/ModalContainer';
const SimpleModal = ({ isOpen, closeModal }) => (
<Fade visible={isOpen}> // example use case with animation components
<Modal>
<Button onClick={closeModal}>
close modal
</Button>
</Modal>
</Fade>
);
const SimpleModalButton = ({ openModal }) => (
<button onClick={openModal}>
open modal
</button>
);
const SimpleButtonWithModal = () => (
<ModalContainer
renderModal={props => <SimpleModal {...props} />}
renderTrigger={props => <SimpleModalButton {...props} />}
/>
);
export default SimpleButtonWithModal;
렌더링 된 모달 및 트리거 구성 요소의 구현에서 상태 관리 및 상용구 논리를 분리하려고하므로 렌더링 함수를 사용합니다. 이를 통해 렌더링 된 구성 요소를 원하는대로 만들 수 있습니다. 귀하의 경우, 모달 구성 요소는 비동기 작업을 전달하는 콜백 함수를 수신하는 연결된 구성 요소 일 수 있다고 가정합니다.
너무 자주 발생하지 않는 트리거 구성 요소에서 모달 구성 요소로 동적 소품을 보내야하는 경우 동적 소품을 자체 상태로 관리하고 원래 렌더링 방법을 향상시키는 컨테이너 구성 요소로 ModalContainer를 래핑하는 것이 좋습니다. 그래서.
import React from 'react'
import partialRight from 'lodash/partialRight';
import ModalContainer from 'components/ModalContainer';
class ErrorModalContainer extends React.Component {
state = { message: '' }
onError = (message, callback) => {
this.setState(
() => ({ message }),
() => callback && callback()
);
}
renderModal = (props) => (
this.props.renderModal({
...props,
message: this.state.message,
})
)
renderTrigger = (props) => (
this.props.renderTrigger({
openModal: partialRight(this.onError, props.openModal)
})
)
render() {
return (
<ModalContainer
renderModal={this.renderModal}
renderTrigger={this.renderTrigger}
/>
)
}
}
ErrorModalContainer.propTypes = (
ModalContainer.propTypes
);
export default ErrorModalContainer;
답변
모달을 연결된 컨테이너에 싸고 여기에서 비동기 작업을 수행하십시오. 이렇게하면 작업을 트리거하기 위해 디스패치와 onClose 소품에 도달 할 수 있습니다. dispatch
소품에서 도달하려면에 함수를 전달 하지 마십시오 .mapDispatchToProps
connect
class ModalContainer extends React.Component {
handleDelete = () => {
const { dispatch, onClose } = this.props;
dispatch({type: 'DELETE_POST'});
someAsyncOperation().then(() => {
dispatch({type: 'DELETE_POST_SUCCESS'});
onClose();
})
}
render() {
const { onClose } = this.props;
return <Modal onClose={onClose} onSubmit={this.handleDelete} />
}
}
export default connect(/* no map dispatch to props here! */)(ModalContainer);
모달이 렌더링되고 가시성 상태가 설정된 앱 :
class App extends React.Component {
state = {
isModalOpen: false
}
handleModalClose = () => this.setState({ isModalOpen: false });
...
render(){
return (
...
<ModalContainer onClose={this.handleModalClose} />
...
)
}
}