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

推荐订阅源

Spread Privacy
Spread Privacy
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Forbes - Security
Forbes - Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
Attack and Defense Labs
Attack and Defense Labs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
WordPress大学
WordPress大学
Microsoft Security Blog
Microsoft Security Blog
P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
V
Visual Studio Blog
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
S
SegmentFault 最新的问题
Cloudbric
Cloudbric
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
N
Netflix TechBlog - Medium
MyScale Blog
MyScale Blog
T
The Blog of Author Tim Ferriss
H
Hacker News: Front Page
C
Cybersecurity and Infrastructure Security Agency CISA
小众软件
小众软件
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
AWS News Blog
AWS News Blog
AI
AI
G
GRAHAM CLULEY
IT之家
IT之家
P
Privacy & Cybersecurity Law Blog
L
Lohrmann on Cybersecurity
Last Week in AI
Last Week in AI
D
Docker
Recent Announcements
Recent Announcements
O
OpenAI News
T
Threat Research - Cisco Blogs
GbyAI
GbyAI
S
Security @ Cisco Blogs
T
Troy Hunt's Blog
C
Check Point Blog
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
N
News and Events Feed by Topic

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? 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 How to add testing to an existing project Profile a React App for Performance
Stop using isLoading booleans
2020-03-02 · via Kent C. Dodds Blog

Watch "Use a status enum instead of booleans" on egghead.io

