React Native Web + GraphQL: The Planning Problem

Charlotte Van der voort
13 min readMar 30, 2021

For a school project, we get to solve the schools planning problem. For this course, we also have to use technologies that we have never used before. In this article, you can read about our journey navigating React Native Web and GraphQL to create a frontend application. This frontend will show the data generated in a Spring Boot + OptaPlanner backend. The article describing our work in the backend can be found here.

React Native is praised among its fans for its versatitily. With one codebase and some small changes, your application works on Android and IOS alike. Nowadays they are expanding to other platforms like Windows, MacOS and Web. But how easy is it to develop a web application, using a framework initially created for mobile?

Because timetables have a lot of data to show (timeslots, teachers, groups, rooms…), we decided to work with GraphQL. GraphQL ensures that we can send the data in whichever structure we need. This ensures that the different filtering queries to retreive timetables, will all be compatible with the React Native Web component structure we set up.

What we’re about to do

  • We will setup a React Native Web project, using Expo.
  • We’ll be making a weekly timetable by using components. We will also discuss the positioning of the events, and how we deal with the data.
  • We’ll create GraphQL queries to retreive data from the backend. To achieve this, we will also need to add the necessary GraphQL configurations.

Let’s get right to it!

Creating the project

What do you need?

  • NodeJs with npm (or yarn)
  • npx
  • An IDE. We will be using Visual Studio Code.

For this project you will need npm, and Nodejs. If you do not have this installed, you can download them by going to this page and downloading the latest stable version.

To create this project you will need to have npx installed. If you are unsure if you have npx installed, you can run this command to check:

which npx

If you do not have this installed, run the following command:

npm install -g npx

Now that that is all set up, it is time to set up your environment by installing expo.

npm install -g expo-cli

After that we install the create-react-native-web-application npm package. This will help us create our React native web project with just one simple command.

npm i create-react-native-web-application

Lets create the project by running this command. You can change your project name by mutating the “myappname” parameter.

npx create-react-native-web-application --name myappname

Thats all folks! We’re all set up and ready to start coding.

React Native Web Timetable

Creating a timetable

React Native Web is still in its early stages, which means that there are not yet a lot of compatible libraries.

