Build a React cookie consent using hooks and context

Build a React cookie consent using hooks and context

In this post I will show you how to build your own cookie consent.  Why run your own? It is really easy to build, lightweight and customisable to your needs.

We will create a provider, wrapping the entire app,  which will contain the cookie data using context and basic logic on showing the popup.

In order to roll our own consent we will:

  1. Creating a provider
  2. Read the initial cookie value
  3. Building a popup to accept or decline cookie usage
  4. Use cookie preferences in a template

TL;DR: entire source in this gist.

Creating a provider

Firstly we will create a provider which will wrap the entire app. Two contexts are created which will be provided to the app, one for the cookie value and one for interacting with the cookie preferences.

For now we will create an empty reducer hook which we will fill later. We use a reducer instead of a basic useState hook since we will be handling more complex state manipulation.

Lastly, we will use a named export since we will have multiple exports from this file.

// CookieConsent.js
import React, { createContext, useReducer } from 'react'

const CookieConsentStateContext = createContext()
const CookieConsentDispatchContext = createContext()

const CookieConsentProvider = ({children}) => {
  const [state, dispatch] = useReducer((state, action) => {
    switch(action.type) {
      default:
        throw new Error()
    }
  })

  return (
    <CookieConsentStateContext.Provider value={state}>
      <CookieConsentDispatchContext.Provider value={dispatch}>
        {children}
      </CookieConsentDispatchContext.Provider>
    </CookieConsentStateContext.Provider>
  )

}

export { CookieConsentProvider }

Now we just have to import the provider in our main app component and wrap it. By doing this our entire app will have access to the same data using context!

// App.js
import React from 'react';
import { CookieConsentProvider } from './CookieConsent'

function App() {
  return (
    <CookieConsentProvider>
      <div>
        <h1>Example App</h1>
      </div>
    </CookieConsentProvider>
  );
}

export default App;

In order to use stored preferences, which we don't have yet, we have to see if a cookie is present. If not we will have a default setting which users will accept if they click the accept button later on. Make sure to provide the initialCookieValue to the reducer hook as well.

// CookieConsent.js
import React, { createContext, useReducer } from 'react'

const COOKIE_NAME='consent'

const CookieConsentStateContext = createContext()
const CookieConsentDispatchContext = createContext()

function getCookie(){
  const regex = new RegExp(`(?:(?:^|.*;\\s*)${COOKIE_NAME}\\s*\\=\\s*([^;]*).*$)|^.*$`)
  const cookie = document.cookie.replace(regex, "$1")
  return cookie.length ? JSON.parse(cookie) : undefined
}

// Initial value is cookie value OR prefered value but not yet set
let initialCookieValue = getCookie() || {
  isSet: 0,
  marketing: 1
}

const CookieConsentProvider = ({children}) => {
  const [state, dispatch] = useReducer((state, action) => {
	  //...
	}, initialCookieValue)
}

export { CookieConsentProvider }

I have chosen to make the cookie setting an object with an isSet and marketing property. When using simple cookies you could use something like

{
	isSet: 0,
	accepted: 0
}

Because normally I like to give users an option on which they want to opt-in and on which they don't I end up with something like the following. In this post I will not do this since this will create a more complex popup. After completing this post you will have enough grasp on this to make it yourself.

{
	isSet: 0,
	performance: 1,
	marketing: 1,
	analytics: 1
}

In order to accept or decline cookies we will have to have a cookie popup or consent bar. The following popup is without styling but imagine wrapping it in a modal to prevent interacting with the site or use a more subtle consent bar, whichever you prefer.

// CookiePopup.js
import React from 'react'

const CookiePopup = () => (
  <section>
    <p>We use cookies!</p>
    <button>Accept</button>
    <button>Decline</button>
  </section>
)

export default CookiePopup
// CookieConsent.js
import React, { createContext, useReducer, useEffect } from 'react'
import CookiePopup from './CookiePopup'

//...

return (
  <CookieConsentStateContext.Provider value={state}>
    <CookieConsentDispatchContext.Provider value={dispatch}>
      <CookiePopup />
      {children}
    </CookieConsentDispatchContext.Provider>
  </CookieConsentStateContext.Provider>
)

//...

To provide the buttons with logic to accept or decline we have to provide the reducer with logic. In this example I have created an acceptCurrent and declineAll action.

Since the default value has marketing set to active, we only have to accept the current value. In order to do this we use ES6 object spread. We take the current state and set isSet to 1. In order to decline I have typed over the object. Whenever the settings would get more complicated this should get refactored to prevent errors.

When the state changes we want to make sure to persist this into the cookie. This way the next time our app gets loaded we are aware of the preferences and those will be loaded as initialCookieValue. To do this we use the useEffect hook on state. This will be triggered when state changes and update the cookie, it's that simple.

// CookieConsent.js
import React, { createContext, useReducer, useEffect } from 'react'
//...

const [state, dispatch] = useReducer((state, action) => {
  switch (action.type) {
    case 'acceptCurrent':
      return {
        ...state,
        isSet: 1,
	      }
    case 'declineAll':
      return {
        isSet: 1,
        marketing: 0,
      }
    default:
      throw new Error()
  }
}, initialCookieValue)

