[パート20 ] GraphSQL、TypesScript、および反応(retweet)でTwitterのクローンを作成する


みんな.
として、私はこのTweeter challengeをやっている
Db diagram

バックエンド


私はretweetsを扱うつもりだった方法を簡素化することを決めた.retweetsは“好き”のように扱われます.私は同じ原理を使うつもりです.
src/db/migrations/createRound - retweetsCountテーブル.TS
import * as Knex from 'knex'

export async function up(knex: Knex): Promise<void> {
  return knex.schema.createTable('retweets', (t) => {
    t.increments('id')
    t.integer('user_id').unsigned().notNullable()
    t.integer('tweet_id').unsigned().notNullable()

    t.unique(['user_id', 'tweet_id'])

    t.foreign('user_id').references('id').inTable('users').onDelete('CASCADE')
    t.foreign('tweet_id').references('id').inTable('tweets').onDelete('CASCADE')
  })
}

export async function down(knex: Knex): Promise<void> {
  return knex.raw('DROP TABLE retweets CASCADE')
}

src/resolvers/retweetResolver.TS
import { ApolloError } from 'apollo-server'
import { Arg, Authorized, Ctx, Mutation, Resolver } from 'type-graphql'
import { MyContext } from '../types/types'

@Resolver()
class RetweetResolver {
  @Mutation(() => String)
  @Authorized()
  async toggleRetweet(
    @Arg('tweet_id') tweet_id: number,
    @Ctx() ctx: MyContext
  ) {
    const { db, userId } = ctx

    const [tweet] = await db('tweets').where('id', tweet_id)

    if (!tweet) {
      throw new ApolloError('Tweet not found')
    }

    const data = {
      user_id: userId,
      tweet_id: tweet_id,
    }

    try {
      const [alreadyRetweeted] = await db('retweets').where(data)

      if (alreadyRetweeted) {
        // Delete the retweet and return
        await db('retweets').where(data).del()

        return 'Retweet deleted'
      }

      await db('retweets').insert(data)

      return 'Retweet added'
    } catch (e) {
      throw new ApolloError(e.message)
    }
  }
}
export default RetweetResolver

retweetscountを取得する方法を変更する必要があります.
src/utils/utilsTS
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"`
    ),
    // What I've changed  
    db.raw(
      `(SELECT count(tweet_id) from retweets where retweets.tweet_id = tweets.id) as "retweetsCount"`
    ),
    'tweets.*',
  ]
}

また、私はTweetResolverのaddTweet機能でretweetのケースを扱う部分を取り除きました.
私はときにつぶやきがretweettedされたときに知っている私のつぶやきエンティティにプロパティを追加しました.
src/entity/tweet.TS
  @Field()
  isRetweeted: boolean
そして、TweetResolverで@ fieldResolverでこれを扱います.
src/resolvers/tweetResolver.TS
@FieldResolver(() => Boolean)
  async isRetweeted(@Root() tweet: Tweet, @Ctx() ctx: MyContext) {
    const {
      userId,
      dataloaders: { isRetweetedDataloader },
    } = ctx

    if (!userId) return false

    const isRetweeted = await isRetweetedDataloader.load({
      tweet_id: tweet.id,
      user_id: userId,
    })

    return isRetweeted !== undefined
  }
src/DataLoader/DataLoader.TS
isRetweetedDataloader: new DataLoader<any, any, unknown>(async (keys) => {
    const tweetIds = keys.map((k: any) => k.tweet_id)
    const userId = keys[0].user_id

    const retweets = await db('retweets')
      .whereIn('tweet_id', tweetIds)
      .andWhere('user_id', userId)
    return tweetIds.map((id) => retweets.find((r) => r.tweet_id === id))
  }),
さあ、フロントエンドの世話をしましょう.

フロントエンド


“like”関数と同じ動作をするので、コードを少しリファクタリングするつもりです.
src/component/tweets/action/tweetActionButton.TSX
import Button from '../../Button'

