Building an NBA Player Profile App with React, Redux-Saga, and Styled Components

As a big basketball fan and web developer, I‘m always looking for fun projects to build that combine my two passions. With the NBA playoffs in full swing, I decided to create a little app that fetches profile information and statistics for any current NBA player. The app allows you to search for a player by name and displays their photo, team, height, weight and stat line for the current season.

To build this app, I used React for the UI components, Redux and Redux-Saga to manage the state and async API calls, and styled-components to add CSS styling directly in the component files. In this post, I‘ll walk through how I used these tools together to quickly build a functional and good-looking app.

If you want to check out the complete code, you can find it in this GitHub repo. Let‘s dive in!

Why Redux-Saga?

Redux is a popular library for managing state in React applications, using the concepts of actions and reducer functions to update data via a single global store. However, reducer functions in Redux must be "pure," meaning they should only take in the current state and action as parameters and return the next state. They shouldn‘t have any side effects like fetching data from an API.

This is where Redux-Saga comes in. Redux-Saga is a middleware library that allows you to define generator functions that can listen for certain actions, make asynchronous calls like API requests, and dispatch new actions with the results.

Here‘s a quick example of what a saga generator function looks like:

function* fetchPlayerProfile(action) {
  try {
    const { name } = action.payload;
    const response = yield axios.get(`https://free-nba.p.rapidapi.com/players?search=${name}`);
    const player = response.data.data[0];

    yield put({ type: ‘GET_PLAYER_SUCCESS‘, payload: player });
  } catch (error) {
    yield put({ type: ‘GET_PLAYER_FAILURE‘, error });
  }
}

This function listens for an action with the type ‘GET_PLAYER‘, makes an API call to fetch a player profile based on the name parameter in the action payload, then dispatches either a success action with the player data or a failure action with the error.

By moving async logic into sagas, your reducers can remain pure and you get better separation of concerns between synchronous state updates and side effects. You can also easily test sagas in isolation.

Why Styled Components?

I‘m a big fan of the CSS-in-JS paradigm, where the CSS styles for a component are defined in the same file as the component itself rather than in a separate stylesheet. This fits perfectly with the modular, composable nature of React components.

Styled Components is one of the leading libraries for CSS-in-JS. It allows you to define your styles as actual React components that can accept props and be easily composed and extended.

For example, here‘s how you might style a simple button component with Styled Components:

import styled from ‘styled-components‘;

const Button = styled.button`
  background-color: blue;
  color: white;
  font-size: 16px;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;

  &:hover {
    background-color: darkblue;
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
`;

The styled.button function creates a new button component with the specified CSS rules. The ampersand (&) is used to refer to the component itself for pseudo-classes like :hover and :disabled.

You can then use this component like any other React component:

<Button disabled={isLoading}>
  {isLoading ? ‘Loading...‘ : ‘Search‘}
</Button>

I find this syntax much cleaner and more intuitive than defining class names and matching them up to selectors in a separate CSS file. You can also easily pass props to a styled component to dynamically change its styling.

Now that we‘ve covered the "why" behind Redux-Saga and Styled Components, let‘s get into the "how" of using them to build our NBA player profile app.

Setting Up the Project

I used create-react-app to quickly bootstrap a new React project. After creating the project, I installed the necessary dependencies:

npm install redux react-redux redux-saga styled-components axios

Next, I set up the basic Redux store, reducers, and actions following the standard conventions. I won‘t go into all the details here, but you can check out the full code in the repo.

The key pieces are:

  • The root reducer that combines the player reducer
  • The player reducer that handles updating the player state
  • The player actions for getting a player profile and handling success/failure
  • The Redux store created with the root reducer and Redux-Saga middleware

With the Redux infrastructure in place, I moved on to the sagas for handling async requests.

Building Redux-Sagas for Async Calls

I created two sagas for this app:

  1. A root saga that watches for the GET_PLAYER action and calls the fetch player saga
  2. A fetch player saga that makes the API call and dispatches success/failure actions

Here‘s what the root saga looks like:

import { takeLatest } from ‘redux-saga/effects‘;
import { getPlayerSaga } from ‘./player‘;

export default function* rootSaga() {
  yield takeLatest(‘GET_PLAYER‘, getPlayerSaga);
}

The takeLatest function listens for the latest GET_PLAYER action and cancels any previous in-flight requests. This prevents any race conditions if multiple searches are fired off quickly.

And here‘s the fetch player saga:

import axios from ‘axios‘;
import { put } from ‘redux-saga/effects‘;

const BASE_URL = ‘https://free-nba.p.rapidapi.com‘;
const API_KEY = ‘your-api-key‘;

export function* getPlayerSaga(action) {
  try {
    const { name } = action.payload;
    const response = yield axios.get(`${BASE_URL}/players?search=${name}`, {
      headers: {
        ‘X-RapidAPI-Host‘: ‘free-nba.p.rapidapi.com‘,
        ‘X-RapidAPI-Key‘: API_KEY,
      },
    });
    const player = response.data.data[0];

    const photoUrl = `https://nba-players.herokuapp.com/players/${player.last_name}/${player.first_name}`;

    yield put({
      type: ‘GET_PLAYER_SUCCESS‘, 
      payload: { ...player, photoUrl },
    });
  } catch (error) {
    yield put({ type: ‘GET_PLAYER_FAILURE‘, error });
  }
}

