Skip to main content

Store and list plants

Finally we need to store our plants in persistent storage (the same way we did with the onboarding state) and display all the existing plants in a list on the home page.

New zustand store for plants

Let's add a new store for our plants, we'll need to be able to:

  • see a list of plants
  • water a plant
  • remove a plant
New file: store/plantsStore.ts
import AsyncStorage from "@react-native-async-storage/async-storage";
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

type PlantType = {
id: string;
name: string;
wateringFrequencyDays: number;
lastWateredAtTimestamp?: number;
};

type PlantsState = {
nextId: number;
plants: PlantType[];
addPlant: (name: string, wateringFrequencyDays: number) => void;
removePlant: (plantId: string) => void;
waterPlant: (plantId: string) => void;
};

export const usePlantStore = create(
persist<PlantsState>(
(set) => ({
plants: [],
nextId: 1,
addPlant: (name: string, wateringFrequencyDays: number) => {
return set((state) => {
return {
...state,
nextId: state.nextId + 1,
plants: [
{
id: String(state.nextId),
name,
wateringFrequencyDays,
},
...state.plants,
],
};
});
},
removePlant: (plantId: string) => {
return set((state) => {
return {
...state,
plants: state.plants.filter((plant) => plant.id !== plantId),
};
});
},
waterPlant: (plantId: string) => {
return set((state) => {
return {
...state,
plants: state.plants.map((plant) => {
if (plant.id === plantId) {
return {
...plant,
lastWateredAtTimestamp: Date.now(),
};
}
return plant;
}),
};
});
},
}),
{
name: "plantly-plants-store",
storage: createJSONStorage(() => AsyncStorage),
},
),
);

Hook up creating plants

Now let's save a new plant to our state from the modal, and close the modal after:

Update: app/new.tsx
@@ -4,8 +4,12 @@ import { PlantlyButton } from "@/components/PlantlyButton";
import { useState } from "react";
import { PlantlyImage } from "@/components/PlantlyImage";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
+import { usePlantStore } from "@/store/plantsStore";
+import { useRouter } from "expo-router";

export default function NewScreen() {
+ const router = useRouter();
+ const addPlant = usePlantStore((state) => state.addPlant);
const [name, setName] = useState<string>();
const [days, setDays] = useState<string>();

@@ -28,7 +32,8 @@ export default function NewScreen() {
);
}

- console.log("Adding plant", name, days);
+ addPlant(name, Number(days));
+ router.navigate("/");
};

return (

List plants

Finally let's list our plants on the home page.

Add two more theme colors we'll need to build our UI:

Update file: theme.ts
@@ -4,5 +4,7 @@ export const theme = {
colorAppleGreen: "#a0d36c",
colorLimeGreen: "#d0e57e",
colorWhite: "#fff",
+ colorBlack: "#000",
colorLightGrey: "#eee",
+ colorGrey: "#808080",
};

If using TypeScript, export the PlantType type from the plantsStore so we could reuse it in our components:

Update file: store/plantsStore.ts
@@ -2,7 +2,7 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

-type PlantType = {
+export type PlantType = {
id: string;
name: string;
wateringFrequencyDays: number;

Update the PlantlyImage component so it could render the image at a different size:

Update file: components/PlantlyImage.tsx
@@ -1,9 +1,9 @@
import { Image, useWindowDimensions } from "react-native";

-export function PlantlyImage() {
+export function PlantlyImage({ size }: { size?: number }) {
const { width } = useWindowDimensions();

- const imageSize = Math.min(width / 1.5, 400);
+ const imageSize = size || Math.min(width / 1.5, 400);

return (
<Image

Create a reusable component for the plant card:

New file: components/PlantCard.tsx
import { StyleSheet, View, Text } from "react-native";
import { theme } from "@/theme";
import { PlantType } from "@/store/plantsStore";
import { PlantlyImage } from "./PlantlyImage";

export function PlantCard({ plant }: { plant: PlantType }) {
return (
<View style={styles.plantCard}>
<PlantlyImage size={100} />
<View style={styles.details}>
<Text numberOfLines={1} style={styles.plantName}>
{plant.name}
</Text>
<Text style={styles.subtitle}>
Water every {plant.wateringFrequencyDays} days
</Text>
</View>
</View>
);
}

const styles = StyleSheet.create({
plantCard: {
flexDirection: "row",
shadowColor: theme.colorBlack,
backgroundColor: theme.colorWhite,
borderRadius: 6,
padding: 12,
marginBottom: 12,
shadowOffset: {
width: 0,
height: 1,
},
shadowOpacity: 0.22,
shadowRadius: 2.22,

elevation: 3,
},
details: {
padding: 14,
justifyContent: "center",
},
plantName: {
fontSize: 18,
marginBottom: 4,
},
subtitle: {
color: theme.colorGrey,
},
});

Update the home page to show a list of plants:

Update file: app/(tabs)/index.tsx
import { FlatList, StyleSheet } from "react-native";
import { theme } from "@/theme";
import { usePlantStore } from "@/store/plantsStore";
import { PlantlyButton } from "@/components/PlantlyButton";
import { useRouter } from "expo-router";
import { PlantCard } from "@/components/PlantCard";

export default function App() {
const router = useRouter();
const plants = usePlantStore((state) => state.plants);

return (
<FlatList
style={styles.container}
contentContainerStyle={styles.contentContainer}
data={plants}
renderItem={({ item }) => <PlantCard plant={item} />}
ListEmptyComponent={
<PlantlyButton
title="Add your first plant"
onPress={() => {
router.navigate("/new");
}}
/>
}
/>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colorWhite,
},
contentContainer: {
padding: 12,
},
});

Shadow generator

Three things you need to know about shadows on React Native:

  1. shadow props only work on iOS. On Android you'll need to add an elevation instead.
  2. a shadow only works when the element has "visible UI styles". If you're finding your shadow isn't getting applied to the containing element and instead is applied on the children, try adding a backgroundColor to the View.
  3. there's a handy website to get you started on a baseline shadow (which you may or may not want to tweak).

Checkpoint

AndroidiOS