惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
V2EX
C
Check Point Blog
博客园 - Franky
月光博客
月光博客
T
Tenable Blog
博客园 - 聂微东
Cyberwarzone
Cyberwarzone
The GitHub Blog
The GitHub Blog
C
CERT Recently Published Vulnerability Notes
Scott Helme
Scott Helme
IT之家
IT之家
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
CXSECURITY Database RSS Feed - CXSecurity.com
F
Full Disclosure
博客园 - 司徒正美
Project Zero
Project Zero
Y
Y Combinator Blog
A
Arctic Wolf
美团技术团队
博客园 - 叶小钗
S
Securelist
F
Fortinet All Blogs
T
Threatpost
T
Threat Research - Cisco Blogs
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
酷 壳 – CoolShell
酷 壳 – CoolShell
MyScale Blog
MyScale Blog
大猫的无限游戏
大猫的无限游戏
Recorded Future
Recorded Future
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
Schneier on Security
Apple Machine Learning Research
Apple Machine Learning Research
L
LangChain Blog
P
Privacy International News Feed
博客园_首页
S
SegmentFault 最新的问题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
腾讯CDC
P
Proofpoint News Feed
Cisco Talos Blog
Cisco Talos Blog
AWS News Blog
AWS News Blog
阮一峰的网络日志
阮一峰的网络日志
G
Google Developers Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
雷峰网
雷峰网
P
Proofpoint News Feed
Latest news
Latest news
G
GRAHAM CLULEY

Kent C. Dodds Blog

Implementing Hybrid Semantic + Lexical Search Simplifying Containers with Cloudflare Sandboxes Migrating to Workspaces and Nx Offloading FFmpeg with Cloudflare Building Semantic Search on my Content Helping YOU ask ME questions with AI How I used Cursor to Migrate Frameworks The Dow's Start on the Covenant Path 2025 in Review The next chapter: EpicAI.pro AI is taking your job How I increased my visibility Launching Epic Web 2023 in Review Stop Being a Junior RSC with Dan Abramov and Joe Savona Live Stream Fixing a Memory Leak in a Production Node.js App 2022 in Review My Car Accident I Migrated from a Postgres Cluster to Distributed SQLite with LiteFS I'm building EpicWeb.dev A review of my time at Remix Remix: The Yang to React's Yin How I help you build better websites Why I Love Remix The State Initializer Pattern How to React ⚛️ Get a catch block error message with TypeScript Building an awesome image loading experience How Remix makes CSS clashes predictable Introducing the new kentcdodds.com How I built a modern website in 2021 How to use React Context effectively Static vs Unit vs Integration vs E2E Testing for Frontend Apps The Testing Trophy and Testing Classifications Array reduce vs chaining vs for loop Don't Solve Problems, Eliminate Them Super Simple Start to Remix Super Simple Start to ESModules in Node.js JavaScript Pass By Value Function Parameters How to write a Constrained Identity Function (CIF) in TypeScript How to optimize your context value How to write a React Component in TypeScript TypeScript Function Syntaxes Listify a JavaScript Array Build vs Buy: Component Libraries edition Using fetch with TypeScript Wrapping React.useState with TypeScript Define function overload types with TypeScript 2020 in Review Business and Engineering alignment Hi, thanks for reaching out to me 👋 useEffect vs useLayoutEffect Super simple start to Firebase functions Super simple start to Netlify functions Super Simple Start to css variables Favor Progress Over Pride in Open Source Testing Implementation Details How getting into Open Source has been awesome for me useState lazy initialization and function updates Use ternaries rather than && in JSX Application State Management with React Use react-error-boundary to handle errors in React JavaScript to Know for React How I structure Express apps What open source project should I contribute to? When I follow TDD AHA Programming 💡 How I Record Educational Videos Should I write a test or fix a bug? Stop mocking fetch Intentional Career Building Improve test error messages of your abstractions Tracing user interactions with React Eliminate an entire category of bugs with a few simple tools Common mistakes with React Testing Library Super Simple Start to React Stop using client-side route redirects The State Reducer Pattern with React Hooks Function forms Replace axios with a simple custom fetch wrapper How to test custom React hooks React Production Performance Monitoring Should I useState or useReducer? Stop using isLoading booleans Make Your Test Fail Make your own DevTools An Argument for Automation Fix the "not wrapped in act(...)" warning Super Simple Start to ESModules in the Browser Implementing a simple state machine library in JavaScript 2010s Decade in Review Why users care about how you write code Why I avoid nesting closures Don't call a React function component Why your team needs TestingJavaScript.com Inversion of Control Understanding React's key prop How to Enable React Concurrent Mode Profile a React App for Performance
React Hooks: What's going to happen to my tests?
2018-12-24 · via Kent C. Dodds Blog

One of the most common questions I hear about the upcoming React Hooks feature is regarding testing. And I can understand the concern when your tests look like this:

