How to add an interactive tutorial to a website with React

Intro, or for what reason you should bother creating interactive tutorials

Hello again. In this article, you will learn how to quickly and easily create an interactive tutorial for your React tool. And the first question I would ask is: is it even necessary?
And indeed, if the functionality of the tool is as simple and intuitive as possible, then there is no need to bother with a tutorial. Ideally, this is what you need to strive for, so that the user starts using your tool right away.
But as it happens in real life, not all tools are easy-to-understand, and many have large and complex functionality. Take the following web tools, for example:
You can't just jump into them and use them. First, you have to learn how to use them. In my case, the tool turned out to be not very intuitive. Therefore, we will make an embedded tutorial on the site.

Creating new events for launching a tutorial

We already have a button to call up a chain of hints.
Now we need to set it up so that this chain works. We will do this by creating our own event.
In the Header.js file, replace
Before
After
export default function Header() {
  const header_buttons = document.getElementById('meta-header')
  const btns = []
  for ( const btn of header_buttons.children){
    const ref = btn.firstElementChild.getAttribute('href')
    if (btn.dataset.type == 'inner-link'){
      btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><LinkIcon className='mb-2' fontSize='small' /></Button>)
    }
    else if (btn.dataset.type == 'tutorial' ){
      btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><QuestionMarkIcon className='mb-2'  fontSize='small'/></Button>)
    }
