[PART 16] GraphQL, Typescript 및 React로 Twitter 복제본 만들기( 트윗 타임라인 )
43501 단어 reactgraphqltypescriptwebdev
참고로 저는 이렇게 하고 있습니다Tweeter challenge
Db diagram
밥을 먹이다
피드에서 작업하는 동안 너무 많은 SQL 요청을 수행하고 있음을 알았습니다. "카운트"데이터 로더를 삭제하고 피드 기능에서 직접 카운트를 가져오기로 결정했습니다.
src/TweetResolver.ts
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
}
그리고 selectCountsForTweet()의 경우:
유틸리티/utils.ts
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.*',
]
}
낙타 케이스 이름을 가지려면 카운트 이름 주위에 큰따옴표를 추가해야 한다는 것을 배웠습니다 ;). 따라서 graphQL 쿼리를 변경할 필요가 없습니다. parentTweetDataloader에도 이 기능이 필요합니다.
src/데이터로더
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))
}),
백엔드에 충분합니다. 나는 당신이 코드를 확인하자
피드 작업
src/페이지/Home.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
레이아웃 구성 요소를 확인할 수 있습니다. Navbar와 어린이 소품이 있는 작은 포장지입니다.
피드 구성 요소도 정말 간단합니다.
src/components/tweets/feed.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
다음은 GraphQL 쿼리입니다.
src/graphql/tweets/queries.ts
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
}
}
`
그리고 구성 요소의 경우:
src/components/tweets/Tweet.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
그 모습은 다음과 같습니다.
나중에 IsLikedButton에 대해 이야기하겠습니다.
리트윗이 무엇인지 알아보겠습니다. 리트윗을 생각하는 방식을 바꿔야 할 것 같습니다. 현재 리트윗은 부모와 함께하는 일반적인 트윗입니다. 하지만 실제로는 리트윗에는 tweet_id와 user_id를 참조하는 테이블만 있어야 한다고 생각합니다. 나중에 변경하고 프런트엔드의 동작을 반영하겠습니다 ;).
ApolloClient와 캐시?
ApolloClient는 캐시와 함께 제공되며 실제로 이를 사용하여 데이터를 업데이트할 수 있습니다(글로벌 스토어처럼). 사용자가 트윗을 좋아할 때 트윗을 업데이트하려고 했습니다. 문제는 사용자가 트윗을 좋아하거나 싫어하면 모든 트윗을 다시 렌더링한다는 것입니다. 제 경우에는 좋아요 버튼만 다시 렌더링하고 싶습니다. 나는 apolloClient로 해결책을 찾지 못했기 때문에 반동을 사용하여 모든 트윗을 저장하고 더 많은 유연성을 가질 것입니다(현재 지식 관점에서 :D).
src/state/tweetsState.ts
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
},
}),
})
tweetsState는 트윗을 저장합니다. singleTweetState를 사용하면 get 메서드에서 tweetsState를 사용하여 단일 트윗을 얻을 수 있습니다. 마지막으로 isLikedState는 트윗의 isLiked 속성에만 관심이 있습니다.
모든 것이 실제로 작동하는 것을 봅시다:
src/components/tweets/feed.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])
GraphQL 쿼리에서 데이터를 가져온 경우 setTweets 메서드를 사용하여 글로벌 스토어에 트윗을 저장합니다.
이제 IsLikedButton을 살펴보겠습니다.
src/components/tweets/actions/IsLikedButton.tsx
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
글로벌 스토어에서 isLiked 선택기를 가져오는 데 필요하므로 tweet_id를 소품으로 전달합니다.
그런 다음 apolloClient의 useMutation을 사용하여 toggleLike 요청을 만듭니다. 변경이 완료되면 업데이트 키를 사용하여 원하는 모든 작업을 수행할 수 있습니다. 여기에서 isLiked 속성을 변경합니다. 이렇게 하면 내 버튼만 다시 렌더링됩니다.
오늘은 이 정도면 충분할 것 같아요!
좋은 하루 되세요;)
Reference
이 문제에 관하여([PART 16] GraphQL, Typescript 및 React로 Twitter 복제본 만들기( 트윗 타임라인 )), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/ipscodingchallenge/part-16-creating-a-twitter-clone-with-graphql-typescript-and-react-feed-24hh텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)