├── .gitignore ├── README.md ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── public ├── file.svg ├── globe.svg ├── lucas.jpeg ├── next.svg ├── vercel.svg └── window.svg ├── src ├── app │ ├── favicon.ico │ ├── layout.tsx │ ├── not-found.ts │ └── post │ │ └── page.tsx ├── components │ ├── Avatar │ │ ├── Avatar.module.scss │ │ └── Avatar.tsx │ ├── CodeBlock │ │ ├── CodeBlock.module.scss │ │ └── CodeBlock.tsx │ ├── Footer │ │ ├── Footer.module.scss │ │ └── Footer.tsx │ ├── Header │ │ ├── Header.module.scss │ │ └── Header.tsx │ ├── Icon │ │ ├── Icon.module.scss │ │ ├── Icon.tsx │ │ └── icons │ │ │ ├── Github.tsx │ │ │ └── Linkedin.tsx │ ├── RenderCount │ │ ├── RenderCount.module.scss │ │ └── RenderCount.tsx │ └── Switch │ │ ├── Switch.module.scss │ │ └── Switch.tsx ├── middleware.ts ├── sections │ └── Home │ │ ├── Home.module.scss │ │ ├── Home.tsx │ │ ├── components │ │ ├── ContextPOC │ │ │ ├── ContextPOC.module.scss │ │ │ ├── ContextPOC.tsx │ │ │ ├── components │ │ │ │ ├── Container │ │ │ │ │ ├── Container.module.scss │ │ │ │ │ └── Container.tsx │ │ │ │ └── Title │ │ │ │ │ ├── Title.module.scss │ │ │ │ │ └── Title.tsx │ │ │ └── providers │ │ │ │ └── SwitcherProvider.tsx │ │ ├── StatePOC │ │ │ ├── StatePOC.module.scss │ │ │ ├── StatePOC.tsx │ │ │ └── components │ │ │ │ ├── Container │ │ │ │ ├── Container.module.scss │ │ │ │ └── Container.tsx │ │ │ │ └── Title │ │ │ │ ├── Title.module.scss │ │ │ │ └── Title.tsx │ │ ├── StateWithMemoPOC │ │ │ ├── StateWithMemoPOC.module.scss │ │ │ ├── StateWithMemoPOC.tsx │ │ │ └── components │ │ │ │ ├── Container │ │ │ │ ├── Container.module.scss │ │ │ │ └── Container.tsx │ │ │ │ └── Title │ │ │ │ ├── Title.module.scss │ │ │ │ └── Title.tsx │ │ ├── ZustandPOC │ │ │ ├── ZustandPOC.module.scss │ │ │ ├── ZustandPOC.tsx │ │ │ ├── components │ │ │ │ ├── Container │ │ │ │ │ ├── Container.module.scss │ │ │ │ │ └── Container.tsx │ │ │ │ └── Title │ │ │ │ │ ├── Title.module.scss │ │ │ │ │ └── Title.tsx │ │ │ └── stores │ │ │ │ └── SwitcherStore.tsx │ │ └── _StressTest │ │ │ ├── StressGroup.module.scss │ │ │ ├── StressGroup.tsx │ │ │ ├── components │ │ │ ├── StateStress │ │ │ │ ├── StateStress.module.scss │ │ │ │ ├── StateStress.tsx │ │ │ │ └── components │ │ │ │ │ ├── Container │ │ │ │ │ ├── Container.module.scss │ │ │ │ │ └── Container.tsx │ │ │ │ │ └── Title │ │ │ │ │ ├── Title.module.scss │ │ │ │ │ └── Title.tsx │ │ │ ├── StateWithMemoStress │ │ │ │ ├── StateWithMemoStress.module.scss │ │ │ │ ├── StateWithMemoStress.tsx │ │ │ │ └── components │ │ │ │ │ ├── Container │ │ │ │ │ ├── Container.module.scss │ │ │ │ │ └── Container.tsx │ │ │ │ │ └── Title │ │ │ │ │ ├── Title.module.scss │ │ │ │ │ └── Title.tsx │ │ │ └── ZustandStress │ │ │ │ ├── ZustandStress.module.scss │ │ │ │ ├── ZustandStress.tsx │ │ │ │ ├── components │ │ │ │ ├── Container │ │ │ │ │ ├── Container.module.scss │ │ │ │ │ └── Container.tsx │ │ │ │ └── Title │ │ │ │ │ ├── Title.module.scss │ │ │ │ │ └── Title.tsx │ │ │ │ └── stores │ │ │ │ └── useSwitcherStore.tsx │ │ │ └── shared │ │ │ ├── Child │ │ │ └── Child.tsx │ │ │ └── MemoizedChild │ │ │ └── MemoizedChild.tsx │ │ └── constants.ts ├── styles │ └── globals.scss └── utils │ └── classNames.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # State Management POC 2 | 3 | Esse projeto basicamente compara como diferentes abordagens de gerenciamento de estado no React impactam as re-renderizações dos componentes. O objetivo é ajudar desenvolvedores a entender como useState, Context API e Zustand se comportam em termos de performance e quando é mais apropriado usar cada um. 4 | 5 | ### 🚀 Propósito 6 | 7 | Este repositório foi criado para: 8 | • Demonstrar como o gerenciamento de estado no React afeta as re-renderizações; 9 | • Comparar useState, Context API e Zustand; 10 | • Servir como um guia prático para ajudar desenvolvedores a decidir qual abordagem escolher para diferentes casos de uso; 11 | 12 | --------- 13 | 14 | ### 📁 Estrutura do Projeto 15 | 16 | O repositório contém quatro exemplos, cada um focado em uma abordagem específica de gerenciamento de estado: 17 | 1. `useState` 18 | Mostra o comportamento básico do useState e como ele lida com o estado local; 19 | 2. `useState & React.memo` 20 | Mesma abordagem anterior mas com um plus de como otimizar o uso do `useState`; 21 | 3. `createContext` 22 | Demonstra como a Context API gerencia o estado global e como isso afeta as re-renderizações dos componentes filhos; 23 | 4. `zustand` 24 | Explora o comportamento do Zustand, destacando sua granularidade e eficiência na notificação de mudanças; 25 | 26 | --------- 27 | 28 | ### 📦 Instalação 29 | 30 | Siga os passos abaixo para rodar o projeto localmente: 31 | 1. Clone o repositório: 32 | ```bash 33 | git clone https://github.com/lucasca2/state-poc.git 34 | cd state-poc 35 | ``` 36 | 37 | 2. Instale as dependências: 38 | ```bash 39 | npm install 40 | ``` 41 | 42 | 3. Rode o projeto: 43 | ```bash 44 | npm run dev 45 | ``` 46 | 47 | 4. Acesse no navegador: http://localhost:3000 48 | 49 | --------- 50 | 51 | ### 📚 Recursos Adicionais 52 | 53 | Links úteis sobre gerenciamento de estado no React: 54 | - [Documentação oficial do React - State e Lifecycle](https://react.dev/learn/state-a-component-s-memory) 55 | - [Documentação oficial da Context API](https://react.dev/learn/passing-data-deeply-with-context) 56 | - [Documentação do Zustand](https://zustand-demo.pmnd.rs/docs/getting-started) 57 | 58 | --------- 59 | 60 | ### 💡 Autor 61 | 62 | Criado por [Lucas Costa Amaral](https://lucas.amaral.dev.br). 63 | Se você achou útil, não esqueça de dar uma ⭐ no repositório e compartilhar com outros devs! 64 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | reactStrictMode: false, 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poc-context", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "next": "15.1.2", 13 | "prism-react-renderer": "^2.4.1", 14 | "react": "^19.0.0", 15 | "react-code-blocks": "^0.1.6", 16 | "react-dom": "^19.0.0", 17 | "sass": "^1.83.0", 18 | "zustand": "^5.0.2" 19 | }, 20 | "devDependencies": { 21 | "@eslint/eslintrc": "^3", 22 | "@types/node": "^20", 23 | "@types/react": "^19", 24 | "@types/react-dom": "^19", 25 | "eslint": "^9", 26 | "eslint-config-next": "15.1.2", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/lucas.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasca2/state-poc/c06df59b3e573a5f1860c22864e91eb1586acd1a/public/lucas.jpeg -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasca2/state-poc/c06df59b3e573a5f1860c22864e91eb1586acd1a/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import '../styles/globals.scss'; 3 | 4 | export const metadata: Metadata = { 5 | title: "Gerenciar estados no React.js", 6 | description: "Aprenda a gerenciar estados no React.js de forma eficiente.", 7 | }; 8 | 9 | export default async function RootLayout({ 10 | children, 11 | }: Readonly<{ 12 | children: React.ReactNode; 13 | }>) { 14 | return ( 15 | 16 | 17 | {children} 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/not-found.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | 4 | export default function NotFound() { 5 | redirect('https://www.linkedin.com/feed/update/urn:li:activity:7276945157403406336/') 6 | } -------------------------------------------------------------------------------- /src/app/post/page.tsx: -------------------------------------------------------------------------------- 1 | import { Home } from "@/sections/Home/Home"; 2 | 3 | export default function PostPage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.module.scss: -------------------------------------------------------------------------------- 1 | .image { 2 | object-fit: cover; 3 | object-position: top; 4 | border-radius: 100%; 5 | 6 | width: 64px; 7 | height: 64px; 8 | 9 | @media (max-width: 768px) { 10 | width: 48px; 11 | height: 48px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import styles from "./Avatar.module.scss"; 4 | import { classNames } from "@/utils/classNames"; 5 | 6 | type AvatarProps = { 7 | src: string; 8 | alt: string; 9 | priority?: boolean; 10 | className?: string; 11 | }; 12 | 13 | export const Avatar = ({ 14 | src, 15 | alt, 16 | priority, 17 | className: _className, 18 | }: AvatarProps) => { 19 | const className = classNames(styles.image, _className); 20 | 21 | return ( 22 | {alt} 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/CodeBlock/CodeBlock.module.scss: -------------------------------------------------------------------------------- 1 | .code { 2 | width: 100%; 3 | padding: 32px; 4 | background-color: #111; 5 | border: 1px solid #111; 6 | border-radius: 8px; 7 | display: flex; 8 | flex-direction: column; 9 | gap: 12px; 10 | 11 | overflow-x: auto; 12 | 13 | .lineNumber { 14 | margin-right: 24px; 15 | } 16 | } -------------------------------------------------------------------------------- /src/components/CodeBlock/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { Highlight, themes } from "prism-react-renderer"; 2 | 3 | import styles from "./CodeBlock.module.scss"; 4 | 5 | type CodeBlockProps = { 6 | code: string; 7 | }; 8 | 9 | export const CodeBlock = ({ code }: CodeBlockProps) => { 10 | return ( 11 | 12 | {({ style, tokens, getLineProps, getTokenProps }) => ( 13 |
14 |           {tokens.map((line, i) => (
15 |             
16 | {line.map((token, key) => ( 17 | 18 | ))} 19 |
20 | ))} 21 |
22 | )} 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | flex-direction: column; 4 | padding: 80px 0 16px 0; 5 | } 6 | 7 | .copyRight { 8 | display: flex; 9 | justify-content: center; 10 | font-size: 10px; 11 | color: #fff; 12 | opacity: .75; 13 | letter-spacing: 0.15rem; 14 | font-weight: 600; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Footer.module.scss"; 2 | 3 | export const Footer = () => { 4 | return ( 5 |
6 |
© 2024 - LUCAS COSTA AMARAL
7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/Header/Header.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | display: flex; 3 | align-items: center; 4 | 5 | width: 100%; 6 | justify-content: space-between; 7 | 8 | padding: 40px 0; 9 | } 10 | 11 | .socialWrapper { 12 | display: flex; 13 | gap: 32px; 14 | 15 | svg { 16 | opacity: .5; 17 | transition: opacity .3s; 18 | will-change: opacity; 19 | } 20 | 21 | svg:hover { 22 | opacity: 1; 23 | } 24 | } -------------------------------------------------------------------------------- /src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@/components/Avatar/Avatar"; 2 | 3 | import styles from "./Header.module.scss"; 4 | import { Icon } from "../Icon/Icon"; 5 | 6 | export const Header = () => { 7 | return ( 8 |
9 | 10 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.module.scss: -------------------------------------------------------------------------------- 1 | .icon { 2 | color: #fff; 3 | 4 | width: 24px; 5 | height: 24px; 6 | 7 | & svg { 8 | width: 100%; 9 | height: 100%; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Icon/Icon.tsx: -------------------------------------------------------------------------------- 1 | import { classNames } from "@/utils/classNames"; 2 | import styles from "./Icon.module.scss"; 3 | 4 | import { Github } from "./icons/Github"; 5 | import { Linkedin } from "./icons/Linkedin"; 6 | 7 | const icons = { 8 | linkedin: Linkedin, 9 | github: Github, 10 | }; 11 | 12 | export type IconName = keyof typeof icons; 13 | 14 | type IconProps = { 15 | name: IconName; 16 | className?: string; 17 | }; 18 | 19 | export const Icon = ({ name, className: _className }: IconProps) => { 20 | const IconComponent = icons[name]; 21 | 22 | const className = classNames(styles.icon, _className); 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/Icon/icons/Github.tsx: -------------------------------------------------------------------------------- 1 | export const Github = () => { 2 | return ( 3 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/Icon/icons/Linkedin.tsx: -------------------------------------------------------------------------------- 1 | export const Linkedin = () => { 2 | return ( 3 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/RenderCount/RenderCount.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | position: relative; 3 | } 4 | 5 | .textWrapper { 6 | position: absolute; 7 | top: 0; 8 | right: 0; 9 | 10 | display: flex; 11 | flex-direction: column; 12 | gap: 4px; 13 | 14 | width: fit-content; 15 | } 16 | 17 | .text { 18 | font-size: 10px; 19 | font-weight: bold; 20 | text-transform: uppercase; 21 | width: 100%; 22 | 23 | background-color: rgba(255, 255, 255, 0.15); 24 | color: #fff; 25 | padding: 4px 8px; 26 | border: 1px solid #000; 27 | border-radius: 2px; 28 | } -------------------------------------------------------------------------------- /src/components/RenderCount/RenderCount.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { useRef, useEffect, Profiler } from "react"; 3 | import styles from "./RenderCount.module.scss"; 4 | import { classNames } from "@/utils/classNames"; 5 | 6 | type RenderCountProps = { 7 | id?: string; 8 | children?: React.ReactNode; 9 | className?: string; 10 | }; 11 | 12 | export const RenderCount = ({ 13 | id, 14 | children, 15 | className: _className, 16 | }: RenderCountProps) => { 17 | const isClient = useRef(false); 18 | const renderCount = useRef(1); 19 | const renderTimeSpan = useRef(null); 20 | 21 | useEffect(() => { 22 | if (!isClient.current) { 23 | isClient.current = true; 24 | } 25 | }, []); 26 | 27 | if (isClient.current) { 28 | renderCount.current += 1; 29 | } 30 | 31 | const className = classNames(styles.wrapper, _className); 32 | 33 | if (id) { 34 | return ( 35 | { 38 | if (renderTimeSpan.current) { 39 | renderTimeSpan.current.textContent = `${phase}: ${actualDuration.toFixed( 40 | 2 41 | )} ms`; 42 | } 43 | }} 44 | > 45 |
46 | {children} 47 |
48 | 49 | Mounting 50 | 51 | 52 | Rendered: {renderCount.current} times 53 | 54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | return ( 61 |
62 | {children} 63 |
64 | 65 | Rendered: {renderCount.current} times 66 | 67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/Switch/Switch.module.scss: -------------------------------------------------------------------------------- 1 | .track { 2 | position: relative; 3 | background-color: #333; 4 | border-radius: 16px; 5 | height: 24px; 6 | width: 44px; 7 | 8 | transition: background-color .3s; 9 | will-change: background-color; 10 | } 11 | 12 | .thumb { 13 | border-radius: 100%; 14 | position: absolute; 15 | height: 20px; 16 | width: 20px; 17 | background-color: #111; 18 | 19 | top: 50%; 20 | transform: translateY(-50%); 21 | 22 | left: 2px; 23 | 24 | transition: transform .3s; 25 | will-change: transform; 26 | 27 | } 28 | 29 | .thumb__enabled { 30 | transform: translateY(-50%) translateX(100%); 31 | } 32 | 33 | .track__enabled { 34 | background-color: #eee; 35 | } -------------------------------------------------------------------------------- /src/components/Switch/Switch.tsx: -------------------------------------------------------------------------------- 1 | import { classNames } from "@/utils/classNames"; 2 | import styles from "./Switch.module.scss"; 3 | 4 | type SwitchProps = { 5 | onToggle: () => void; 6 | enabled: boolean; 7 | }; 8 | 9 | export const Switch = ({ onToggle, enabled }: SwitchProps) => { 10 | const trackClassName = classNames(styles.track, { 11 | [styles.track__enabled]: enabled, 12 | }); 13 | 14 | const thumbClassName = classNames(styles.thumb, { 15 | [styles.thumb__enabled]: enabled, 16 | }); 17 | 18 | return ( 19 |
20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | 3 | export default async function middleware(req: NextRequest) { 4 | const referer = req.headers.get('referer'); 5 | 6 | if (referer) { 7 | console.log('Usuário veio de:', referer); 8 | } else { 9 | console.log('Usuário acessou diretamente ou não há referer.'); 10 | } 11 | 12 | return NextResponse.next() 13 | } 14 | 15 | export const config = { 16 | matcher: [ 17 | /* 18 | * Match all request paths except for the ones starting with: 19 | * - api (API routes) 20 | * - _next/static (static files) 21 | * - _next/image (image optimization files) 22 | * - favicon.ico, sitemap.xml, robots.txt (metadata files) 23 | */ 24 | '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)', 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /src/sections/Home/Home.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | margin: 0 auto; 3 | max-width: 1000px; 4 | width: 100%; 5 | 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | gap: 24px; 10 | 11 | overflow-x: hidden; 12 | 13 | padding: 40px 24px; 14 | 15 | p { 16 | width: 100%; 17 | line-height: 28px; 18 | } 19 | 20 | > h1 { 21 | margin-bottom: 40px; 22 | } 23 | 24 | h2 { 25 | margin-top: 40px; 26 | width: 100%; 27 | } 28 | 29 | code { 30 | background-color: #333; 31 | padding: 2px 4px; 32 | border-radius: 2px; 33 | } 34 | 35 | a { 36 | color: #5dced9; 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/sections/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import { ContextPOC } from "./components/ContextPOC/ContextPOC"; 2 | import { StatePOC } from "./components/StatePOC/StatePOC"; 3 | 4 | import styles from "./Home.module.scss"; 5 | import { CodeBlock } from "@/components/CodeBlock/CodeBlock"; 6 | import { StateWithMemoPOC } from "./components/StateWithMemoPOC/StateWithMemoPOC"; 7 | import { 8 | contextPOC, 9 | contextPOCProvider, 10 | contextPOCSwitcher, 11 | contextPOCTitle, 12 | statePOC, 13 | statePOCTitleWithMemo, 14 | zustandPOC, 15 | zustandPOCStore, 16 | zustandPOCSwitcher, 17 | zustandPOCTitle, 18 | } from "./constants"; 19 | import { ZustandPOC } from "./components/ZustandPOC/ZustandPOC"; 20 | import React from "react"; 21 | import { StressGroup } from "./components/_StressTest/StressGroup"; 22 | import { Header } from "@/components/Header/Header"; 23 | import { Footer } from "@/components/Footer/Footer"; 24 | 25 | export const Home = () => { 26 | return ( 27 |
28 |
29 |

30 | Gerenciamento de estado no React.js 31 |

32 |

33 | Primeiramente, vamos pensar em um cenário simples onde temos que 34 | gerenciar um estado que será compartilhado entre dois componentes. 35 |
36 | Então teremos um componente pai, que irá renderizar dois componentes 37 | filhos, e esses componentes filhos precisam compartilhar o mesmo estado. 38 |
39 | Um deles vai receber apenas um texto (que poderia vir de uma API) e o 40 | outro vai ser um Switcher que vai servir apenas pra 41 | conseguir simular essa mudança de estado. 42 |

43 | 44 |

45 | Usando useState; 46 |

47 |

48 | Acredito que a primeira coisa que vem à mente é usar o{" "} 49 | useState para gerenciar o estado no componente pai e passar 50 | os valores e funções para os componentes filhos, que é a abordagem mais 51 | comum de gerenciamento de estados. 52 |
53 | Algo mais ou menos assim: 54 |

55 | 56 | 57 |

58 | Se notar nessa abordagem, cada vez que o state do{" "} 59 | switcherValue muda, todos os componentes são 60 | re-renderizados novamente, até mesmo o Title que não tem 61 | nada a ver com isso! 62 |

63 |

64 | Isso acontece por que toda vez que um estado muda, o componente que o 65 | contém é re-renderizado, e como o App é o componente pai de 66 | todos os outros, todos eles são re-renderizados juntos. 67 |

68 |

69 | Existe uma forma de otimizar isso utilizando o React.memo, 70 | que basicamente faz com que um componente só seja re-renderizado se 71 | alguma propriedade que ele recebe for alterada. 72 |
73 | Algo mais ou menos assim: 74 |

75 | 76 |

77 | Com essa simples abordagem, ele já resolve o problema de re-renderizar o{" "} 78 | Title toda vez que o switcherValue muda. 79 |

80 | 81 |

82 | Porém note que o App continua re-renderizando toda vez que 83 | o switcherValue muda... Isso é um problema? não 84 | necessariamente, por que nesse caso o unico componente que vai ter 85 | impacto de fato é o Container, que é o esperado. O{" "} 86 | App é re-renderizado, mas ele só vai causar o re-render do{" "} 87 | Container que realmente deve ser re-renderizado, já que seu 88 | estado mudou, enquanto o Title vai continuar lá intacto por 89 | conta do React.memo. 90 |

91 | 92 |

93 | Usando createContext; 94 |

95 |

96 | Um problema que temos quando começamos a usar a abordagem de{" "} 97 | useState é que a medida que a aplicação cresce, a 98 | quantidade de props que precisamos passar para os componentes filhos 99 | também cresce, e isso pode se tornar um problema a longo prazo. 100 |

101 |

102 | E então é que surge a brilhante ideia de resolver isso utilizando um{" "} 103 | Context do React, que basicamente é um objeto que vai ser 104 | compartilhado entre todos os componentes que estão dentro dele. 105 |

106 |

107 | Mas ao mesmo tempo que o código fica muito mais “limpo“ e organizado, 108 | ele também é muito perigoso e pode ser um inimigo pra performance da sua 109 | aplicação. 110 |

111 |

112 | Vamos pegar e passar toda essa lógica do state e colocar dentro de um{" "} 113 | Context então pra ver como ele se comporta. 114 |

115 | 116 | 117 |

118 | Então, como podemos ver, o código realmente fica visualmente mais limpo, 119 | por que não precisamos mais passar as props para os componentes filhos. 120 |
121 | Dentro de cada componente filho a gente simplesmente chamaria o contexto 122 | e pegaria o estado que a gente precisa. 123 |

124 | 125 | 126 |

127 | Muito mais organizado, mas vamos ver na prática como isso se comporta? 128 |

129 | 130 |

131 | Note que o App realmente parou de re-renderizar, ok! Mas o{" "} 132 | Title está re-renderizando toda vez que o{" "} 133 | switcherValue muda, e isso é um problema. 134 |

135 |

136 | Isso acontece por que o React não consegue identificar qual 137 | estado do contexto aquele componente depende, então ele re-renderiza 138 | toda vez que o estado do contexto muda. E não há React.memo{" "} 139 | que resolva, por que nesse caso não tem nenhuma propriedade que muda, o 140 | que muda é o estado do contexto. 141 |

142 |

Como resolver isso então? Bom, a resposta é: Depende...

143 |

144 | Em alguns casos a gente pode dividir o contexto em vários contextos 145 | menores, e assim a gente consegue controlar melhor o que cada componente 146 | depende. Mas essa abordagem não é muito escalável, por que se o estado é 147 | muito complexo, acaba que é necessário criar diversos contextos e acaba 148 | deixando o código muito complexo e confuso. 149 |
150 | Então geralmente o Context é mais recomendado para estados 151 | que são mais simples ou para estados que não são alterados 152 | frequentemente como um tema(light/dark) por exemplo. 153 |

154 |

155 | Usando Zustand; 156 |

157 |

158 | Se usar useState, fica muito verboso, se usar{" "} 159 | createContext, não fica performático, o que usar então? 160 |

161 |

162 | Nesses casos, a gente pode optar por usar um gerenciador de estados mais 163 | robusto, como por exemplo um Redux, Zustand, etc...{" "} 164 |

165 |

166 | A diferença entre esses gerenciadores de estados robustos e o{" "} 167 | Context é que a gente consegue observar apenas um pedaço do 168 | estado invés de sempre observar o estado inteiro. 169 |

170 |

Nesse exemplo vou usar o Zustand pela facilidade da implementação:

171 | 172 | 173 |

174 | Note que até mesmo nosso arquivo principal fica mais limpo e organizado, 175 | por que ele não precisa saber sobre os estados dos componentes filhos. 176 |

177 | 178 | 179 |

180 | Dessa forma, o componente Title vai re-renderizar apenas se 181 | o estado title for alterado, por que agora ele não está 182 | mais observando o restante dos estados 183 |

184 |

185 | O mesmo vale pro Container, se por alguma razão o estado{" "} 186 | title fosse alterado, isso não o afetaria em nada. 187 |

188 |

Mas vamos ver na prática como isso fica:

189 | 190 |

191 | Usando um gerenciador mais robusto, a gente até consegue evitar o uso do{" "} 192 | React.memo por que não é mais necessário já que o 193 | componente pai não está mais sendo re-renderizado desnecessariamente 194 | (como acontece usando o useState) 195 |

196 |

Considerações finais

197 |

198 | Na maioria das vezes, um simples useState é o suficiente 199 | pra resolver os problemas simples do dia a dia, se souber trabalhar bem 200 | com ele, ele é tão performático quanto qualquer outro gerenciador de 201 | estados. 202 |

203 |

204 | Porém usar React.memo demais não é muito legal, por que ele 205 | também tem um custo de performance, então é sempre bom usar com 206 | moderação. 207 |

208 |

209 | Do mesmo jeito também que não é legal usar um Redux/Zustand/etc.. pra 210 | qualquer estado simples da nossa aplicação, a gente estaria adicionando 211 | uma complexidade desnecessária. 212 |

213 |

214 | Mas que custo é esse? falar é fácil, então vamos fazer um teste de 215 | stress e ver na prática! 216 |

217 |

Componentes pequenos

218 | 219 | 220 |

Componentes gigantes

221 | 222 | 223 |

224 | Em resumo, a melhor abordagem vai depender do seu caso de uso, se você 225 | tem um estado simples, use um useState, se você tem um 226 | estado mais complexo, use um gerenciador de estados mais robusto. Se 227 | você tem um estado interno e quer usar um useState de uma 228 | forma mais limpa, use um Context. 229 |

230 |

231 | Na maioria das vezes o useState é o suficiente, se você 232 | notar que seu código está começando a ficar muito complexo e confuso, aí 233 | sim é hora de pensar em usar um gerenciador de estados mais robusto. 234 |

235 |

236 | Mas tome cuidado com re-renderizações desnecessárias antes que vire uma 237 | bola de neve e você não consiga mais controlar. 238 |

239 |

240 | Fique a vontade pra instalar o{" "} 241 | repositório e fazer mais testes por 242 | conta própria! 243 |

244 |
245 |
246 | ); 247 | }; 248 | -------------------------------------------------------------------------------- /src/sections/Home/components/ContextPOC/ContextPOC.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | padding: 32px 24px; 22 | height: 200px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .content { 28 | width: 100%; 29 | max-width: 350px; 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | gap: 32px; 35 | } -------------------------------------------------------------------------------- /src/sections/Home/components/ContextPOC/ContextPOC.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Container } from "./components/Container/Container"; 4 | import { Title } from "./components/Title/Title"; 5 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 6 | 7 | import styles from "./ContextPOC.module.scss"; 8 | import { SwitcherProvider } from "./providers/SwitcherProvider"; 9 | 10 | export const ContextPOC = () => { 11 | return ( 12 | 13 | 14 |
15 | 16 | <Container /> 17 | </div> 18 | </RenderCount> 19 | </SwitcherProvider> 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/sections/Home/components/ContextPOC/components/Container/Container.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } -------------------------------------------------------------------------------- /src/sections/Home/components/ContextPOC/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 2 | import { Switch } from "@/components/Switch/Switch"; 3 | 4 | import styles from "./Container.module.scss"; 5 | import { useSwitcher } from "../../providers/SwitcherProvider"; 6 | 7 | export const Container = () => { 8 | const { switcherValue, toggle } = useSwitcher(); 9 | 10 | return ( 11 | <RenderCount className={styles.wrapper}> 12 | <Switch onToggle={toggle} enabled={switcherValue} /> 13 | </RenderCount> 14 | ); 15 | }; 16 | 17 | Container.displayName = "Container"; 18 | -------------------------------------------------------------------------------- /src/sections/Home/components/ContextPOC/components/Title/Title.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } 7 | 8 | .title { 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: #ddd; 12 | margin-bottom: 10px; 13 | margin: 0; 14 | } -------------------------------------------------------------------------------- /src/sections/Home/components/ContextPOC/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 3 | 4 | import styles from "./Title.module.scss"; 5 | import { useSwitcher } from "../../providers/SwitcherProvider"; 6 | 7 | export const Title = () => { 8 | const { title } = useSwitcher(); 9 | 10 | return ( 11 | <RenderCount className={styles.wrapper}> 12 | <h1 className={styles.title}>{title}</h1> 13 | </RenderCount> 14 | ); 15 | }; 16 | 17 | Title.displayName = "Title"; 18 | -------------------------------------------------------------------------------- /src/sections/Home/components/ContextPOC/providers/SwitcherProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from "react"; 2 | 3 | type SwitcherContextType = { 4 | switcherValue: boolean; 5 | toggle: () => void; 6 | title: string; 7 | }; 8 | 9 | export const SwitcherContext = createContext<SwitcherContextType>({} as SwitcherContextType); 10 | 11 | type SwitcherProviderProps = { 12 | children: React.ReactNode; 13 | }; 14 | 15 | const SwitcherProvider = ({ children }: SwitcherProviderProps) => { 16 | const [switcherValue, setSwitcherValue] = useState(false); 17 | const [title] = useState("Context"); 18 | 19 | const handleToggle = () => { 20 | setSwitcherValue(!switcherValue); 21 | }; 22 | 23 | return ( 24 | <SwitcherContext.Provider value={{ switcherValue, toggle: handleToggle, title }}> 25 | {children} 26 | </SwitcherContext.Provider> 27 | ); 28 | }; 29 | 30 | const useSwitcher = () => { 31 | const context = useContext(SwitcherContext); 32 | 33 | if (!context) { 34 | throw new Error("useSwitcher must be used within a SwitcherProvider"); 35 | } 36 | 37 | return context; 38 | }; 39 | 40 | export { useSwitcher, SwitcherProvider }; -------------------------------------------------------------------------------- /src/sections/Home/components/StatePOC/StatePOC.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | padding: 32px 24px; 22 | height: 200px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .content { 28 | width: 100%; 29 | max-width: 350px; 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | gap: 32px; 35 | } -------------------------------------------------------------------------------- /src/sections/Home/components/StatePOC/StatePOC.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useState } from "react"; 4 | import { Container } from "./components/Container/Container"; 5 | import { Title } from "./components/Title/Title"; 6 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 7 | 8 | import styles from "./StatePOC.module.scss"; 9 | 10 | export const StatePOC = () => { 11 | const [switcherValue, setSwitcherValue] = useState(false); 12 | const [title] = useState("State"); 13 | 14 | const handleToggle = useCallback(() => { 15 | setSwitcherValue((prev) => !prev); 16 | }, []); 17 | 18 | return ( 19 | <RenderCount className={styles.wrapper}> 20 | <div className={styles.content}> 21 | <Title title={title} /> 22 | <Container onToggle={handleToggle} enabled={switcherValue} /> 23 | </div> 24 | </RenderCount> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/sections/Home/components/StatePOC/components/Container/Container.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } -------------------------------------------------------------------------------- /src/sections/Home/components/StatePOC/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 2 | import { Switch } from "@/components/Switch/Switch"; 3 | 4 | import styles from "./Container.module.scss"; 5 | 6 | type ContainerProps = { 7 | onToggle: () => void; 8 | enabled: boolean; 9 | }; 10 | 11 | export const Container = ({ onToggle, enabled }: ContainerProps) => { 12 | return ( 13 | <RenderCount className={styles.wrapper}> 14 | <Switch onToggle={onToggle} enabled={enabled} /> 15 | </RenderCount> 16 | ); 17 | }; 18 | 19 | Container.displayName = "Container"; 20 | -------------------------------------------------------------------------------- /src/sections/Home/components/StatePOC/components/Title/Title.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } 7 | 8 | .title { 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: #ddd; 12 | margin-bottom: 10px; 13 | margin: 0; 14 | } -------------------------------------------------------------------------------- /src/sections/Home/components/StatePOC/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 3 | 4 | import styles from "./Title.module.scss"; 5 | 6 | type TitleProps = { 7 | title: string; 8 | }; 9 | 10 | export const Title = ({ title }: TitleProps) => { 11 | return ( 12 | <RenderCount className={styles.wrapper}> 13 | <h1 className={styles.title}>{title}</h1> 14 | </RenderCount> 15 | ); 16 | }; 17 | 18 | Title.displayName = "Title"; 19 | -------------------------------------------------------------------------------- /src/sections/Home/components/StateWithMemoPOC/StateWithMemoPOC.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | padding: 32px 24px; 22 | height: 200px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .content { 28 | width: 100%; 29 | max-width: 350px; 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | gap: 32px; 35 | } -------------------------------------------------------------------------------- /src/sections/Home/components/StateWithMemoPOC/StateWithMemoPOC.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useState } from "react"; 4 | import { Container } from "./components/Container/Container"; 5 | import { Title } from "./components/Title/Title"; 6 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 7 | 8 | import styles from "./StateWithMemoPOC.module.scss"; 9 | 10 | export const StateWithMemoPOC = () => { 11 | const [switcherValue, setSwitcherValue] = useState(false); 12 | const [title] = useState("State"); 13 | 14 | const handleToggle = useCallback(() => { 15 | setSwitcherValue((prev) => !prev); 16 | }, []); 17 | 18 | return ( 19 | <RenderCount className={styles.wrapper}> 20 | <div className={styles.content}> 21 | <Title title={title} /> 22 | <Container onToggle={handleToggle} enabled={switcherValue} /> 23 | </div> 24 | </RenderCount> 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/sections/Home/components/StateWithMemoPOC/components/Container/Container.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } -------------------------------------------------------------------------------- /src/sections/Home/components/StateWithMemoPOC/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 2 | import { Switch } from "@/components/Switch/Switch"; 3 | 4 | import styles from "./Container.module.scss"; 5 | 6 | type ContainerProps = { 7 | onToggle: () => void; 8 | enabled: boolean; 9 | }; 10 | 11 | export const Container = ({ onToggle, enabled }: ContainerProps) => { 12 | return ( 13 | <RenderCount className={styles.wrapper}> 14 | <Switch onToggle={onToggle} enabled={enabled} /> 15 | </RenderCount> 16 | ); 17 | }; 18 | 19 | Container.displayName = "Container"; 20 | -------------------------------------------------------------------------------- /src/sections/Home/components/StateWithMemoPOC/components/Title/Title.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } 7 | 8 | .title { 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: #ddd; 12 | margin-bottom: 10px; 13 | margin: 0; 14 | } 15 | -------------------------------------------------------------------------------- /src/sections/Home/components/StateWithMemoPOC/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 3 | 4 | import styles from "./Title.module.scss"; 5 | import React from "react"; 6 | 7 | type TitleProps = { 8 | title: string; 9 | }; 10 | 11 | export const Title = React.memo(({ title }: TitleProps) => { 12 | return ( 13 | <RenderCount className={styles.wrapper}> 14 | <h1 className={styles.title}>{title}</h1> 15 | </RenderCount> 16 | ); 17 | }); 18 | 19 | Title.displayName = "Title"; -------------------------------------------------------------------------------- /src/sections/Home/components/ZustandPOC/ZustandPOC.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | padding: 32px 24px; 22 | height: 200px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .content { 28 | width: 100%; 29 | max-width: 350px; 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | gap: 32px; 35 | } -------------------------------------------------------------------------------- /src/sections/Home/components/ZustandPOC/ZustandPOC.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Container } from "./components/Container/Container"; 4 | import { Title } from "./components/Title/Title"; 5 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 6 | 7 | import styles from "./ZustandPOC.module.scss"; 8 | 9 | export const ZustandPOC = () => { 10 | return ( 11 | <RenderCount className={styles.wrapper}> 12 | <div className={styles.content}> 13 | <Title /> 14 | <Container /> 15 | </div> 16 | </RenderCount> 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/sections/Home/components/ZustandPOC/components/Container/Container.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } -------------------------------------------------------------------------------- /src/sections/Home/components/ZustandPOC/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 2 | import { Switch } from "@/components/Switch/Switch"; 3 | 4 | import styles from "./Container.module.scss"; 5 | import { useSwitcherStore } from "../../stores/SwitcherStore"; 6 | 7 | export const Container = () => { 8 | const toggle = useSwitcherStore(state => state.toggle); 9 | const switcherValue = useSwitcherStore(state => state.switcherValue); 10 | 11 | return ( 12 | <RenderCount className={styles.wrapper}> 13 | <Switch onToggle={toggle} enabled={switcherValue} /> 14 | </RenderCount> 15 | ); 16 | }; 17 | 18 | Container.displayName = "Container"; 19 | -------------------------------------------------------------------------------- /src/sections/Home/components/ZustandPOC/components/Title/Title.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } 7 | 8 | .title { 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: #ddd; 12 | margin-bottom: 10px; 13 | margin: 0; 14 | } -------------------------------------------------------------------------------- /src/sections/Home/components/ZustandPOC/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 3 | 4 | import styles from "./Title.module.scss"; 5 | import { useSwitcherStore } from "../../stores/SwitcherStore"; 6 | 7 | export const Title = () => { 8 | const title = useSwitcherStore((state) => state.title); 9 | 10 | return ( 11 | <RenderCount className={styles.wrapper}> 12 | <h1 className={styles.title}>{title}</h1> 13 | </RenderCount> 14 | ); 15 | }; 16 | 17 | Title.displayName = "Title"; 18 | -------------------------------------------------------------------------------- /src/sections/Home/components/ZustandPOC/stores/SwitcherStore.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | type SwitcherStore = { 4 | switcherValue: boolean; 5 | toggle: () => void; 6 | title: string; 7 | }; 8 | 9 | export const useSwitcherStore = create<SwitcherStore>((set) => ({ 10 | title: "Zustand", 11 | switcherValue: false, 12 | toggle: () => set((state) => ({ switcherValue: !state.switcherValue })), 13 | })) -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/StressGroup.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | padding: 32px 24px; 22 | height: 200px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .button { 28 | border: none; 29 | background-color: #222; 30 | color: #fff; 31 | width: 100%; 32 | max-width: 350px; 33 | padding: 0 24px; 34 | height: 44px; 35 | font-size: 16px; 36 | border-radius: 4px; 37 | 38 | cursor: pointer; 39 | transition: opacity .3s; 40 | will-change: opacity; 41 | 42 | &:hover { 43 | opacity: .8; 44 | } 45 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/StressGroup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { StateStress } from "./components/StateStress/StateStress"; 5 | import { StateWithMemoStress } from "./components/StateWithMemoStress/StateWithMemoStress"; 6 | import { ZustandStress } from "./components/ZustandStress/ZustandStress"; 7 | 8 | import styles from "./StressGroup.module.scss"; 9 | 10 | type StressGroupProps = { 11 | countOfChildren: number; 12 | }; 13 | 14 | export const StressGroup = ({ countOfChildren }: StressGroupProps) => { 15 | const [shouldShow, setShouldShow] = useState(false); 16 | 17 | 18 | if (shouldShow) { 19 | return ( 20 | <> 21 | <StateStress countOfChildren={countOfChildren} /> 22 | <StateWithMemoStress countOfChildren={countOfChildren} /> 23 | <ZustandStress countOfChildren={countOfChildren} /> 24 | </> 25 | ); 26 | } 27 | 28 | return ( 29 | <div className={styles.wrapper}> 30 | <button className={styles.button} onClick={() => setShouldShow(true)}> 31 | Iniciar testes 32 | </button> 33 | </div> 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateStress/StateStress.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | height: fit-content; 22 | padding: 120px 24px 32px 24px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .content { 28 | width: 100%; 29 | max-width: 350px; 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | gap: 32px; 35 | } 36 | 37 | .array { 38 | display: flex; 39 | flex-direction: column; 40 | gap: 8px; 41 | max-height: 100px; 42 | overflow: auto; 43 | width: 100%; 44 | 45 | > div { 46 | width: 100%; 47 | } 48 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateStress/StateStress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useState } from "react"; 4 | import { Container } from "./components/Container/Container"; 5 | import { Title } from "./components/Title/Title"; 6 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 7 | 8 | import styles from "./StateStress.module.scss"; 9 | import { Child } from "../../shared/Child/Child"; 10 | 11 | type StateStressProps = { 12 | countOfChildren: number; 13 | }; 14 | 15 | export const StateStress = ({ countOfChildren }: StateStressProps) => { 16 | const [switcherValue, setSwitcherValue] = useState(false); 17 | const [title] = useState("Use State"); 18 | 19 | const handleToggle = useCallback(() => { 20 | setSwitcherValue((prev) => !prev); 21 | }, []); 22 | 23 | return ( 24 | <RenderCount className={styles.wrapper} id="State"> 25 | <div className={styles.content}> 26 | <Title title={title} /> 27 | <Container onToggle={handleToggle} enabled={switcherValue} /> 28 | <div className={styles.array}> 29 | <span>List with {countOfChildren} items</span> 30 | {[...Array(countOfChildren)].map((_, i) => ( 31 | <RenderCount key={i}> 32 | <Child value={(i + 1).toString()} /> 33 | </RenderCount> 34 | ))} 35 | </div> 36 | </div> 37 | </RenderCount> 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateStress/components/Container/Container.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div:not(:first-child) { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateStress/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 2 | import { Switch } from "@/components/Switch/Switch"; 3 | 4 | import styles from "./Container.module.scss"; 5 | 6 | type ContainerProps = { 7 | onToggle: () => void; 8 | enabled: boolean; 9 | }; 10 | 11 | export const Container = ({ onToggle, enabled }: ContainerProps) => { 12 | return ( 13 | <RenderCount className={styles.wrapper}> 14 | <Switch onToggle={onToggle} enabled={enabled} /> 15 | </RenderCount> 16 | ); 17 | }; 18 | 19 | Container.displayName = "Container"; 20 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateStress/components/Title/Title.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } 7 | 8 | .title { 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: #ddd; 12 | margin-bottom: 10px; 13 | margin: 0; 14 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateStress/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 3 | 4 | import styles from "./Title.module.scss"; 5 | 6 | type TitleProps = { 7 | title: string; 8 | }; 9 | 10 | export const Title = ({ title }: TitleProps) => { 11 | return ( 12 | <RenderCount className={styles.wrapper}> 13 | <h1 className={styles.title}>{title}</h1> 14 | </RenderCount> 15 | ); 16 | }; 17 | 18 | Title.displayName = "Title"; 19 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateWithMemoStress/StateWithMemoStress.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | height: fit-content; 22 | padding: 120px 24px 32px 24px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .content { 28 | width: 100%; 29 | max-width: 350px; 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | gap: 32px; 35 | } 36 | 37 | .array { 38 | display: flex; 39 | flex-direction: column; 40 | gap: 8px; 41 | max-height: 100px; 42 | overflow: auto; 43 | width: 100%; 44 | 45 | > div { 46 | width: 100%; 47 | } 48 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateWithMemoStress/StateWithMemoStress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useMemo, useState } from "react"; 4 | import { Container } from "./components/Container/Container"; 5 | import { Title } from "./components/Title/Title"; 6 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 7 | 8 | import styles from "./StateWithMemoStress.module.scss"; 9 | import { MemoizedChild } from "../../shared/MemoizedChild/MemoizedChild"; 10 | 11 | type StateWithMemoStressProps = { 12 | countOfChildren: number; 13 | }; 14 | 15 | export const StateWithMemoStress = ({ 16 | countOfChildren, 17 | }: StateWithMemoStressProps) => { 18 | const [switcherValue, setSwitcherValue] = useState(false); 19 | const [title] = useState("Use State & Memo"); 20 | 21 | const handleToggle = useCallback(() => { 22 | setSwitcherValue((prev) => !prev); 23 | }, []); 24 | 25 | const array = useMemo(() => { 26 | return [...Array(countOfChildren)].map((_, i) => ( 27 | <MemoizedChild key={i} value={(i + 1).toString()} /> 28 | )); 29 | }, [countOfChildren]); 30 | 31 | return ( 32 | <RenderCount className={styles.wrapper} id="State-Memo"> 33 | <div className={styles.content}> 34 | <Title title={title} /> 35 | <Container onToggle={handleToggle} enabled={switcherValue} /> 36 | <div className={styles.array}> 37 | <span>List with {countOfChildren} items</span> 38 | {array} 39 | </div> 40 | </div> 41 | </RenderCount> 42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateWithMemoStress/components/Container/Container.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div:not(:first-child) { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateWithMemoStress/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 2 | import { Switch } from "@/components/Switch/Switch"; 3 | 4 | import styles from "./Container.module.scss"; 5 | 6 | type ContainerProps = { 7 | onToggle: () => void; 8 | enabled: boolean; 9 | }; 10 | 11 | export const Container = ({ onToggle, enabled }: ContainerProps) => { 12 | return ( 13 | <RenderCount className={styles.wrapper}> 14 | <Switch onToggle={onToggle} enabled={enabled} /> 15 | </RenderCount> 16 | ); 17 | }; 18 | 19 | Container.displayName = "Container"; 20 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateWithMemoStress/components/Title/Title.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } 7 | 8 | .title { 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: #ddd; 12 | margin-bottom: 10px; 13 | margin: 0; 14 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/StateWithMemoStress/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 3 | 4 | import styles from "./Title.module.scss"; 5 | import React from "react"; 6 | 7 | type TitleProps = { 8 | title: string; 9 | }; 10 | 11 | export const Title = React.memo(({ title }: TitleProps) => { 12 | return ( 13 | <RenderCount className={styles.wrapper}> 14 | <h1 className={styles.title}>{title}</h1> 15 | </RenderCount> 16 | ); 17 | }); 18 | 19 | Title.displayName = "Title"; 20 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/ZustandStress/ZustandStress.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | width: 100%; 3 | height: 300px; 4 | border: 1px solid #444; 5 | border-radius: 4px; 6 | 7 | display: flex; 8 | flex-direction: column; 9 | justify-content: center; 10 | align-items: center; 11 | gap: 16px; 12 | 13 | padding: 40px; 14 | 15 | > div { 16 | top: 24px; 17 | right: 24px; 18 | } 19 | 20 | @media (max-width: 768px) { 21 | height: fit-content; 22 | padding: 120px 24px 32px 24px; 23 | justify-content: flex-end; 24 | } 25 | } 26 | 27 | .content { 28 | width: 100%; 29 | max-width: 350px; 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | gap: 32px; 35 | } 36 | 37 | .array { 38 | display: flex; 39 | flex-direction: column; 40 | gap: 8px; 41 | max-height: 100px; 42 | overflow: auto; 43 | width: 100%; 44 | 45 | > div { 46 | width: 100%; 47 | } 48 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/ZustandStress/ZustandStress.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Container } from "./components/Container/Container"; 4 | import { Title } from "./components/Title/Title"; 5 | 6 | import styles from "./ZustandStress.module.scss"; 7 | import { Child } from "../../shared/Child/Child"; 8 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 9 | 10 | type ZustandStressProps = { 11 | countOfChildren: number; 12 | }; 13 | 14 | export const ZustandStress = ({ countOfChildren }: ZustandStressProps) => { 15 | return ( 16 | <RenderCount className={styles.wrapper} id="Zustand"> 17 | <div className={styles.content}> 18 | <Title /> 19 | <Container /> 20 | <div className={styles.array}> 21 | <span>List with {countOfChildren} items</span> 22 | {[...Array(countOfChildren)].map((_, i) => ( 23 | <RenderCount key={i}> 24 | <Child value={(i + 1).toString()} /> 25 | </RenderCount> 26 | ))} 27 | </div> 28 | </div> 29 | </RenderCount> 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/ZustandStress/components/Container/Container.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/ZustandStress/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | import { Switch } from "@/components/Switch/Switch"; 2 | 3 | import { useSwitcherStore } from "../../stores/useSwitcherStore"; 4 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 5 | 6 | export const Container = () => { 7 | const toggle = useSwitcherStore((state) => state.toggle); 8 | const switcherValue = useSwitcherStore((state) => state.switcherValue); 9 | 10 | return ( 11 | <RenderCount> 12 | <Switch onToggle={toggle} enabled={switcherValue} /> 13 | </RenderCount> 14 | ); 15 | }; 16 | 17 | Container.displayName = "Container"; 18 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/ZustandStress/components/Title/Title.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | > div { 3 | top: 50%; 4 | transform: translateY(-50%); 5 | } 6 | } 7 | 8 | .title { 9 | font-size: 20px; 10 | font-weight: bold; 11 | color: #ddd; 12 | margin-bottom: 10px; 13 | margin: 0; 14 | } -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/ZustandStress/components/Title/Title.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import styles from "./Title.module.scss"; 4 | import { useSwitcherStore } from "../../stores/useSwitcherStore"; 5 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 6 | 7 | export const Title = () => { 8 | const title = useSwitcherStore((state) => state.title); 9 | 10 | return ( 11 | <RenderCount> 12 | <h1 className={styles.title}>{title}</h1> 13 | </RenderCount> 14 | ); 15 | }; 16 | 17 | Title.displayName = "Title"; 18 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/components/ZustandStress/stores/useSwitcherStore.tsx: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand' 2 | 3 | type SwitcherStore = { 4 | title: string; 5 | switcherValue: boolean; 6 | toggle: () => void; 7 | }; 8 | 9 | export const useSwitcherStore = create<SwitcherStore>((set) => ({ 10 | title: "Zustand", 11 | switcherValue: false, 12 | toggle: () => set((state) => ({ switcherValue: !state.switcherValue })), 13 | })) -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/shared/Child/Child.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Child = ({ value }: { value: string }) => { 4 | return <div>{value}</div>; 5 | }; 6 | -------------------------------------------------------------------------------- /src/sections/Home/components/_StressTest/shared/MemoizedChild/MemoizedChild.tsx: -------------------------------------------------------------------------------- 1 | import { RenderCount } from "@/components/RenderCount/RenderCount"; 2 | import React from "react"; 3 | 4 | export const MemoizedChild = React.memo(({ value }: { value: string }) => { 5 | return <RenderCount>{value}</RenderCount>; 6 | }); 7 | 8 | MemoizedChild.displayName = "MemoizedChild"; 9 | -------------------------------------------------------------------------------- /src/sections/Home/constants.ts: -------------------------------------------------------------------------------- 1 | export const statePOC = `export const App = () => { 2 | const [switcherValue, setSwitcherValue] = useState(false); 3 | const [title] = useState("State"); 4 | 5 | 6 | const handleToggle = useCallback(() => { 7 | setSwitcherValue(prev => !prev); 8 | }, []); 9 | 10 | return ( 11 | <div> 12 | <Title title={title} /> 13 | <Container onToggle={handleToggle} enabled={switcherValue} /> 14 | </div> 15 | ); 16 | };`; 17 | 18 | export const statePOCTitleWithMemo = `export const Title = React.memo(({ title }: TitleProps) => { 19 | return ( 20 | <h1>{title}</h1> 21 | ); 22 | }); 23 | 24 | Title.displayName = "Title";` 25 | 26 | export const contextPOC = `export const App = () => { 27 | return ( 28 | <SwitcherProvider> 29 | <div> 30 | <Title /> 31 | <Container /> 32 | </div> 33 | </SwitcherProvider> 34 | ); 35 | };`; 36 | 37 | export const contextPOCProvider = `const SwitcherProvider = ({ children }: SwitcherProviderProps) => { 38 | const [switcherValue, setSwitcherValue] = useState(false); 39 | const [title] = useState("Context"); 40 | 41 | const handleToggle = () => { 42 | setSwitcherValue(!switcherValue); 43 | }; 44 | 45 | return ( 46 | <SwitcherContext.Provider value={{ switcherValue, toggle: handleToggle, title }}> 47 | {children} 48 | </SwitcherContext.Provider> 49 | ); 50 | }; 51 | 52 | const useSwitcher = () => { 53 | const context = useContext(SwitcherContext); 54 | 55 | return context; 56 | };`; 57 | 58 | export const contextPOCTitle = `export const Title = () => { 59 | const { title } = useSwitcher(); 60 | 61 | return ( 62 | <h1>{title}</h1> 63 | ); 64 | };`; 65 | 66 | export const contextPOCSwitcher = `export const Container = () => { 67 | const { switcherValue, toggle } = useSwitcher(); 68 | 69 | return ( 70 | <Switch onToggle={toggle} enabled={switcherValue} /> 71 | ); 72 | };`; 73 | 74 | export const zustandPOCStore = `export const useSwitcherStore = create<SwitcherStore>((set) => ({ 75 | title: "Zustand", 76 | switcherValue: false, 77 | toggle: () => set((state) => ({ switcherValue: !state.switcherValue })), 78 | }))` 79 | 80 | export const zustandPOC = `export const App = () => { 81 | return ( 82 | <div> 83 | <Title /> 84 | <Container /> 85 | </div> 86 | ); 87 | }` 88 | 89 | export const zustandPOCTitle = `export const Title = () => { 90 | const title = useSwitcherStore((state) => state.title); 91 | 92 | return ( 93 | <h1>{title}</h1> 94 | ); 95 | };`; 96 | 97 | export const zustandPOCSwitcher = `export const Container = () => { 98 | const toggle = useSwitcherStore(state => state.toggle); 99 | const switcherValue = useSwitcherStore(state => state.switcherValue); 100 | 101 | return ( 102 | <Switch onToggle={toggle} enabled={switcherValue} /> 103 | ); 104 | };`; -------------------------------------------------------------------------------- /src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | max-width: 100vw; 4 | overflow-x: hidden; 5 | } 6 | 7 | body { 8 | color: #fff; 9 | background: #000; 10 | font-family: Arial, Helvetica, sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | padding: 0; 18 | margin: 0; 19 | } 20 | 21 | a { 22 | color: inherit; 23 | text-decoration: none; 24 | } -------------------------------------------------------------------------------- /src/utils/classNames.ts: -------------------------------------------------------------------------------- 1 | export const classNames = ( 2 | ...classes: (string | undefined | Record<string, boolean>)[] 3 | ): string => { 4 | const concatedClasses = classes.reduce( 5 | (className, curr: string | undefined | Record<string, boolean>) => { 6 | if (!curr) return className; 7 | 8 | if (typeof curr === "string") { 9 | return `${className} ${curr}`; 10 | } 11 | 12 | return `${className} ${Object.entries(curr) 13 | .filter(([, value]) => !!value) 14 | .map(([key]) => key) 15 | .join(" ")}`; 16 | }, 17 | "" 18 | ) as string; 19 | 20 | return concatedClasses.trim(); 21 | }; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------