I didn't realize until recently how much I loved the React Hook useReducer. It's one of those advanced hooks, and while I read the documentation about it and already have a good amount of experience with Redux, it took a little while for me to fully understand just how powerful useReducer
can make your components.
Why do I love useReducer?
The simple answer is that it lets you separate the What from the How. To expand upon that, it may be that What a user wants to do is login
.
With useState
when a user wants to login
I create function that handles a lot of the How. How my component has to behave when a user wants to login
:
- Sets
loading
to true - Clears out old
error
state - Disables the button.
With useReducer
all my component has to do is think about What the user wants. Which is:
dispatch('login')
After that all the How is handled inside the loginReducer
function.
Furthermore, any future How questions become completely centralized inside of that one loginReducer
function. My component can just keep on thinking about the What.
It's a subtle distinction but an extremely powerful one.
To further show the point you can check out the full source code here or see these inline examples.
I'm going to ignore showing the UI, if you want to see that you can check out the repo. For now I just want to focus on the data we're storing and updating.
Using useState
Here I have 5 calls to useState to manage all the distinct state transitions.
In my onSubmit
call I have to careful orchestrate all the state changes that I want.
They're tightly coupled to the onSubmit handler and awkward to extract.
function LoginUseState() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoading, showLoader] = useState(false);
const [error, setError] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
const onSubmit = async e => {
e.preventDefault();
setError('');
showLoader(true);
try {
await login({ username, password });
setIsLoggedIn(true);
} catch (error) {
setError('Incorrect username or password!');
showLoader(false);
setUsername('');
setPassword('');
}
};
return; // remaining UI code here
}
Using useReducer
While it may be overall longer, I would argue that it's much easier to read and track what's going on.
If you jump straight to the onSubmit
function I can now clearly show the intent of the user. There's only 3 behaviors that can happen, 'login', 'success', and 'error'. What that means is not a concern of my component, it's all handled in the loginReducer
.
Even better, it becomes easier for me to make wide-ranging changes to state changes because all the state changes are centrally located.
And even more exciting is that all state changes become easy to share by default.
If I want to show my error state from elsewhere in the component I can easily re-use the same dispatch({ type: 'error' })
and I'm good to go.
function LoginUseReducer() {
const [state, dispatch] = useReducer(loginReducer, initialState);
const { username, password, isLoading, error, isLoggedIn } = state;
const onSubmit = async e => {
e.preventDefault();
dispatch({ type: 'login' });
try {
await login({ username, password });
dispatch({ type: 'success' });
} catch (error) {
dispatch({ type: 'error' });
}
};
return; // UI here
}
function loginReducer(state, action) {
switch (action.type) {
case 'field': {
return {
...state,
[action.fieldName]: action.payload,
};
}
case 'login': {
return {
...state,
error: '',
isLoading: true,
};
}
case 'success': {
return {
...state,
isLoggedIn: true,
isLoading: false,
};
}
case 'error': {
return {
...state,
error: 'Incorrect username or password!',
isLoggedIn: false,
isLoading: false,
username: '',
password: '',
};
}
case 'logOut': {
return {
...state,
isLoggedIn: false,
};
}
default:
return state;
}
}
const initialState = {
username: '',
password: '',
isLoading: false,
error: '',
isLoggedIn: false,
};
Think like the user
useReducer
gets you to write code the way a user will interact with your component.
You are encouraged to think in the What and centralize all How questions inside the reducer.
I'm so excited useReducer
is now built-in to React. It's one more reason why I love it.
If you enjoyed this article you can find more like this on my blog!
And if you like to see my talk about things you can check out my YouTube channel for tutorial videos!
Top comments (1)
That's a benefit I never considered for using
useReducer
. That opened my eyes πThis also reminds me hiding implementation details so whenever the logic for
login/out
changes, the Component doesn't have to be changed, only the hook responsible for managing the state. (Single Responsibility)