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!
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:
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](http://localhost:8080/login)
, while HashRouter
URL would look like this [http://localhost:8080/#/login](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/](http://localhost:3000/)
, router will render the Home
component. For example, path with value **/home**
translates to to [http://localhost:3000**/home**](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/](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**](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):
window.location
to the full URLnull
as we aren’t rendering anythingA 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:
<Routepath="/: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]](http://localhost:3000/[HASH],)
we try to figure out the long URL for the provided hash and redirect appropriately:
Short URL redirects in action!
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
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 {idurldescriptionhashstats {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:
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 }) {idurlstats {idclicks}}}`;
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).
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.
types.graphql
and add a dummy:String
field to the Link type and deploy the changes (graphcool deploy
).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!
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!