How to Scale with React.js

(without losing quality)

Follow Principles

  • S.O.L.I.D. (with React.js?)
  • D.R.Y. (Don’t Repeat Yourself)

  • K.I.S.S. (Keep It Simple, Stupid)

  • Y.A.G.N.I. (You Aren't Gonna Need It)

S in SOLID

"A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class."

S in SOLID

"A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class."

S in SOLID (❌ Bad Code)

import React, { useState } from 'react'
import { Box, Button } from '@chakra-ui/core'

function PresentationalComponent() {
  const [someState, setSomeState] = useState(false)

  const onSubmit = async () => {
    setSomeState(true)
    await fetch('https://api.com')
    setSomeState(false)
  }

  if (someState) {
    return (
      <Box mt={1} backgroundColor="red">
        Loading
      </Box>
    )
  }

  return (
    <div>
      Some view <Button onClick={onSubmit}>Submit</Button>
    </div>
  )
}

export default PresentationalComponent

S in SOLID (✅ Good Code)

import React, { useState } from 'react'
import { Box, Button } from '@chakra-ui/core'

// Loading.tsx
const Loading = () => (
  <Box mt={1} backgroundColor="red">
    Loading
  </Box>
)

// MyView.tsx
const MyView = ({ onSubmit }) => (
  <div>
    Some view <Button onClick={onSubmit}>Submit</Button>
  </div>
)

// useMyView.ts
function useMyView() {
  const [someState, setSomeState] = useState(false)

  const onSubmit = async () => {
    setSomeState(true)
    await fetch('https://api.com')
    setSomeState(false)
  }

  return { onSubmit, someState }
}
// PresentationalComponent.tsx
function PresentationalComponent() {
  const { onSubmit, someState } = useMyView()

  if (someState) {
    return <Loading />
  }

  return <MyView onSubmit={onSubmit} />
}

export default PresentationalComponent

O in SOLID

"Software entities … should be open for extension, but closed for modification."

O in SOLID (❌ Bad Code)

function CustomInput({ icon = 'user' }) {
  const icons = {
    user: <i className="fas fa-user"></i>,
  }

  return (
    <div>
      <input type="text" />
      {icons[icon]}
    </div>
  )
}

export default CustomInput

O in SOLID (✅ Good Code)

type Props = {
  icon: ReactNode,
}

function CustomInput({ icon }: Props) {
  return (
    <div>
      <input type="text" />
      {icon}
    </div>
  )
}

export default CustomInput

// MyView.tsx
import { FaBeer } from 'react-icons/fa';
import { DiApple } from 'react-icons/di';
import { MdArrowCircleRight } from 'react-icons/md';

<CustomInput icon={<FaBeer />} /> 
<CustomInput icon={<DiApple />} /> 
<CustomInput icon={<MdArrowCircleRight />} /> 

L in SOLID

"Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program."

L in SOLID (❌ Bad Code)

import { useForm } from 'react-hook-form'

function FormA({ onSubmit }) {
  return (
    <form onSubmit={onSubmit}>
      <input name="email" type="email" />
      <input name="password" type="password" />
    </form>
  )
}

function FormB({ onSubmit }) {
  const { handleSubmit, register } = useForm()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="email" type="email" {...register("email")} />
      <input name="password" type="password" {...register("password")} />
    </form>
  )
}

const MyView = () => (
  <>
    <FormA onSubmit={console.log} />
    <FormB onSubmit={console.log} />
  </>
)

L in SOLID (✅ Good Code)

import { useState } from 'React'
import { useForm } from 'react-hook-form'

function FormA({ onSubmit }) {
  const [form, setForm] = useState({})

  const handleSubmit = (event) => {
    event.preventDefault()
    onSubmit(form)
  }

  const handleChange = (name) => (event) =>
    setForm({ ...form, [name]: event.target.value })

  return (
    <form onSubmit={handleSubmit}>
      <input
        name="email"
        type="email"
        onChange={handleChange('email')}
        value={form.email}
      />
      <input
        name="password"
        type="password"
        onChange={handleChange('password')}
        value={form.password}
      />
    </form>
  )
}

