Building a StackOverflow Clone: React Native Tutorial for Beginners

Let’s build a mobile client of the real StackOverflow app in React Native using Expo.

We will integrate with StackOverflow API, so all the data will be real. This will allow us to use StackOverflow on our mobile phone, and search for a solution everytime we get the "undefined is not a function" error.

This project will be split into 3 parts:

  1. Building the UI of StackOverflow app in React Native
  2. Converting the StackOverflow REST API to a GraphQL API using StepZen
  3. Querying our GraphQL API inside our app using URQL

Our app will include the next features:

  • Browse question on the home page
  • Search for specific questions
  • Open the detail page of the question
  • Browse answers

Follow the full build on yotube, and use this post for snippets and as a step-by-step guide.

Asset bundle

Download the asset bundle here: https://assets.notjust.dev/stackoverflow

Building the UI of StackOverflow app in React Native

Let’s start with the UI. For the mobile app, we will use Expo and Expo Router.

Create a new project wiht Expo

  1. Initialize the project with npx create-expo-app@latest StackOverflow -e with-router
  2. Open it in VScode
  3. Start the dev server with npm start
  4. Run the project on a simulator or a physical device
  5. Download Asset Bundle and import the data folder. This will be the dummy data we will use before we integrate the API.

Expo Router

Let’s create a _layout.js file where we can manage the Root Layout and Navigator of our app.

JAVASCRIPT
import { Stack, } from 'expo-router';
const _layout = () => {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'StackOverflow' }} />
</Stack>
);
};
export default _layout;

Display a questions

Let’s start by building a component that can render 1 question.

Question List Item Component

The question will have the next structure:

  • Stats (votes, answers, views)
  • TItle
  • Body
  • Tags + Date

After we implement the render logic of a question, let’s send the data through props.

QuestionListItem.js

JAVASCRIPT
// src/components/QuestionListItem
import { View, Text, StyleSheet } from 'react-native';
import { Entypo } from '@expo/vector-icons';
const QuestionListItem = ({ question }) => {
return (
<View style={styles.container}>
<Text style={styles.stats}>
{question.score} votes •{' '}
{question.is_answered && (
<Entypo name="check" size={12} color="limegreen" />
)}
{question.answer_count} answers • {question.view_count} views
</Text>
<Text style={styles.title}>{question.title}</Text>
<Text style={styles.body} numberOfLines={2}>
{question.body_markdown}
</Text>
<View style={styles.tags}>
{question.tags.map((tag) => (
<Text key={tag} style={styles.tag}>{tag}</Text>
))}
<Text style={styles.time}>
asked {new Date(question.creation_date * 1000).toDateString()}
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
padding: 10,
borderBottomWidth: 0.5,
borderColor: 'lightgray',
},
stats: {
fontSize: 12,
},
title: {
color: '#0063bf',
marginVertical: 5,
},
body: {
fontSize: 11,
color: 'dimgrey',
lineHeight: 15,
},
tags: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 5,
marginVertical: 10,
alignItems: 'center',
},
tag: {
backgroundColor: '#e1ecf4',
color: '#39739d',
padding: 5,
fontSize: 12,
borderRadius: 3,
overflow: 'hidden',
},
time: {
marginLeft: 'auto',
fontSize: 12,
color: '#6a737c',
},
});
export default QuestionListItem;

Display a List of question

We have rendered one question. Now, let’s render a list of question on the home page using a FlatList.

JAVASCRIPT
<FlatList
data={questionsData.items}
renderItem={({ item }) => <QuestionListItem question={item} />}
/>

Search input inside the Header

While we are working on the home page, let’s add the ability to search by adding a SearchBar inside the header of our screen.

https://reactnavigation.org/docs/native-stack-navigator/#headersearchbaroptions

JAVASCRIPT
const [searchTerm, setSearchTerm] = useState('');
const navigation = useNavigation();
useLayoutEffect(() => {
navigation.setOptions({
headerSearchBarOptions: {
onChangeText: (event) => setSearchTerm(event.nativeEvent.text),
onBlur: search,
},
});
}, [navigation, searchTerm, setSearchTerm]);

