Source: features/pokemons/pokemonsSlice.jsx

import { 
  createSlice, 
  createAsyncThunk, 
  createSelector, 
  createEntityAdapter 
} from '@reduxjs/toolkit';
import { fetchPokemons } from '../api/api'
import {
  filterByQuery,
  filterByType,
  filterByAbility,
  sortPokemons
} from '../../common/utils/helper-functions/filters'

/**
 * Entity adapter
 * @const pokemonsAdapter
 * 
 */
const pokemonsAdapter = createEntityAdapter({
  selectId: pokemon => pokemon.id,
});

/**
 * Initial state
 * @const initialState
 */
const initialState = pokemonsAdapter.getInitialState({
  status: 'idle',
  error: null,
  searchQuery: '',
  filteredPokemons: [],
  typeFilter: '',
  abilityFilter: '',
  sortBy: '',
});

/**
 * Info: Pluralization of Pokemon in the code is Pokemons to simplify naming.
 */

/**
 * Fetches the Pokemon with a thunk function
 * @func fetchPokemonsThunk 
 */
export const fetchPokemonsThunk = createAsyncThunk(
  'pokemons/fetchPokemons',
  fetchPokemons
)

/**
 * Slice containing the reducers and actions.
 * Generates action creators and action types.
 * @function pokemonSlice
 */
const pokemonsSlice = createSlice({
  name: 'pokemons',
  initialState,
  reducers: {
    /**
     * Reducer: Toggles the save state for pokemons (myPokemon)
     * @func toggleSavePokemon
     * @param {Object}
     */
    toggleSavePokemon: pokemonsAdapter.updateOne,
    /**
     * Reducer: Updates the search query and filters the Pokemon based on the query.
     * @func updateSearchQuery
     * @param {Object} state - Current state.
     * @param {Object} action - Action containing the search query.
     */
    updateSearchQuery: (state, action) => {
      state.searchQuery = action.payload;
      state.filteredPokemons = filterPokemons(state);
    },
    /**
     * Reducer: Updates the type filter and filters Pokemon based on chosen type.
     * @func updateTypeFilter
     * @param {Object} state - Current state.
     * @param {Object} action - Action containing the Pokemon type, e.g. "Grass".
     */
    updateTypeFilter: (state, action) => {
      const typeFilter = action.payload;
      state.typeFilter = typeFilter;
      state.filteredPokemons = filterPokemons(state);
    },
    /**
     * Reducer: Updates the ability filter and filters Pokemon by chosen ability.
     * @func updateAbilityFilter
     * @param {Object} state - Current state.
     * @param {Object} action - Action containing the ability to filter by, e.g. "Run-Away"
     */
    updateAbilityFilter: (state, action) => {
      const abilityFilter = action.payload;
      state.abilityFilter = abilityFilter;
      state.filteredPokemons = filterPokemons(state);
    },
    /**
     * Reducer: Updates the name of a saved pokemon.
     * @func updatePokemonName
     * @param {Object}
     */
    updatePokemonName: pokemonsAdapter.updateOne,
    /**
     * Reducer: Updates the sort by in state and sorts either alphabetically or by HP.
     * @func updateSortBy
     * @param {Object} state - Current state.
     * @param {Object} action - Action containing either "name" or "hp".
     */
    updateSortBy: (state, action) => {
      const sortBy = action.payload;
      state.sortBy = sortBy;
      state.filteredPokemons = filterPokemons(state);
    },
  },
  /**
   * Handles the async actions for fetching Pokemon. 
   */
  extraReducers: builder => {
    builder
      .addCase(fetchPokemonsThunk.pending, state => {
        state.status = 'loading';
      })
      .addCase(fetchPokemonsThunk.fulfilled, (state, action) => {
        state.status = 'succeeded';
        pokemonsAdapter.upsertMany(state, action.payload);
      })
      .addCase(fetchPokemonsThunk.rejected, (state, action) => {
        state.status = 'failed';
        state.error = `
          The fetching of Pokémon failed with the following 
          error message: "${action.error.message}"
        `; 
      });
  },
});

/**
 * Selector: Filters the Pokemon based on search query and filters.
 * @const filterPokemons
 */
const filterPokemons = createSelector(
  state => state.searchQuery,
  state => state.typeFilter,
  state => state.abilityFilter,
  state => state.sortBy,
  pokemonsAdapter.getSelectors().selectAll,
  (searchQuery, typeFilter, abilityFilter, sortBy, allPokemons) => {
    const filteredPokemons = filterByQuery(searchQuery, allPokemons)
      .filter(pokemon => filterByType(typeFilter, pokemon))
      .filter(pokemon => filterByAbility(abilityFilter, pokemon))
      .filter(pokemon => !pokemon.myPokemon);

    return sortPokemons(sortBy, filteredPokemons);
  }
);

/**
 * Actions exported from the slice.
 */
export const { 
  toggleSavePokemon,
  updateSearchQuery,
  updateTypeFilter,
  updateAbilityFilter,
  updatePokemonName,
  updateSortBy,
} = pokemonsSlice.actions;

export default pokemonsSlice.reducer;

/**
 * Creates a selector object for working with the entity data, e.g. selectEntities.
 * @const pokemonsSelectors
 */
const pokemonsSelectors = pokemonsAdapter.getSelectors(
  state => state.pokemons
);

/**
 * Selector: Selects all Pokemon that are not saved by the user.
 * @func selectAllPokemons
 */
export const selectAllPokemons = createSelector(
  pokemonsSelectors.selectAll,
  pokemons => pokemons.filter(pokemon => !pokemon.myPokemon)
);

/**
 * Selector: Selects all Pokemon that are saved by the user.
 * @func selectAllPokemons
 */
export const selectSavedPokemons = createSelector(
  pokemonsSelectors.selectAll,
  pokemons => pokemons.filter(pokemon => pokemon.myPokemon)
);