export default function Header() {
  const header_buttons = document.getElementById('meta-header')
  const btns = []
  for ( const btn of header_buttons.children){
    const ref = btn.firstElementChild.getAttribute('href')
    if (btn.dataset.type == 'inner-link'){
      btns.push(<Button color='primary'><a href={ref}>{btn.innerText}</a><LinkIcon className='mb-2' fontSize='small' /></Button>)
    }
    else if (btn.dataset.type == 'tutorial' ){
      btns.push(<Button id='onTutorial' onClick={(e)=>{
        const onTutorialEvent = new Event('onTutorial')
        e.currentTarget.dispatchEvent(onTutorialEvent)
      }} color='primary'><a href={ref}>{btn.innerText}</a><QuestionMarkIcon className='mb-2'  fontSize='small'/></Button>)
    }
We added an ID to the button + our own click event.

Creating a new component, Tutorial.js

Now let's create a new component. In the src/components directory, create a file Tutorial.js. Then add the following code there:

import * as React from 'react';
import { createRoot } from 'react-dom/client';
import Popover from '@mui/material/Popover';

export default function Hint({children, anchor}) {
    const [anchorEl, setAnchorEl] = React.useState(null);
    const [counter, setCounter] = React.useState(1);
    const [message, setMessage] = React.useState('');

    const clearAllHints = () => {
        const hints = document.querySelectorAll('.tutorial_hint')
        hints.forEach( (hint_target) => {
            hint_target.style = ''
            if (hint_target.classList.contains('to_be_hidden')){
                hint_target.classList.add('hidden')
                hint_target.classList.remove('to_be_hidden')
            }
        })
    }

    const displayNextHint = () => {
        const hints = document.querySelectorAll('.tutorial_hint')
        hints.forEach( (hint_target) => {
            if (parseInt(hint_target.dataset.queue) == counter){
                hint_target.style = 'background-color: orange'
                if (hint_target.classList.contains('hidden')){
                    hint_target.classList.remove('hidden')
                    hint_target.classList.add('to_be_hidden')
                }
                var hint_msg = document.getElementById(`tutorial_hint_${hint_target.dataset.queue}`)
                setMessage(hint_msg.innerText)
                setAnchorEl(hint_target)
            }
        })
        setCounter(counter + 1)
    }

    const handleClick = (event) => {
        clearAllHints()
        displayNextHint()
    };

    let tutorial_button = document.getElementById('onTutorial')
    tutorial_button.addEventListener('onTutorial', handleClick, {once: true})

    const handleClose = () => {
        const hints = document.querySelectorAll('.tutorial_hint')
        if (hints.length < counter){
            setMessage('')
            setCounter(1)
            setAnchorEl(null);
            clearAllHints()
        }
        else{
            clearAllHints()
            displayNextHint()
        }
    };

    const open = Boolean(anchorEl);
    const id = open ? 'simple-popover' : undefined;

    return (
        <Popover 
            id={id}
            open={open}
            anchorEl={anchorEl}
            onClose={handleClose}
            anchorOrigin={{
                vertical: 'bottom',
                horizontal: 'left',
            }}
            transformOrigin={{
                vertical: 'top',
                horizontal: 'left',
            }}
        >
        {message}
        </Popover>
    );
}

const popover = document.getElementById('popover-tutorial');
const root = createRoot(popover);
root.render(<Hint></Hint>);
The essence of this code boils down to searching for marked elements with the tutorial_hint class and rendering a hint next to this element. The function that is worth paying attention to is displayNextHint. Having found all the marked elements, it finds the element associated with it and takes the hint text from there. Then it selects the hint element and shows the hint itself.
In order for the component to be able to render, we will add another element to app.html, right before the app_settings element.

<div id="popover-tutorial"></div>
And we will connect the new component to index.js

Marking elements for hints

All that remains is to mark the necessary elements for hints.
AppActions.js
AppQueries.js
AppSettings.js
AppUtils.js
export default function AppActions(){
        const [isModal, setModal] = React.useState(false);
        const req = {'setModal': setModal}
        return (
            <Box className="flex gap-1">
                <div id="tutorial_hint_4" className="hidden"> Optional: Then you can save all of your configurations</div>
                <IconButton id='onSaveRequest'  onClick={()=>{setModal(true); waitTillModalIsUp(SaveRequest, req)}} className='w-fit tutorial_hint' data-queue="4"><SaveIcon className=" border-2 rounded-md"/></IconButton>
                <Modal
                    open={isModal}
                    onClose={()=>{setModal(false)}}
                    >
                    <Box id='utils-modal' className="absolute top-1/2 left-1/2 -translate-x-2/4 -translate-y-2/4 shadow-md p-4 bg-white">
                    </Box>
                </Modal>
                <div id="tutorial_hint_3"  className="hidden">Step 3: And now you can gather all informatino from SERP results</div>
                <IconButton id='onStartParsing'  data-queue="3" onClick={()=>{StartParsingRequest(req)}} className='w-fit tutorial_hint'><NotStartedIcon className=" border-2 rounded-md"/></IconButton>
            </Box>
        )
    }
export default function AppQueries(){
    …
        return (
            <div>
                <Popover
                    id={id}
                    open={open}
                    anchorEl={anchorEngine}
                    onClose={handleClose}
                    anchorOrigin={{
                        vertical: 'bottom',
                        horizontal: 'left',
                    }}
                    >
                    <List sx={{ width: '100%', maxWidth: 360, bgcolor: 'background.paper' }}>
                        {engine_list}
                    </List>
                </Popover>
                <div id="tutorial_hint_2" className="hidden">Step 2: Choose an required engine to parse. And then type your query</div>
                <IconButton onClick={(ev)=>handleClick(ev,engine_list)} className='w-fit tutorial_hint' data-queue="2"><AddBoxIcon/></IconButton>
            </div>
        )
    }
export default function AppSettings(){
        ...
    return (
            <div>
                <Paper elevation={2} className="z-10">
                    <div id="tutorial_hint_1" className="hidden" >Step 1: Check at least one checkbox, to choose what to save</div>
                    <div id="settings_content" className='hidden'>
                        <SettingsContent />
                    </div>
                </Paper>
                {/* Icon button is gonna be changed */}
                <IconButton onClick={ToggleSettings} className='w-fit tutorial_hint' data-queue="1">
                    { isSettings    ? <CloseIcon  className=" border-2 rounded-md"/>
                                    : <SettingsApplicationsIcon className=" border-2 rounded-md"/>}
                </IconButton>
            </div>
        )
    }
export default function AppUtils(){
        ...
        return (
            <Box className="flex flex-col">
                <Box className="flex gap-1">
                    <div id="tutorial_hint_5" className="hidden">Here you can find the most popular and ready-to-use presets</div>
                    <IconButton onClick={ToggleTrending(true)} className='w-fit border tutorial_hint' data-queue="5"><TrendingUpIcon className=" border-2 rounded-md"/></IconButton>
                    <div id="tutorial_hint_6" className="hidden">Here you will find your own presets</div>
                    <IconButton onClick={ToggleOwnSaves(true)} className='w-fit tutorial_hint'  data-queue="6"><SavedSearchIcon className=" border-2 rounded-md"/></IconButton>
                    <div id="tutorial_hint_7" className="hidden">Here you will see the actual proccess of gathering information</div>
                    <div id="console-button" className='hidden tutorial_hint' data-queue="7"><IconButton onClick={ToggleConsole(true)} className='w-fit'><FeaturedPlayListIcon  className=" border-2 rounded-md"/></IconButton></div>
                    <div id="tutorial_hint_8" className="hidden">It is a link to a results of parsing proccess</div>
                    <div id="results-button" className='hidden tutorial_hint' data-queue="8"><IconButton id="results-button-ref" href='#' onClick={()=>{}} className='w-fit'><DownloadIcon color='warning' className=" border-2 rounded-md"></DownloadIcon></IconButton></div>
                </Box>
            ...
    }

Conclusions or how it is supposed to look

As a result, it will all look something like this:
If you missed everything and want the project as-is, download it here.
In the next article, we will add user logging and authentication.

heart 0
3 connected dots 0