Question details page

Start by creating a new file [id].js inside the app folder for the Question Details Screen. The [id] part is the dynamic part of the url.

Inside the component, you can get the dynamic question id using:

const { id } = useSearchParams();

Now, let’s duplicate the QuestionListItem Component to QuestionHeader component. Adjust the Title, body and other minor things.

Don’t forget to render this <QuestionHedear question={question} /> component, on our details page.

QuestionHeader.js

JAVASCRIPT
import { View, Text, StyleSheet } from 'react-native';
import { Entypo } from '@expo/vector-icons';
const QuestionHeader = ({ question }) => {
return (
<>
<Text style={styles.title}>{question.title}</Text>
<Text style={styles.stats}>
{question.score} votes •{' '}
{question.is_answered && (
<Entypo name="check" size={12} color="limegreen" />
)}
{question.answer_count} answers • {question.view_count} views
</Text>
<View style={styles.separator} />
<Text style={styles.body}>{question.body_markdown}</Text>
<View style={styles.tags}>
{question.tags.map((tag) => (
<Text key={tag} style={styles.tag}>{tag}</Text>
))}
<Text style={styles.time}>
asked {new Date(question.creation_date * 1000).toDateString()}
</Text>
</View>
<Text style={{ fontSize: 16, marginVertical: 15 }}>
{question.answer_count} Answers
</Text>
</>
);
};
const styles = StyleSheet.create({
stats: {
fontSize: 12,
},
title: {
marginVertical: 5,
fontSize: 20,
lineHeight: 28,
color: '#3b4045',
fontWeight: '500',
},
body: {
lineHeight: 18,
color: '#232629',
},
tags: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 5,
marginVertical: 10,
alignItems: 'center',
},
tag: {
backgroundColor: '#e1ecf4',
color: '#39739d',
padding: 5,
fontSize: 12,
borderRadius: 3,
overflow: 'hidden',
},
time: {
marginLeft: 'auto',
fontSize: 12,
color: '#6a737c',
},
separator: {
borderTopWidth: StyleSheet.hairlineWidth,
borderColor: 'lightgray',
marginVertical: 10,
},
});
export default QuestionHeader;

Render a list of answers

Create an AnswerListItem component, and render it in a FlatList on the Question details page (app/[id].js)

Untitled.png

AnswerListItem.js

JAVASCRIPT
import { View, Text, StyleSheet } from 'react-native';
import { AntDesign, Entypo } from '@expo/vector-icons';
const AnswerListItem = ({ answer }) => {
return (
<View style={styles.container}>
<View style={styles.leftContainer}>
<AntDesign name="upcircleo" size={24} color="dimgray" />
<Text style={styles.score}>{answer.score}</Text>
<AntDesign name="downcircleo" size={24} color="dimgray" />
{answer.is_accepted && (
<Entypo
name="check"
size={22}
color="limegreen"
style={{ marginTop: 10 }}
/>
)}
</View>
<View style={styles.bodyContainer}>
<Text style={styles.body}>{answer.body_markdown}</Text>
<Text style={styles.time}>
answered {new Date(answer.creation_date * 1000).toDateString()}
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
marginBottom: 25,
paddingBottom: 20,
borderBottomWidth: 0.5,
borderColor: 'lightgray',
},
leftContainer: {
paddingHorizontal: 10,
alignItems: 'center',
},
score: {
fontSize: 16,
fontWeight: '500',
marginVertical: 10,
},
bodyContainer: {
flex: 1,
},
body: {
lineHeight: 18,
color: '#232629',
},
time: {
marginLeft: 'auto',
fontSize: 12,
color: '#6a737c',
marginTop: 10,
},
});
export default AnswerListItem;

Render the list of answers:

JAVASCRIPT
<FlatList
data={answers.items}
renderItem={({ item }) => <AnswerListItem answer={item} />}
ListHeaderComponent={() => <QuestionHeader question={question} />}
/>

