Section 7: State Management and Redux - kitiya/rn-tasty-recipes GitHub Wiki

146. Module Introduction

  • State management & Redux
  • Managing data in your app

147. What is State & What is Redux

Redux

Central Store

  • Central Store: Redux introduces a central store in JavaScript memory to keep track of your entire application state.
  • Component
  • Dispatches
  • Actions
  • Reducers
  • Update the central storage
  • Triggers
  • (Automatic) Subscription

148. Redux & Store Setup

Project Code:

store/reducers/recipes.js

import { RECIPES } from "../../data/dummy-data";

const initialState = {
  Recipes: RECIPES,
  filteredRecipes: RECIPES,
  favoriteRecipes: [],
};

const recipesReducer = (state = initialState, action) => {
  return state;
};

export default recipesReducer;

store/reducers/recipes.js

import { createStore, combineReducers } from "redux";
import { Provider } from "react-redux";

const rootReducer = combineReducers({
  recipes: recipesReducer,
});

const store = createStore(rootReducer);

App.js

import { createStore, combineReducers } from "redux";
import { Provider } from "react-redux";

import RecipesNavigator from "./navigation/RecipesNavigator";
import recipesReducer from "./store/reducers/recipes";

// Combined all reducers into the `rootReducer`
const rootReducer = combineReducers({
  recipes: recipesReducer,
});

const store = createStore(rootReducer);

export default function App() {

  // more code

  return (
    // wrapping the `Provider` component around our root component
    <Provider store={store}>
      <RecipesNavigator />
    </Provider>
  );
}

149. Selecting State Slices by Using useSelector()

  • useSelector allows you to extract data from the Redux store state, using a selector function.

Project Code:

screens/CategoryRecipesScreen.js

import { useSelector } from "react-redux";

const CategoryRecipesScreen = (props) => {
  const availableRecipes = useSelector(
    (state) => state.recipes.filteredRecipes
  );

  const displayedRecipes = availableRecipes.filter(
    (recipe) => recipe.categoryIds.indexOf(catId) >= 0
  );

  // more code ..
};

screens/FavoritesRecipesScreen.js

import { useSelector } from "react-redux";

const FavoritesScreen = (props) => {
  const favRecipe = useSelector((state) => state.recipes.favoriteRecipes);

  return (
    <RecipeList recipeListData={favRecipe} navigation={props.navigation} />
  );
};

screens/RecipeDetailScreen.js

import { useSelector } from "react-redux";

const RecipeDetailScreen = (props) => {
  const availabeRecipes = useSelector((state) => state.recipes.recipes);

  const selectedRecipe = availabeRecipes.find(
    (recipe) => recipe.id === recipeId
  );
};

150. Redux Data & Navigation Options

Project Code:

  • Passing recipeTitle data from parent components (CategoryRecipesScreen and FavoritesScreen) to a child component (RecipeDetailScreen)

component/RecipeList.js