This saga does the following:

  1. Extracts the name parameter from the action payload
  2. Makes a GET request to the Free NBA API, passing the name as a search parameter
  3. Pulls the first matching player from the response data
  4. Constructs a URL for the player‘s photo
  5. Dispatches a GET_PLAYER_SUCCESS action with the player object and photo URL
  6. If any errors occur, dispatches a GET_PLAYER_FAILURE action with the error

With the heavy lifting done by the sagas, the reducers can simply handle updating the state when they receive the success or failure actions. For example:

const initialState = {
  player: null,
  loading: false,
  error: null,
};

export default function playerReducer(state = initialState, action) {
  switch (action.type) {
    case ‘GET_PLAYER‘:
      return { ...state, loading: true, error: null };
    case ‘GET_PLAYER_SUCCESS‘:
      return { player: action.payload, loading: false, error: null };
    case ‘GET_PLAYER_FAILURE‘:
      return { player: null, loading: false, error: action.error };
    default:
      return state;
  }
}

Now our Redux layer is fully set up to handle searching for and fetching player profiles. Time to build the React components to display this data!

Creating the React Components

I broke down my UI into four main components:

  1. Search: A form with an input and submit button to search for a player by name
  2. PlayerPhoto: An image component to display the player‘s photo
  3. StatBox: A box that displays a single stat such as points per game or rebounds per game
  4. Player: The main component that fetches a player profile and renders their photo and stats

I used styled-components to add the CSS styling for each component right in the same file. Here‘s a simplified version of the Player component:

import { useDispatch, useSelector } from ‘react-redux‘;
import styled from ‘styled-components‘;
import { getPlayer } from ‘../actions/player‘;
import PlayerPhoto from ‘./PlayerPhoto‘;
import StatBox from ‘./StatBox‘;

const PlayerContainer = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: 16px;
`;

const PlayerInfo = styled.div`
  margin-top: 8px;
`;

const PlayerName = styled.h2`
  font-size: 24px;
  margin-bottom: 4px;
`;

const PlayerTeam = styled.p`
  font-size: 18px;
  color: gray;
`;

const StatContainer = styled.div`
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
  margin-top: 16px;
`;

export default function Player() {
  const dispatch = useDispatch();
  const { player, loading, error } = useSelector((state) => state.player);

  const searchPlayer = (name) => {
    dispatch(getPlayer(name));
  };

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!player) return <Search onSubmit={searchPlayer} />;

  return (
    <PlayerContainer>
      <PlayerPhoto url={player.photoUrl} />
      <PlayerInfo>
        <PlayerName>{player.first_name} {player.last_name}</PlayerName>
        <PlayerTeam>{player.team.full_name}</PlayerTeam>
      </PlayerInfo>
      <StatContainer>
        <StatBox name="Points" value={player.points_per_game} />
        <StatBox name="Rebounds" value={player.rebounds_per_game} />
        <StatBox name="Assists" value={player.assists_per_game} />
      </StatContainer>
    </PlayerContainer>
  );
}

The Player component is connected to the Redux store using the useSelector and useDispatch hooks. It displays a loading message while fetching, an error message if the request fails, or a Search component if no player has been searched yet.

Once a player has been successfully fetched, it displays their photo, name, team, and stats using the PlayerPhoto and StatBox components.

Styled Components made it easy to style each element of the UI with simple, scoped CSS rules. No more worrying about global class names or specificity issues!

The other components follow a similar pattern – they‘re functional React components that use Styled Components for CSS. You can check out the full implementation in the code repo.

Putting It All Together

With all the pieces in place, the final step was to render the Player component in App.js:

import React from ‘react‘;
import { Provider } from ‘react-redux‘;
import store from ‘./store‘;
import Player from ‘./components/Player‘;

function App() {
  return (
    <Provider store={store}>
      <Player />
    </Provider>
  );
}

export default App;

Wrapping the app in a Redux Provider component allows the Player and its child components to access the Redux store.

Here‘s the complete flow of what happens when a user searches for a player:

  1. The user submits a player name in the Search component
  2. The Search component dispatches a GET_PLAYER action with the name payload
  3. The root saga in Redux-Saga sees the GET_PLAYER action and calls the fetch player saga
  4. The fetch player saga makes an async API call to the NBA Stats API with the player name and dispatches a GET_PLAYER_SUCCESS or GET_PLAYER_FAILURE action
  5. The player reducer handles the success action by updating its state with the fetched player object
  6. The Player component receives the updated player data from the Redux store and re-renders the UI with the player photo and stats

And that‘s it! The app is fully functional and fetches real data from the NBA API.

Next Steps

There are plenty of ways to enhance this app:

  • Add more stats and information to the player profile
  • Allow searching for players by team or position
  • Implement a typeahead search bar to autosuggest player names
  • Improve the visual design and layout
  • Add loading skeletons or spinners
  • Handle errors more gracefully
  • Add unit tests for the components, reducers, and sagas

Hopefully this post gives you a good sense of how Redux-Saga and Styled Components can help manage side effects and CSS in a React app. I found them to be a powerful combo for building UIs that are functionally robust and visually sleek.

If you have any questions or feedback, feel free to reach out to me on Twitter or GitHub. Happy coding!

Similar Posts