React useEffect cleanup function not undoing changes caused by React strict mode in development

This issue references to the fact that React intentionally remounts your components in development to find bugs.

I know I can turn off strict mode, but React doesn’t recommend it. I’m just looking for a recommended approach to fix this.

I’m trying to build an Open Modal Component that closes either when the close button is clicked, or the user clicks outside the modal contents.

So, to implement the outside click closing of the modal, I’m using a useEffect hook that I want its callback function to only run when the isModalPopedUp state changes, and won’t run on the initial render of the component. For this I use a custom hook:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>
import { useRef } from "react";
import { useEffect } from "react";
// This custom hook sets if a useEffect callback runs at initial render or not
export default function useDidMountEffect(argObject = {}) {
/*
argObject = {
runOnInitialRender: boolean,
callback: () => void,
dependencies: any[] // array of dependencies for useEffect
}
*/
const didMountRef = useRef(argObject.runOnInitialRender);
// useEffect to run
useEffect(() => {
if (didMountRef.current) {
// callback will run on first render if argObject.runOnInitialRender is true
argObject.callback();
} else {
// callback will now run on dependencies change
didMountRef.current = true;
}
}, argObject.dependencies); // only run if dependencies change
}
</code>
<code> import { useRef } from "react"; import { useEffect } from "react"; // This custom hook sets if a useEffect callback runs at initial render or not export default function useDidMountEffect(argObject = {}) { /* argObject = { runOnInitialRender: boolean, callback: () => void, dependencies: any[] // array of dependencies for useEffect } */ const didMountRef = useRef(argObject.runOnInitialRender); // useEffect to run useEffect(() => { if (didMountRef.current) { // callback will run on first render if argObject.runOnInitialRender is true argObject.callback(); } else { // callback will now run on dependencies change didMountRef.current = true; } }, argObject.dependencies); // only run if dependencies change } </code>

import { useRef } from "react";

import { useEffect } from "react";

// This custom hook sets if a useEffect callback runs at initial render or not
export default function useDidMountEffect(argObject = {}) {
    /* 
    argObject = {
        runOnInitialRender: boolean,
        callback: () => void,
        dependencies: any[] // array of dependencies for useEffect
    }
    */
    const didMountRef = useRef(argObject.runOnInitialRender);

    // useEffect to run
    useEffect(() => {
        if (didMountRef.current) {
            // callback will run on first render if argObject.runOnInitialRender is true
            argObject.callback();
        } else {
            // callback will now run on dependencies change
            didMountRef.current = true;
        }
    }, argObject.dependencies); // only run if dependencies change
}

