[PART 16] Creating a Twitter clone with GraphQL, Typescript, and React ( Tweets timeline )
Tags: react, graphql, webdev, typescript
Published: 2021-01-23 12:44:09

Hi everyone ;).

As a reminder, I'm doing this [Tweeter challenge](https://devchallenges.io/challenges/rleoQc34THclWx1cFFKH)

[Github repository ( Backend )](https://github.com/ips-coding-challenge/challenge_twitter)

[Github repository ( Frontend )](https://github.com/ips-coding-challenge/challenge_twitter_front)

[Db diagram](https://dbdiagram.io/d/5f75c0243a78976d7b75f7be)



## Feed

While working on the feed, I noticed that I was doing too many SQL requests. I decided to delete the "counts" dataloaders and get the count directly in the **feed** function

*src/TweetResolver.ts*

```typescript
async feed(@Ctx() ctx: MyContext) {
    const { db, userId } = ctx

    const followedUsers = await db('followers')
      .where({
        follower_id: userId,
      })
      .pluck('following_id')

    const tweets = await db('tweets')
      .whereIn('user_id', followedUsers)
      .orWhere('user_id', userId)
      .orderBy('id', 'desc')
      .select(selectCountsForTweet(db))
      .limit(20)

    return tweets
  }
```

And for the **selectCountsForTweet()**:

*utils/utils.ts*

```typescript
export const selectCountsForTweet = (db: Knex) => {
  return [
    db.raw(
      '(SELECT count(tweet_id) from likes where likes.tweet_id = tweets.id) as "likesCount"'
    ),
    db.raw(
      `(SELECT count(t.parent_id) from tweets t where t.parent_id = tweets.id and t.type = 'comment') as "commentsCount"`
    ),
    db.raw(
      `(SELECT count(t.parent_id) from tweets t where t.parent_id = tweets.id and t.type = 'retweet') as "retweetsCount"`
    ),
    'tweets.*',
  ]
}
```

I learned that I have to add double quotes around the *count name* to have camelCase name ;). Therefore, I'll not have to change my graphQL queries. I will also need this function in the parentTweetDataloader.

*src/dataloaders*

```typescript
parentTweetDataloader: new DataLoader<number, Tweet, unknown>(async (ids) => {
    const parents = await db('tweets')
      .whereIn('id', ids)
      .select(selectCountsForTweet(db))
    return ids.map((id) => parents.find((p) => p.id === id))
  }),
```

Enough for the backend. I let you check the code on the [Github Repository](https://github.com/ips-coding-challenge/challenge_twitter)

## Working on the Feed

*src/pages/Home.tsx*

```tsx
import React from 'react'
import Layout from '../components/Layout'
import Feed from '../components/tweets/Feed'

const Home = () => {
  return (
    <Layout>
      {/* Tweet Column */}
      <div className="container max-w-container flex mx-auto gap-4">
        <div className="w-full md:w-tweetContainer">
          {/* Tweet Form */}

          {/* Tweet Feed */}
          <Feed />
        </div>

        {/* Home Sidebar */}
        <div className="hidden md:block w-sidebarWidth bg-gray5 flex-none">
          Sidebar
        </div>

        {/* Hashtags */}

        {/* Followers Suggestions */}
      </div>
    </Layout>
  )
}

export default Home

```

I let you check the **Layout** component. It's a little wrapper with the **Navbar** and a **children prop**.

The **Feed** component is really simple too:

*src/components/tweets/feed.tsx*

```tsx
import { useQuery } from '@apollo/client'
import React, { useEffect } from 'react'
import { useRecoilState, useSetRecoilState } from 'recoil'
import { FEED } from '../../graphql/tweets/queries'
import { tweetsState } from '../../state/tweetsState'
import { TweetType } from '../../types/types'
import Tweet from './Tweet'

const Feed = () => {
  const [tweets, setTweets] = useRecoilState(tweetsState)
  const { data, loading, error } = useQuery(FEED)

  useEffect(() => {
    if (data && data.feed && data.feed.length > 0) {
      setTweets(data.feed)
    }
  }, [data])

  if (loading) return <div>Loading...</div>
  return (
    <div className="w-full">
      {tweets.length > 0 && (
        <ul>
          {tweets.map((t: TweetType) => (
            <Tweet key={t.id} tweet={t} />
          ))}
        </ul>
      )}
    </div>
  )
}

export default Feed
```

Here is the GraphQL query:

*src/graphql/tweets/queries.ts*

```typescript
import { gql } from '@apollo/client'

export const FEED = gql`
  query {
    feed {
      id
      body
      visibility
      likesCount
      retweetsCount
      commentsCount
      parent {
        id
        body
        user {
          id
          username
          display_name
          avatar
        }
      }
      isLiked
      type
      visibility
      user {
        id
        username
        display_name
        avatar
      }
      created_at
    }
  }
`

```

And for the **<Tweet />** component:

*src/components/tweets/Tweet.tsx*

```tsx
import React from 'react'
import { MdBookmarkBorder, MdLoop, MdModeComment } from 'react-icons/md'
import { useRecoilValue } from 'recoil'
import { userState } from '../../state/userState'
import { TweetType } from '../../types/types'
import { formattedDate, pluralize } from '../../utils/utils'
import Avatar from '../Avatar'
import Button from '../Button'
import IsLikedButton from './actions/IsLikedButton'

type TweetProps = {
  tweet: TweetType
}

const Tweet = ({ tweet }: TweetProps) => {
  const user = useRecoilValue(userState)

  const showRetweet = () => {
    if (tweet.user.id === user!.id) {
      return <div>You have retweeted</div>
    } else {
      return <div>{tweet.user.display_name} retweeted</div>
    }
  }

  return (
    <div className="p-4 shadow bg-white rounded mb-6">
      {/* Retweet */}
      {tweet.type === 'retweet' ? showRetweet() : ''}
      {/* Header */}
      <div className="flex items-center">
        <Avatar className="mr-4" display_name={tweet.user.display_name} />

        <div>
          <h4 className="font-bold">{tweet.user.display_name}</h4>
          <p className="text-gray4 text-xs mt-1">
            {formattedDate(tweet.created_at)}
          </p>
        </div>
      </div>

      {/* Media? */}
      {tweet.media && <img src={tweet.media} alt="tweet media" />}
      {/* Body */}
      <div>
        <p className="mt-6 text-gray5">{tweet.body}</p>
      </div>

      {/* Metadata */}
      <div className="flex justify-end mt-6">
        <p className="text-gray4 text-xs ml-4">
          {pluralize(tweet.commentsCount, 'Comment')}
        </p>
        <p className="text-gray4 text-xs ml-4">
          {pluralize(tweet.retweetsCount, 'Retweet')}{' '}
        </p>
      </div>

      <hr className="my-2" />
      {/* Buttons */}
      <div className="flex justify-around">
        <Button
          text="Comments"
          variant="default"
          className="text-sm"
          icon={<MdModeComment />}
          alignment="left"
        />
        <Button
          text="Retweets"
          variant="default"
          className="text-sm"
          icon={<MdLoop />}
          alignment="left"
        />

        <IsLikedButton id={tweet.id} />

        <Button
          text="Saved"
          variant="default"
          className="text-sm"
          icon={<MdBookmarkBorder />}
          alignment="left"
        />
      </div>
    </div>
  )
}

export default Tweet

```

Here is what its looks like:

![Feed page](https://dev-to-uploads.s3.amazonaws.com/i/fx1dbtryxqgap1x1yyta.png)

I will talk later about the **IsLikedButton**. 

Let's talk about what's a **retweet**. I think I should change the way I consider a **retweet**. For now, a retweet is a normal **tweet** with a parent. But in reality, I think the retweet should only have a table referencing the tweet_id and the user_id. I will change that later and reflect the behavior in the frontend ;).



### ApolloClient and the cache?

ApolloClient comes with a cache and you can actually use it to update your data ( like a global store ). I tried to do that to update the tweet when the user **likes** a tweet. The problem is that, when a user likes/dislikes a tweet, it will rerender all the tweets. In my case, I only want to rerender the likes button. I didn't found a solution with the **apolloClient** so I will use **recoil** to store all the tweets and have more flexibility ( from my current knowledge perspective :D ).

*src/state/tweetsState.ts*

```typescript
import { atom, atomFamily, selectorFamily } from 'recoil'
import { TweetType } from '../types/types'

export const tweetsState = atom<TweetType[]>({
  key: 'tweetsState',
  default: [],
})

export const singleTweetState = atomFamily<TweetType | undefined, number>({
  key: 'singleTweetState',
  default: selectorFamily<TweetType | undefined, number>({
    key: 'singleTweetSelector',
    get: (id: number) => ({ get }) => {
      return get(tweetsState).find((t) => t.id === id)
    },
  }),
})

export const isLikedState = atomFamily({
  key: 'isLikedTweet',
  default: selectorFamily({
    key: 'isLikedSelector',
    get: (id: number) => ({ get }) => {
      return get(singleTweetState(id))?.isLiked
    },
  }),
})

```

The **tweetsState** store the tweets. The **singleTweetState** will allow us to get a single tweet using the **tweetsState** in the get method. Finally, the **isLikedState** will be interested only on the tweet's **isLiked** property.

Let's see everything in action:

*src/components/tweets/feed.tsx*

```tsx
const Feed = () => {
  const [tweets, setTweets] = useRecoilState(tweetsState)
  const { data, loading, error } = useQuery(FEED)

  useEffect(() => {
    if (data && data.feed && data.feed.length > 0) {
      setTweets(data.feed)
    }
  }, [data])

```

If I got data from the GraphQL Query, I save the tweets in my global store with the *setTweets* method.

Now let's take a look at the **IsLikedButton**

*src/components/tweets/actions/IsLikedButton.tsx*

```typescript
import { useMutation } from '@apollo/client'
import React from 'react'
import { MdFavoriteBorder } from 'react-icons/md'
import { useRecoilState, useRecoilValue } from 'recoil'
import { TOGGLE_LIKE } from '../../../graphql/tweets/mutations'
import { isLikedState } from '../../../state/tweetsState'
import Button from '../../Button'

type IsLIkedButtonProps = {
  id: number
}

const IsLikedButton = ({ id }: IsLIkedButtonProps) => {
  const [isLiked, setIsLiked] = useRecoilState(isLikedState(id))

  const [toggleLike, { error }] = useMutation(TOGGLE_LIKE, {
    variables: {
      tweet_id: id,
    },
    update(cache, { data: { toggleLike } }) {
      setIsLiked(toggleLike.includes('added'))
    },
  })
  return (
    <Button
      text={`${isLiked ? 'Liked' : 'Likes'}`}
      variant={`${isLiked ? 'active' : 'default'}`}
      className={`text-sm`}
      onClick={() => toggleLike()}
      icon={<MdFavoriteBorder />}
      alignment="left"
    />
  )
}

export default IsLikedButton

```

I pass the tweet_id as a prop as I need it to get the isLiked selector from the global store. 

Then I use the **useMutation** from the apolloClient to make the **toggleLike** request. You can use the **update key** to do whatever you want once the mutation is complete. Here, I change the **isLiked** property. This way, only my button is re-rendered.

![Toggle isLiked tweet](https://dev-to-uploads.s3.amazonaws.com/i/4lmwovq29c0vzqg4suo6.gif)


I think that's enough for today!

Have a nice day;)