DEV Community

Cover image for Why I Love useReducer
Harry Wolff
Harry Wolff

Posted on

Why I Love useReducer

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)

Collapse
 
dance2die profile image
Sung M. Kim • Edited

Furthermore, any future How questions become completely centralized inside of that one loginReducer function. My component can just keep on thinking about the What.

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)