// Update the cookie when state changes
useEffect(() => {
  document.cookie = `${COOKIE_NAME}=${JSON.stringify(state)}`
}, [state])

//...

Now we have the logic to accept or reject the cookies we have to update our popup. Since the popup is loaded in the provider template we can just pass the dispatch method as a prop and call the actions when a button is clicked.

// CookieConsent.js
//...

return (
  <CookieConsentStateContext.Provider value={state}>
    <CookieConsentDispatchContext.Provider value={dispatch}>
      <CookiePopup dispatch={dispatch} />
      {children}
    </CookieConsentDispatchContext.Provider>
  </CookieConsentStateContext.Provider>
)

//...
// CookiePopup.js
import React from 'react'

const CookiePopup = ({dispatch}) => (
  <section>
    <p>We use cookies!</p>
    <button onClick={() => dispatch({type: 'acceptCurrent'})}>Accept</button>
    <button onClick={() => dispatch({type: 'declineAll'})}>Decline</button>
  </section>
)

export default CookiePopup

That's awesome! The cookies get updated and are set. We are almost finished with updating the cookie preferences. The only thing we need to do is to hide the popup when either the cookies are accepted/declined or if the preference is already set on page load.

We will use the useState hook to keep track of the open state of the popup. The default value is based on initialCookieValue which gets populated by the cookie on load.

// CookieConsent.js
import React, { createContext, useReducer, useEffect, useState } from 'react'
//...

const CookieConsentProvider = ({ children }) => {
  const [popupIsOpen, setPopupIsOpen] = useState(!initialCookieValue.isSet)
	
	//...
	
	return (
    <CookieConsentStateContext.Provider value={state}>
      <CookieConsentDispatchContext.Provider value={dispatch}>
        {popupIsOpen && <CookiePopup />}
        {children}
      </CookieConsentDispatchContext.Provider>
    </CookieConsentStateContext.Provider>
  )
}

//...

It's easy to make the popup go away when a button is pressed, just update the state from within the reducer by calling setPopupIsOpen.

// CookieConsent.js
//...

const [state, dispatch] = useReducer((state, action) => {
  switch (action.type) {
    case 'acceptCurrent':
      setPopupIsOpen(false)
      return {
        ...state,
        isSet: 1,
      }
    case 'declineAll':
      setPopupIsOpen(false)
      return {
        isSet: 1,
        marketing: 0,
      }
    default:
      throw new Error()
  }
}, initialCookieValue)

//...

Setting cookie preferences is fun but of no use when not used. To make it easy we will create a function which returns the consentState context.

First we need to import useContext, create the function and add it to the exports.

// CookieConsent.js
import React, { createContext, useReducer, useEffect, useState, useContext } from 'react'
//...

function useCookieConsentState() {
  const context = useContext(CookieConsentStateContext)
  if (context === undefined) {
    throw new Error('useCookieConsentState must be used within CookieProvider')
  }
  return context
}

export { CookieConsentProvider, useCookieConsentState }

Then on a random page we import the helper and initiate it which returns the useContext hook. In the template we can check if the consent is set and if marketing is accepted. That's all!

import React from 'react'
import { useCookieConsentState } from './CookieConsent'

const Page = () => {
  const cookieConsentState = useCookieConsentState()
  
  return (
    <article>
      <h1>Title</h1>
      {cookieConsentState.isSet && cookieConsentState.marketing ? 
        <script>
          {/* Marketing code here */}
        </script>
      : ''}
    </article>
  )
}

export default Page

You see? No use for packages, this is easy to set up, well maintainable and works like a charm.  If you have any questions I would love to answer them on my Twitter.

The complete source can be found on this gist.

Maybe you have noticed we have an unused provider. This comes in handy when performing actions from other places in your application. A common example is triggering the cookie notice from the footer of the website in order to update the preferences. As a bonus I have added a snippet which would show you how to do that.

After that you would have no problems to add multiple cookie types like performance and analytics. Also by then you can extend the popup with a form to manage this and expand the reducer to handle an update on the preferences. Tip: see how we used type on the buttons calling the dispatch? You can provide a payload property as well on a form submission and use that data within the reducer to set the new preferences.

// CookieConsent.js
//...

const [state, dispatch] = useReducer((state, action) => {
  switch (action.type) {
		//...
    case 'showCookiePopup':
      setPopupIsOpen(true)
      return state
   //...
  }
}, initialCookieValue)

//...

function useCookieConsentDispatch() {
	const context = useContext(CookieConsentDispatchContext)
	if (context === undefined) {
		throw new Error('useCookieConsentDispatch must be used within CookieProvider')
	}
	return context
}

export { CookieConsentProvider, useCookieConsentState, useCookieConsentDispatch }
import React from 'react'
import { useCookieConsentState, useCookieConsentDispatch } from './CookieConsent'

const Page = () => {
  const cookieConsentState = useCookieConsentState()
  const cookieConsentDispatch = useCookieConsentDispatch()

  return (
    <article>
      <h1>Title</h1>
      <button onClick={() => cookieConsentDispatch({type: 'showCookiePopup  '})}>Update cookie settings</button>
      {cookieConsentState.isSet && cookieConsentState.marketing ? 
        <script>
          {/* Marketing code here */}
        </script>
      : ''}
    </article>
  )
}

export default Page