Tách biệt sự kiện và Effect
Event handler chỉ chạy lại khi bạn thực hiện lại cùng một tương tác. Khác với event handler, Effect sẽ đồng bộ lại nếu một giá trị mà nó đọc, như một prop hoặc một biến state, khác với giá trị ở lần render trước. Đôi khi, bạn cũng muốn kết hợp cả hai hành vi: một Effect chạy lại khi một số giá trị thay đổi nhưng không phải tất cả. Trang này sẽ hướng dẫn bạn cách làm điều đó.
Bạn sẽ được học
- Cách lựa chọn giữa event handler và Effect
- Vì sao Effect là reactive, còn event handler thì không
- Làm gì khi bạn muốn một phần code trong Effect không bị reactive
- Effect Event là gì, và cách tách chúng ra khỏi Effect
- Cách đọc giá trị props và state mới nhất từ Effect bằng Effect Event
Lựa chọn giữa event handler và Effect
Trước tiên, hãy cùng ôn lại sự khác biệt giữa event handler và Effect.
Hãy tưởng tượng bạn đang triển khai một component phòng chat. Yêu cầu của bạn như sau:
- Component của bạn nên tự động kết nối tới phòng chat được chọn.
- Khi bạn nhấn nút “Send”, nó sẽ gửi tin nhắn tới phòng chat.
Giả sử bạn đã triển khai code cho chúng, nhưng bạn không chắc nên đặt ở đâu. Bạn nên dùng event handler hay Effect? Mỗi khi cần trả lời câu hỏi này, hãy cân nhắc vì sao đoạn code đó cần chạy.
Event handler chạy để phản hồi các tương tác cụ thể
Từ góc nhìn của người dùng, việc gửi tin nhắn chỉ nên xảy ra bởi vì nút “Send” cụ thể đã được nhấn. Người dùng sẽ rất khó chịu nếu bạn gửi tin nhắn của họ vào bất kỳ thời điểm nào khác hoặc vì lý do nào khác. Đó là lý do gửi tin nhắn nên là một event handler. Event handler cho phép bạn xử lý các tương tác cụ thể:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Send</button>
</>
);
}
Với event handler, bạn có thể chắc chắn rằng sendMessage(message)
sẽ chỉ chạy nếu người dùng nhấn nút.
Effect chạy bất cứ khi nào cần đồng bộ hóa
Hãy nhớ rằng bạn cũng cần giữ cho component luôn kết nối với phòng chat. Đoạn code đó nên đặt ở đâu?
Lý do để chạy đoạn code này không phải là một tương tác cụ thể nào. Không quan trọng vì sao hoặc bằng cách nào người dùng điều hướng tới màn hình phòng chat. Khi họ đang xem nó và có thể tương tác, component cần giữ kết nối với server chat đã chọn. Ngay cả khi component phòng chat là màn hình đầu tiên của ứng dụng, và người dùng chưa thực hiện bất kỳ tương tác nào, bạn vẫn cần kết nối. Đó là lý do nó nên là một Effect:
function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}
Với đoạn code này, bạn có thể chắc chắn rằng luôn có một kết nối hoạt động tới server chat hiện tại, bất kể người dùng đã thực hiện những tương tác nào. Dù người dùng chỉ vừa mở app, chọn phòng khác, hay điều hướng sang màn hình khác rồi quay lại, Effect của bạn đảm bảo component sẽ luôn đồng bộ với phòng hiện tại, và sẽ kết nối lại khi cần thiết.
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId }) { const [message, setMessage] = useState(''); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.connect(); return () => connection.disconnect(); }, [roomId]); function handleSendClick() { sendMessage(message); } return ( <> <h1>Welcome to the {roomId} room!</h1> <input value={message} onChange={e => setMessage(e.target.value)} /> <button onClick={handleSendClick}>Send</button> </> ); } export default function App() { const [roomId, setRoomId] = useState('general'); const [show, setShow] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <button onClick={() => setShow(!show)}> {show ? 'Close chat' : 'Open chat'} </button> {show && <hr />} {show && <ChatRoom roomId={roomId} />} </> ); }
Giá trị reactive và logic reactive
Một cách trực quan, bạn có thể nói rằng event handler luôn được kích hoạt “thủ công”, ví dụ như khi nhấn một nút. Ngược lại, Effect là “tự động”: chúng sẽ chạy và chạy lại khi cần để giữ cho mọi thứ đồng bộ.
Có một cách chính xác hơn để suy nghĩ về điều này.
Props, state, và các biến được khai báo bên trong thân component của bạn được gọi là giá trị reactive. Trong ví dụ này, serverUrl
không phải là giá trị reactive, nhưng roomId
và message
thì có. Chúng tham gia vào luồng dữ liệu khi render:
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
}
Những giá trị reactive như vậy có thể thay đổi do một lần render lại. Ví dụ, người dùng có thể chỉnh sửa message
hoặc chọn một roomId
khác trong dropdown. Event handler và Effect phản hồi sự thay đổi này theo cách khác nhau:
- Logic bên trong event handler không phải là reactive. Nó sẽ không chạy lại trừ khi người dùng thực hiện lại cùng một tương tác (ví dụ: click). Event handler có thể đọc giá trị reactive mà không “phản ứng” với sự thay đổi của chúng.
- Logic bên trong Effect là reactive. Nếu Effect của bạn đọc một giá trị reactive, bạn phải khai báo nó là dependency. Khi một lần render lại làm giá trị đó thay đổi, React sẽ chạy lại logic của Effect với giá trị mới.
Hãy xem lại ví dụ trước để minh họa sự khác biệt này.
Logic bên trong event handler không phải là reactive
Xem dòng code này. Logic này có nên là reactive không?
// ...
sendMessage(message);
// ...
Từ góc nhìn của người dùng, việc thay đổi message
không có nghĩa là họ muốn gửi tin nhắn. Nó chỉ có nghĩa là người dùng đang gõ. Nói cách khác, logic gửi tin nhắn không nên là reactive. Nó không nên chạy lại chỉ vì giá trị reactive đã thay đổi. Đó là lý do nó thuộc về event handler:
function handleSendClick() {
sendMessage(message);
}
Event handler không phải là reactive, nên sendMessage(message)
chỉ chạy khi người dùng nhấn nút Send.
Logic bên trong Effect là reactive
Bây giờ hãy quay lại các dòng này:
// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...
Từ góc nhìn của người dùng, việc thay đổi roomId
có nghĩa là họ muốn kết nối tới phòng khác. Nói cách khác, logic kết nối tới phòng nên là reactive. Bạn muốn các dòng code này “theo sát” giá trị reactive, và chạy lại nếu giá trị đó thay đổi. Đó là lý do nó nên nằm trong một Effect:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);
Effect là reactive, nên createConnection(serverUrl, roomId)
và connection.connect()
sẽ chạy cho mỗi giá trị khác nhau của roomId
. Effect của bạn giữ cho kết nối chat luôn đồng bộ với phòng hiện tại.
Tách logic không reactive ra khỏi Effect
Mọi thứ trở nên phức tạp hơn khi bạn muốn kết hợp logic reactive với logic không reactive.
Ví dụ, hãy tưởng tượng bạn muốn hiển thị thông báo khi người dùng kết nối tới chat. Bạn đọc theme hiện tại (tối hoặc sáng) từ props để có thể hiển thị thông báo với màu đúng:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
// ...
Tuy nhiên, theme
là một giá trị reactive (nó có thể thay đổi do render lại), và mọi giá trị reactive được đọc bởi Effect phải được khai báo làm dependency. Bây giờ bạn phải khai báo theme
là dependency của Effect:
function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Connected!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ All dependencies declared
// ...
Thử với ví dụ này và xem bạn có thể phát hiện vấn đề với trải nghiệm người dùng này không:
import { useState, useEffect } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { showNotification('Connected!', theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Khi roomId
thay đổi, chat sẽ kết nối lại như bạn mong đợi. Nhưng vì theme
cũng là một dependency, chat cũng kết nối lại mỗi khi bạn chuyển giữa theme tối và sáng. Điều đó không tốt!
Nói cách khác, bạn không muốn dòng này là reactive, mặc dù nó nằm trong một Effect (vốn là reactive):
// ...
showNotification('Connected!', theme);
// ...
Bạn cần một cách để tách logic không reactive này ra khỏi Effect reactive xung quanh nó.
Khai báo Effect Event
Sử dụng một Hook đặc biệt gọi là useEffectEvent
để tách logic không reactive này ra khỏi Effect:
import { useEffect, useEffectEvent } from 'react';
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
// ...
Ở đây, onConnected
được gọi là Effect Event. Nó là một phần của logic Effect, nhưng nó hoạt động giống như một event handler hơn. Logic bên trong nó không phải là reactive, và nó luôn “thấy” giá trị mới nhất của props và state.
Bây giờ bạn có thể gọi Effect Event onConnected
từ bên trong Effect:
function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Connected!', theme);
});
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
Điều này giải quyết vấn đề. Lưu ý rằng bạn phải xóa theme
khỏi danh sách dependency của Effect, vì nó không còn được sử dụng trong Effect nữa. Bạn cũng không cần thêm onConnected
vào đó, vì Effect Event không phải là reactive và phải được bỏ qua khỏi dependency.
Xác minh rằng hành vi mới hoạt động như bạn mong đợi:
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; import { createConnection, sendMessage } from './chat.js'; import { showNotification } from './notifications.js'; const serverUrl = 'https://localhost:1234'; function ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification('Connected!', theme); }); useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on('connected', () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]); return <h1>Welcome to the {roomId} room!</h1> } export default function App() { const [roomId, setRoomId] = useState('general'); const [isDark, setIsDark] = useState(false); return ( <> <label> Choose the chat room:{' '} <select value={roomId} onChange={e => setRoomId(e.target.value)} > <option value="general">general</option> <option value="travel">travel</option> <option value="music">music</option> </select> </label> <label> <input type="checkbox" checked={isDark} onChange={e => setIsDark(e.target.checked)} /> Use dark theme </label> <hr /> <ChatRoom roomId={roomId} theme={isDark ? 'dark' : 'light'} /> </> ); }
Bạn có thể nghĩ về Effect Event như rất giống với event handler. Sự khác biệt chính là event handler chạy để phản hồi tương tác của người dùng, trong khi Effect Event được kích hoạt bởi bạn từ Effect. Effect Event cho phép bạn “phá vỡ chuỗi” giữa tính reactive của Effect và code không nên là reactive.
Đọc props và state mới nhất bằng Effect Event
Effect Event cho phép bạn sửa nhiều pattern mà bạn có thể muốn bỏ qua dependency linter.
Ví dụ, giả sử bạn có một Effect để ghi lại lượt truy cập trang:
function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}
Sau đó, bạn thêm nhiều route vào trang web. Bây giờ component Page
nhận một prop url
với đường dẫn hiện tại. Bạn muốn truyền url
như một phần của lời gọi logVisit
, nhưng dependency linter phàn nàn:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url'
// ...
}
Hãy suy nghĩ về những gì bạn muốn code làm. Bạn muốn ghi nhật ký một lần truy cập riêng biệt cho các URL khác nhau vì mỗi URL đại diện cho một trang khác nhau. Nói cách khác, lời gọi logVisit
này nên là reactive đối với url
. Đó là lý do trong trường hợp này, việc tuân theo dependency linter và thêm url
làm dependency là hợp lý:
function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Bây giờ giả sử bạn muốn bao gồm số lượng mặt hàng trong giỏ hàng cùng với mỗi lần truy cập trang:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}
Bạn đã sử dụng numberOfItems
bên trong Effect, nên linter yêu cầu bạn thêm nó làm dependency. Tuy nhiên, bạn không muốn lời gọi logVisit
là reactive đối với numberOfItems
. Nếu người dùng đưa thứ gì đó vào giỏ hàng và numberOfItems
thay đổi, điều này không có nghĩa là người dùng đã truy cập trang lại. Nói cách khác, việc truy cập trang theo một nghĩa nào đó là một “sự kiện”. Nó xảy ra tại một thời điểm chính xác.
Chia code thành hai phần:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Ở đây, onVisit
là một Effect Event. Code bên trong nó không phải là reactive. Đó là lý do bạn có thể sử dụng numberOfItems
(hoặc bất kỳ giá trị reactive nào khác!) mà không lo lắng rằng nó sẽ khiến code xung quanh chạy lại khi có thay đổi.
Mặt khác, bản thân Effect vẫn là reactive. Code bên trong Effect sử dụng prop url
, nên Effect sẽ chạy lại sau mỗi lần render lại với url
khác nhau. Điều này, đến lượt nó, sẽ gọi Effect Event onVisit
.
Kết quả là, bạn sẽ gọi logVisit
cho mỗi thay đổi của url
, và luôn đọc numberOfItems
mới nhất. Tuy nhiên, nếu numberOfItems
thay đổi một mình, điều này sẽ không khiến bất kỳ code nào chạy lại.
Tìm hiểu sâu
Trong các codebase hiện có, đôi khi bạn có thể thấy quy tắc lint bị bỏ qua như thế này:
function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;
useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 Avoid suppressing the linter like this:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}
Sau khi useEffectEvent
trở thành một phần ổn định của React, chúng tôi khuyến nghị không bao giờ bỏ qua linter.
Nhược điểm đầu tiên của việc bỏ qua quy tắc là React sẽ không còn cảnh báo bạn khi Effect của bạn cần “phản ứng” với một dependency reactive mới mà bạn đã thêm vào code. Trong ví dụ trước, bạn đã thêm url
vào dependency bởi vì React nhắc nhở bạn làm điều đó. Bạn sẽ không còn nhận được những lời nhắc nhở như vậy cho bất kỳ chỉnh sửa nào trong tương lai của Effect đó nếu bạn vô hiệu hóa linter. Điều này dẫn đến bugs.
Đây là một ví dụ về một bug khó hiểu do việc bỏ qua linter. Trong ví dụ này, function handleMove
được cho là sẽ đọc giá trị biến state canMove
hiện tại để quyết định xem dấu chấm có nên theo con trỏ hay không. Tuy nhiên, canMove
luôn là true
bên trong handleMove
.
Bạn có thể thấy tại sao không?
import { useState, useEffect } from 'react'; export default function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [canMove, setCanMove] = useState(true); function handleMove(e) { if (canMove) { setPosition({ x: e.clientX, y: e.clientY }); } } useEffect(() => { window.addEventListener('pointermove', handleMove); return () => window.removeEventListener('pointermove', handleMove); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> <label> <input type="checkbox" checked={canMove} onChange={e => setCanMove(e.target.checked)} /> The dot is allowed to move </label> <hr /> <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity: 0.6, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> </> ); }
Vấn đề với code này nằm ở việc bỏ qua dependency linter. Nếu bạn xóa comment bỏ qua, bạn sẽ thấy rằng Effect này nên phụ thuộc vào function handleMove
. Điều này hợp lý: handleMove
được khai báo bên trong thân component, điều này khiến nó trở thành một giá trị reactive. Mọi giá trị reactive phải được khai báo làm dependency, hoặc nó có thể trở nên cũ theo thời gian!
Tác giả của code gốc đã “nói dối” React bằng cách nói rằng Effect không phụ thuộc ([]
) vào bất kỳ giá trị reactive nào. Đó là lý do tại sao React không đồng bộ lại Effect sau khi canMove
thay đổi (và handleMove
cùng với nó). Bởi vì React không đồng bộ lại Effect, handleMove
được gắn làm listener là function handleMove
được tạo trong lần render đầu tiên. Trong lần render đầu tiên, canMove
là true
, đó là lý do tại sao handleMove
từ lần render đầu tiên sẽ mãi mãi thấy giá trị đó.
Nếu bạn không bao giờ bỏ qua linter, bạn sẽ không bao giờ gặp vấn đề với giá trị cũ.
Với useEffectEvent
, không cần “nói dối” linter, và code hoạt động như bạn mong đợi:
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; export default function App() { const [position, setPosition] = useState({ x: 0, y: 0 }); const [canMove, setCanMove] = useState(true); const onMove = useEffectEvent(e => { if (canMove) { setPosition({ x: e.clientX, y: e.clientY }); } }); useEffect(() => { window.addEventListener('pointermove', onMove); return () => window.removeEventListener('pointermove', onMove); }, []); return ( <> <label> <input type="checkbox" checked={canMove} onChange={e => setCanMove(e.target.checked)} /> The dot is allowed to move </label> <hr /> <div style={{ position: 'absolute', backgroundColor: 'pink', borderRadius: '50%', opacity: 0.6, transform: `translate(${position.x}px, ${position.y}px)`, pointerEvents: 'none', left: -20, top: -20, width: 40, height: 40, }} /> </> ); }
Điều này không có nghĩa là useEffectEvent
luôn là giải pháp đúng. Bạn chỉ nên áp dụng nó cho những dòng code mà bạn không muốn là reactive. Trong sandbox ở trên, bạn không muốn code của Effect là reactive đối với canMove
. Đó là lý do việc tách ra một Effect Event có ý nghĩa.
Đọc Removing Effect Dependencies để biết các lựa chọn khác đúng đắn thay cho việc bỏ qua linter.
Giới hạn của Effect Event
Effect Event rất hạn chế trong cách bạn có thể sử dụng chúng:
- Chỉ gọi chúng từ bên trong Effect.
- Không bao giờ truyền chúng cho component hoặc Hook khác.
Ví dụ, đừng khai báo và truyền Effect Event như thế này:
function Timer() {
const [count, setCount] = useState(0);
const onTick = useEffectEvent(() => {
setCount(count + 1);
});
useTimer(onTick, 1000); // 🔴 Avoid: Passing Effect Events
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Need to specify "callback" in dependencies
}
Thay vào đó, luôn khai báo Effect Event trực tiếp bên cạnh Effect sử dụng chúng:
function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}
function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});
useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Good: Only called locally inside an Effect
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // No need to specify "onTick" (an Effect Event) as a dependency
}
Effect Event là những “mảnh” không reactive của code Effect. Chúng nên được đặt gần Effect sử dụng chúng.
Tóm tắt
- Event handler chạy để phản hồi các tương tác cụ thể.
- Effect chạy bất cứ khi nào cần đồng bộ hóa.
- Logic bên trong event handler không phải là reactive.
- Logic bên trong Effect là reactive.
- Bạn có thể chuyển logic không reactive từ Effect vào Effect Event.
- Chỉ gọi Effect Event từ bên trong Effect.
- Không truyền Effect Event cho component hoặc Hook khác.
Challenge 1 of 4: Sửa biến không cập nhật
Component Timer
này giữ một biến state count
tăng lên mỗi giây. Giá trị mà nó tăng lên được lưu trong biến state increment
. Bạn có thể điều khiển biến increment
bằng các nút cộng và trừ.
Tuy nhiên, dù bạn nhấn nút cộng bao nhiêu lần, bộ đếm vẫn chỉ tăng lên một mỗi giây. Có gì sai với code này? Vì sao increment
luôn bằng 1
bên trong code Effect? Tìm lỗi và sửa nó.
import { useState, useEffect } from 'react'; export default function Timer() { const [count, setCount] = useState(0); const [increment, setIncrement] = useState(1); useEffect(() => { const id = setInterval(() => { setCount(c => c + increment); }, 1000); return () => { clearInterval(id); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <> <h1> Counter: {count} <button onClick={() => setCount(0)}>Reset</button> </h1> <hr /> <p> Every second, increment by: <button disabled={increment === 0} onClick={() => { setIncrement(i => i - 1); }}>–</button> <b>{increment}</b> <button onClick={() => { setIncrement(i => i + 1); }}>+</button> </p> </> ); }