Decode escaped HTML entities

We see that some data is HTML encoded. Ex: &gt; is the code for the greater symbol (>).

Let’s install the html-entities library and decode all the text received from the API.

JAVASCRIPT
npm install html-entities --legacy-peer-deps

Markdown

The body of our question and answers is using Markdown format. The same format as a README.md file on github. We have to parse this markdown and transform it to native components. For that, let’s use the next library. Even though it’s quite outdated.

If you know a better library for this, please let me know.

https://www.npmjs.com/package/react-native-markdown-display

JAVASCRIPT
npm install react-native-markdown-display --legacy-peer-deps

What next?

Good job. We have finished the first part of this build and we have a React Native application that looks and feels like StackOverflow UI.

Now, let’s move to the backend side, and integrate with StackOverflow’s API to get real data in our application.

StackOverflow API

StackOverflow has a pretty well documented API, and also has a lot of endpoints that are public. You don’t need any API keys to query it.

For more details, check out the docs here: https://api.stackexchange.com/docs

API endpoints

  • List of questions:
    • https://api.stackexchange.com/2.3/questions?order=desc&sort=votes&tagged=react-native&site=stackoverflow&filter=!0WVkZUE2aUd61A)oNLydqYFhc
  • Question by id:
    • https://api.stackexchange.com/2.3/questions/34641582?order=desc&sort=votes&site=stackoverflow&filter=!0WVkZUE2aUd61A)oNLydqYFhc
  • Answers:
    • https://api.stackexchange.com/2.3/questions/34641582/answers?order=desc&sort=votes&site=stackoverflow&filter=!3vByVnFcNyZ01KAKv
  • Search:
    • https://api.stackexchange.com/2.3/search?order=desc&sort=votes&intitle=undefined is not a function&site=stackoverflow&filter=!0WVkZUE2aUd61A)oNLydqYFhc

Data needed

The API has a lot of properties, but we only need some of them. We can createa filter to specify what fields are we interested in.

About questions:

  • question_id
  • creation_date
  • title
  • body_markdown
  • score
  • answer_count
  • view_count
  • tags
  • is_answered
  • link

About Answers:

  • answer_id
  • creation_date
  • body_markdown
  • score
  • is_accepted
  • question_id

Transform the REST API to GraphQL API using StepZen

I prefer working with a GraphQL API from client side. It’s easier to query exactly the data we need in front-end. For that reason, let’s use StepZen and convert the StackOverflow Rest API to a GraphQL API.

  1. Install and configure the cli (Check docs)
  2. Create a new folder stepzen in our project and run stepzen init to initialize the api.
  3. Create the index.graphql file
JAVASCRIPT
schema @sdl(files: []) {
query: Query
}
  1. Create the stackoverflow.graphql file where we will define the types of our api and the queries.

Schema for questions

JAVASCRIPT
type Question {
question_id: Int!
creation_date: Int!
title: String!
body_markdown: String!
score: Int!
answer_count: Int!
view_count: Int!
tags: [String!]!
is_answered: Boolean!
link: String!
}
type QuestionsResponse {
items: [Question]
has_more: Boolean!
quota_max: Int!
quota_remaining: Int!
}
type Query {
questions: QuestionsResponse
@rest(
endpoint: "https://api.stackexchange.com/2.3/questions?order=desc&sort=votes&tagged=react-native&site=stackoverflow&filter=!0WVkZUE2aUd61A)oNLydqYFhc"
)
question(questionId: Int!): QuestionsResponse
@rest(
endpoint: "https://api.stackexchange.com/2.3/questions/$questionId?order=desc&sort=activity&site=stackoverflow&filter=!0WVkZUE2aUd61A)oNLydqYFhc"
)
}

Deploy and Run the StepZen API

Run stepzen start to deploy and run our API on StepZen.

Varaibles

We can also add dynamic varaibles to our queries. For example, sending different tags.

