Online: han-hooks.netlify.com/
In this article, I will create an HTML Canvas drawing site using React Hooks. I will build the project from scratch using create-React-app scaffolding. Finally, the application has basic functions such as clear, undo, and use localStorage.
In this article I’ll show you any building custom Hooks and reusing stateful logic in plain old Hooks.
Basic setup
We’ll start by creating a new React application using create-react-app.
$ npx create-react-app canvas-and-hooks
$ cd canvas-and-hooks/
$ yarn start
Copy the code
Your browser will open http://localhost:3000/ and you’ll see a rotated React logo image, so you’re ready to start…
The first hook: useRef
Open the SRC/app.js file 📃 with your favorite editor and replace it with the following:
import React from 'react'
function App() {
return (
<canvas
width={window.innerWidth}
height={window.innerHeight}
onClick={e => {
alert(e.clientX)
}}
/>
)
}
export default App
Copy the code
Click anywhere in the browser window, if a pop-up box will pop up: show the x coordinate of your mouse click 🖱️, good! The application runs.
Now, let’s actually draw something. In this case we need the canvas element ref, so let’s start using today’s first hook useRef:
import React from 'react'
function App() {
const canvasRef = React.useRef(null)
return (
<canvas
ref={canvasRef}
width={window.innerWidth}
height={window.innerHeight}
onClick={e => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
// implement draw on ctx here
}}
/>
)
}
export default App
Copy the code
In general, you don’t need a REF to do updates in React. But canvas is not like other DOM elements. Most DOM elements have an attribute, such as: value, that you can update directly. In canvas allow ✅ you use context (Ben 🌰 : CTX) to draw something. To do this, we have to use ref, which is a reference to the actual Canvas DOM element.
Now that we have the Canvas context, it’s time to draw something. To do this, paste and copy the following code to draw an SVG hook. It has nothing to do with hooks, and don’t worry about 😓 if you don’t understand it.
import React from 'react'
const HOOK_SVG = 'm129.03125 63.3125 c0-34.914062-28.941406-63.3125-63.519531-63.3125-35.574219 0-64.511719 28.398438-64.511719 63.3125 0 29.488281 20.671875 54.246094 48.511719 61.261719v162.898437c0 53.222656 44.222656 96.527344 98.585937 96.527344 h10.316406 c54.363282 0, 98.585938 to 43.304688 98.585938-96.527344 v - 95.640625 c0-7.070312-4.640625-13.304687-11.414062-15.328125-6.769532-2.015625-14.082032.625-17.9609 38 6.535156 L-42.328125 64.425782 C-4.847656 7.390625-2.800781 17.3125 4.582031 22.167968 7.386719 4.832032 17.304688 2.792969 22.160156 4.585937 l12.960938 19.71875 v42.144531 c0 35.582032 29.863281 64.527344 66.585938 64.527344 h - 10.316406 - c - 36.714844-0-66.585937-28.945312-66.585937-64.527344 v c27.847656-162.898437-7.015625 48.519531-31.773438 48.519531-61.261719 ZM-97.03125 0c0-17.265625 14.585938-31.3125 32.511719-31.3125 17.929687 0 32.511719 14.046875 32.511719 31.3125 0 17.261719-14.582032 31.3125-32.511719 31.3125-17.925781 0-32.511719-14.050781-32.511719-31.3125 zm0 0 'Const HOOK_PATH = new Path2D(HOOK_SVG) const SCALE = 0.3 const OFFSET = 80function draw(ctx, location) {
ctx.fillStyle = 'deepskyblue'
ctx.shadowColor = 'dodgerblue'
ctx.shadowBlur = 20 ctx.save()
ctx.scale(SCALE, SCALE) ctx.translate(location.x / SCALE - OFFSET, location.y / SCALE - OFFSET)
ctx.fill(HOOK_PATH)
ctx.restore()
}
function App() {
const canvasRef = React.useRef(null)
return (
<canvas
ref={canvasRef}
width={window.innerWidth}
height={window.innerHeight}
onClick={e => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
draw(ctx, { x: e.clientX, y: e.clientY })
}}
/>
)
}
export default App
Copy the code
The above code is to draw an SVG shape (a fishhook) at coordinates (x,y).
Give it a try and see if it works.
Second hook: useState
The next feature we will add is the Clean and Undo buttons 🔘. To do this, we will use useState Hook to track user interactions.
import React from 'react'
// ...
// canvas draw function
// ...
function App() {
const [locations, setLocations] = React.useState([])
const canvasRef = React.useRef(null)
return (
<canvas
ref={canvasRef}
width={window.innerWidth}
height={window.innerHeight}
onClick={e => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
const newLocation = { x: e.clientX, y: e.clientY }
setLocations([...locations, newLocation])
draw(ctx, newLocation)
}}
/>
)
}
export default App
Copy the code
So, we added state to our app. You can verify this by adding console.log(locations) to the return statement. As the user clicks, you see the printed array.
The third hook: useEffect
Currently, we have nothing to do with state. We drew hooks as before. Let’s look at how to fix this problem with useEffect hook.
import React from 'react'
// ...
// canvas draw function
// ...
function App() {
const [locations, setLocations] = React.useState([])
const canvasRef = React.useRef(null)
React.useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
locations.forEach(location => draw(ctx, location))
})
return( <canvas ref={canvasRef} width={window.innerWidth} height={window.innerHeight} onClick={e => { const newLocation = { x: e.clientX, y: e.clientY }setLocations([...locations, newLocation])
}}
/>
)
}
export default App
Copy the code
There’s a lot of stuff going on here and let’s break it down a little bit. We move the onClick event handler’s draw function back into useEffect. This is important because drawing on the canvas is determined by the state of the app, which is a side effect. We’ll use localStorage later to maintain persistence, which can also be a side effect when state is updated.
I also made some changes to the actual drawing of the canvas itself. In the current implementation, the canvas is cleared every time the render is rendered and then all the positions are drawn. We can do a little smarter than that. But to keep it simple, leave it to the reader to optimize.
Now that we’ve done all the hard work, it should be easy to add new features. Let’s create a clear button.
import React from 'react'
// ...
// canvas draw function
// ...
function App() {
const [locations, setLocations] = React.useState([])
const canvasRef = React.useRef(null)
React.useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
locations.forEach(location => draw(ctx, location))
})
function handleCanvasClick(e) {
const newLocation = { x: e.clientX, y: e.clientY }
setLocations([...locations, newLocation])
}
function handleClear() {
setLocations([])
}
return (
<>
<button onClick={handleClear}>Clear</button>
<canvas
ref={canvasRef}
width={window.innerWidth}
height={window.innerHeight}
onClick={handleCanvasClick}
/>
</>
)
}
export default App
Copy the code
The cleanup is just a simple state update: we clear the state by setting it to an empty array, which is easy, right?
Further, I have also moved the Canvas onClick event handling into a separate function.
Let’s add another feature: undo. Same principle, even if this kind of status update is a bit tricky.
import React from 'react'
// ...
// canvas draw function
// ...
function App() {
const [locations, setLocations] = React.useState([])
const canvasRef = React.useRef(null)
React.useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
locations.forEach(location => draw(ctx, location))
})
function handleCanvasClick(e) {
const newLocation = { x: e.clientX, y: e.clientY }
setLocations([...locations, newLocation])
}
function handleClear() {
setLocations([])
}
function handleUndo() {
setLocations(locations.slice(0, -1))
}
return (
<>
<button onClick={handleClear}>Clear</button>
<button onClick={handleUndo}>Undo</button>
<canvas
ref={canvasRef}
width={window.innerWidth}
height={window.innerHeight}
onClick={handleCanvasClick}
/>
</>
)
}
export default App
Copy the code
Because any state update in React must be immutable, we can’t use things like locations.pop() to clear the most recent entry in the array. Our operation cannot change the original Locations array. The method is to use slice, copying all items until the last one. You can use locations. Slice (0, locations. Length-1), but slice has a smarter way of manipulating the -1 at the last bit of the array.
Before we start, let’s tidy up the HTML and add a CSS style file. Add the following div outside the buttons.
import React from 'react'
import './App.css'
// ...
// canvas draw function
// ...
function App() {/ /...return (
<>
<div className="controls"> <button onClick={handleClear}>Clear</button> <button onClick={handleUndo}>Undo</button> </div> <canvas ref={canvasRef} width={window.innerWidth} height={window.innerHeight} onClick={handleCanvasClick} /> </> ) }export default App
Copy the code
The CSS style is as follows:
*, *:before, *:after { box-sizing: border-box; } body { background-color: black; } .controls { position: absolute; top: 0; left: 0; } button { height: 3em; width: 6em; margin: 1em; font-weight: bold; The font - size: 0.5 em. text-transform: uppercase; cursor: pointer; color: white; border: 1px solid white; background-color: black; } button:hover { color: black; background-color:#00baff;
}
button:focus {
border: 1px solid #00baff;
}
button:active {
background-color: #1f1f1f;
color: white;
}
Copy the code
Looking good, let’s look at the next feature: persistence.
Add the localStorage
As we mentioned earlier, we also want our drawing to be saved in localStroage, which is another side effect, we will add another useEffect.
import React from 'react'
import './App.css'/ /... drawfunction
function App() {
const [locations, setLocations] = React.useState(
JSON.parse(localStorage.getItem('draw-app')) || [] )
const canvasRef = React.useRef(null)
React.useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
locations.forEach(location => draw(ctx, location))
})
React.useEffect(() => {
localStorage.setItem('draw-app', JSON.stringify(locations))
})
function handleCanvasClick(e) {
const newLocation = { x: e.clientX, y: e.clientY }
setLocations([...locations, newLocation])
}
function handleClear() {
setLocations([])
}
function handleUndo() {
setLocations(locations.slice(0, -1))
}
return (
<>
<div className="controls"> <button onClick={handleClear}>Clear</button> <button onClick={handleUndo}>Undo</button> </div> <canvas ref={canvasRef} width={window.innerWidth} height={window.innerHeight} onClick={handleCanvasClick} /> </> ) }export default App
Copy the code
Now we have all the functionality we need to build, but not enough. One of the coolest things about Books is that you can use existing hooks to build new custom hooks. I create a custom usePersistentState hook to demonstrate this.
The first custom hook: usePersistentState
import React from 'react'
import './App.css'/ /... drawfunction
// our first custom hook!
function usePersistentState(init) {
const [value, setValue] = React.useState(
JSON.parse(localStorage.getItem('draw-app')) || init
)
React.useEffect(() => {
localStorage.setItem('draw-app', JSON.stringify(value))
})
return [value, setValue]}
function App() {
const [locations, setLocations] = usePersistentState([])
const canvasRef = React.useRef(null)
React.useEffect(() => {
const canvas = canvasRef.current
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, window.innerHeight, window.innerWidth)
locations.forEach(location => draw(ctx, location))
})
function handleCanvasClick(e) {
const newLocation = { x: e.clientX, y: e.clientY }
setLocations([...locations, newLocation])
}
function handleClear() {
setLocations([])
}
function handleUndo() {
setLocations(locations.slice(0, -1))
}
return(/ /...). }export default App
Copy the code
Here, we create our first custom hook and extract all the logic associated with saving and retrieving state from localStorage from the App component. The way we do this is that the usePersistentState Hook can be reused by other components. There is nothing specific to this component.
Let’s repeat this trick to manipulate canvas-related logic.
Second custom hook: usePersistentCanvas
import React from 'react'
import './App.css'/ /... drawfunction
// our first custom hook
function usePersistentState(init) {
const [value, setValue] = React.useState(
JSON.parse(localStorage.getItem('draw-app')) || init
)
React.useEffect(() => {
localStorage.setItem('draw-app', JSON.stringify(value))
})
return [value, setValue]
}
// our second custom hook: a composition of the first custom hook // and React's useEffect + useRef function usePersistentCanvas() { const [locations, setLocations] = usePersistentState([]) const canvasRef = React.useRef(null) React.useEffect(() => { const canvas = canvasRef.current const ctx = canvas.getContext('2d') ctx.clearRect(0, 0, window.innerWidth, window.innerHeight) locations.forEach(location => draw(ctx, location)) }) return [locations, setLocations, canvasRef] } function App() { const [locations, setLocations, canvasRef] = usePersistentCanvas() function handleCanvasClick(e) { const newLocation = { x: e.clientX, y: e.clientY } setLocations([...locations, newLocation]) } function handleClear() { setLocations([]) } function handleUndo() { setLocations(locations.slice(0, -1)) } return ( <>
) } export default AppCopy the code
As you can see, our App components became very small. All logic related to storing state in localStorage and drawing on canvas is extracted to custom hooks. You can further clean this file by moving hooks into the hooks file. This allows other components to reuse this logic, for example to make better hooks.
conclusion
If you compare hooks to life cycle methods such as componentDidMount, componentDidUpdate, what makes hooks so special? Take a look at the example above:
- Hooks allow you to reuse lifecycle hook logic in different components
- You can synthesize hooks to build richer custom hooks, just as you can synthesize richer UI components.
- Hooks are smaller and more concise, no longer bloated, and the lifecycle methods are sometimes confusing.
It’s too early to tell if hooks really want to fix all of these issues – and what new bad practices might emerge – but look above I’m very excited and optimistic about the future of React!