const RecipeList = (props) => {
  const renderRecipeItem = (itemData) => {
    return (
      <RecipeItem

        // more code ..

        onSelectRecipe={() => {
          props.navigation.navigate({
            routeName: "RecipeDetail",
            params: {
              recipeId: itemData.item.id,

              // added new
              recipeTitle: itemData.item.title,
            },
          });
        }}
      />
    );
  };

component/RecipeList.js

RecipeDetailScreen.navigationOptions = (navigationData) => {
  // const recipeId = navigationData.navigation.getParam("recipeId");
  // const selectedRecipe = RECIPES.find((recipe) => recipe.id === recipeId);

  // added new
  const recipeTitle = navigationData.navigation.getParam("recipeTitle");

  return {
    headerTitle: recipeTitle,
    // more code ...
  };
};

151. Dispatching Actions & Reducer Logic

Project Code:

store/actions/recipe.js

export const TOGGLE_FAVORITE = "TOGGLE_FAVORITE";

export const toggleFavorite = (id) => {
  return {
    type: TOGGLE_FAVORITE,
    recipeId: id,
  };
};

store/reducers/recipe.js

  • Add or remove recipe from the favoriteRecipes list
import { RECIPES } from "../../data/dummy-data";
import { TOGGLE_FAVORITE } from "../actions/recipes";

const initialState = {
  recipes: RECIPES,
  filteredRecipes: RECIPES,
  favoriteRecipes: [],
};

const recipesReducer = (state = initialState, action) => {
  switch (action.type) {
    case TOGGLE_FAVORITE:
      const existingIndex = state.favoriteRecipes.findIndex(
        (recipe) => recipe.id === action.recipeId
      );

      if (existingIndex >= 0) { // remove if found recipe in the favorite list
        // copied the favoriteRecipes
        const updatedFavRecipes = [...state.favoriteRecipes];

        // removed the recipe
        updatedFavRecipes.splice(existingIndex, 1);

        return {
          ...state,
          favoriteRecipes: updatedFavRecipes,
        };
      } else { // add if not found in the favorite list
        // search for the `recipe` object
        const recipe = state.recipes.find(
          (recipe) => recipe.id === action.recipeId
        );
        return {
          ...state,

          // add the `recipe` object to the list
          favoriteRecipes: state.favoriteRecipes.concat(recipe),
        };
      }
      break;
    default:
      // use case: first reloaded the app
      return state;
  }
};

export default recipesReducer;

screens/RecipeDetailScreen.js

  • Dispatching the toggleFavorite action
import { useSelector, useDispatch } from "react-redux";
import { toggleFavorite } from "../store/actions/recipes";

const RecipeDetailScreen = (props) => {
  const recipeId = props.navigation.getParam("recipeId");

  const availabeRecipes = useSelector((state) => state.recipes.recipes);
  const selectedRecipe = availabeRecipes.find(
    (recipe) => recipe.id === recipeId
  );

  const dispatch = useDispatch();

  // using `useCallback()` function to avoid a infinite loop
  const toggleFavoriteHandler = useCallback(() => {
    dispatch(toggleFavorite(recipeId));
  }, [dispatch, recipeId]);

  // Passing the `toggleFavoriteHandler()` function to the navigation header through the `setParams()`
  useEffect(() => {
    props.navigation.setParams({ toggleFav: toggleFavoriteHandler });
  }, [toggleFavoriteHandler]);

  return (
    <ScrollView>
      // more code to display recipe detail
    </ScrollView>
  );
};

RecipeDetailScreen.navigationOptions = (navigationData) => {
  const recipeTitle = navigationData.navigation.getParam("recipeTitle");
  const toggleFavorite = navigationData.navigation.getParam("toggleFav");

  return {
    headerTitle: recipeTitle,
    headerRight: () => (
      <HeaderButtons HeaderButtonComponent={HeaderButton}>
        <Item title="Favorite" iconName="ios-star" onPress={toggleFavorite} /> 
      </HeaderButtons>
    ),
  };
};

152. Switching the Favorite Icon (solid / outline)

Project Code:

screen/RecipeDetailScreen.js

const RecipeDetailScreen = (props) => {
  const recipeId = props.navigation.getParam("recipeId");

  // Check if the displayed recipe item is in the favorite recipe list
  const currentRecipeIsFavorite = useSelector((state) =>
    state.recipes.favoriteRecipes.some((recipe) => recipe.id === recipeId)
  );

  // Update navigation params when the `isFav` property changes
  useEffect(() => {
    props.navigation.setParams({ isFav: currentRecipeIsFavorite });
  }, [currentRecipeIsFavorite]);
};

RecipeDetailScreen.navigationOptions = (navigationData) => {

  // loaded the `isFav` prop from the navigation param
  const isFavorite = navigationData.navigation.getParam("isFav");

  return {
    headerTitle: recipeTitle,
    headerRight: () => (
      <HeaderButtons HeaderButtonComponent={HeaderButton}>
        <Item
          title="Favorite"

          // update icon from solid <=> ouline, depending on the `isFavorite` property
          iconName={isFavorite ? "ios-star" : "ios-star-outline"}
          onPress={toggleFavorite}
        />
      </HeaderButtons>
    ),
  };
};

components/RecipeList.js

import { useSelector } from "react-redux";

const RecipeList = (props) => {

  // pulling `favoriteRecipes` list from the central store.
  const favoriteRecipes = useSelector((state) => state.recipes.favoriteRecipes);

  const renderRecipeItem = (itemData) => {

    // check if the curerent recipe item is in the `favoriteRecipes` list
    const isFavorite = favoriteRecipes.find(
      (recipe) => recipe.id === itemData.item.id
    );

    return (
      <RecipeItem
        // more code...

        onSelectRecipe={() => {
          props.navigation.navigate({
            routeName: "RecipeDetail",
            params: {
              recipeId: itemData.item.id,
              recipeTitle: itemData.item.title,

              // passing this prop through the navigation param to make sure that
              // the star gets painted right away when the RecipeDeatilScreen is loaded.
              isFav: isFavorite,
            },
          });
        }}
      />
    );
  };

  return (
    <View style={styles.recipeList}>
      <FlatList
        keyExtractor={(item, index) => item.id}
        data={props.recipeListData}
        renderItem={renderRecipeItem}
        style={{ width: "100%" }}
      />
    </View>
  );
};

153. Rendering a fallback text (for an empty favorite list)

Project Code:

screen/FavoritesScreen.js

import { View, StyleSheet } from "react-native";
import DefaultText from "../components/DefaultText";

const FavoritesScreen = (props) => {
  const favRecipe = useSelector((state) => state.recipes.favoriteRecipes);

  // rendering a fallback text when the `favRecipe` list is empty
  if (favRecipe.length === 0 || !favRecipe) {
    return (
      <View style={styles.emptyFav}>
        <DefaultText>No favorite recipes found. Start adding some!</DefaultText>
      </View>
    );
  }

  return (
    <RecipeList recipeListData={favRecipe} navigation={props.navigation} />
  );
};

154. Adding filtering logic (recipesReducer)

Project Code:

store/actions/recipes.js

export const SET_FILTERS = "SET_FILTERS";

export const setFilters = (filterSettings) => {
  return {
    type: SET_FILTERS,
    filters: filterSettings,
  };
};

store/reducer/recipes.js

import { SET_FILTERS } from "../actions/recipes";

const initialState = {
  recipes: RECIPES,
  filteredRecipes: RECIPES,
  favoriteRecipes: [],
};

const recipesReducer = (state = initialState, action) => {
  switch (action.type) {
    case TOGGLE_FAVORITE:
      // more code..

    case SET_FILTERS:
      const appliedFilters = action.filters;
      const updatedFilteredRecipes = state.recipes.filter((recipe) => {
        if (appliedFilters.glutenFree && !recipe.isGlutenFree) {
          return false;
        }
        if (appliedFilters.lactoseFree && !recipe.isLactoseFree) {
          return false;
        }
        if (appliedFilters.vegetarian && !recipe.isVegetarian) {
          return false;
        }
        if (appliedFilters.vegan && !recipe.isVegan) {
          return false;
        }
        return true;
      });

      return { ...state, filteredRecipes: updatedFilteredRecipes };

      break;
    default:
      return state;
  }
};

155. Dispatching filters actions

Project Code:

screen/FiltersScreen.js

import { useDispatch } from "react-redux";
import { setFilters } from "../store/actions/recipes";

const FiltersScreen = (props) => {
  const { navigation } = props;
  const dispatch = useDispatch();

  const saveFilters = useCallback(() => {
    const appliedFilters = {
      glutenFree: isGluetenFree,
      lactoseFree: isLactoseFree,
      vegan: isVegan,
      vegetarian: isVegetarian,
    };

    // dispatching the setFilters action from the central store
    dispatch(setFilters(appliedFilters));

    // kty: redirect to the category screen
    navigation.navigate("Categories");
  }, [dispatch, isGluetenFree, isLactoseFree, isVegan, isVegetarian]);

  useEffect(() => {
    navigation.setParams({ save: saveFilters });
  }, [saveFilters]);
};

screen/CategoryRecipesScreen.js

const CategoryRecipesScreen = (props) => {

  // rendering a fallback text when displayedRecipes list is empty
  if (displayedRecipes.length === 0) {
    return (
      <View style={styles.emptyRecipeList}>
        <DefaultText>No recipes found. Maybe, check your filters?</DefaultText>
      </View>
    );
  }
};
⚠️ **GitHub.com Fallback** ⚠️