For the modal component, I split it into two components, the parent-modal that handles state logic:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>
import { useEffect, useState } from "react";
import Modal from "./modal";
import useDidMountEffect from "./useDidMountHook";
import "./modal.css";
export default function ModalParent() {
const [isModalPopedUp, setIsModalPopedUp] = useState(false);
function handleToggleModalPopup() {
setIsModalPopedUp((prevState) => !prevState);
}
function handleOnClose() {
setIsModalPopedUp(false);
}
function handleOutsideModalClick(event) {
!event.target.classList.contains("modal-content") && handleOnClose();
}
// add event listener for outside modal click using the useDidMountEffect hook
useDidMountEffect({
runOnInitialRender: false,
callback: () => {
// add event listener when modal is shown
if (isModalPopedUp) {
document.addEventListener("click", handleOutsideModalClick);
} else {
// remove event listener when modal is closed
document.removeEventListener("click", handleOutsideModalClick);
}
// return a cleanup function that removes the event listener when component unmounts
return () => {
document.removeEventListener("click", handleOutsideModalClick);
};
},
dependencies: [isModalPopedUp], // only re-run the effect when isModalPopedUp changes
});
return (
<div>
<button
onClick={() => {
handleToggleModalPopup();
}}>
Open Modal Popup
</button>
{isModalPopedUp && (
<Modal
header={<h1>Customised Header</h1>}
footer={<h1>Customised Footer</h1>}
onClose={handleOnClose}
body={<div>Customised Body</div>}
/>
)}
</div>
);
}
</code>
<code> import { useEffect, useState } from "react"; import Modal from "./modal"; import useDidMountEffect from "./useDidMountHook"; import "./modal.css"; export default function ModalParent() { const [isModalPopedUp, setIsModalPopedUp] = useState(false); function handleToggleModalPopup() { setIsModalPopedUp((prevState) => !prevState); } function handleOnClose() { setIsModalPopedUp(false); } function handleOutsideModalClick(event) { !event.target.classList.contains("modal-content") && handleOnClose(); } // add event listener for outside modal click using the useDidMountEffect hook useDidMountEffect({ runOnInitialRender: false, callback: () => { // add event listener when modal is shown if (isModalPopedUp) { document.addEventListener("click", handleOutsideModalClick); } else { // remove event listener when modal is closed document.removeEventListener("click", handleOutsideModalClick); } // return a cleanup function that removes the event listener when component unmounts return () => { document.removeEventListener("click", handleOutsideModalClick); }; }, dependencies: [isModalPopedUp], // only re-run the effect when isModalPopedUp changes }); return ( <div> <button onClick={() => { handleToggleModalPopup(); }}> Open Modal Popup </button> {isModalPopedUp && ( <Modal header={<h1>Customised Header</h1>} footer={<h1>Customised Footer</h1>} onClose={handleOnClose} body={<div>Customised Body</div>} /> )} </div> ); } </code>

import { useEffect, useState } from "react";
import Modal from "./modal";
import useDidMountEffect from "./useDidMountHook";
import "./modal.css";

export default function ModalParent() {
    const [isModalPopedUp, setIsModalPopedUp] = useState(false);

    function handleToggleModalPopup() {
        setIsModalPopedUp((prevState) => !prevState);
    }

    function handleOnClose() {
        setIsModalPopedUp(false);
    }

    function handleOutsideModalClick(event) {
        !event.target.classList.contains("modal-content") && handleOnClose();
    }

    // add event listener for outside modal click using the useDidMountEffect hook
    useDidMountEffect({
        runOnInitialRender: false,
        callback: () => {
            // add event listener when modal is shown
            if (isModalPopedUp) {
                document.addEventListener("click", handleOutsideModalClick);
            } else {
                // remove event listener when modal is closed
                document.removeEventListener("click", handleOutsideModalClick);
            }

            // return a cleanup function that removes the event listener when component unmounts
            return () => {
                document.removeEventListener("click", handleOutsideModalClick);
            };
        },
        dependencies: [isModalPopedUp], // only re-run the effect when isModalPopedUp changes
    });

    return (
        <div>
            <button
                onClick={() => {
                    handleToggleModalPopup();
                }}>
                Open Modal Popup
            </button>
            {isModalPopedUp && (
                <Modal
                    header={<h1>Customised Header</h1>}
                    footer={<h1>Customised Footer</h1>}
                    onClose={handleOnClose}
                    body={<div>Customised Body</div>}
                />
            )}
        </div>
    );
}

and the main modal component:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>
export default function Modal({ id, header, body, footer, onClose }) {
return (
<div id={id || "modal"} className="modal">
<div className="modal-content">
<div className="header">
<span onClick={onClose} className="close-modal-icon">
× {/* an X icon */}
</span>
<div>{header ? header : "Header"}</div>
</div>
<div className="body">
{body ? (
body
) : (
<div>
<p>This is our Modal body</p>
</div>
)}
</div>
<div className="footer">
{footer ? footer : <h2>footer</h2>}
</div>
</div>
</div>
);
}
</code>
<code> export default function Modal({ id, header, body, footer, onClose }) { return ( <div id={id || "modal"} className="modal"> <div className="modal-content"> <div className="header"> <span onClick={onClose} className="close-modal-icon"> × {/* an X icon */} </span> <div>{header ? header : "Header"}</div> </div> <div className="body"> {body ? ( body ) : ( <div> <p>This is our Modal body</p> </div> )} </div> <div className="footer"> {footer ? footer : <h2>footer</h2>} </div> </div> </div> ); } </code>

export default function Modal({ id, header, body, footer, onClose }) {
    return (
        <div id={id || "modal"} className="modal">
            <div className="modal-content">
                <div className="header">
                    <span onClick={onClose} className="close-modal-icon">
                        × {/* an X icon */}
                    </span>
                    <div>{header ? header : "Header"}</div>
                </div>
                <div className="body">
                    {body ? (
                        body
                    ) : (
                        <div>
                            <p>This is our Modal body</p>
                        </div>
                    )}
                </div>
                <div className="footer">
                    {footer ? footer : <h2>footer</h2>}
                </div>
            </div>
        </div>
    );
}

So, the problem is that React remounts the parent component after initial render, making the callback for the useDidMountEffect to immediately add the click event listener to the document element without having the isModalPopedUp state being changed to true by the “open modal” button click.

So, when clicking on the “open modal” button, isModalPopedUp is toggled to true but then is immediately changed to false due to the click event listener being added to the document prematurely. So, it eventually makes the modal unable to be opened by clicking the “open modal” button.

React.dev gives an easy fix by using the cleanup function returned by the useEffect callback to undo the changes from the remounting:

Usually, the answer is to implement the cleanup function. The cleanup function should stop or undo whatever the Effect was doing. The rule of thumb is that the user shouldn’t be able to distinguish between the Effect running once (as in production) and a setup → cleanup → setup sequence (as you’d see in development).

My cleanup function removes the click event listener from the document element, but it still doesn’t fix the problem.

For the styling of the modal, I’m using:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>
/** @format */
.modal {
position: fixed;
z-index: 1;
padding-top: 2rem;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #b19b9b;
color: black;
overflow: auto;
padding-bottom: 2rem;
}
.modal-content {
position: relative;
background-color: #fefefe;
margin: auto;
padding: 0;
border: 1px solid red;
width: 80%;
animation-name: animateModal;
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
}
.close-modal-icon {
cursor: pointer;
font-size: 40px;
position: absolute;
top: 0.5rem;
right: 0.5rem;
font-weight: bold;
}
.header {
padding: 4px 10px;
background-color: #5cb85c;
color: white;
}
.body {
padding: 2px 16px;
height: 200px;
}
.footer {
padding: 4px 16px;
background-color: #5cb85c;
color: white;
}
@keyframes animateModal {
from {
top: -200px;
opacity: 0;
}
to {
top: 0;
opacity: 1;
}
}
</code>
<code> /** @format */ .modal { position: fixed; z-index: 1; padding-top: 2rem; left: 0; top: 0; width: 100%; height: 100%; overflow: hidden; background-color: #b19b9b; color: black; overflow: auto; padding-bottom: 2rem; } .modal-content { position: relative; background-color: #fefefe; margin: auto; padding: 0; border: 1px solid red; width: 80%; animation-name: animateModal; animation-duration: 0.5s; animation-timing-function: ease-in-out; } .close-modal-icon { cursor: pointer; font-size: 40px; position: absolute; top: 0.5rem; right: 0.5rem; font-weight: bold; } .header { padding: 4px 10px; background-color: #5cb85c; color: white; } .body { padding: 2px 16px; height: 200px; } .footer { padding: 4px 16px; background-color: #5cb85c; color: white; } @keyframes animateModal { from { top: -200px; opacity: 0; } to { top: 0; opacity: 1; } } </code>

/** @format */

.modal {
    position: fixed;
    z-index: 1;
    padding-top: 2rem;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
    background-color: #b19b9b;
    color: black;
    overflow: auto;
    padding-bottom: 2rem;
}

.modal-content {
    position: relative;
    background-color: #fefefe;
    margin: auto;
    padding: 0;
    border: 1px solid red;
    width: 80%;
    animation-name: animateModal;
    animation-duration: 0.5s;
    animation-timing-function: ease-in-out;
}

.close-modal-icon {
    cursor: pointer;
    font-size: 40px;
    position: absolute;
    top: 0.5rem;
    right: 0.5rem;
    font-weight: bold;
}

.header {
    padding: 4px 10px;
    background-color: #5cb85c;
    color: white;
}

.body {
    padding: 2px 16px;
    height: 200px;
}

.footer {
    padding: 4px 16px;
    background-color: #5cb85c;
    color: white;
}

@keyframes animateModal {
    from {
        top: -200px;
        opacity: 0;
    }

    to {
        top: 0;
        opacity: 1;
    }
}

2

This following will help you setup what you will need to handle the clicking outside of your Modal.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code> const modalRef = useRef<HTMLDivElement>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleClickOutside = (e: MouseEvent) => {
if (modalRef.current && !modalRef.current.contains(e.target as Node)){
setIsModalOpen(false);
}
};
</code>
<code> const modalRef = useRef<HTMLDivElement>(null); const [isModalOpen, setIsModalOpen] = useState(false); const handleClickOutside = (e: MouseEvent) => { if (modalRef.current && !modalRef.current.contains(e.target as Node)){ setIsModalOpen(false); } }; </code>
    const modalRef = useRef<HTMLDivElement>(null);
    const [isModalOpen, setIsModalOpen] = useState(false);

    const handleClickOutside = (e: MouseEvent) => {
       if (modalRef.current && !modalRef.current.contains(e.target as Node)){
         setIsModalOpen(false);
       }
    };

Along with this, you will need to set the reference and within a useEffect have something like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>useEffect(() => {
if(isModalOpen){
window.addEventListener('mousedown', handleClickOutside)
}
}, [isModalOpen]);
</code>
<code>useEffect(() => { if(isModalOpen){ window.addEventListener('mousedown', handleClickOutside) } }, [isModalOpen]); </code>
useEffect(() => {
   if(isModalOpen){
      window.addEventListener('mousedown', handleClickOutside)
   }
}, [isModalOpen]);

New contributor

justnut is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

5

I think you misunderstand the concept of cleanup function. It’s your responsibility to write state, effects and interaction in a way that when everything is remounted and all effects do their thing, you are back at square one. In your case, you attach event listener and change state, but then in cleanup you only remove listener and don’t touch state. This means that you didn’t cleaned all up.

Another thing here is that it would be much easier to implement without effects at all. Modal dialog is “modal”, meaning that user actually can’t interact with a page underneath it. Simple solution is to make an overlay above the visible content (it can be transparent if you want it “absent”) and listen to the click on it (same one as on button). This way, to control a modal state you will need single boolean state variable and a single handler to change that variable.

Based on the guidelines given by @justnut and @Mr. Hedgehog, I was able to come up with a working solution.

The issue is caused by the click event listener being set on the document element prematurely when the “open modal” button is clicked (due to the way the useEffect hook works).

So, when the “open modal” button is clicked, the event bubbles/propagates up to the document element, thereby unexpectedly triggering the handOutsideClick function.

The fix is therefore to stop the event propagation up to the document element by using the event.stopPropagation() method in the callback of the “open modal” button click event.

For checking whether the click was outside the modal, I used @justnut approach.
Based on @Mr. Hedgehog explanations, I returned a better cleanup function in the useEffect hook.

This is the overall modified code in the parent-modal component (the useDidMountEffect discarded):

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>
/** @format */
import { useEffect, useRef, useState } from "react";
import Modal from "./modal";
import "./modal.css";
export default function ModalParent() {
const [isModalPopedUp, setIsModalPopedUp] = useState(false);
function handleToggleModalPopup() {
setIsModalPopedUp((prevState) => !prevState);
}
function handleOnClose() {
setIsModalPopedUp(false);
}
function handleClickOutside(event) {
if (modalRef.current && !modalRef.current.contains(event.target))
handleOnClose();
}
// create modalRef for handling outside modal click events
const modalRef = useRef(null);
// create a useEffect for handling outside modal click events
useEffect(() => {
if (isModalPopedUp) {
window.addEventListener("click", (event) => {
handleClickOutside(event);
});
} else {
// Remove the event listener when modal is closed
window.removeEventListener("click", handleClickOutside);
}
// Cleanup: Remove the event listener when component unmounts
return () => {
window.removeEventListener("click", handleClickOutside);
};
}, [isModalPopedUp]);
return (
<div>
<button
onClick={(event) => {
event.stopPropagation();
// toggle modal popup state
handleToggleModalPopup();
}}>
Open Modal Popup
</button>
{isModalPopedUp && (
<Modal
header={<h1>Customised Header</h1>}
footer={<h1>Customised Footer</h1>}
onClose={handleOnClose}
body={<div>Customised Body</div>}
modalRef={modalRef}
/>
)}
</div>
);
}
</code>
<code> /** @format */ import { useEffect, useRef, useState } from "react"; import Modal from "./modal"; import "./modal.css"; export default function ModalParent() { const [isModalPopedUp, setIsModalPopedUp] = useState(false); function handleToggleModalPopup() { setIsModalPopedUp((prevState) => !prevState); } function handleOnClose() { setIsModalPopedUp(false); } function handleClickOutside(event) { if (modalRef.current && !modalRef.current.contains(event.target)) handleOnClose(); } // create modalRef for handling outside modal click events const modalRef = useRef(null); // create a useEffect for handling outside modal click events useEffect(() => { if (isModalPopedUp) { window.addEventListener("click", (event) => { handleClickOutside(event); }); } else { // Remove the event listener when modal is closed window.removeEventListener("click", handleClickOutside); } // Cleanup: Remove the event listener when component unmounts return () => { window.removeEventListener("click", handleClickOutside); }; }, [isModalPopedUp]); return ( <div> <button onClick={(event) => { event.stopPropagation(); // toggle modal popup state handleToggleModalPopup(); }}> Open Modal Popup </button> {isModalPopedUp && ( <Modal header={<h1>Customised Header</h1>} footer={<h1>Customised Footer</h1>} onClose={handleOnClose} body={<div>Customised Body</div>} modalRef={modalRef} /> )} </div> ); } </code>

/** @format */

import { useEffect, useRef, useState } from "react";
import Modal from "./modal";
import "./modal.css";

export default function ModalParent() {
    const [isModalPopedUp, setIsModalPopedUp] = useState(false);

    function handleToggleModalPopup() {
        setIsModalPopedUp((prevState) => !prevState);
    }

    function handleOnClose() {
        setIsModalPopedUp(false);
    }

    function handleClickOutside(event) {
        if (modalRef.current && !modalRef.current.contains(event.target))
            handleOnClose();
    }

    // create modalRef for handling outside modal click events
    const modalRef = useRef(null);

    // create a useEffect for handling outside modal click events
    useEffect(() => {
        if (isModalPopedUp) {
            window.addEventListener("click", (event) => {
                handleClickOutside(event);
            });
        } else {
            // Remove the event listener when modal is closed
            window.removeEventListener("click", handleClickOutside);
        }

        // Cleanup: Remove the event listener when component unmounts
        return () => {
            window.removeEventListener("click", handleClickOutside);
        };
    }, [isModalPopedUp]);

    return (
        <div>
            <button
                onClick={(event) => {
                    event.stopPropagation();
                    // toggle modal popup state
                    handleToggleModalPopup();
                }}>
                Open Modal Popup
            </button>
            {isModalPopedUp && (
                <Modal
                    header={<h1>Customised Header</h1>}
                    footer={<h1>Customised Footer</h1>}
                    onClose={handleOnClose}
                    body={<div>Customised Body</div>}
                    modalRef={modalRef}
                />
            )}
        </div>
    );
}

and a little addition to the main modal component where I added the modalRef prop:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<code>
/** @format */
export default function Modal({ id, header, body, footer, onClose, modalRef }) {
return (
<div id={id || "modal"} className="modal">
<div className="modal-content" ref={modalRef}>
<div className="header">
<span onClick={onClose} className="close-modal-icon">
× {/* an X icon */}
</span>
<div>{header ? header : "Header"}</div>
</div>
<div className="body">
{body ? (
body
) : (
<div>
<p>This is our Modal body</p>
</div>
)}
</div>
<div className="footer">
{footer ? footer : <h2>footer</h2>}
</div>
</div>
</div>
);
}
</code>
<code> /** @format */ export default function Modal({ id, header, body, footer, onClose, modalRef }) { return ( <div id={id || "modal"} className="modal"> <div className="modal-content" ref={modalRef}> <div className="header"> <span onClick={onClose} className="close-modal-icon"> × {/* an X icon */} </span> <div>{header ? header : "Header"}</div> </div> <div className="body"> {body ? ( body ) : ( <div> <p>This is our Modal body</p> </div> )} </div> <div className="footer"> {footer ? footer : <h2>footer</h2>} </div> </div> </div> ); } </code>

/** @format */

export default function Modal({ id, header, body, footer, onClose, modalRef }) {
    return (
        <div id={id || "modal"} className="modal">
            <div className="modal-content" ref={modalRef}>
                <div className="header">
                    <span onClick={onClose} className="close-modal-icon">
                        × {/* an X icon */}
                    </span>
                    <div>{header ? header : "Header"}</div>
                </div>
                <div className="body">
                    {body ? (
                        body
                    ) : (
                        <div>
                            <p>This is our Modal body</p>
                        </div>
                    )}
                </div>
                <div className="footer">
                    {footer ? footer : <h2>footer</h2>}
                </div>
            </div>
        </div>
    );
}

Help: If anyone can turn this into a runnable react stack snippet, it’d much appreciated. The modal component and css for styling is in the question above.

1

Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa Dịch vụ tổ chức sự kiện 5 sao Thông tin về chúng tôi Dịch vụ sinh nhật bé trai Dịch vụ sinh nhật bé gái Sự kiện trọn gói Các tiết mục giải trí Dịch vụ bổ trợ Tiệc cưới sang trọng Dịch vụ khai trương Tư vấn tổ chức sự kiện Hình ảnh sự kiện Cập nhật tin tức Liên hệ ngay Thuê chú hề chuyên nghiệp Tiệc tất niên cho công ty Trang trí tiệc cuối năm Tiệc tất niên độc đáo Sinh nhật bé Hải Đăng Sinh nhật đáng yêu bé Khánh Vân Sinh nhật sang trọng Bích Ngân Tiệc sinh nhật bé Thanh Trang Dịch vụ ông già Noel Xiếc thú vui nhộn Biểu diễn xiếc quay đĩa Dịch vụ tổ chức tiệc uy tín Khám phá dịch vụ của chúng tôi Tiệc sinh nhật cho bé trai Trang trí tiệc cho bé gái Gói sự kiện chuyên nghiệp Chương trình giải trí hấp dẫn Dịch vụ hỗ trợ sự kiện Trang trí tiệc cưới đẹp Khởi đầu thành công với khai trương Chuyên gia tư vấn sự kiện Xem ảnh các sự kiện đẹp Tin mới về sự kiện Kết nối với đội ngũ chuyên gia Chú hề vui nhộn cho tiệc sinh nhật Ý tưởng tiệc cuối năm Tất niên độc đáo Trang trí tiệc hiện đại Tổ chức sinh nhật cho Hải Đăng Sinh nhật độc quyền Khánh Vân Phong cách tiệc Bích Ngân Trang trí tiệc bé Thanh Trang Thuê dịch vụ ông già Noel chuyên nghiệp Xem xiếc khỉ đặc sắc Xiếc quay đĩa thú vị
Trang chủ Giới thiệu Sinh nhật bé trai Sinh nhật bé gái Tổ chức sự kiện Biểu diễn giải trí Dịch vụ khác Trang trí tiệc cưới Tổ chức khai trương Tư vấn dịch vụ Thư viện ảnh Tin tức - sự kiện Liên hệ Chú hề sinh nhật Trang trí YEAR END PARTY công ty Trang trí tất niên cuối năm Trang trí tất niên xu hướng mới nhất Trang trí sinh nhật bé trai Hải Đăng Trang trí sinh nhật bé Khánh Vân Trang trí sinh nhật Bích Ngân Trang trí sinh nhật bé Thanh Trang Thuê ông già Noel phát quà Biểu diễn xiếc khỉ Xiếc quay đĩa
Thiết kế website Thiết kế website Thiết kế website Cách kháng tài khoản quảng cáo Mua bán Fanpage Facebook Dịch vụ SEO Tổ chức sinh nhật