Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Testing React Hooks with Confidence
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Radoslav Stankov
January 16, 2021
Technology
240
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Testing React Hooks with Confidence
Code can be found at
https://github.com/rstankov/talks-code
Radoslav Stankov
January 16, 2021
More Decks by Radoslav Stankov
See All by Radoslav Stankov
Building LLM Powered Features
rstankov
0
150
Tips for Tailwind CSS
rstankov
0
58
Building LLM Powered Features (lightning talk)
rstankov
0
74
All you need is CSS
rstankov
0
150
Ruby on Rails The Single Engineer Framework
rstankov
0
58
Rails: The Missing Parts
rstankov
1
260
The dream that turned into nightmare
rstankov
0
330
The dream that turned into nightmare (lightning)
rstankov
0
140
Ruby on Rails - The Single Engineer Framework
rstankov
0
360
Other Decks in Technology
See All in Technology
ABEMA の Datadog × OTel 基盤、 中から見るか? 外から見るか?
tetsuya28
0
110
タクシーアプリ『GO』の実践的データ活用
mot_techtalk
3
160
はじめてのDatadog
kairim0
0
290
個人最適 から 全体最適 へ AI情報共有会・AIギルド・AI-DLC で進める カンリーの組織展開
rfdnxbro
0
1.7k
関西に縁あるMicrosoft MVPsが語るCopilotの未来
kasada
0
1.2k
「気づいたら仕事が終わっている」バクラクAIエージェント本番運用の裏側 / layerx-bakuraku-aie2026
yuya4
19
11k
AI Engineering Summit Tokyo 2026 AIの前に、やることがある 〜医療データ企業の4フェーズ〜
dtaniwaki
0
2k
Agentic ERPをどう設計するか ー 受発注エージェントを動かす、現場の知見と設計思想ー
recerqainc
1
1.7k
2026.06.13_AI時代に事業会社が「SIer出身エンジニア」を求める理由 / Why Businesses Seek Engineers with a System Integrator Background in the AI Era
jumtech
0
580
Agentic Web
dynamis
1
160
Claude code Orchestra
ozakiomumkj
3
980
Amazon Bedrock AgentCore ワークショップ JAWS UG TOHOKU / amazon-bedrock-agentcore-workshop-jawsug-tohoku-2026
gawa
8
360
Featured
See All Featured
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.2k
Technical Leadership for Architectural Decision Making
baasie
3
400
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
3.3k
Become a Pro
speakerdeck
PRO
31
6k
Learning to Love Humans: Emotional Interface Design
aarron
275
41k
Introduction to Domain-Driven Design and Collaborative software design
baasie
1
820
Ruling the World: When Life Gets Gamed
codingconduct
0
250
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
194
17k
Large-scale JavaScript Application Architecture
addyosmani
515
110k
Design and Strategy: How to Deal with People Who Don’t "Get" Design
morganepeng
133
19k
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
300
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
Transcript
Testing React Hooks with Confidence Radoslav Stankov 29/01/2020
Radoslav Stankov @rstankov blog.rstankov.com twitter.com/rstankov github.com/rstankov speakerdeck.com/rstankov
None
None
https://rstankov.com/appearances
None
Automated Testing Test Driven Development
None
None
import * as React from 'react'; import Memory from './Memory';
import Grid from './Grid'; import Button from './Button'; const OPERATIONS = { '+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, }; export default function Calculator() { const [memory, setMemory] = React.useState('0'); const actions = React.useMemo( () => ({ add(input) { if (!input.match(/\d/)) { if (!memory.match(/\d$/)) { return; } if (!OPERATIONS[input]) { return; } setMemory(`${memory} ${input} `); return;
const actions = React.useMemo( () => ({ add(input) { if
(!input.match(/\d/)) { if (!memory.match(/\d$/)) { return; } if (!OPERATIONS[input]) { return; } setMemory(`${memory} ${input} `); return; } if (memory.match(/(^| )0$/)) { setMemory(`${memory.slice(0, -1)}${input}`); return; } setMemory(`${memory}${input}`); }, remove() { const newMemory = memory.match(/\d$/) ? memory.slice(0, -1) : memory.slice(0, -3); setMemory(newMemory || '0');
if (memory.match(/(^| )0$/)) { setMemory(`${memory.slice(0, -1)}${input}`); return; } setMemory(`${memory}${input}`); },
remove() { const newMemory = memory.match(/\d$/) ? memory.slice(0, -1) : memory.slice(0, -3); setMemory(newMemory || '0'); }, reset() { setMemory('0'); }, evaluate() { const chunks = memory.split(' '); let value = parseInt(chunks[0], 10); for (let i = 1; i < chunks.length; i += 2) { const other = chunks[i + 1]; if (typeof other === 'undefined' || other.length === 0) { continue; } const operation = OPERATIONS[chunks[i]];
reset() { setMemory('0'); }, evaluate() { const chunks = memory.split('
'); let value = parseInt(chunks[0], 10); for (let i = 1; i < chunks.length; i += 2) { const other = chunks[i + 1]; if (typeof other === 'undefined' || other.length === 0) { continue; } const operation = OPERATIONS[chunks[i]]; if (!operation) { throw new Error(`Invalid "${chunks[i]}" operand`); } value = operation(value, parseInt(other, 10)); } setMemory(value.toString()); }, }), [memory, setMemory], ); return (
); return ( <Grid size="4"> <Grid.Column size="3"> <Memory>{memory.trim()}</Memory> <Button onClick={()
=> actions.add('1')}>1</Button> <Button onClick={() => actions.add('2')}>2</Button> <Button onClick={() => actions.add('3')}>3</Button> <Button onClick={() => actions.add('4')}>4</Button> <Button onClick={() => actions.add('5')}>5</Button> <Button onClick={() => actions.add('6')}>6</Button> <Button onClick={() => actions.add('7')}>7</Button> <Button onClick={() => actions.add('8')}>8</Button> <Button onClick={() => actions.add('9')}>9</Button> <Button onClick={actions.reset}>C</Button> <Button onClick={() => actions.add('0')}>0</Button> <Button onClick={actions.remove}>←</Button> </Grid.Column> <Grid.Column size="1"> <Button onClick={actions.evaluate}>=</Button> <Button onClick={() => actions.add('+')}>+</Button> <Button onClick={() => actions.add('-')}>-</Button> <Button onClick={() => actions.add('*')}>*</Button> </Grid.Column> </Grid> ); }
None
! Smoke Test
return ( <Grid size="4"> <Grid.Column size="3" data-testid="buttons"> <Memory data-testid="result">{memory.trim()}</Memory> <Button
data-testid="1" onClick={() => actions.add('1')}>1</Button> <Button data-testid="2" onClick={() => actions.add('2')}>2</Button> <Button data-testid="3" onClick={() => actions.add('3')}>3</Button> <Button data-testid="4" onClick={() => actions.add('4')}>4</Button> <Button data-testid="5" onClick={() => actions.add('5')}>5</Button> <Button data-testid="6" onClick={() => actions.add('6')}>6</Button> <Button data-testid="7" onClick={() => actions.add('7')}>7</Button> <Button data-testid="8" onClick={() => actions.add('8')}>8</Button> <Button data-testid="9" onClick={() => actions.add('9')}>9</Button> <Button data-testid="reset" onClick={actions.reset}>C</Button> <Button data-testid="0" onClick={() => actions.add('0')}>0</Button> <Button data-testid="remove" onClick={actions.remove}>← </Button> </Grid.Column> <Grid.Column size="1" data-testid="operations"> <Button data-testid="evaluate" onClick={actions.evaluate}>=</Button> <Button data-testid="+" onClick={() => actions.add('+')}>+</Button> <Button data-testid="-" onClick={() => actions.add('-')}>-</Button> <Button data-testid="*" onClick={() => actions.add('*')}>*</Button> </Grid.Column> </Grid> ); }
return ( <Grid size="4"> <Grid.Column size="3" data-testid="buttons"> <Memory data-testid="result">{memory.trim()}</Memory> <Button
data-testid="1" onClick={() => actions.add('1')}>1</Button> <Button data-testid="2" onClick={() => actions.add('2')}>2</Button> <Button data-testid="3" onClick={() => actions.add('3')}>3</Button> <Button data-testid="4" onClick={() => actions.add('4')}>4</Button> <Button data-testid="5" onClick={() => actions.add('5')}>5</Button> <Button data-testid="6" onClick={() => actions.add('6')}>6</Button> <Button data-testid="7" onClick={() => actions.add('7')}>7</Button> <Button data-testid="8" onClick={() => actions.add('8')}>8</Button> <Button data-testid="9" onClick={() => actions.add('9')}>9</Button> <Button data-testid="reset" onClick={actions.reset}>C</Button> <Button data-testid="0" onClick={() => actions.add('0')}>0</Button> <Button data-testid="remove" onClick={actions.remove}>← </Button> </Grid.Column> <Grid.Column size="1" data-testid="operations"> <Button data-testid="evaluate" onClick={actions.evaluate}>=</Button> <Button data-testid="+" onClick={() => actions.add('+')}>+</Button> <Button data-testid="-" onClick={() => actions.add('-')}>-</Button> <Button data-testid="*" onClick={() => actions.add('*')}>*</Button> </Grid.Column> </Grid> ); }
yarn add --dev @testing-library/react
import { render, fireEvent, screen } from '@testing-library/react'; const click
= (dt) => fireEvent.click(screen.getByTestId(dt)); const expectComponent = (dt) => expect(screen.getByTestId(dt)); describe(Component.name, () => { it('works', () => { render(<Component />); expectComponent('result').toHaveTextContent('0'); click('1'); click('+'); click('1'); expectComponent('result').toHaveTextContent('1 + 1'); click('remove'); expectComponent('result').toHaveTextContent('1 +'); click('2'); expectComponent('result').toHaveTextContent('1 + 2'); click('evaluate'); expectComponent('result').toHaveTextContent('3'); click('reset'); expectComponent('result').toHaveTextContent('0'); }); });
import { render, fireEvent, screen } from '@testing-library/react'; const click
= (dt) => fireEvent.click(screen.getByTestId(dt)); const expectComponent = (dt) => expect(screen.getByTestId(dt)); describe(Component.name, () => { it('works', () => { render(<Component />); expectComponent('result').toHaveTextContent('0'); click('1'); click('+'); click('1'); expectComponent('result').toHaveTextContent('1 + 1'); click('remove'); expectComponent('result').toHaveTextContent('1 +'); click('2'); expectComponent('result').toHaveTextContent('1 + 2'); click('evaluate'); expectComponent('result').toHaveTextContent('3'); click('reset'); expectComponent('result').toHaveTextContent('0'); }); });
import { render, fireEvent, screen } from '@testing-library/react'; const click
= (dt) => fireEvent.click(screen.getByTestId(dt)); const expectComponent = (dt) => expect(screen.getByTestId(dt)); describe(Component.name, () => { it('works', () => { render(<Component />); expectComponent('result').toHaveTextContent('0'); click('1'); click('+'); click('1'); expectComponent('result').toHaveTextContent('1 + 1'); click('remove'); expectComponent('result').toHaveTextContent('1 +'); click('2'); expectComponent('result').toHaveTextContent('1 + 2'); click('evaluate'); expectComponent('result').toHaveTextContent('3'); click('reset'); expectComponent('result').toHaveTextContent('0'); }); });
import { render, fireEvent, screen } from '@testing-library/react'; const click
= (dt) => fireEvent.click(screen.getByTestId(dt)); const expectComponent = (dt) => expect(screen.getByTestId(dt)); describe(Component.name, () => { it('works', () => { render(<Component />); expectComponent('result').toHaveTextContent('0'); click('1'); click('+'); click('1'); expectComponent('result').toHaveTextContent('1 + 1'); click('remove'); expectComponent('result').toHaveTextContent('1 +'); click('2'); expectComponent('result').toHaveTextContent('1 + 2'); click('evaluate'); expectComponent('result').toHaveTextContent('3'); click('reset'); expectComponent('result').toHaveTextContent('0'); }); });
import { render, fireEvent, screen } from '@testing-library/react'; const click
= (dt) => fireEvent.click(screen.getByTestId(dt)); const expectComponent = (dt) => expect(screen.getByTestId(dt)); describe(Component.name, () => { it('works', () => { render(<Component />); expectComponent('result').toHaveTextContent('0'); click('1'); click('+'); click('1'); expectComponent('result').toHaveTextContent('1 + 1'); click('remove'); expectComponent('result').toHaveTextContent('1 +'); click('2'); expectComponent('result').toHaveTextContent('1 + 2'); click('evaluate'); expectComponent('result').toHaveTextContent('3'); click('reset'); expectComponent('result').toHaveTextContent('0'); }); });
" This is placeholder " Not a "real" unit test.
It doesn't follow the 4 phases of good test: setup ➡ action ➡ assert ➡ teardown
import * as React from 'react'; import Memory from './Memory';
import Grid from './Grid'; import Button from './Button'; const OPERATIONS = { '+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, }; export default function Calculator() { const [memory, setMemory] = React.useState('0'); const actions = React.useMemo( () => ({ add(input) { if (!input.match(/\d/)) { if (!memory.match(/\d$/)) { return; } if (!OPERATIONS[input]) { return; } setMemory(`${memory} ${input} `); return; $
% Extract Custom Hook
import * as React from 'react'; import Memory from './Memory';
import Grid from './Grid'; import Button from './Button'; import useCalculatorState from './useCalculatorState'; export default function Calculator() { const [memory, actions] = useCalculatorState(); return ( <Grid size="4"> <Grid.Column size="3" data-testid="buttons"> <Memory data-testid="result">{memory.trim()}</Memory> <Button data-testid="1" onClick={() => actions.add('1')}>1</Button> <Button data-testid="2" onClick={() => actions.add('2')}>2</Button> <Button data-testid="3" onClick={() => actions.add('3')}>3</Button> <Button data-testid="4" onClick={() => actions.add('4')}>4</Button> <Button data-testid="5" onClick={() => actions.add('5')}>5</Button> <Button data-testid="6" onClick={() => actions.add('6')}>6</Button> <Button data-testid="7" onClick={() => actions.add('7')}>7</Button> <Button data-testid="8" onClick={() => actions.add('8')}>8</Button> <Button data-testid="9" onClick={() => actions.add('9')}>9</Button> <Button data-testid="reset" onClick={actions.reset}>C</Button> <Button data-testid="0" onClick={() => actions.add('0')}>0</Button> <Button data-testid="remove" onClick={actions.remove}>← </Button> </Grid.Column> <Grid.Column size="1" data-testid="operations">
import * as React from 'react'; import Memory from './Memory';
import Grid from './Grid'; import Button from './Button'; import useCalculatorState from './useCalculatorState'; export default function Calculator() { const [memory, actions] = useCalculatorState(); return ( <Grid size="4"> <Grid.Column size="3" data-testid="buttons"> <Memory data-testid="result">{memory.trim()}</Memory> <Button data-testid="1" onClick={() => actions.add('1')}>1</Button> <Button data-testid="2" onClick={() => actions.add('2')}>2</Button> <Button data-testid="3" onClick={() => actions.add('3')}>3</Button> <Button data-testid="4" onClick={() => actions.add('4')}>4</Button> <Button data-testid="5" onClick={() => actions.add('5')}>5</Button> <Button data-testid="6" onClick={() => actions.add('6')}>6</Button> <Button data-testid="7" onClick={() => actions.add('7')}>7</Button> <Button data-testid="8" onClick={() => actions.add('8')}>8</Button> <Button data-testid="9" onClick={() => actions.add('9')}>9</Button> <Button data-testid="reset" onClick={actions.reset}>C</Button> <Button data-testid="0" onClick={() => actions.add('0')}>0</Button> <Button data-testid="remove" onClick={actions.remove}>← </Button> </Grid.Column> <Grid.Column size="1" data-testid="operations">
import * as React from 'react'; const OPERATIONS = {
'+': (a, b) => a + b, '-': (a, b) => a - b, '*': (a, b) => a * b, }; export default function useCalculatorState(initalState = '0') { const [memory, setMemory] = React.useState(initalState); const actions = React.useMemo( () => ({ add(input) { if (!input.match(/\d/)) { if (!memory.match(/\d$/)) { return; } if (!OPERATIONS[input]) { return; } setMemory(`${memory} ${input} `); return; } if (memory.match(/(^| )0$/)) {
& Test Custom hook
import useCalculatorState from './useCalculatorState'; import { renderHook, act } from
'@testing-library/react-hooks'; describe(useCalculatorState.name, () => { function initHook(initalState) { return renderHook(() => useCalculatorState(initalState)).result; } describe('add', () => { function call(initalState, value) { const hook = initHook(initalState); act(() => { hook.current[1].add(value); }); return hook.current[0]; } it('can add digits', () => { expect(call('1', '2')).toEqual('12'); }); it('ensures numbers cannot start with 0', () => { expect(call('0', '1')).toEqual('1'); expect(call('1 + 0', '0')).toEqual('1 + 0'); expect(call('1 + 0', '1')).toEqual('1 + 1');
yarn add --dev @testing-library/react-hooks
import useCalculatorState from './useCalculatorState'; import { renderHook, act } from
'@testing-library/react-hooks'; describe(useCalculatorState.name, () => { function initHook(initalState) { return renderHook(() => useCalculatorState(initalState)).result; } describe('add', () => { function call(initalState, value) { const hook = initHook(initalState); act(() => { hook.current[1].add(value); }); return hook.current[0]; } it('can add digits', () => { expect(call('1', '2')).toEqual('12'); }); it('ensures numbers cannot start with 0', () => { expect(call('0', '1')).toEqual('1'); expect(call('1 + 0', '0')).toEqual('1 + 0'); expect(call('1 + 0', '1')).toEqual('1 + 1');
import useCalculatorState from './useCalculatorState'; import { renderHook, act } from
'@testing-library/react-hooks'; describe(useCalculatorState.name, () => { function initHook(initalState) { return renderHook(() => useCalculatorState(initalState)).result; } describe('add', () => { function call(initalState, value) { const hook = initHook(initalState); act(() => { hook.current[1].add(value); }); return hook.current[0]; } it('can add digits', () => { expect(call('1', '2')).toEqual('12'); }); it('ensures numbers cannot start with 0', () => { expect(call('0', '1')).toEqual('1'); expect(call('1 + 0', '0')).toEqual('1 + 0'); expect(call('1 + 0', '1')).toEqual('1 + 1');
import useCalculatorState from './useCalculatorState'; import { renderHook, act } from
'@testing-library/react-hooks'; describe(useCalculatorState.name, () => { function initHook(initalState) { return renderHook(() => useCalculatorState(initalState)).result; } describe('add', () => { function call(initalState, value) { const hook = initHook(initalState); act(() => { hook.current[1].add(value); }); return hook.current[0]; } it('can add digits', () => { expect(call('1', '2')).toEqual('12'); }); it('ensures numbers cannot start with 0', () => { expect(call('0', '1')).toEqual('1'); expect(call('1 + 0', '0')).toEqual('1 + 0'); expect(call('1 + 0', '1')).toEqual('1 + 1');
import useCalculatorState from './useCalculatorState'; import { renderHook, act } from
'@testing-library/react-hooks'; describe(useCalculatorState.name, () => { function initHook(initalState) { return renderHook(() => useCalculatorState(initalState)).result; } describe('add', () => { function call(initalState, value) { const hook = initHook(initalState); act(() => { hook.current[1].add(value); }); return hook.current[0]; } it('can add digits', () => { expect(call('1', '2')).toEqual('12'); }); it('ensures numbers cannot start with 0', () => { expect(call('0', '1')).toEqual('1'); expect(call('1 + 0', '0')).toEqual('1 + 0'); expect(call('1 + 0', '1')).toEqual('1 + 1');
import useCalculatorState from './useCalculatorState'; import { renderHook, act } from
'@testing-library/react-hooks'; describe(useCalculatorState.name, () => { function initHook(initalState) { return renderHook(() => useCalculatorState(initalState)).result; } describe('add', () => { function call(initalState, value) { const hook = initHook(initalState); act(() => { hook.current[1].add(value); }); return hook.current[0]; } it('can add digits', () => { expect(call('1', '2')).toEqual('12'); }); it('ensures numbers cannot start with 0', () => { expect(call('0', '1')).toEqual('1'); expect(call('1 + 0', '0')).toEqual('1 + 0'); expect(call('1 + 0', '1')).toEqual('1 + 1');
it('can add digits', () => { expect(call('1', '2')).toEqual('12'); }); it('ensures
numbers cannot start with 0', () => { expect(call('0', '1')).toEqual('1'); expect(call('1 + 0', '0')).toEqual('1 + 0'); expect(call('1 + 0', '1')).toEqual('1 + 1'); expect(call('10', '0')).toEqual('100'); }); it('can add operand', () => { expect(call('1', '+')).toEqual('1 + '); }); it('guards against add operand before digit', () => { expect(call('', '+')).toEqual(''); }); it('guards against two operands one after the other', () => { expect(call('1 + ', '+')).toEqual('1 + '); }); it('guards against add invalid operator', () => { expect(call('1', '~')).toEqual('1'); }); });
describe('remove', () => { function call(initalState) { const hook =
initHook(initalState); act(() => { hook.current[1].remove(); }); return hook.current[0]; } it('removes one digit', () => { expect(call('1234')).toEqual('123'); }); it('removes one operand', () => { expect(call('1 + ')).toEqual('1'); }); it('returns at least 0 when fully cleared', () => { expect(call('1')).toEqual('0'); }); }); describe('evaluate', () => { function call(initalState) { const hook = initHook(initalState);
describe('evaluate', () => { function call(initalState) { const hook =
initHook(initalState); act(() => { hook.current[1].evaluate(); }); return hook.current[0]; } describe('operations', () => { it('handles noop', () => { expect(call('12')).toEqual('12'); }); it('handles "1 + 1" input', () => { expect(call('1 + 1')).toEqual('2'); }); it('handles "1 - 1" input', () => { expect(call('2 - 1')).toEqual('1'); }); it('handles "2 * 3" input', () => { expect(call('2 * 3')).toEqual('6'); }); });
expect(call('2 * 3')).toEqual('6'); }); }); describe('edge cases', () => {
it('handles zero in input', () => { expect(call('1 + 0')).toEqual('1'); }); it('handles larger number in input', () => { expect(call('123 + 456')).toEqual((123 + 456).toString()); }); it('handles multiple operations input', () => { expect(call('1 + 1 + 1')).toEqual('3'); }); it('handles incomplete operation input', () => { expect(call('1 + ')).toEqual('1'); }); it('handles negative values', () => { expect(call('-1 + 2')).toEqual('1'); }); }); }); });
' Refactor Custom Hook
import * as React from 'react'; export default function useCalculatorState(initalState
= '0') { const [memory, dispatch] = React.useReducer(calculatorReducer, initalState); const actions = React.useMemo( () => ({ add(value) { dispatch({ type: 'add', payload: value }); }, remove() { dispatch({ type: 'remove' }); }, reset() { dispatch({ type: 'reset' }); }, evaluate() { dispatch({ type: 'evaluate' }); }, }), [dispatch], ); return [memory, actions]; } const OPERATIONS = {
function calculatorReducer(memory, action) { switch (action.type) { case 'add': const
input = action.payload; if (!input.match(/\d/)) { if (!memory.match(/\d$/)) { return memory; } if (!OPERATIONS[input]) { return memory; } return `${memory} ${input} `; } if (memory.match(/(^| )0$/)) { return `${memory.slice(0, -1)}${input}`; } return `${memory}${input}`; case 'remove': const newMemory = memory.match(/\d$/) ? memory.slice(0, -1) : memory.slice(0, -3);
case 'reset': return '0'; case 'evaluate': const chunks = memory.split('
'); let value = parseInt(chunks[0], 10); for (let i = 1; i < chunks.length; i += 2) { const other = chunks[i + 1]; if (typeof other === 'undefined' || other.length === 0) { continue; } const operation = OPERATIONS[chunks[i]]; if (!operation) { throw new Error(`Invalid "${chunks[i]}" operand`); } value = operation(value, parseInt(other, 10)); } return value.toString(); default: throw new Error(`Invalid action - ${action.type}`); } }
Recap
( started with messy code ) added "smoke test" *
extracted custom hook + added test for the custom hook , refactored the hook - removed "smoke test"
None
Thanks .
Thanks . https://github.com/rstankov/talks-code
Thanks . https://rstankov.com/appearances