Давайте подробнее рассмотрим, как мы отделяем презентацию от логики.

Наши презентационные компоненты представляют собой библиотеку компонентов, которая разработана в сборнике рассказов и совершенно не знает логики приложения.

Каждый компонент презентации состоит из трех файлов. Файл компонента, файл сборника рассказов и файл стилей. Компоненты презентации — это единственные компоненты, которым разрешены html и css. Они могут содержать базовое локальное состояние, но никогда не будут выполнять какие-либо операции CRUD с любым API.

Ниже приведен пример компонента Avatar.

src/presentational/navigation/Avatar/Avatar.tsx

import React from 'react'
import { useAvatarStyles } from './Avatar.styles'
import { Avatar as MuiAvatar } from '@material-ui/core'
export interface IAvatarProps {
   title: string
   avatar: string
   testid?: string
}
export const Avatar: React.FunctionComponent<IAvatarProps> = ({ title, testid, avatar }) => {
  const classes = useAvatarStyles()
  return (
    <div data-testid={testid} className={classes.root}>
      <MuiAvatar avatar={avatar} className={classes.avatar}>{title}</MuiAvatar>
    </div>
  )
}

src/presentational/navigation/Avatar/Avatar.styles.tsx

import { makeStyles } from '@material-ui/core/styles'
import { fluidHeight } from '../../../utils/fluidHeight'
export const useAvatarStyles = makeStyles(() => ({
  root: {
    ...fluidHeight(165, 2),
    paddingTop: 5,
    display: 'flex',
    justifyContent: 'center',
  },
    avatar: {
      width: '122px',
      height: '122px',
    },
  }),
  { name: 'avatar' }
)

src/presentational/navigation/Avatar/Avatar.stories.tsx

import React from 'react'
import { Story, Meta } from '@storybook/react/types-6-0'
import { Avatar, IAvatarProps } from './Avatar'
export default {
  title: 'navigation/Avatar',
  component: Avatar,
} as Meta
const Template: Story<IAvatarProps> = (args) => <Avatar {...args} />
export const Default = Template.bind({})
Default.args = {  
  title: 'Some Title',
}

Наше изображение аватара исходит из CMS, и, поскольку мы хотим, чтобы наши презентационные компоненты были чистыми, мы извлекаем данные из их компонентов-контейнеров, которые мы называем компонентами «Просмотр».

Компоненты просмотра никогда не имеют html или css, и вся логика находится в файле ловушки, чтобы упростить тестирование. Потому что тогда мы можем протестировать хук с помощью react-hooks-testing-library, а не тестировать весь компонент.

src/views/navigation/AvatarView/AvatarView.tsx

import React from 'react'
import { Avatar } from '../../../presentational/navigation/Avatar/Avatar'
import { useAvatar } from './useAvatarView'
export const AvatarView: React.FunctionComponent = () => {
  const { ...props } = useAvatar()
  
  return <Avatar testid={'user-avatar'} {...props} />
}

src/views/navigation/AvatarView/useAvatarView.tsx

import { useUser } from '../../../context/UserContextProvider'
export const useAvatar = () => {
  const { avatar, isLoading, error } = getAvatar()
  const { user } = useUser()
return {
    isLoading,
    error,
    avatar,
    title: user.name
  }
}

Все операции чтения выполняются в файлах с префиксом «get» и находятся в папке запросов.

источник/запросы/getAvatar.ts

import useSWR from 'swr'
import { swrKeys } from '../constants/swrKeys'
import { IAvatarDTO } from '../pages/api/avatar'
export function getAvatar() {
  const swr = useSWR<IAlertsDTO>(swrKeys.avatar)
  return {
    ...swr,
    avatar: swr.data?.avatar,
    isLoading: !swr.data && !swr.error,
  }
}

В веб-разработке мы обычно читаем гораздо больше, чем выполняем операции записи, поэтому мы разделяем наши запросы в отдельную папку.

Каждое чтение получает свой собственный файл, он никогда не загромождается.