// borrowed from a previous blog post:
// https://kcd.im/implementation-details
test('setOpenIndex sets the open index state properly', () => {
	const wrapper = mount(<Accordion items={[]} />)
	expect(wrapper.state('openIndex')).toBe(0)
	wrapper.instance().setOpenIndex(1)
	expect(wrapper.state('openIndex')).toBe(1)
})

That enzyme test works when Accordion is a class component where the instance actually exists, but there's no concept of a component "instance" when your components are function components. So doing things like .instance() or .state() wont work when you refactor your components from class components with state/lifecycles to function components with hooks.

So if you were to refactor the Accordion component to a function component, those tests would break. So what can we do to make sure that our codebase is ready for hooks refactoring without having to either throw away our tests or rewrite them? You can start by avoiding enzyme APIs that reference the component instance like the test above. You can read more about this in my "implementation details" blog post.

Let's look at a simpler example of a class component. My favorite example is a <Counter /> component:

// counter.js
import * as React from 'react'

class Counter extends React.Component {
	state = { count: 0 }
	increment = () => this.setState(({ count }) => ({ count: count + 1 }))
	render() {
		return <button onClick={this.increment}>{this.state.count}</button>
	}
}

export default Counter

Now let's see how we could test it in a way that's ready for refactoring it to use hooks:

// __tests__/counter.js
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'

test('counter increments the count', () => {
	render(<Counter />)
	const button = screen.getByRole('button')
	expect(button).toHaveTextContent('0')
	userEvent.click(button)
	expect(button).toHaveTextContent('1')
})

That test will pass. Now, let's refactor this to a hooks version of the same component:

// counter.js
import * as React from 'react'

function Counter() {
	const [count, setCount] = useState(0)
	const incrementCount = () => setCount((c) => c + 1)
	return <button onClick={incrementCount}>{count}</button>
}

export default Counter

Guess what! Because our tests avoided implementation details, our hooks are passing! How neat is that!? :)

useEffect is not componentDidMount + componentDidUpdate + componentWillUnmount

Another thing to consider is the useEffect hook because it actually is a little unique/special/different/awesome. When you're refactoring from class components to hooks, you'll typically move the logic from componentDidMount, componentDidUpdate, and componentWillUnmountto one or more useEffect callbacks (depending on the number of concerns your component has in those lifecycles). But this is actually not a refactor. Let's get a quick review of what a "refactor" actually is.

When you refactor code, you're making changes to the implementation without making user-observable changes. Here's what wikipedia says about "code refactoring":

Code refactoring is the process of restructuring existing computer code — changing the factoring  without changing its external behavior.

Ok, let's try that idea out with an example:

const sum = (a, b) => a + b

Here's a refactor of this function:

const sum = (a, b) => b + a

It still works exactly the same, but the implementation itself is a little different. Fundamentally that's what a "refactor" is. Ok, now, here's what a refactor is not:

const sum = (...args) => args.reduce((s, n) => s + n, 0)

This is awesome, our sum is more capable, but what we did was not technically a refactor, it was an enhancement. Let's compare:

| call         | result before | result after |
|--------------|---------------|--------------|
| sum()        | NaN           | 0            |
| sum(1)       | NaN           | 1            |
| sum(1, 2)    | 3             | 3            |
| sum(1, 2, 3) | 3             | 6            |

So why was this not a refactor? It's because we are "changing its external behavior." Now, this change is desirable, but it is a change.

So what does all this have to do with useEffect? Let's look at another example of our counter component as a class with a new feature:

class Counter extends React.Component {
	state = {
		count: Number(window.localStorage.getItem('count') || 0),
	}
	increment = () => this.setState(({ count }) => ({ count: count + 1 }))
	componentDidMount() {
		window.localStorage.setItem('count', this.state.count)
	}
	componentDidUpdate(prevProps, prevState) {
		if (prevState.count !== this.state.count) {
			window.localStorage.setItem('count', this.state.count)
		}
	}
	render() {
		return <button onClick={this.increment}>{this.state.count}</button>
	}
}

Ok, so we're saving the value of count in localStorage using componentDidMount and componentDidUpdate. Here's what our implementation-details-free test would look like:

// __tests__/counter.js
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'

import Counter from '../counter'

afterEach(() => {
	window.localStorage.removeItem('count')
})

test('counter increments the count', () => {
	render(<Counter />)
	const button = screen.getByRole('button')
	expect(button).toHaveTextContent('0')
	userEvent.click(button)
	expect(button).toHaveTextContent('1')
})

test('reads and updates localStorage', () => {
	window.localStorage.setItem('count', 3)
	render(<Counter />)
	const button = screen.getByRole('button')
	expect(button).toHaveTextContent('3')
	userEvent.click(button)
	expect(button).toHaveTextContent('4')
	expect(window.localStorage.getItem('count')).toBe('4')
})

That test passes! Woo! Now let's "refactor" this to hooks again with these new features:

import React, { useState, useEffect } from 'react'

