React自定义一个小Modal
涉及到的API和动画库
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
ReactDOM.createPortal(child, container)
第一个参数(child
)是任何可渲染的 React 子元素,例如一个元素,字符串或 fragment。
第二个参数(container
)是一个 DOM 元素。
- React Transition
- @emotion CSS IN JS 的方案!
思路
写一个Modal组件 (样式占满全屏~ 背景灰色透明,插槽可插入元素)其实蛮简单的!
然后外部状态控制Modal组件是否显示!(是否挂载到document.body)
使用CSS Transition 控制其动画效果!
首先封装一个Modal组件
// 容器来写样式
const CldModalContainer = styled.div`
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
color: #FFFFFF;
`;
// 简单的定义一下组件的props,用于外部传递值控制组件的显示隐藏!
interface ModalProps {
open: boolean,
onClose: () => void
}
然后我们的组件就需要 ReactDOM.createPortal
渲染我们的组件,Modal此类组件一般是挂在到 body 的
const CldModal: React.FC<ModalProps> = (props) => {
const {open, onClose,children} = props
return ReactDOM.createPortal(
open ?
<CldModalContainer onClick={onClose} >
{children}
</CldModalContainer>
: <></>,
document.body
)
}
export default CldModal;
上面代码中,open 用来控制modal的显示与隐藏! onClose 用来回调外部组件调用关闭的函数!可以看看下面的使用modal组件
const Layout = ()=>{
const [openModal,setOpenModal] = useState(false)
return <LayoutContainer>
<Content>
<button onClick={()=>setOpenModal(!openModal)}>显示</button>
<CldModal open={openModal} onClose={()=>setOpenModal(false)}>
123123
</CldModal>
</Content>
</LayoutContainer>
}
这样一个基础版本的就完成了,可以看看效果!
开启关闭model增加动画效果
感觉上面的效果比较生硬~ 需要添加一些动画效果~
可以使用 react-transition-group
这个库,来控制组件显示隐藏时候的动画~
我们可以使用 styled 的继承组件效果直接在 CSSTransition
的基础上对内部的动画效果进行自定义
当一个元素从进入页面到退出
进入:(包裹组件第一次挂载的时候,会先执行一次进入的状态改变 appear 相关!)
appear/enter/exit 都有对应 enter(开始进入的样式!),active(进入中的样式!),done(进入结束后的最终样式!)
过程目标动画 (xx-active)和完成动画(xxx-done)基本上都会写成相同的~ 除非可能会有那种反复横跳的效果!
const ModalCSSTransition = styled(CSSTransition)`
transition: all ${prop=>prop.timeout}ms ease; // 保证过渡时间和下面组件指定的timeout一样
&.appear,
&.enter {
opacity: 0;
}
&.appear-active,
&.appear-done {
opacity: 1;
}
&.enter-done,
&.enter-active {
opacity: 1;
}
&.exit-done,
&.exit-active {
opacity: 0;
}
`
那么上面的样式基本上就是组件从透明,到显示成最终样式的代码!
然后用这个 ModalCSSTransition 包裹住我们的 Modal组件
这里还需要传一些参数 in 为true的时候,动画状态就会开始变化,timeout表示动画的持续时间! appear 表示元素第一次进入的时候也需要添加动画
更为详细的使用可以参考官方文档~
return ReactDOM.createPortal(
open ?
<ModalCSSTransition onClick={onClose} in={open} timeout={300} appear={true}>
<CldModalContainer onClick={onClose} >
{children}
</CldModalContainer>
</ModalCSSTransition>
: <></>,
document.body
)
我们来看看效果!
可以看到我们打开Modal的时候样式是有过渡效果的,但是关闭的时候是直接关闭的,这是为什么呢?可以思考一下🤔!
过渡动画关闭的优化
其实原因就是我们
ReactDOM.createPortal
上面挂载到body的逻辑有问题!
因为我们使用open同时控制了动画效果和DOM的挂载!当open为false的时候!我们的dom已经从body上干掉了!
那我们的过渡效果肯定也就看不到了!
那么解决方式其实也很简单!我们可以内部维护一个和 open 同步的参数 mountOpen 用来控制我们dom的挂载!
open 就专门用来控制动画效果!
当我们的modal从页面上关闭的时候,我们可以让这个mountOpen延迟变为false,延迟的时间的就和我们指定的动画时间一样!
当动画执行结束,我们在让modal组件从body中移除
代码如下:
const CldModal: React.FC<ModalProps> = (props) => {
const {open, onClose, children} = props
const [mountOpen, setMountOpen] = useState(open) // 用于控制mount
const timeOut = 1000;
useEffect(() => {
let timer: NodeJS.Timeout | null;
if (open) {
setMountOpen(open)
} else {
// 关闭的时候使用定时器延迟关闭!
timer = setTimeout(() => {
setMountOpen(false)
if (timer) {
clearTimeout(timer)
timer = null;
}
}, timeOut)
}
}, [open])
return ReactDOM.createPortal(
mountOpen ?
<ModalCSSTransition onClick={onClose} in={open} timeout={timeOut} appear={true}>
<CldModalContainer onClick={onClose}>
{children}
</CldModalContainer>
</ModalCSSTransition>
: <></>,
document.body
)
}
这样呢~ 这个modal
组件的体验就比较好了!
评论区