Go to content

Restore faith in Redux thanks to React hooks

Photo by Daniel Jiménez on Unsplash

Redux is a formidable library that applies the Flux architecture principles to manage a centralized data store for your app. Since Dan Abramov presented it at ReactEurope 2015, it quickly becomes one of the main libraries of React ecosystem, so that we could have wonder if it was conceivable to write a React app without Redux.

Last June, 85% of the React apps and websites we maintain at Troopers were using Redux.

Redux problems

Lots of React developers have installed Redux into their React app because they think it was the standard way of doing it. The global state, which was simple at the beginning of the product, became a complete spaghetti bowl when the app grows up.

Young kid eating spaghetti with his hand

Reducers, action-creators, selectors get usually grouped by domain, but they're not split into several sub-stores when it could be required. From time to time, those three types of utils get defined into an only file that grows fastly to hundreds of lines and becomes complex to analyze.

We also noted that operations on the state (ie: .map or .filter) get not memoized. It leads to over rendering React components on each state change, even though they aren't concerned by the state that has changed.

A misused Redux can become an anchor to your app: less and less effective and more and more complex to debug. Back in November 2016, Dan Abramov already warned that Redux was not a prerequisite for every React project, in his article called You might not need Redux

The Context API and Hooks to the rescue

Since they came out (respectively in March 2018 and February 2019), Context API and Hooks helped to reconsider how we develop React apps. Those features come natively with React and they are a solution to share a complex state all over an application. This article is not about that pattern, but if you're interested in digging into it, Kent C Dodds wrote a complete article the present the method.

Should I re-write the way my state is managed?

tl,dr: NO!

If your app has been developed with Redux and you're satisfied with it, don't lose time and money to re-develop something that's working. It would have no impact for your end-user, and it would be much like throwing money out of the window.

However, you can apply the boy scout rule to create custom hooks based on those offered by react-redux to gradually improve your codebase (#ContinuousImprovement, #Agile, #Kaizen).

Russel, the boy-scout from the movie Up!

How to use react-redux Hooks ?

useSelector

useSelector requires a selector function, which would receive as the first parameter the global state of the app just like mapStateToProps,. It can then return any value extracted from the state.

import { useSelector } from 'react-redux'

const Avatar: React.FC<AvatarProps> = ({ userId }) => {
  const avatar : useSelector((state) => {
    const currentUser = state.users.find(user => user.id === userId)
    return currentUser?.avatar
  })

  return <img src={avatar} alt="" />
}

If the result from useSelector differs from the previous render, React will trigger a sub-components re-render. However, the basic comparison function uses strict equality (ie: ===), which can lead to unnecessary re-render if the selector returns a new object or array for every run. React-redux offers two solutions to solve this problem:

  • use reselect to create memoized selectors
  • add a 2nd parameter to useSelector to define a custom comparison function (ex: shallow-compare from react-redux, _.isEqual…)
import { useSelector, shallowEqual } from 'react-redux'

const UserFollorwers: React.FC<UserFollorwersProps> = ({ userId }) => {
	const followrers = useSelector(state => {
		const currentUser = state.users.find(user => user.id === userId)
		return state.users.map(user => currentUser.followerIds.includes(user.id))
	}, shallowEqual)

	return <UserList users={followers}/>
}

useDispatch

useDispatch returns a reference to dispatch method to trigger an event in the event bus.

import { useDispatch, useSelector, shallowEqual } from 'react-redux'

const UserFollorwers: React.FC<UserFollorwersProps> = ({ userId }) => {

	const dispatch = useDispatch()
	useEffect(() => {
		dispatch({
			type: 'FETCH_USERS_REQUEST'
		})
	}, [])

	/*
		...
	*/
}

Create custom hooks for mode reusability

Combining useSelector and useDispatch into a custom hook is a solution to simplify components. The code will be easier to read and more digest to the maintainers.

// useUser.ts
import { useSelector, shallowEqual, useDispatch } from 'react-redux'

const useUser: User | undefined  = (userId: number) => {
	const dispatch = useDispatch()
	
	useEffect(({
		type: 'FETCH_USER_REQUEST',
		payload: {
			userId
		}
	}), [userId])

	const user = useSelector((state) => {
		return state.users.find(user => user.id === userId)
	}, shallowEqual)

	return user
}

// UserProfile.tsx
const UserProfile: React.FC<UserProfileProps> => ({ userId }) => {
	const user = useUser(userId)
	return <div>
		<span>{user.firstname} {user.lastname}</span>
		<button onClick={logout}>Logout</button>
	</div>
}

// ClientInfo.tsx
const ClientInfo: ReactFC<ClientInfoProps> = ({ clientId })  => {
	const client = useUser(userId)
	return {!!client && <div>
			<div>{client.firstname}</div>
			<div>{client.lastname}</div>
			<div>{client.email}</div>
			<div>{client.address}</div>
			<div>{client.phone}</div>
		</div>}
}

In the example above, we will search for a user into the global state and return it if we find it. Simultaneously, we'll perform an API call to fetch the user. If it changes, the sub-components will get re-renderd and updated with fresher data.

In the end

I prefer to avoid adding Redux in the new React projects I bootstrap. Beside, since I decided to question this lib's appropriateness with my team, we didn't add them into our new apps.

However, we still need to maintain projects with Redux. Thanks to those kind of custom hooks, we can modernize our code-bases, optimize the performance and improve readability.

Terms and conditions - © Johan Soulet

Made with ❤️ in Nantes