在線體驗地址:han-hooks.netlify.com/ css
![]()
在本文中,我將使用React Hooks建立一個html canvas 畫圖網站,我將使用create-react-app腳手架從零開始構建項目。最後這個應用程序有諸如清除、撤銷和使用localStorage基本功能。html
本文我將向您展現任何構建自定義Hooks和在普通的Hooks中重用有狀態邏輯。react
咱們首先使用create-react-app建立一個新的React應用程序。canvas
$ npx create-react-app canvas-and-hooks
$ cd canvas-and-hooks/
$ yarn start
複製代碼
您的瀏覽器會打開http://localhost:3000/
,而後您會看到一個旋轉的React logo圖片,那麼,您如今能夠開始了...數組
用您喜歡的編輯器打開src/App.js
文件📃,而後替換成如下內容:瀏覽器
import React from 'react'
function App() {
return (
<canvas
width={window.innerWidth}
height={window.innerHeight}
onClick={e => {
alert(e.clientX)
}}
/>
)
}
export default App
複製代碼
在瀏覽器窗口中點擊任意一處,若是會彈出一個彈出框:顯示您鼠標🖱️點擊的x座標,很好!應用程序跑起來了。bash
如今,咱們真正的畫一些東西。這樣的話咱們就須要canvas 元素的ref,因此,開始使用今天的第一個hook useRef吧:app
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
複製代碼
一般,在React中你不須要一個ref來作更新的操做。可是canvas不像其它的DOM元素。大多數DOM元素都有一個屬性,好比說:value,你能夠直接更新它。在canvas中容許✅您使用context(本🌰:ctx)來畫一些東西。爲此,咱們不得不使用ref,它是對實際canvas DOM元素的引用。編輯器
如今咱們有了canvas上下文,是時候畫一些東西了。爲此,粘貼複製如下代碼繪製一個SVG hook。它與hooks無關,若是您不理解它也不須要擔憂😓。函數
import React from 'react'
const HOOK_SVG = 'm129.03125 63.3125c0-34.914062-28.941406-63.3125-64.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.527344h10.316406c54.363282 0 98.585938-43.304688 98.585938-96.527344v-95.640625c0-7.070312-4.640625-13.304687-11.414062-15.328125-6.769532-2.015625-14.082032.625-17.960938 6.535156l-42.328125 64.425782c-4.847656 7.390625-2.800781 17.3125 4.582031 22.167968 7.386719 4.832032 17.304688 2.792969 22.160156-4.585937l12.960938-19.71875v42.144531c0 35.582032-29.863281 64.527344-66.585938 64.527344h-10.316406c-36.714844 0-66.585937-28.945312-66.585937-64.527344v-162.898437c27.847656-7.015625 48.519531-31.773438 48.519531-61.261719zm-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.3125zm0 0'
const HOOK_PATH = new Path2D(HOOK_SVG)
const SCALE = 0.3
const OFFSET = 80
function 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
複製代碼
上面的代碼是爲了在座標(x,y)繪製一個SVG形狀(一個魚鉤)。
試一試,看看它是否起做用。
咱們要添加的下一個功能是Clean和Undo按鈕🔘。爲此,咱們將使用useState hook來跟蹤用戶交互。
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
複製代碼
因此,咱們爲app添加了state。您能夠在return語句上面添加console.log(locations)
來驗證一下。隨着用戶點擊,您會看到打印的數組。
目前,咱們對state沒有任何操做。咱們仍是像之前同樣繪製了hooks。咱們來看看用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
複製代碼
這裏作了不少事情咱們來一一拆解一下。咱們把onClick事件處理函數的繪製函數移動到useEffect回掉裏。這很重要,由於在畫布上繪製由app的狀態決定,這是個反作用。後面咱們會使用localStorage來保持持久化,在state更新的時候這也會是個反作用。
我也對canvas自己的實際繪製作了一些更改,在當前實現中,每次render渲染先清除canvas而後再繪製全部位置,咱們能夠作的比這聰明一點。但爲了保持簡單,就留給讀者去優化吧。
咱們已經完成了全部最難的部分,如今添加新功能應該很簡單了。咱們來建立清除按鈕吧。
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
複製代碼
清除功能只是一個簡單的state更新:咱們經過設置它爲一個空數組來清除state,這很簡單,對嗎?
進一步,我也把canvas onClick事件處理移動到一個單獨的函數裏。
咱們來添加另一個功能:撤銷。一樣的原則,即便這種狀態更新有點棘手。
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
複製代碼
由於React中任何state更新都必須是不可變的,因此咱們不能使用像locations.pop()
來清除數組中最近的一項。咱們的操做不能改變原始的locations數組。方法是使用slice,複製全部項直到最後一個。你可使用locations.slice(0, locations.length - 1)
,可是slice有個更聰明的操做數組最後一位的-1。
在咱們開始以前,咱們整理一下html,而後添加一個css樣式文件。在buttons按鈕外面添加以下的div。
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
複製代碼
css樣式以下:
*,
*: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;
font-size: 0.5em;
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;
}
複製代碼
看起來不錯,咱們來看看下一個功能:持久化。
咱們以前提過,咱們也想要咱們的繪製保存在localStroage中,這也是另一個反作用,咱們將添加另一個useEffect。
import React from 'react'
import './App.css'
// ...draw function
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
複製代碼
如今咱們已經完成了咱們要構建的全部功能,但還不夠。關於books最酷的一件事是您可使用現有的hooks來組建新的自定義hooks。我建立一個自定義的usePersistentState hook來展現這一點。
import React from 'react'
import './App.css'
// ...draw function
// 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
複製代碼
這裏,咱們建立了第一個自定義hook而且從App組件中提取了與從localStorage保存和獲取狀態相關的全部邏輯。咱們這樣作的方式是usePersistentState hook能夠被其它組件重用。這裏沒有任何特定於此組件的內容。
咱們重複這個技巧來操做canvas相關的邏輯。
import React from 'react'
import './App.css'
// ...draw function
// 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 ( <> <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 複製代碼
正如您所看到的,咱們的App組件變得很是小。 在localStorage中存儲狀態和在canvas上繪圖相關的全部邏輯都被提取到自定義hooks。 您能夠經過將hooks移動到hooks文件中來進一步清理此文件。 這樣,其餘組件能夠重用這種邏輯,例如構成更好的hooks。
若是將hooks與生命週期方法(如componentDidMount,componentDidUpdate)進行比較,是什麼讓hooks如此特別? 看看上面的例子:
如今判斷hooks是否真的要解決全部這些問題還爲時尚早 - 以及可能會出現什麼新的不良作法 - 但看看上面我對React的將來感到很是興奮和樂觀!