On my Gatsby site I want a Youtube component that only shows up if the user has accepted cookies, because of GDPR. If they have declined cookies I want it to give guidance for resetting their preferences, and if cookie acceptance state is unknown (say they're using an extension that has blocked the cookie notice) I want it to say that it doesn't know whether cookies are allowed or not and if you don't see the banner then please check your adblocker.
I have already set up global state via react-redux that checks for the 'acceptCookies' cookie and all the logic for displaying the banner etc (it's all done using react-cookie-consent) and sets state.acceptCookies to true. This has worked on various pages where for example I only want a contact form to show up if cookies are accepted.
This is my component (which I include in any page which is to have a Youtube video, passing it the Youtube code as a prop) :
import React from "react"
import { useSelector } from "react-redux"
import { Alert } from "react-bootstrap"
import { Link } from "gatsby"
const YouTubeVideo = (props) => {
const acceptCookies = useSelector(state => state.acceptCookies)
return (
<>
{acceptCookies === '' &&
<Alert variant="warning">
<p>
We cannot show this YouTube video as cookies have not been accepted,
but are required for YouTube to work. To comply with GDPR law, the
video is not displayed until you have accepted cookies.
</p>
<p>
If you do not see the cookie banner, please try resetting your
preferences from the <Link to="/privacy-policy">Privacy Policy</Link> or
disabling any content blockers.
</p>
</Alert>
}
{acceptCookies === 'false' &&
<Alert variant="warning">
<p>
We cannot show this YouTube video as cookies have been declined, but
are required for YouTube to work. To comply with GDPR law, the video
is not displayed.
</p>
<p>
You may reset your preferences from
the <Link to="/privacy-policy">Privacy Policy</Link>
</p>
</Alert>
}
{acceptCookies === 'true' &&
<div className="embed-container">
<iframe src={`https://www.youtube.com/embed/${props.code}?rel=0`}
title={`YouTube Video ${props.code}`}
frameBorder="0"
allowFullScreen
>
</iframe>
</div>
}
</>
)
}
export default YouTubeVideo
I've tried other syntax such as using if statements and multiple render blocks, no difference
I've tried rewriting it as a class and using connect(), no difference
For the life of me I can't get it to work properly. It works fine in develop mode - initial state shows the 'cookies haven't been accepted' alert, declining shows the 'cookies have been declined' alert, and accepting shows the video, and all is well and good. I refresh and things continue to behave.
But once I build and deploy the site it behaves really weird. It seems fine at first - initial state shows the 'cookies haven't been accepted' alert, declining shows the 'cookies have been declined' alert, and accepting shows the video. And then you refresh the page and.... it shows a small version of the video inside an alert! (With no alert text). It's really odd. It's like it's following the 'cookies not accepted' logic and starting to draw an alert, and then before writing in the paragraphs, hitting some kind of race condition and rendering the 'cookies accepted' part of the code i.e. the video. I don't get it at all, I'd expect it to render one of the 3 things, not somehow manage to mash the outcomes together.
What am I missing here? I'm completely baffled. I think I must be tripping over something in the pre-rendering, but just can't get my head around it.
Edit: With a clearer head I thought to at least try setting acceptCookies to a static 'true' to see whether the problem is with my rendering (in which case it'd still not display correctly) or the global state implementation (in which case it would) and proved that it's the latter. The problem really is that I don't know how to properly do cookie permission or global state in Gatsby ("caching" it as global state so that upon acceptance of cookies the output will update without having to refresh the page) and could only really go off a mash-up of tutorials about vanilla React and Gatsby. I'm probably best off rewriting it - at this point I can't remember which tutorial(s) I used!
To add though, this is what I have
gatsby-ssr.js and gatsby-browser.js both contain:
import wrapWithProvider from "./wrap-with-provider"
export const wrapRootElement = wrapWithProvider
wrap-with-provider.js:
import React from "react"
import { Provider } from "react-redux"
import createStore from "./src/state/createStore"
// eslint-disable-next-line react/display-name,react/prop-types
export default ({ element }) => {
// Instantiating store in `wrapRootElement` handler ensures:
// - there is fresh store for each SSR page
// - it will be called only once in browser, when React mounts
const store = createStore()
return <Provider store={store}>{element}</Provider>
}
src/state/createStore.js:
import { createStore as reduxCreateStore } from "redux"
import Cookies from 'js-cookie'
const reducer = (state, action) => {
if (action.type === `ACCEPT_COOKIES`) {
return Object.assign({}, state, {
acceptCookies: 'true',
})
}
if (action.type === `DECLINE_COOKIES`) {
return Object.assign({}, state, {
acceptCookies: 'false',
})
}
if (action.type === `RESET_COOKIES`) {
return Object.assign({}, state, {
acceptCookies: '',
})
}
return state
}
const initialState = { acceptCookies: Cookies.get('acceptCookies') || '' }
const createStore = () => reduxCreateStore(reducer, initialState)
export default createStore
The cookie consent notice is part of layout.js so that it's on every page whenever it's needed:
import CookieConsent from "react-cookie-consent"
const OurCookieConsent = ({ acceptCookies, accept, decline }) => (
<CookieConsent
location="bottom"
enableDeclineButton={true}
cookieName="acceptCookies"
onDecline={decline}
onAccept={accept}
>
<p>This website uses cookies for:</p>
<ul>
<li>REQUIRED: Saving your cookie preferences</li>
<li>OPTIONAL: The contact form on the "Contact Us" page - for spam
control (reCAPTCHA).</li>
<li>OPTIONAL: Embedded YouTube videos e.g. videos in our news pages
</li>
</ul>
<p>Declining consent will disable the features marked as OPTIONAL.</p>
<p>For more in-depth detail or to withdraw or re-establish your
consent at any time, please see the <Link to="/privacy-policy">Privacy Policy</Link>.</p>
</CookieConsent>
)
OurCookieConsent.propTypes = {
acceptCookies: PropTypes.string.isRequired,
accept: PropTypes.func.isRequired,
decline: PropTypes.func.isRequired,
}
const mapStateToProps = ({ acceptCookies }) => {
return { acceptCookies }
}
const mapDispatchToProps = dispatch => {
return {
accept: () => dispatch({ type: `ACCEPT_COOKIES` }),
decline: () => dispatch({ type: `DECLINE_COOKIES` })
}
}
const ConnectedCookieConsent = connect(mapStateToProps, mapDispatchToProps)(OurCookieConsent)
Then ConnectedCookieConsent is included inside the layout.