// ...
import { useState } from 'React'
import { useForm } from 'react-hook-form'

// ...

function FormB({ onSubmit }) {
  const { handleSubmit, register } = useForm()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
	  <input name="email" type="email" 
 		{...register("email")} 
	  />
      <input name="password" type="password" 
		{...register("password")} 
	  />
    </form>
  )
}

function MyView() {
  return (
    <>
      <FormA onSubmit={console.log} />
      <FormB onSubmit={console.log} />
    </>
  )
}

I in SOLID

"Many client-specific interfaces are better than one general-purpose interface."

I in SOLID (❌ Bad Code)

import { Avatar } from '@chakra-ui/core'

type User = {
  name: string
  age: number
  email: string
  photoURL: string
}

function UserAvatar({ name, photoURL }: Pick<User, 'name' | 'photoURL'>) {
  return <Avatar name={name} src={photoURL} />
}

function UserInput({ name }: Pick<User, 'name'>) {
  return <Input placeholder={name} />
}

I in SOLID (✅ Good Code)

import { Avatar } from '@chakra-ui/core'

type Props = {
  name: string
  photoURL: string
}

function StyledAvatar({ name, photoURL }: Props) {
  return <Avatar name={name} src={photoURL} />
}

function MyView() {
  return <Input placeholder={user.name} />
}

D in SOLID

One should “depend upon abstractions, [not] concretions.”

Concretions

D in SOLID (❌ Bad Code)

import { useForm } from 'react-hook-form'

function MyView() {
  const { handleSubmit, register } = useForm()

  const onSubmit = async (values) => {
    await axios.post('/api/login', values)
  }

  return (
    <Container>
      <form onSubmit={handleSubmit(onSubmit)}>
        <input name="email" type="email" {...register('email')} />
        <input name="password" type="password" {...register('password')} />
      </form>
    </Container>
  )
}

D in SOLID (✅ Good Code)

// AuthService.tsx
class AuthService {
  login({ email, password }): Promise<void> {
    if (!email || !password) {
      throw new Error('Email and password are required')
    }
    return axios.post('/api/login', values)
  }
}

export default new AuthService()

// LoginForm.tsx
import { useForm } from 'react-hook-form'

function LoginForm({ onSubmit }) {
  const { handleSubmit, register } = useForm()

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input name="email" type="email" 
		{...register('email')} 
	  />
      <input name="password" type="password" 
		{...register('password')} 
	  />
    </form>
  )
}
// MyView.tsx
function MyView() {
  const onSubmit = async (values) => {
    // bonus tip: don't hide what's inside 
    // "values" object. Show it to the world 
    // for better debugging/readability
    const { email, password } = values
    await AuthService.login({ email, password })
  }

  return (
    <Container>
      <LoginForm onSubmit={onSubmit} />
    </Container>
  )
}

S.O.L.I.D. for React.js

  • Single Responsability: Small components, small hooks and small contexts that can be reused many times.
  • Open-closed: Usually the usage of ReactNode/Children as props, so you don't need to keep touching your component to be extendible.
  • Liskov Substitution: Make sure you keep using the latest npm libs for form, UI, etc by making sure your components receive the same props, always.
  • Interface segregation: Identify which components can be reused in many contexts, instead of duplicating many components for each Software Entity (User, Client, Admin, etc)
  • Dependency Inversion: Create your abstraction.

D.R.Y

Don't Repeat Yourself (It ain't obvious!)

D.R.Y (❌ Bad Code)

import { Button } from '@chakra-ui/core'

function MyView() {
  return (
    <Button variant={'brand'} size={'sm'} width={'200px'} fontSize="12px">
      Submit
    </Button>
  )
}

D.R.Y (✅ Good Code)

import { Button } from '@chakra-ui/core'

function MyView() {
  return (
    <Button variant={'brand'} size={'sm'}>
      Submit
    </Button>
  )
}

// theme/components/button.ts
export const buttonStyles = {
  components: {
    Button: {
      variants: {
        brand: () => ({
          fontSize: '12px',
        }),
      },
    },
  },
}
Made with Slides.com