https://stepzen.com/docs/connecting-backends/how-to-connect-a-rest-service#using-variables

Schema for Answers

JAVASCRIPT
type Answer {
answer_id: Int!
creation_date: Int!
body_markdown: String!
score: Int!
is_accepted: Boolean
question_id: Int!
}
type AnswersResponse {
items: [Answer!]!
has_more: Boolean!
quota_max: Int!
quota_remaining: Int!
}
type Query {
...
answers(questionId: Int): AnswersResponse
@rest(
endpoint: "https://api.stackexchange.com/2.3/questions/$questionId/answers?order=desc&sort=votes&site=stackoverflow&filter=!3vByVnFcNyZ01KAKv"
)
}

Connect Question and Answers using Materializer

https://stepzen.com/docs/connecting-backends/stitching

JAVASCRIPT
answers: [Answer]!
@materializer(
query: "answers {items}"
arguments: [{ name: "questionId", field: "question_id" }]
)

Data fetching with URQL

Now that we have a functional GraphQL API, we are ready to integrate it in our application.

There are multiple GraphQL Client libraries, like Relay, Apollo and URQL.

In this tutorail, let’s give it a try and use urql library, which is a lightweigth graphql client library.

More info about urql: https://formidable.com/open-source/urql/docs/basics/react-preact/

JAVASCRIPT
npm install urql graphql --legacy-peer-deps

Troubleshoot:

Setup the Client

Inside _layout.js, let’s steup the client.

JAVASCRIPT
import { Client, Provider, cacheExchange, fetchExchange } from 'urql';
const client = new Client({
url: 'https://chagallu.stepzen.net/api/tinseled-cat/__graphql',
exchanges: [cacheExchange, fetchExchange],
fetchOptions: {
headers: {
Authorization:
'Apikey chagallu::stepzen.net+1000::186c1b2cf5cff2214b43700408e5e419b5d01be431bd22094b8894c49b883d84',
'Content-Type': 'application/json',
},
},
});

Then, make sure to use the Provider, to give access from our screens to this client

JAVASCRIPT
const _layout = () => {
return (
<Provider value={client}>
<Stack>
<Stack.Screen name="index" options={{ title: 'StackOverflow' }} />
<Stack.Screen name="[id]" options={{ title: 'Question' }} />
</Stack>
</Provider>
);
};

Query questions

Now, we are ready to query question from our API inside app/index.js.

For that, we need a graphql query:

JAVASCRIPT
const getQuestions = gql`
query GetQuestions {
questions(tags: "php") {
has_more
quota_max
quota_remaining
items {
answer_count
body_markdown
creation_date
is_answered
link
question_id
score
tags
title
view_count
}
}
}
`;

And then, using useQuery hook from urql, we can fetch our questions

JAVASCRIPT
const [result] = useQuery({ query: getQuestions });
...
if (result.fetching) {
return (
<SafeAreaView>
<ActivityIndicator />
</SafeAreaView>
);
}
if (result.error) {
return (
<SafeAreaView>
<Text>Error: {result.error.message}</Text>
</SafeAreaView>
);
}
return (
<View style={styles.container}>
<FlatList
data={result.data.questions.items}
renderItem={({ item }) => <QuestionListItem question={item} />}
/>
</View>
);

Query Question Details

GraphQL Query

JAVASCRIPT
const getQuestionQuery = gql`
query GetQuestion($id: Int!) {
question(questionId: $id) {
has_more
quota_max
quota_remaining
items {
title
answer_count
body_markdown
creation_date
is_answered
link
question_id
score
tags
view_count
answers {
body_markdown
score
answer_id
creation_date
is_accepted
question_id
}
}
}
}
`;

Then query

JAVASCRIPT
const [result] = useQuery({
query: getQuestionQuery,
variables: { id },
});

Congrats 🥳

Our application is complete. We can navigate through StackOverflow questions and view their details and answers.

But don’t stop here. Try to implement additional features. Add more endpoints, for example the search endpoint, and integrate it in your application.

That’s the best way to learn something new.