type TweetActionButton = {
  id: number
  isSth: boolean | undefined
  icon: JSX.Element
  activeIcon?: JSX.Element
  text: string
  activeText: string
  activeClass: string
  onClick:
    | ((event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void)
    | undefined
}

const TweetActionButton = ({
  id,
  isSth,
  icon,
  activeIcon,
  text,
  activeText,
  activeClass,
  onClick,
}: TweetActionButton) => {
  return (
    <Button
      text={`${isSth ? activeText : text}`}
      variant={`${isSth ? activeClass : 'default'}`}
      className={`text-lg md:text-sm`}
      onClick={onClick}
      icon={isSth && activeIcon ? activeIcon : icon}
      alignment="left"
      hideTextOnMobile={true}
    />
  )
}

export default TweetActionButton

ボタンとretweetbuttonボタンは次のようになります.
import { useMutation } from '@apollo/client'
import React, { useEffect } from 'react'
import { MdFavorite, MdFavoriteBorder } from 'react-icons/md'
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'
import { TOGGLE_LIKE } from '../../../graphql/tweets/mutations'
import { isLikedState, singleTweetState } from '../../../state/tweetsState'
import Button from '../../Button'
import TweetActionButton from './TweetActionButton'

const LikeButton = ({ id }: { id: number }) => {
  const [isLiked, setIsLiked] = useRecoilState(isLikedState(id))
  const setTweet = useSetRecoilState(singleTweetState(id))

  const [toggleLike, { error }] = useMutation(TOGGLE_LIKE, {
    variables: {
      tweet_id: id,
    },
    update(cache, { data: { toggleLike } }) {
      setIsLiked(toggleLike.includes('added'))
      setTweet((oldTweet) => {
        if (oldTweet) {
          let count = oldTweet.likesCount
          toggleLike.includes('added') ? count++ : count--
          return {
            ...oldTweet,
            likesCount: count,
          }
        }
      })
    },
  })

  useEffect(() => {
    if (error) {
      console.log('Toggle like error', error)
    }
  }, [error])

  return (
    <TweetActionButton
      id={id}
      isSth={isLiked}
      icon={<MdFavoriteBorder />}
      activeIcon={<MdFavorite />}
      onClick={() => toggleLike()}
      text="Like"
      activeText="Liked"
      activeClass="red"
    />
  )
}

export default LikeButton

import { useMutation } from '@apollo/client'
import React, { useEffect } from 'react'
import { MdLoop } from 'react-icons/md'
import { useRecoilState, useSetRecoilState } from 'recoil'
import { TOGGLE_RETWEET } from '../../../graphql/tweets/mutations'
import { isRetweetedState, singleTweetState } from '../../../state/tweetsState'
import TweetActionButton from './TweetActionButton'

const RetweetButton = ({ id }: { id: number }) => {
  const setTweet = useSetRecoilState(singleTweetState(id))
  const [isRetweeted, setIsRetweeted] = useRecoilState(isRetweetedState(id))

  const [toggleRetweet, { error }] = useMutation(TOGGLE_RETWEET, {
    variables: {
      tweet_id: id,
    },
    update(cache, { data: { toggleRetweet } }) {
      setIsRetweeted(toggleRetweet.includes('added'))
      setTweet((oldTweet) => {
        if (oldTweet) {
          let count = oldTweet.retweetsCount
          toggleRetweet.includes('added') ? count++ : count--
          return {
            ...oldTweet,
            retweetsCount: count,
          }
        }
      })
    },
  })

  useEffect(() => {
    if (error) {
      console.log('Toggle retweet error', error)
    }
  }, [error])

  return (
    <TweetActionButton
      id={id}
      isSth={isRetweeted}
      icon={<MdLoop />}
      onClick={() => toggleRetweet()}
      text="Retweet"
      activeText="Retweeted"
      activeClass="green"
    />
  )
}

export default RetweetButton

閉じるこの動画はお気に入りから削除されています.それで、私はフックconst settweet = usesetrecoilstate(singletweetstate(id))を通して私のつぶやきを取り戻しました.これは、ローカルつぶやきを更新することができます.
別のカウンタをレンダリングする新しいコンポーネントを作成します.
src/component/tweets/tweetstats.TSX
import { useRecoilValue } from 'recoil'
import { singleTweetState } from '../../state/tweetsState'
import { pluralize } from '../../utils/utils'

const TweetStats = ({ id }: { id: number }) => {
  const tweet = useRecoilValue(singleTweetState(id))

  return (
    <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>
      <p className="text-gray4 text-xs ml-4">
        {pluralize(tweet!.likesCount, 'Like')}
      </p>
    </div>
  )
}

export default TweetStats

ここでは、私は私のつぶやきをRecolで取得し、結果として、コンポーネントがツイートのように毎回再調整されます.

今日はこれだけです.)
さあさあ.