loading...
Cover image for Opinionated React: State Management

Opinionated React: State Management

farazamiruddin profile image faraz ahmad Updated on ・4 min read

Intro

I’ve been working with React for over four years. During this time, I’ve formed some opinions on how I think applications should be. This is part 3 in the series of such opinionated pieces.

What I'll be covering

There are a lot of parts to state management. I won't be able to cover them all in one sitting. For this post, I'll show you how I use plain React to manage state in my components.

Make sure to follow me for my future posts related to state management, where I'll write about:

  • Component level state vs global state
  • Good use cases and my pattern for React context
  • Status enums instead of booleans

Just use React

Too often have I seen teams adopt state management libraries like Redux, MobX, or something else before using React‘s built in state management solution.

There's nothing wrong with these libraries, but they are not necessary to build a fully functioning React application. In my experience, it is significantly easier to use plain React.

If you have a reason to use one of these libraries instead of using useState or useReducer, please leave a comment because I would love to know your use case.

Next time you build a component, try using plain React.

Hooks

I mentioned two hooks above, useState and useReducer. Here’s how I use each of them.

Start with useState

I start by building my components with the useState hook. It’s quick and gets the job done.

const MovieList: React.FC = () => {
  const [movies, setMovies] = React.useState<Movie[]>([])

  React.useEffect(() => {
    MovieService
      .fetchInitialMovies()
      .then(initialMovies => setMovies(initialMovies))
  }, [])

  return (
    <ul>
      {movies.map(movie => <li key={movie.id}>{movie.title}</li>}
    </ul>
  )
}

If we need another piece of state, simply add another useState hook

const MovieList: React.FC = () => {
  const [isLoading, setIsLoading] = React.useState<boolean>(true)
  const [movies, setMovies] = React.useState<Movie[]>([])

  React.useEffect(() => {
    MovieService
      .fetchInitialMovies()
      .then(initialMovies => setMovies(initialMovies))
      .then(() => setIsLoading(false))
  }, [])

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

  return (
    <ul>
      {movies.map(movie => <li key={movie.id}>{movie.title}</li>}
    </ul>
  )
}

useReducer when you have a lot of state

My limit for related pieces of state is 2. If I have 3 pieces of state that are related to each other, I opt for useReducer.

Following the above example, let's say we wanted to display an error message if fetching the movies failed.

We could add another useState call, but I think it looks a bit messy 😢.

export const MovieList: React.FC = () => {
  const [isLoading, setIsLoading] = React.useState<boolean>(true);
  const [movies, setMovies] = React.useState<Movie[]>([]);
  const [error, setError] = React.useState<string>("");

  const handleFetchMovies = () => {
    setIsLoading(true); // 😢
    setError(""); // 😢
    return MovieService.fetchInitialMovies()
      .then(initialMovies => {
        setMovies(initialMovies);
        setIsLoading(false); // 😢
      })
      .catch(err => {
        setError(err.message); // 😢
        setIsLoading(false); // 😢
      });
  };

  React.useEffect(() => {
    handleFetchMovies();
  }, []);

  if (isLoading) {
    return <div>Loading movies...</div>;
  }

  if (error !== "") {
    return (
      <div>
        <p className="text-red">{error}</p>
        <button onClick={handleFetchMovies}>Try again</button>
      </div>
    );
  }

  return (
    <ul>
      {movies.map(movie => (
        <li key={movie.id}>{movie.title}</li>
      ))}
    </ul>
  );
};

Let's refactor this to use useReducer, which will simplify our logic.

interface MovieListState {
  isLoading: boolean;
  movies: Movie[];
  error: string;
}

type MoveListAction =
  | { type: "fetching" }
  | { type: "success"; payload: Movie[] }
  | { type: "error"; error: Error };

const initialMovieListState: MovieListState = {
  isLoading: true,
  movies: [],
  error: ""
};

const movieReducer = (state: MovieListState, action: MoveListAction) => {
  switch (action.type) {
    case "fetching": {
      return { ...state, isLoading: true, error: "" };
    }
    case "success": {
      return { ...state, isLoading: false, movies: action.payload };
    }
    case "error": {
      return { ...state, isLoading: false, error: action.error.message };
    }
    default: {
      return state;
    }
  }
};

export const MovieList: React.FC = () => {
  const [{ isLoading, error, movies }, dispatch] = React.useReducer(
    movieReducer,
    initialMovieListState
  );

  const handleFetchMovies = () => {
    dispatch({ type: "fetching" });
    return MovieService.fetchInitialMovies()
      .then(initialMovies => {
        dispatch({ type: "success", payload: initialMovies });
      })
      .catch(error => {
        dispatch({ type: "error", error });
      });
  };

  React.useEffect(() => {
    handleFetchMovies();
  }, []);

  if (isLoading) {
    return <div>Loading movies...</div>;
  }

  if (error !== "") {
    return (
      <div>
        <p className="text-red">{error}</p>
        <button onClick={handleFetchMovies}>Try again</button>
      </div>
    );
  }

  return (
    <ul>
      {movies.map(movie => (
        <li key={movie.id}>{movie.title}</li>
      ))}
    </ul>
  );
};

Q&A

Every post I will answer a question I received on twitter. Here's this week's question.

I don't use redux anymore. I haven't used it since React's context api was released. IMO, I think hooks + context are enough to build your application.

Wrapping Up

This is the 3rd installment in a series of pieces I will be writing. If you enjoyed this, please comment below. What else would you like me to cover? As always, I’m open to feedback and recommendations.

Thanks for reading.

P.S. If you haven’t already, be sure to check out my previous posts in this series:

  1. An Opinionated Guide to React: Folder Structure and File Naming
  2. An Opinionated Guide to React: Component File Structure

Posted on by:

farazamiruddin profile

faraz ahmad

@farazamiruddin

I'm a software engineer with a lot of React and startup experience. I write about my opinions on React, using Firebase with React, and lessons from building my 1st startup, Retro

Discussion

markdown guide
 

Hey Faraz,
great Impressions for a React Newbi :) Thanks for your six parts, hope more comming soon. The Enumeration of Status is a great idea. I've used 3 booleans for checking state of loading and showing a message :)

But i have a question about how you query your MovieService. This looks pretty awesome how you call your functions:
MovieService
.fetchInitialMovies()
.then(initialMovies => setMovies(initialMovies))
.then(() => setIsLoading(false))

Can you show me how your MovieService works? Haven't found any code of your MovieService :(

Thanks :)

 

