Peter Jausovec

@pjausovec

Building URL shortener using React, Apollo and GraphQL — Part IV: Tracking URL clicks

Photo by Carlos Muza (https://unsplash.com/@kmuza)

Table of Contents

Welcome back! If you want to start following along with this post, you can check out the GitHub repo for the project code, or start from the first part.

With the way project is currently, we can list and create short URLs, but we can’t do anything with those short links yet. In this post we will set up routes to handle short links as well as start tracking the number of clicks and updating the link list to show number of clicks in real time. Let’s get started!

Routing

The routing library we are going to use is React Router (if you want to learn more in-depth about routing and react router, check this great (free) training).

Let’s start with installing the react router dependency first:

$ yarn add react-router-dom

To make our code more readable, we are going to create a new file called AppRouter.js where we implement our routing logic. In this file we are going to declare which component should render, based on the URL that was requested. For example, if you navigate to root http://localhost:3000/ we should render the App.js component; since App.js will server as our main/home page, let’s rename it to Home.js.

Here’s how the AppRouter.js file with simple routing logic looks like:

App router with a single route (/)

React router has two types of routers: a BrowserRouter and a HashRouter. The main difference between the two is in a way they generate URLs. For example, this would be a BrowserRouter URL http://localhost:8080/login, while HashRouter URL would look like this http://localhost:8080/#/login.

Inside of the BrowserRouter we define a single Route that renders some UI (component={Home}) if location matches the routes path (path=”/”). To put it even simpler: if I go to http://localhost:3000/, router will render the Home component. For example, path with value /home translates to to http://localhost:3000/home URL. Since BrowserRouter only supports a single child, we are going to use a Switch later on.

Finally, we need to replace <App /> component in index.js with AppRouter:

ReactDOM.render(
withApolloProvider(<AppRouter />),
document.getElementById('root'),
);

If you run yarn start and go to http://localhost:3000/ you should get the exact same view as before.

In order to handle short URLs and resolve them, we need to define another Route. So, if user visits http://localhost:3000/shorturl, we need to get the shorturl part of the URL, figure out the expanded URL by making a GraphQL query and redirecting to the full URL. If we don’t find the full URL or if there’s an error, we show a simple error message.

Since the short URLs are going to be dynamic, we will use a variable in the URL path to capture the short URL hash. If the router matches the URL to this route, we are going to extract the shortUrl and render a simple component that’s going to do a lookup and redirect to the full URL. Let’s create a file src/components/ShortLinkRedirect.js and implement the component. Here’s the code for that component:

Let’s explain what’s happening in the code above. First, we are creating a new GraphQL query that takes a $hash as an input and returns us an URL that matches that hash. In the ShortLinkRedirect component we are passing in the hash and a couple of properties from the data object (this gets injected by the graphql container in line 31). Similarly as in previous components, we do the following (lines 15–28):

  1. Check for any errors and return an error
  2. Check if the query is still loading and return a loading message
  3. Check if we got any results or not
  4. Set the window.location to the full URL
  5. Return null as we aren’t rendering anything

A thing to note in line 36 is how we’re extracting hash variable that’s being passed to the component from the AppRouter.js file:

<Route
path="/:hash"
render={props => (
<ShortLinkRedirect hash={props.match.params.hash} />
)}
/>

With :hash we are indicating that we want to capture the URL parameter — we read that parameter from props.match.params object and send it as a prop to ShortLinkRedirect, where it gets used as an input variable to the GET_FULL_LINK_QUERY.

Try going to the web site and creating either a new short link or accessing a short link you’ve created before. Anytime you visit http://localhost:3000/[HASH] we try to figure out the long URL for the provided hash and redirect appropriately:

Short URL redirects in action!

Tracking clicks

Now that we have redirects working, we can think about how to store the number of clicks each short link gets.

The simplest solution would be to add a field called Clicks to the existing Link type and use the updateLink mutation to increment the number of clicks each link got. Even though it’s an ok solution, it’s not very extensible in case we later decide that we want to track more things when short link is clicked.

We will implement this using a new type (LinkStats) and we will use a relation to define a connection between the link stats and links.

Let’s open the graphcool/types.graphql file, add a the LinkStats type and create a relation to the Link type:

We created a stats field on Link type and added the @relation directive to define a relation to the LinkStats type. We do a similar thing on the LinkStats type and we connect the field link back to the Link type.

Let’s deploy these changes to Graph.cool by running graphcool deploy and then we can update the code and queries to increment the click count and display the click counts in LinkList component.

With service changes deployed, let’s update the ListList.js component first

  1. Update the ALL_LINKS_QUERY to include the number of clicks and an ID in the results (updates are in bold):
const ALL_LINKS_QUERY = gql`
query AllLinksQuery {
allLinks {
id
url
description
hash
stats {
clicks
}

}
}`;

2. Update the render method in Link.js to get the number of clicks and show it next to the link:

If you refresh the web site at this point, you should see the changes we added to the UI with number of clicks on each link being 0. On to updating the code that increments the link count! First, we need a simple mutation that’s going to take the id of the link and update the click count. Open the ShortLinkRedirect.js and follow the steps below:

  1. Update the GET_FULL_LINK_QUERY to return the id field and the clicks field as well (updates are in bold):
const GET_FULL_LINK_QUERY = gql`
query GetFullLink($hash: String!) {
allLinks(filter: { hash: $hash }) {
id
url
stats {
id
clicks
}
}
}`;

2. Create the UPDATE_CLICK_COUNT_MUTATION for updating the click count:

const UPDATE_CLICK_COUNT_MUTATION = gql`
mutation UpdateClickCount($id: ID!, $clicks: Int!) {
updateLink(id: $id, stats: { clicks: $clicks }) {
id
}
}`;

3. Create the CREATE_LINK_STATS_MUTATION for creating new link stats (i.e. first time someone clicks on a short link):

const CREATE_LINK_STATS_MUTATION = gql`
mutation CreateLinkStats($linkId: ID!, $clicks: Int!) {
createLinkStats(linkId: $linkId, clicks: $clicks) {
id
}
}`;

This should look familiar to previous queries we did — we are passing in the variables and calling updateLink mutation to update the clicks. And in the second mutation we are creating new links stats for the provided link.

4.. Add another graphql container to the export statements and wrap them with compose:

export default compose(
graphql(UPDATE_CLICK_COUNT_MUTATION, { name: 'updateClickCount' })
,
graphql(CREATE_LINK_STATS_MUTATION, { name: 'createLinkStats' }),
graphql(GET_FULL_LINK_QUERY, {
options: ({ hash }) => ({ variables: { hash } }),
}),
)(ShortLinkRedirect);

4. Update the ShortLinkRedirect function to pass in the updateClickCount and createLinkStats functions, increment the click count and call the mutation function to update it. Here’s the full ShortLinkRedirect.js file:

That’s it! (Well, almost). If you try and access any of the short links, you’ll notice that the click count doesn’t refresh automatically; this is because we are only subscribed to new link created mutation. Unfortunately, we have to use a workaround at this point (first one in the whole series!!), because updates on relations are not supported at the moment yet (see here for the discussion on this issue).

Updating the subscription

The workaround is fairly simple: we will add a dummy field to the Link type and update it each time we update the click count.

  1. Open types.graphql and add a dummy:String field to the Link type and deploy the changes (graphcool deploy).
  2. In ShortLinkRedirect.js, set a value to the dummy field in UPDATE_CLICK_COUNT_MUTATION:
updateLink(id: $id, dummy: "dummy", stats: { clicks: $clicks })

3. Open the LinkList.js file and update the LINKS_SUBSCRIPTION query to include the UPDATED event:

...
Link(filter: { mutation_in: [CREATED, UPDATED] }) {
...

4. Add a check to updateQuery in componentDidMount to ensure we don’t add the links twice. Since subscription is firing now on both created and updated events, we need to update the code so it doesn’t just blindly append links to the previous list. We need to check if the item that was updated is already in the list of links (i.e. click count was updated) and just return the list of previous links without doing any merges. Here’s the snippet to add at the top of updateQuery:

if (prev.allLinks.find(
l => l.id === subscriptionData.data.Link.node.id)) {
return prev;
}

Yay! Let’s see this in action:

Auto-refresh the click count with a workaround.

Great success! If you want to get the latest code, check the GitHub repo.

For the next post, I might skip the link expiry feature and just dive straight into adding the user authentication (login, signup, etc.).

After that, I was thinking of going through the exercise of actually deploying this — dockerizing the web site, maybe creating some tests and CI/CD pipeline, setting up SSL etc.

Let me know if this is something that would interest you!

Thanks for Reading!

Any feedback on these series is more than welcome! You can also follow me on Twitter and GitHub. If you liked this and want to get notified when other parts are ready, you should subscribe to my newsletter!

More by Peter Jausovec

Topics of interest

More Related Stories