Sadly, we noticed that there was no timetable library that suited our needs. The React Native Week View (https://github.com/hoangnm/react-native-week-view) framework comes close, but is not yet compatible with web. So we based ourselves of of the Week View framework, and started creating our own timetable for Web.

Our timetable exsist out of different components, one Weekview components contains:

  • Header: Monday-Sunday
  • Title: Lessonweek + number
  • Times: hours on the left side, customized for school planning
  • Events => which contains several seperate Event components: These can be customized in the Event component code

The timetable can be customized by changing some values (TimeTableHelper.js). This is an easy way to change the minutes between a step, the time label height, the begin of the day and the amount of hours displayed.

Because of the data supplied by the planning department, we chose to go by weekday. This can also easily be changed to dates.

The complete code for these components can be found on gitlab.

Event positioning

As a prop, we get a list filled with different events and their timeslots (the json format is specific to our data). The Events component has the responsibilty of making sure the events are placed correctly on the timetable.

The first step is making sure the events are sorted by day. This way, they can be placed in the correct column.

// example: [[event1, event2], [event3, event4], [event5]]
const totalEvents = WEEKDAYS.map((dayOfWeek) => {
const events = [];
eventsByDate.forEach((event) => {
if (event.lesson && event.lesson.timeslot) {
var startmoment = event.lesson.timeslot.dayOfWeek.toLowerCase()
if (startmoment == dayOfWeek.toLowerCase()) {
events.push(event)
}
}
});
return events;
});

Afterwards we will create the style element for each event. The methods underneath calculate the top (based on startTime and beginOfDay) and height (based on the duration of the event). Both methods also need the time label height and amount of minutes in a step to get to the correct result. These variables can easily be changed, and the timetable will remain accurate.

function getStyleForEvent(item) {
const startTime = moment(item.lesson.timeslot.startTime, "HH:mm:ss");
const top = calculateTop(startTime);
const height = calculateHeight(item.lesson.course.duration);
const width = EVENT_WIDTH;
return {
top: top + CONTENT_OFFSET,
left: 0,
height,
width,
};
};
function calculateHeight(duration) {
return (duration / MINUTES_STEP) * (TIME_LABEL_HEIGHT);
}
function calculateTop(startTime) {
const begintime = beginOfDay * MINUTES_IN_HOUR;
const minutesUntillStart = (startTime.hours() * MINUTES_IN_HOUR) + startTime.minutes() - begintime;
return (minutesUntillStart / MINUTES_STEP) * (TIME_LABEL_HEIGHT);
}

Lastly, an TouchableWithoutFeedback will be rendered for every day in the array of events. In this touchable, we will also render an Event component for every event in the day.

Timetable data

We use many of the React Hooks in the project. A very important one for the timetable is the useState hook. In the WeekView components, the events array is a state const. This allows us to alter the events and React Native will automatically rerender the component.

Because updating and event happens in a seperate component, we exchange the needed data through React Context (the useContext hook is used for this purpose). The context variables and methods are available throughout the entire application.

Then in a useEffect hook we ask our component to watch the eventIsUpdated context variable. Every time this variable changes, we will change the state. In turn, the change of state will make sure The component is rerendered.

const [events, setEvents] = useState([]) //events as state//get data from the context
const { selectedEvent, eventIsUpdated, setEventUpdated } = useContext(PlannerContext)
useEffect(() => {
setEvents(lessons)
}, []); //[] = execute at first rendering
useEffect(() => {
updateEvent(); //in this method we change the state by adding a new event
}, [eventIsUpdated]) // [eventIsUpdated] = execute everytime eventIsUpdated changes

Rerendering the Weekview component would also mean rerendering all the nested components. Performance wise this does not seem very efficient. After all, only the events have been changed.

To ensure that the other components are not unnecessarily rerendered, we used React.memo. React.memo memoizes the result of the (functional) component. At every rerender, React.memo checks for prop changes in the component. If there are none, React will skip rendering the component and reuse the last rendered result. It is however important to note that if the component uses hooks like useState or useContext, it will still be rerendered if these change.

export default React.memo(WeekViewHeader);

GraphQL

Setting up GraphQL Apollo

First we should import some dependencies:

"dependencies": {   
...
"apollo-link": "^1.2.14",
"apollo-link-http": "^1.5.17",
"apollo-utilities": "^1.3.4",
"apollo3-cache-persist": "^0.6.0"
...
}

After that its time to start the real work. We should make an ApolloProvider that encapsulates the whole application. This way the Apollo client is available everywhere in the application.

We should start by making an index.js file and importing some components.

...
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
import { WebSocketLink } from '@apollo/client/link/ws';
import { HttpLink } from 'apollo-link-http';
import { ApolloLink, split } from 'apollo-link';
import { getMainDefinition } from 'apollo-utilities';
...

The first step is to create an InMemory cache. This will take care of caching the data and getting rid of the __typename property in your GraphQl data. This way you can send data mutations back to your server without having the __typename field causing trouble in some mutation.

Because the __typename field is not know to the backend, out backend will throw an error when the data in out mutation contains this field.

const cache = new InMemoryCache({
addTypename: false
});

We can also finetune the settings for out cache by adding defaultOptions to our ApolloClient.

Here we can specify how we want the cache to behave, among other settings. For now, lets turn caching off.

const defaultOptions = {
watchQuery: {
fetchPolicy: 'no-cache',
errorPolicy: 'ignore',
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
}

After all that is set up, it is time to set up out connection to the backend server.

We specifiy the uri. This is the uri Apollo will use to take care of all the communication.

As you can see, we’ve also added an authorization header. Our JWT token will be added to all requests so that the backend can validate each request.

const httpLink = new HttpLink({
uri: '<http://localhost:8080/graphql>',
headers: {
authorization: `Bearer ${sessionStorage.getItem("token")}`,
},
});

Later on in the project we will be using GraphQl subscriptions so we should also add a websocket link.

const wsLink = new WebSocketLink({
uri: 'ws://localhost:8080/graphql',
options: {
reconnect: true,
connectionParams: {
authToken: sessionStorage.getItem("token"),
},
},
});

Now we can go on to combining out httpLink and our wsLink. We have to define which one should be used for which kind of operation and we can do that like so:

The split function takes three parameters: A function that’s called for each operation to execute. The Link to use for an operation if the function returns a “truthy” value. The Link to use for an operation if the function returns a “falsy” value.

const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);

Finally, we can create our ApolloClient and wrap out application with the ApolloProvider to expose this client to the whole application.

export const client = new ApolloClient({
link,
cache,
defaultOptions: defaultOptions
});
export default function Providers() {
return (
<ApolloProvider client={client}>
<PlannerContextProvider>
<AuthProvider>
<Routes />
</AuthProvider>
</PlannerContextProvider>
</ApolloProvider>
);
}

Our GraphQL set up is now complete !

How to make queries

Now that our set-up is complete, it is time to actually start querying some data from our server.

You want to get started but uhm…how can I see which queries, mutations and subscriptions are available to me?

Well we’ve got you covered. The guys at Apollo did a great job at making this process extremely easy for you.

You can use the Apollo Studio to explore your schema and start building your first queries.

Here you can find a great tutorial to get you started by setting up your environment and making a connection to your GraphQL server.

Once you’ve made the connection to your server, lets have a look at what we’re working with.

This is what you will see when you first open your newly created graph.

You can explore your query, mutation and subscription possibilities by clicking on the “+” sign.

This will open a new menu where you can see more information about your GraphQL.

Let’s click “query” to see what data we can query in our frontend application.

As you can see, we get a nice overview of the queries we can use. To start making your query simply press the “+” again and this will take you to the query builder.

Let’s click on Campuses and see what we get.

By clicking on the “+”, Apollo started creating a query for us. We still get a small error on line 4 because in GraphQL you cannot make empty queries. We will have to add at least one field to our query. Likewise, we can once again do this by clicking the “+” sign in front of the fields we wish to query.

After selecting your fields, Apollo will automatically add them to the query. Your query is now ready to be used! You can run the query in the Apollo studio, or you can save it for later when we start implementing queries in our React Native web application.

how to query data with Apollo

To query data you can simply wrap your previously built query in a gql tag and use the apollo client to send your request. I will show you how to this in a few basic steps.

First, we can create a folder within our React Native web project. I named my folder “services”, but you are free to call it however you want.

In this folder we can create a file for each query we need to execute, or group all similar queries in one file, how you want to structure this is completely up to you.

Let’s take a look at our first query.

To make Apollo understand that this is a GraphQL query, you have to wrap is in a gql tag. You do this by importing gql from “@apollo/client” and putting your query withing the backticks.

Okay now that you have made a query, it is time to send it to your server.

Go to the place where you want to receive your data and import the Apollo client that we passed to our provider along with the query we want to execute.

import { client } from "../../navigation/index";
import { getAllClassGroups } from '../../services/getAllClassGroups';

Create a function that you will to call when you want to fetch your data;

function getAllClassgroups() {
client
.query({
query: getAllClassGroups,
})
.then((response) => {
setClassgroups(response.data.ClassGroups)
setFilteredClassgroups(response.data.ClassGroups)
})
.catch((error) => {
console.log("ERROR ==>", error);
});

}

As you can see, we are calling “client.query” and passing the imported query. By doing this Apollo will send and create a HTTP request containing our query.

The returned data is caught in the then clause. If an error occurred, the error can be retrieved in the catch clause.

Now that you have your data, you can start showing the data to your users or performing some actions on it.

You might be wondering how we can pass arguments to our query. Well fear not.

We’ve got a simple example to show you how it’s done.

Start by creating a query that accepts one or more parameters, like so;

import { gql } from "@apollo/client";

export const getTimetableForTeacherWithId = gql`
query Query(
$timetableForWeekAndTeacherWeekId: Long!
$timetableForWeekAndTeacherTeacherId: Long!
) {
TimetableForWeekAndTeacher(
weekId: $timetableForWeekAndTeacherWeekId
teacherId: $timetableForWeekAndTeacherTeacherId
) {
lessonDtos {
lesson {

//all the lesson data that you want to query
}
}
}
`;

As you can see, this query takes two parameters. The first parameter will be the week we want to retrieve and the second parameter will be the teacher whose timetable we wish to receive.

We will use the same methodology as before.

first we import the elements we need:

import { getTimetableForTeacherWithId } from '../../services/getTimetableForTeacherWithId'
import { client } from "../../navigation/index";

Then we will create another function to retrieve our data. Putting our query in a separate function helps us to keep our code clean. The function will look something like this:

function getTeacherTimetable(teacherId, navigation) {
setIsLoading(true)
client
.query({
query: getTimetableForTeacherWithId,
variables: {
timetableForWeekAndTeacherWeekId: weekNr,
timetableForWeekAndTeacherTeacherId: teacherId
}
})
.then((response) => {
console.log(response)
navigation.navigate('TimeTableScreen', {
lessons: response.data.TimetableForWeekAndTeacher.lessonDtos,
weekId: weekNr
});
setIsLoading(false)

})
.catch((error) => {
console.log("ERROR ==>", error);
})
}

As you can see, we are passing the parameters in the “variables” property. Something you do have to pay attention to is that the names you used to add values to the parameters have to be exactly the same as the variable names you use in your query. If the names do not match, the value of the parameter will result in a null value.

How to mutate data

Making a mutation is very similar to querying data.

Here you see an example of a mutation we created to make our solver start solving a timetable for a specific week.

import { gql } from "@apollo/client";

export const SolveTimetableMutation = gql`
mutation SolveTimetableMutation($solveTimetableWeekId: Long!) {
solveTimetable(weekId: $solveTimetableWeekId)
}
`;

We again start by importing the apollo client and the mutation, just like we did in the previous part about how to query data.

import { SolveTimetableMutation } from ".././../services/SolveTimetableMutation";
import { client } from "../../navigation/index";

Then we make another function that will be called when we press the” start solving button”.

As you can see, the methodology is very similar to querying data. The only that you have top change is the client.query. This has become “client.mutate”, since we are executing a mutation.

function startSolving() {
client
.mutate({
mutation: SolveTimetableMutation,
variables: {
solveTimetableWeekId: weekNr,
},
})
.then((response) => {
console.log("response : " + JSON.stringify(response));
setSolving(true)
})
.catch((error) => {
console.log("ERROR ==>", error);
});
}

How to use a subscription

Lastly we will be discussing how to make a subscription.

By now you should know how to import your required files, so we won’t be showing that anymore.

Here you can see the subscription that we will be executing.

We are subscribing to the score of a timetable for a specific week. This means that we can follow the progress of how many constraints have been respected or broken by our solver in real time. The server will send us updates when the score changes.

We can achieve this being making a subscription like so;

import { gql } from "@apollo/client";

export const timetableScoreSubscription = gql`
subscription Subscription($timetableScoreUpdatedSubscriptionCode: String) {
timetableScoreUpdatedSubscription(
code: $timetableScoreUpdatedSubscriptionCode
) {
softScore
hardScore,
solverStatus
}
}
`;

To use the subscription, we will subscribe to the subscription every time the selected week changes. Because we have to do this for multiple weeks, we will use the “useEffect” hook. This will be fired when the “weekNr” value changes. If that happens a new subscription is started and the old subscription is cleaned up. This way we make sure that we do not create memory leaks.

useEffect(() => {
const observer = client.subscribe({
query: timetableScoreSubscription,
variables: {
timetableScoreUpdatedSubscriptionCode: weekNr.toString(),
},
});

const subscription = observer.subscribe(({ data, loading, error }) => {
console.log(
"this is the data form sthe subscription : " + JSON.stringify(data)
);
setData(data);
});

//cleanup
return () => subscription.unsubscribe();
}, [weekNr]);

Okay now that you know all about querying, mutating and subscribing in Apollo GraphQL you are all set to start working on your own and start experimenting!

I hope this tutorial was helpful, and you now know more about how querying data in GraphQL works.

We’re done!

We now have a clean timetable, with efficient GraphQL communication to retreive the data.

The 6 week project has come to an end, and we have learned a lot from it. If you have any comments or suggestions, then please do let us know.

--

--