React: generating elements wherever you click on the screen
Zachary Shifrel
Posted on December 6, 2022
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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
// App.tsx
import { useSpring, animated } from 'react-spring'
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>
)
}
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.
Posted on December 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.