Hey! Sure. I made a mock service just for learning purposes. I have some hard-coded values that I return using a setTimeout and a Promise.

Here it is:

import { Movie } from "../types/movie";

const MOVIES: Movie[] = [
  { id: 1, title: "Iron Man" },
  { id: 2, title: "The Incredible Hulk" },
  { id: 3, title: "Iron Man 2" },
  { id: 4, title: "Thor" },
  { id: 5, title: "Captain America: The First Avenger" },
  { id: 6, title: "The Avengers" }
];

export class MovieService {
  static fetchMovieTitles(): Promise<Movie[]> {
    return new Promise(resolve => {
      return setTimeout(() => resolve(MOVIES), 2000);
    });
  }
}

What this does is returns a list of movies after 2000ms.

 

What would this component look like if you were using graphql to fetch the movies like in your component file structure article? Would you still use the useQuery hook and put it in this component? Would you no longer use the hook and put the query in the MovieService?

 
 

Thanks for the response :) I enjoyed that article too. My question was if you would merge the two approaches here if you were using both Apollo and some sort of state management. In this article the data fetching was abstracted into a separate service. What's your thought process on deciding if the data fetching should be within the component or within a separate service?

Personally, I like to keep non-graphql data fetching in a separate file. This keeps my view logic & data logic isolated.

However, for graphql, the way I use it at least, the shape of the graphql query is tied to the component that is using it. so that is why I keep my graphql queries co-located in my react component.

hopefully that makes sense lol.