React: generating elements wherever you click on the screen

dataseyo

Zachary Shifrel

Posted on December 6, 2022

React: generating elements wherever you click on the screen

After seeing Josh's animated sparkles in react tutorial, I played around with doing something similar using react-spring. Suppose you want to generate an image, text, “+1 exp,” etc. whenever a user clicks on a certain part of the screen.

First, create the component to be generated on the user’s click. I’ll call it Spawn:

// App.tsx
const Spawn = () => {
  return (
    <div>
      Spawn
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

We want to place this at a certain (x,y) coordinate on the screen, which we can grab from a mouse event, specifically from the clientX and clientY properties.

In a higher up component, create a click handler and pass the event as a parameter. Then we’ll destructure the clientX/clientY properties from event:

// App.tsx
const App = () => {
    const clickHandler = (event: React.MouseEvent) => {
        const { clientX, clientY } = event
        console.log(clientX, clientY)
    }

    return (
        <div
            className="App"
            onClick={(event) => clickHandler(event)} # hover over event to see type
        >
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Whenever you click inside the div, the x and y coordinates of the click should be logged to your console. Make sure to give the div some height and width (and a border to see its bounds).

We’re going to want to keep track of the Spawns that are being generated, and pass the clientX/clientY properties to our Spawns component whenever the user clicks in a new spot. We can do this in a state array, typing the array with an array of Spawn types that will have x and y coordinates:

// App.tsx
type Spawn = {
    x: number,
    y: number
}

const App = () => {
    const [spawns, setSpawns] = useState<Spawn[]>([]) # state array

    const clickHandler = (event: React.MouseEvent) => {
        const { clientX, clientY } = event
        console.log(clientX, clientY)

    // on each click, add a new x, y pair to the spawns array
    setSpawns([
            ...spawns, 
            {
                x: clientX,
                y: clientY
            }
        ])
    }

    return (
        <div
            className="App"
            onClick={(event) => clickHandler(event)} # hover over event to see type
        >
            {/* map over the spawns array and return the Spawn component for each
            x, y coordinate pair inside the spawns state array
            */}
            {
                spawns.map(spawn => {
                    return (
                        <Spawn
                            x={spawn.x}
                            y={spawn.y}
                        />
                    )
                })
            }
        </div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Now, when you click inside the App div, you should see the word “Spawn” appear. We need to make sure the word appears near the click.

Let’s give the Spawn div a style to correct its placement. I also changed the text in the div to “+1” instead.

// App.tsx
const Spawn = ({x, y}: Spawn) => {
    const spawnStyles = {
    position: 'absolute',
        left: x + 'px',
        top: y + 'px',
        transform: `translate(-50%, -50%)`
    }

    return (
        <animated.div 
            style={spawnStyles}
        >
            <p>+1</p>
        </animated.div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Next, declutter the screen by cleaning up the generated Spawns. We can use the combination of a useEffect hook and setInterval to do so:

// App.tsx
const App = () => {
    const [spawns, setSpawns] = useState<Spawn[]>([])

    const clickHandler = (event: React.MouseEvent) => {
        const { clientX, clientY } = event
        console.log(clientX, clientY)

        setSpawns([
            ...spawns,
            {
                x: clientX,
                y: clientY
            }
        ])
    }

    useEffect(() => {
        // until there are no spawns, remove the 
    // first added spawn every interval
        const interval = setInterval(() => {
            if (spawns.length > 0) {
                const newSpawns = [...spawns]
                newSpawns.shift()
                setSpawns(newSpawns)
            }
        }, 200)

        // cleanup
        return () => clearInterval(interval)
    }, [spawns])

    return (
        <div 
            className="main-container"
            onClick={(event) => clickHandler(event)}
        >
           {/* map over the spawns array and return the Spawn component for each
        x, y coordinate pair inside the spawns state array
        */}
            {
                spawns.map(spawn => {
                    return (
                        <Spawn
                            x={spawn.x}
                            y={spawn.y}
                        />
                    )
                })
            }
        </div>
    )
}

Enter fullscreen mode Exit fullscreen mode

The effect takes spawns as a dependency, and is run whenever the spawns array changes. Then, at an interval of 200 ms, the first added spawn (because of the .shift() method) is removed from the state array until the array is empty.

Animating the Spawns [ Optional ]

The only thing left to do is animate the spawns once they’re added to the screen. We could do this with css keyframes, framer motion, or react spring. I chose react spring.

npm install react-spring
Enter fullscreen mode Exit fullscreen mode
// App.tsx
import { useSpring, animated } from 'react-spring'
Enter fullscreen mode Exit fullscreen mode

Divs and other elements can be turned into animated.divs in order to apply the useSpring hook to them.

react-spring can be used to change the styles applied to a div over a certain duration (I use the default duration). Config options can also be supplied to make the animation more or less springy, changing the bounce, velocity, and so on. Here, I simply change the Y value of transform: translate(x%, y%) to make the +1 spawn rise on the screen before it disappears. I also change the opacity from 0 to 1. Lastly, Object.assign() lets us combine two style objects into one.

const Spawn = ({x, y}: Spawn) => {
    const animation = useSpring({
        to: { 
                transform: `translate(-50%, -130%)`,
                opacity: 1
            },
        from: { 
                transform: `translate(-50%, -50%)`,
                opacity: 0
            },
        config: {
            frequency: .5,
            bounce: 2,
            damping: .8
        }
    })

    const spawnStyles = {
    position: absolute,
        left: x + 'px',
        top: y + 'px',
        // transform: `translate(-50%, -50%)`,
    }
    return (
        <animated.div
    // Object.assign() combines the two different style objects
            style={Object.assign(animation, spawnStyles)}
        >
            <p>+1</p>
        </animated.div>
    )
}
Enter fullscreen mode Exit fullscreen mode

Here's a sandbox of the code without typescript.

Bugs

A problem with this implementation is that the cleanup method leaves a lot to be desired. If the user clicks too quickly, too many elements will be generated on the screen and will take noticeably longer times to leave the screen. The way Josh handled this in his blog (under "generation and cleanup") is way more elegant.

💖 💪 🙅 🚩
dataseyo
Zachary Shifrel

Posted on December 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related