Watch "Handle HTTP Errors with React" on egghead.io (part of The Beginner's Guide to ReactJS)

Let's play around with the Geolocation API a bit and learn about the perils of isLoading booleans (and similar booleans like: isRejected, isIdle, or isResolved) while we're at it. I'll be using React to demonstrate this, but the concepts apply generally to any framework or language.

function geoPositionReducer(state, action) {
	switch (action.type) {
		case 'error': {
			return {
				...state,
				isLoading: false,
				error: action.error,
			}
		}
		case 'success': {
			return {
				...state,
				isLoading: false,
				position: action.position,
			}
		}
		default: {
			throw new Error(`Unhandled action type: ${action.type}`)
		}
	}
}

function useGeoPosition() {
	const [state, dispatch] = React.useReducer(geoPositionReducer, {
		isLoading: true,
		position: null,
		error: null,
	})

	React.useEffect(() => {
		if (!navigator.geolocation) {
			dispatch({
				type: 'error',
				error: new Error('Geolocation is not supported'),
			})
			return
		}
		const geoWatch = navigator.geolocation.watchPosition(
			(position) => dispatch({ type: 'success', position }),
			(error) => dispatch({ type: 'error', error }),
		)
		return () => navigator.geolocation.clearWatch(geoWatch)
	}, [])

	return state
}

This is pretty standard stuff for code like this. I've seen this in both applications and open source libraries all over (React or not). However, there are some problems with this approach. Can you think of what those might be? Let me show you how a developer might use this particular hook:

function YourPosition() {
	const { isLoading, position, error } = useGeoPosition()

	if (isLoading) {
		return <div>Loading your position...</div>
	}

	if (position) {
		return (
			<div>
				Lat: {position.coords.latitude}, Long: {position.coords.longitude}
			</div>
		)
	}

	if (error) {
		return (
			<div>
				<div>Oh no, there was a problem getting your position:</div>
				<pre>{error.message}</pre>
			</div>
		)
	}
}

Can you identify the problem yet? Yes? Awesome! No? No problem, let me help with a little real-world scenario.

Imagine what would happen if the user were to hop into a car, started driving around, and the device failed with a GeolocationPositionError.POSITION_UNAVAILABLE or GeolocationPositionError.TIMEOUT or even if the user decided to disable the geoposition permission for your app and you got a GeolocationPositionError.PERMISSION_DENIED. What would happen then? With the way the component above is written, we'd always show the last recorded position and never show the user the error message!

If we swapped things around and only show the position if there's no error, then we'd have the opposite problem: we'd only show an error, even if subsequent requests for the position succeeded.

There are a few ways we could change this to avoid that problem:

  1. Ensure users of the hook always show the position AND error if they're available.
  2. Clear the error when getting the position is successful, and clear the position if there's an error.
  3. Return an additional property indicating the current status of the geoposition information.

As a creator of something reusable, it's always easier to make it so people can't do the wrong thing even if they want to. Or at least it's more natural to do the right thing than the wrong thing (pit of success and all that). So option #1 is out.

For option #2, there may be some people who want to show the most recent position even if there was an error (and display the error message as well in that case), so that won't work.

So let's go with option #3:

function geoPositionReducer(state, action) {
	switch (action.type) {
		case 'error': {
			return {
				...state,
				status: 'rejected',
				error: action.error,
			}
		}
		case 'success': {
			return {
				...state,
				status: 'resolved',
				position: action.position,
			}
		}
		case 'started': {
			return {
				...state,
				status: 'pending',
			}
		}
		default: {
			throw new Error(`Unhandled action type: ${action.type}`)
		}
	}
}

function useGeoPosition() {
	const [state, dispatch] = React.useReducer(geoPositionReducer, {
		status: 'idle',
		position: null,
		error: null,
	})

	React.useEffect(() => {
		if (!navigator.geolocation) {
			dispatch({
				type: 'error',
				error: new Error('Geolocation is not supported'),
			})
			return
		}
		dispatch({ type: 'started' })
		const geoWatch = navigator.geolocation.watchPosition(
			(position) => dispatch({ type: 'success', position }),
			(error) => dispatch({ type: 'error', error }),
		)
		return () => navigator.geolocation.clearWatch(geoWatch)
	}, [])

	return state
}

You'll notice that we added another dispatch so we could differentiate from idle and pending states. In this particular case, there's not really a difference between the two (before we just started with isLoading as true which is effectively what this means), but there are cases where distinguishing between these two finite states is important and I worry that if I don't include them someone's going to copy/paste this and miss that important nuance.

Awesome, now users can use the status to render instead:

function YourPosition() {
	const { status, position, error } = useGeoPosition()

	if (status === 'idle' || status === 'pending') {
		return <div>Loading your position...</div>
	}

	if (status === 'resolved') {
		return (
			<div>
				Lat: {position.coords.latitude}, Long: {position.coords.longitude}
			</div>
		)
	}

	if (status === 'rejected') {
		return (
			<div>
				<div>Oh no, there was a problem getting your position:</div>
				<pre>{error.message}</pre>
			</div>
		)
	}

	// could also use a switch or nested ternary if that's your jam...
}

Play with this on codesandbox here

By using a status variable rather than a simple isLoading indicator we enable our users to know exactly what the state is at any given point in time.

Now, if you'd like to ditch those variable === 'string' expressions in those if statements because you like how terse a isBoolean is, have no fear, that's simple enough:

const { status, position, error } = useGeoPosition()
const isLoading = status === 'idle' || status === 'pending'
const isResolved = status === 'resolved'
const isRejected = status === 'rejected'

And one might argue that you could store those variables in your reducer's state rather than derive their values. But that leaves you vulnerable to impossible states! But, if you really want your users to not have to use the variable === 'string', then just make sure you stick with the status in your state to ensure you only have one possible finite state value and then you can derive boolean states as a convenience if you want to:

function useGeoPosition() {
	// ... clipped out for brevity ...
	return {
		isLoading: status === 'idle' || status === 'pending',
		isIdle: status === 'idle',
		isPending: status === 'pending',
		isResolved: status === 'resolved',
		isRejected: status === 'rejected',
		...state,
	}
}

State machines

Everything is a state machine, and this one is particularly noticeable. XState is an awesome library for representing state machines in code, so I thought I'd show you what that could be like:

import { createMachine, assign } from 'xstate'
import { useMachine } from '@xstate/react'

const context = { position: null, error: null }
const RESOLVE = {
	target: 'resolved',
	actions: 'setPosition',
}
const REJECT = {
	target: 'rejected',
	actions: 'setError',
}
const geoPositionMachine = createMachine(
	{
		id: 'geoposition',
		initial: 'idle',
		context,
		states: {
			idle: {
				on: {
					START: 'pending',
					REJECT_NOT_SUPPORTED: 'rejectedNotSupported',
				},
			},
			pending: {
				on: { RESOLVE, REJECT },
			},
			resolved: {
				on: { RESOLVE, REJECT },
			},
			rejected: {
				on: { RESOLVE, REJECT },
			},
			rejectedNotSupported: {},
		},
	},
	{
		actions: {
			setPosition: assign({
				position: (context, event) => event.position,
			}),
			setError: assign({
				error: (context, event) => event.error,
			}),
		},
	},
)

function useGeoPosition() {
	const [state, send] = useMachine(geoPositionMachine)

	React.useEffect(() => {
		if (!navigator.geolocation) {
			send('REJECT_NOT_SUPPORTED')
			return
		}
		send('START')
		const geoWatch = navigator.geolocation.watchPosition(
			(position) => send({ type: 'RESOLVE', position }),
			(error) => send({ type: 'REJECT', error }),
		)
		return () => navigator.geolocation.clearWatch(geoWatch)
	}, [send])

	return state
}

Play with this on codesandbox here

Watch me build this in my livestream

Firstly, if you're unfamiliar with state machines or xstate, be careful of familiarity bias against this code. It may not be straightforward to you today, but just like every abstraction, it becomes more straightforward the more time you spend with it.

You'll notice several things about this example. I added a special state for when geolocation is not supported. This state doesn't have any "event handlers" attached to it and is therefore called a "terminal state." That makes sense for our use case because if it's not supported then it's impossible to transition to any other state and our state machine ensures that will never happen.

But more than this, I want you to notice that there is no longer a status state we have to maintain. The status state is now just part of the machine's finite state value. So the status is built into the machine, rather than managed separately.

Here's how we would use that (with as minimal changes as possible):

function YourPosition() {
	const state = useGeoPosition()
	const { position, error } = state.context

	if (state.matches('rejectedNotSupported')) {
		return <div>This browser does not support Geolocation</div>
	}

	if (state.matches('idle') || state.matches('pending')) {
		return <div>Loading your position...</div>
	}

	if (state.matches('resolved')) {
		return (
			<div>
				Lat: {position.coords.latitude}, Long: {position.coords.longitude}
			</div>
		)
	}

	if (state.matches('rejected')) {
		return (
			<div>
				<div>Oh no, there was a problem getting your position:</div>
				<pre>{error.message}</pre>
			</div>
		)
	}
}

And if we liked the previous API, we could preserve that API:

function useGeoPosition() {
	const [state, send] = useMachine(geoPositionMachine)
	// ... clipped out for brevity ...
	return { status: state.value, ...state.context }
}

You may personally find the reducer example simpler or more understandable than the state machine. If you're unfamiliar with state machines or xstate then that's understandable. I'm not going to get into the merits of state machines in this post, but I have written about them a bit before: Implementing a simple state machine library in JavaScript.

Oh, and take a look at this implementation from David Khourshid (author of xstate) in which he completely removes the need for useEffect and instead puts the subscription logic within the machine itself!!! This is cool because it means that our machine is totally framework agnostic and can work regardless of which UI framework you're using (or even if you've built your own). He even built in a timeout (just for fun). Very cool stuff here.

Conclusion

At the end of all of this, I hope that you appreciate the value of defining the finite states that you have in your code and avoid bugs by doing so. This isn't a pitch for finite state machines as much as it is a plea for reducing our reliance on booleans that cannot represent all of the actual states our code can be in at any given time. Use a state machine or an enum. Not a boolean. Thank you, and good luck!