Introduction
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:
- Building the UI of StackOverflow app in React Native
- Converting the StackOverflow REST API to a GraphQL API using StepZen
- 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
- Initialize the project with npx create-expo-app@latest StackOverflow -e with-router
- Open it in VScode
- Start the dev server with npm start
- Run the project on a simulator or a physical device
- 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.
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.
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
// src/components/QuestionListItemimport { 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.
<FlatListdata={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
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
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)
AnswerListItem.js
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 && (<Entyponame="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:
<FlatListdata={answers.items}renderItem={({ item }) => <AnswerListItem answer={item} />}ListHeaderComponent={() => <QuestionHeader question={question} />}/>
Decode escaped HTML entities
We see that some data is HTML encoded. Ex: > is the code for the greater symbol (>).
Let’s install the html-entities library and decode all the text received from the API.
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
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
Usefull docs links
- List of questions: https://api.stackexchange.com/docs/questions
- Question by id: https://api.stackexchange.com/docs/questions-by-ids
- Answers to a question: https://api.stackexchange.com/docs/answers-on-questions
- Filter to include the fields needed: https://api.stackexchange.com/docs/filters
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.
- Install and configure the cli (Check docs)
- Create a new folder stepzen in our project and run stepzen init to initialize the api.
- Create the index.graphql file
schema @sdl(files: []) {query: Query}
- Create the stackoverflow.graphql file where we will define the types of our api and the queries.
Schema for questions
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
type Answer {answer_id: Int!creation_date: Int!body_markdown: String!score: Int!is_accepted: Booleanquestion_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
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/
npm install urql graphql --legacy-peer-deps
Troubleshoot:
Setup the Client
Inside _layout.js, let’s steup the client.
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
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:
const getQuestions = gql`query GetQuestions {questions(tags: "php") {has_morequota_maxquota_remainingitems {answer_countbody_markdowncreation_dateis_answeredlinkquestion_idscoretagstitleview_count}}}`;
And then, using useQuery hook from urql, we can fetch our questions
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}><FlatListdata={result.data.questions.items}renderItem={({ item }) => <QuestionListItem question={item} />}/></View>);
Query Question Details
GraphQL Query
const getQuestionQuery = gql`query GetQuestion($id: Int!) {question(questionId: $id) {has_morequota_maxquota_remainingitems {titleanswer_countbody_markdowncreation_dateis_answeredlinkquestion_idscoretagsview_countanswers {body_markdownscoreanswer_idcreation_dateis_acceptedquestion_id}}}}`;
Then query
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.