Creating a Fun Facts App Using Typescript, Expo, and Redux

Six is the only number that is both the sum and the product of three consecutive positive numbers.

Goals Of This Project

So What Are We Building, Exactly?

The Code

Installing Tools

npm -v
npm install -g yarn
yarn add global expo-cli
expo --version

Initializing The Project

expo init fun-facts --yarn
? Choose a template: › - Use arrow-keys. Return to submit.----- Managed workflow -----blank                 a minimal app as clean as an empty canvasblank (TypeScript)    same as blank but with TypeScript configurationtabs (TypeScript)     several example screens and tabs using react-navigation and TypeScript----- Bare workflow -----minimal               bare and minimal, just the essentials to get you startedminimal (TypeScript)  same as minimal but with TypeScript configuration
cd fun-facts
yarn start

Organizing The Project

assets/
src/
api/
common/
navigations/
store/
styles/
views/
types.ts
App.tsx
app.json
package.json
...other files

Creating The Home Page

Creating App Styles

import * as Colors from "src/styles/Colors";
import * as Spacing from "src/styles/Spacing";
...Colors.Dark;
Spacing.padding;

Revamping The Home Screen

Making The Common FunFactCard Component

export interface FunFact {
fact: string;
rating: 1 | -1 | 0;
}
interface Props { 
funFact: FunFact;
}
...const FunFactCard: React.FC<Props> = ({ funFact }) => { ... }
import * as Colors from "../styles/Colors";
import * as Spacing from "../styles/Spacing";
import FunFactCard from "../common/FunFactCard"; // New
<FunFactCard
funFact={{ fact: "Placeholder unrated fact", rating: 0 }}
/>
<FunFactCard
funFact={{ fact: "Placeholder liked fact", rating: 1 }}
/>
<FunFactCard
funFact={{ fact: "Placeholder disliked fact", rating: -1 }}
/>

Setting Up The Redux Store

yarn add @reduxjs/toolkit
yarn add react-redux
interface FactsState {
status: "loaded" | "loading" | "errored";
error: Error | null;
pendingFact?: FunFact;
ids: EntityId[];
entities: Dictionary<FunFact>;
}
const FactsAdapter = createEntityAdapter<FunFact>({
selectId: (funFact) => funFact.fact,
});
export const getNewFact = createAsyncThunk<FunFact, number>(
"facts/getNew",
async (num) => {
throw Error("Not implemented yet");
}
);
const initialState: FactsState = {
status: "loaded",
error: null,
pendingFact: undefined,
...FactsAdapter.getInitialState(),
};
const FactsSlice = createSlice({
name: "facts",
initialState,
reducers: {
addFact: FactsAdapter.addOne,
},
extraReducers: (builder) => {
builder.addCase(getNewFact.pending, (state) => {
state.status = "loading";
state.error = null;
});
builder.addCase(getNewFact.fulfilled, (state, action) => {
state.status = "loaded";
state.pendingFact = action.payload;
});
builder.addCase(getNewFact.rejected, (state, action) => {
state.status = "errored";
state.error = action.error as Error;
});
},
});
export const { addFact } = FactsSlice.actions;
export default FactsSlice.reducer;

Providing The Store To React

Using The Store In The Home Screen

// State variables
const [num, setNum] = useState(6);
...<TextInput
value={num >= 0 ? num.toString() : ""}
onChangeText={(val) => {
if (val.length) setNum(parseInt(val));
else setNum(-1);
}}
style={Styles.inputContainer}
keyboardType="number-pad"
/>
const HomeScreen: React.FC = () => {
// Store values
const dispatch = useTypedDispatch();
const pendingFact = useTypedSelector((state) =>
state
.facts.pendingFact
);
const factError = useTypedSelector((state) => state.facts.error);
// State variables
const [num, setNum] = useState(6);
...
<TextInput
...
/>
<Button
title="Submit"
color={Colors.LIGHT}
onPress={() => {
Keyboard.dismiss();
dispatch(getNewFact(num));
}}
/>
<TextInput
...
/>
<Button
...
/>
{pendingFact && <FunFactCard funFact={pendingFact} />}
const HomeScreen: React.FC = () => {
// Store values
const dispatch = useTypedDispatch();
const pendingFact = useTypedSelector((state) =>
state
.facts.pendingFact
);
const factError = useTypedSelector((state) => state.facts.error);
// State variables
const [num, setNum] = useState(6);
// Respond to the factError changing
useEffect(() => {
if (factError) {
alert(factError.message);
}
}, [factError]);
return ( ... );
}

Integrating The Numbers API

throw Error("Not implemented yet");
const factString = await fetchFunFact(num);
return {
fact: factString,
rating: 0,
};
import { addFact, getNewFact } from "../store/modules/Facts";...<View style={Styles.feedbackContainer}>
<TouchableOpacity
style={Styles.feedbackButton}
onPress={() => {
if (!pendingFact) return;
const newFact = {
...pendingFact,
rating: 1 as 1,
};
dispatch(addFact(newFact));
dispatch(getNewFact(num));
}}
>
<Text
style={[Styles.feebackButtonText, { color: Colors.PRIMARY }]}
>
LIKE
</Text>
</TouchableOpacity>
<TouchableOpacity
style={Styles.feedbackButton}
onPress={() => {
if (!pendingFact) return;
const newFact = {
...pendingFact,
rating: -1 as -1,
};
dispatch(addFact(newFact));
dispatch(getNewFact(num));
}}
>
<Text
style={[
Styles.feebackButtonText,
{ color: Colors.SECONDARY }
]}
>
DISLIKE
</Text>
</TouchableOpacity>
</View>
reducers: {
upsertFact: FactsAdapter.upsertOne,
// Previously addFact and addOne
},
...export const { upsertFact } = FactsSlice.actions;
// Previously addFact
import { upsertFact, getNewFact } from "../store/modules/Facts";...dispatch(upsertFact(newFact));
// Previously dispatch(addFact(newFact));
...dispatch(upsertFact(newFact));
// Previously dispatch(addFact(newFact));

Creating a Navigator

yarn add @react-navigation/native
expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
yarn add @react-navigation/bottom-tabs
...import Navigator from "./src/navigations";...<Provider store={store}>
<View style={Styles.statusBarContainer}>
<StatusBar barStyle="light-content" />
</View>
<Navigator />
</Provider>

Creating The History Screen

...export const { upsertFact } = FactsSlice.actions;export const { selectAll: selectAllFacts } =
FactsAdapter.getSelectors();
export default FactsSlice.reducer;
...import HistoryScreen from "../views/History";...<Tab.Navigator
tabBarOptions={{
style: { backgroundColor: "black" },
keyboardHidesTabBar: true,
labelStyle: { fontSize: 24, color: "white" },
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="History" component={HistoryScreen} />
</Tab.Navigator>

Software engineer @ Ameelio

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store