function Counter() {
	const [count, setCount] = useState(() =>
		Number(window.localStorage.getItem('count') || 0),
	)
	const incrementCount = () => setCount((c) => c + 1)
	useEffect(() => {
		window.localStorage.setItem('count', count)
	}, [count])
	return <button onClick={incrementCount}>{count}</button>
}

export default Counter

Cool, as far as the user is concerned, this component will work exactly the same as it had before. But it's actually working differently from how it was before. The real trick here is that the useEffect callback is scheduled to run at a later time. So before, we set the value of localStorage synchronously after rendering. Now, it's scheduled to run later after rendering. Why is this? Let's checkout this tip from the React Hooks docs:

Unlike componentDidMount or componentDidUpdate, effects scheduled with useEffect don't block the browser from updating the screen. This makes your app feel more responsive. The majority of effects don't need to happen synchronously. In the uncommon cases where they do (such as measuring the layout), there is a separate useLayoutEffect Hook with an API identical to useEffect.

Ok, so by using useEffect that's better for performance! Awesome! We've made an enhancement to our component and our component code is actually simpler to boot! NEAT!

However, this is not a refactor. It's actually a change in behavior. As far as the end user is concerned, that change is unobservable. In our efforts to ensure that our tests are free of implementation details, that change should be unobservable as well.

Whelp, thanks to the new act utility from react-dom/test-utils we can make that happen. So React Testing Library integrates with that utility and makes it so all our tests continue to pass as written, allowing the tests we write to be free of implementation details and continue to resemble the way our software is used as closely as possible.

What about render props components?

This is probably my favorite actually. Here's a simple counter render prop component:

class Counter extends React.Component {
	state = { count: 0 }
	increment = () => this.setState(({ count }) => ({ count: count + 1 }))
	render() {
		return this.props.children({
			count: this.state.count,
			increment: this.increment,
		})
	}
}
// usage:
// <Counter>
//   {({ count, increment }) => <button onClick={increment}>{count}</button>}
// </Counter>

Here's how I would test this:

// __tests__/counter.js
import * as React from 'react'
import { render } from '@testing-library/react'

import Counter from '../counter'

function renderCounter(props) {
	let utils
	const children = jest.fn((stateAndHelpers) => {
		utils = stateAndHelpers
		return null
	})
	return {
		...render(<Counter {...props}>{children}</Counter>),
		children,
		// this will give us access to increment and count
		...utils,
	}
}

test('counter increments the count', () => {
	const { children, increment } = renderCounter()
	expect(children).toHaveBeenCalledWith(expect.objectContaining({ count: 0 }))
	increment()
	expect(children).toHaveBeenCalledWith(expect.objectContaining({ count: 1 }))
})

Ok, so let's refactor the counter to a component that uses hooks:

function Counter(props) {
	const [count, setCount] = useState(0)
	const increment = () => setCount((currentCount) => currentCount + 1)
	return props.children({
		count: count,
		increment,
	})
}

Cool, and because we wrote our test the way we did, it's actually still passing. Woo! BUT! As we learned from "React Hooks: What's going to happen to render props?" custom hooks are a better primitive for code sharing in React. So let's rewrite this to a custom hook:

function useCounter() {
	const [count, setCount] = useState(0)
	const increment = () => setCount((currentCount) => currentCount + 1)
	return { count, increment }
}

export default useCounter

// usage:
// function Counter() {
//   const {count, increment} = useCounter()
//   return <button onClick={increment}>{count}</button>
// }

Awesome... but how do we test useCounter? And wait! We can't update our entire codebase to the new useCounter! We were using the <Counter /> render prop based component in like three hundred places!? Rewrites are the worst!

Nah, I got you. Do this instead:

function useCounter() {
	const [count, setCount] = useState(0)
	const increment = () => setCount((currentCount) => currentCount + 1)
	return { count, increment }
}

const Counter = ({ children, ...props }) => children(useCounter(props))

export default Counter
export { useCounter }

Our new <Counter /> render-prop based component there is actually exactly the same as the one we had before. So this is a true refactor. But now anyone who can take the time to upgrade can use our useCountercustom hook.

Oh, and guess what. Our tests are still passing!!! WHAT! How neat right?

So when everyone's upgraded we can remove the Counter function component right? You may be able to do that, but I would actually move it to the __tests__ because that's how I like testing custom hooks! I prefer making a render-prop based component out of a custom hook, and actually rendering that and asserting on what the function is called with.

Fun trick right? I show you how to do this in my new course on egghead.io. Enjoy!

What about hooks libraries?

If you're writing a generic or open source hook, then you may want to test it without a specific component in mind. In that case, I recommend using renderHook from @testing-library/react.

Conclusion

One of the best things you can do before you refactor code is have a good test suite/type definitions in place so when you inadvertently break something you can be made aware of the mistake right away. But your test suite can't do you any good if you have to throw it away when you refactor it. Take my advice: avoid implementation details in your tests. Write tests that will work today with classes, and in the future if those classes are refactored to functions with hooks. Good luck!