# iwill.dev | Senior Frontend Engineer - Complete Documentation This file contains all documentation concatenated into a single file for easy consumption by LLMs. > Senior Frontend Engineer com mais de 10 anos de experiência em desenvolvimento web. Especialista em arquitetura React (Remix, React Router, Next.js), TypeScript e Design Systems. ## Table of Contents This document includes all content from this project. Each section is separated by a horizontal rule (---) for easy parsing. --- # Redirecting to: /#experiencia URL: https://iwill.dev/curriculo Redirecting to: /#experiencia [Redirecting from /curriculo/ to /#experiencia](/#experiencia) --- # Redirecting to: /en#experiencia URL: https://iwill.dev/en/curriculo Redirecting to: /en#experiencia [Redirecting from /en/curriculo/ to /en#experiencia](/en#experiencia) --- # iwill.dev URL: https://iwill.dev/en > Senior Frontend Engineer with 10+ years of web development experience. I specialize in React architecture (Remix, React Router, Next.js), TypeScript, and Design Systems. FRONTEND ENGINEER REACT/TYPESCRIPT AI/ML ENTHUSIAST ## William**Gonçalves Building solutions that bridge people and technology. Scroll to explore ## About me Hi! I'm William Gonçalves** Senior Front-End Engineer with 10+ years of web development experience, backed by a 20-year professional career in engineering environments. I specialize in React architecture (Remix, React Router, Next.js), TypeScript, and Design Systems. Currently leading front-end architecture at ConstruCode, I have conducted complete platform migrations (.NET Legacy → Next.js → Remix) and established the company's Design System. I focus on technical decisions that directly impact performance, maintainability, and developer experience. In parallel, I'm focusing on **AI Engineering** — developing expertise in Generative AI, RAG, and Agentic AI to integrate intelligent solutions into my work as a software engineer. My background is distinct: I built a strong foundation in Computer Science fundamentals during my early academic studies (2006–2010) before spending 13 years managing critical infrastructure for enterprise clients. This experience instilled a zero-tolerance mindset for failure. A highlight in my career as a creative was creating the first public transportation map of Rio on Moovit, work that was featured in "O Dia" newspaper. ## Experience ### ConstruCode Frontend Engineer • Mar 2023 — Present Leading front-end development for a construction management SaaS platform serving enterprise clients in Brazil's construction industry. - **Platform migration:** Led complete migration to Next.js 13 then Remix/React-Router, implementing MVVM and domain-oriented architecture. - **Design System:** Built and maintain the company Design System using Storybook, Tailwind, and Radix-UI. - **Critical features:** Insights dashboards, revision comparison tools, Gantt planning, and task management. - **CI/CD:** GitLab pipelines for multi-environment deployments on Fly.io. - **Technical expansion:** Backend development with C#/.NET for legacy systems and new APIs. ### Virgo Inc. Frontend Developer • Jul 2022 — Feb 2023 Contributed to rebuilding a back-office application for capital operations management. - **Re-architecture:** Migrating business logic to server layer for improved security. - **Accessibility:** Implementing WCAG standards. - **Performance:** Reduced front-end computational overhead. ### Petlove Frontend Developer • Nov 2021 — Jul 2022 Front-end developer for Petlove's health insurance business unit. - **Sales platform:** Online platform for pet health insurance proposals. - **Landing page:** First landing page for Petlove Saúde. ### Taghos Tecnologia Frontend Developer • Jun 2021 — Nov 2021 Front-end developer for an OTT streaming platform. - Implemented features following Clean Code and SOLID patterns. - Built reusable component architecture. ### Independent Web Developer & Designer • Jun 2014 — Jun 2021 Multidisciplinary consultant bridging visual design and technical implementation. - **End-to-end solutions:** Responsive websites, web apps, and visual identities. - **Design-Engineering:** Unique workflow integrating design principles with front-end engineering. - **Business management:** Independent operations, client negotiations, and project lifecycles. ### Critical Infrastructure (various companies) Technical Consultant / Coordinator • Mar 2006 — Dec 2019 13-year career in critical infrastructure and energy systems, progressing to leadership managing enterprise clients (RIOgaleão, Santander, Petrobras). - **Crisis management:** Problem-solving methodology with zero tolerance for failure. - **Project leadership:** Coordinated technical teams for mission-critical operations. ## Education ### Bachelor's Degree in Artificial Intelligence Faculdade UniBF • Jan 2026 - Dec 2027 Accelerated Bachelor’s Program (Credit Transfer): Admitted via academic transfer, leveraging extensive credits from a previous Information Systems degree (approx. 80% completed). This advanced standing allowed for an immediate focus on core Artificial Intelligence coursework, streamlining the degree completion timeline to 2 years. Key Coursework & Competencies: • Core AI & Data Science: Statistical Modeling, Algorithmic Complexity, Machine Learning, and Advanced Statistical Inference. • Deep Tech: Deep Neural Networks, Cognitive Architectures, Computer Vision, and Natural Language Processing (NLP). • AI Frontiers: Generative AI, Autonomous Systems, and Intelligent Robotics. • Responsible AI: Algorithmic Ethics, Explainable AI (XAI), and System Governance. ### Technologist Degree in Systems Analysis and Development (CST) Descomplica Faculdade Digital • Jan 2025 - Jul 2027 Training in systems development with a focus on modern software engineering and application architecture. ### Bachelor's Degree in Information Systems FEUC - Fundação Educacional Unificada Campograndense • Aug 2006 - Jul 2010 • Status: Completed ~80% of the academic curriculum (Bachelor Degree) • Focus: Strong foundation in IT fundamentals, including Software Engineering, Algorithms, Data Structures and System Analysis. • Note: Studies paused to pursue a full-time career in Critical Infrastructure Engineering. ### Technical High School in Electronics FAETEC - ETE Ferreira Viana • Feb 2003 - Dec 2005 Technical background in electronics, digital systems, and logic — foundations that contribute to my analytical approach in development. ## Certifications Google #### Prompting Essentials Specialization Jan 2026 • ID: LC4NPH24OUW5 branas.io #### Clean Code and Clean Architecture Dec 2023 DIO - Digital Innovation One #### Node.js Development Aug 2020 • ID: F90C5B98 DIO - Digital Innovation One #### Front-end Developer ReactJS Jun 2020 • ID: 21810F13 ## Expertise ### Main Skills React / TypeScript / JavaScript / React Router / Remix / Next.js / Vue.js / Nuxt.js / Tailwind CSS / Sass / Vite / Node.js / Express.js / Jest / Vitest / Git / Figma / Storybook / Pinia / Zustand / jQuery ### AI & Learning Generative AIRAGAgentic AIPrompt EngineeringPythonLangChain ### Secondary Skills PostgreSQLMySQLSQLiteRedisFirebaseSupabaseNestJSDockerKubernetesAmazon Web ServicesGoogle CloudAzureLinuxBashPostmanInsomniaSwaggerC#.NETKotlinElectron > "Building solutions that bridge people and technology." — William Gonçalves ## Posts [TypeScript — January 27, 2026 — 13 minutes of reading 6 TypeScript Habits for AI-Friendly Code](/en/posts/ai-friendly-typescript) [React Router — June 26, 2025 — 25 minutes of reading React Router 7: 'Multiple Actions' in a Single Route](/en/posts/rr7-multiple-actions) [TypeScript — June 19, 2025 — 8 minutes of reading TypeScript Fundamentals with Cars Teaching TypeScript to my autistic s](/en/posts/ensinando-ts-meu-filho-pt1) [See all articles](/en/posts) --- # 6 TypeScript Habits for AI-Friendly Code — iwill.dev URL: https://iwill.dev/en/posts/ai-friendly-typescript > 6 TypeScript Habits for AI-Friendly Code 6 TypeScript Habits for AI-Friendly Code — iwill.dev | Senior Frontend Engineer ** You ask your AI assistant for a simple refactor. It returns code that looks correct but is subtly wrong. It creates routes that don't exist, handles errors that can't happen, or invents impossible state combinations. If your AI doesn't understand your code, maybe your types aren't telling the full story.** We're not just writing code for compilers anymore. We're also writing context for our AI teammates. The more explicit your types are, the less room for hallucinations. Here are 6 strategies to write TypeScript that helps your AI (and your team) work better. --- ### 1. Stop Using Strings for Routes One easy way to break an app is a typo in a route string. When you write router.push('/users/' + id), you're trusting yourself (and the AI) to remember the exact path every time. AI models love to guess here. They often suggest /user/ instead of /users/ or miss a slash. **The problem:** // Ambiguous. The AI has to guess what string goes here. function navigateTo(path: string) { ... } **The solution: Template Literal Types** Define the shape of your routes. This creates a "multiple choice" for the AI instead of an open question. const ROUTES = { HOME: '/', USERS: '/users', USER_DETAIL: '/users/:id', } as const; type AppRoute = typeof ROUTES[keyof typeof ROUTES]; function navigate(route: AppRoute) { /* ... */ } // Usage navigate(ROUTES.USER_DETAIL); **Why it works:** You create a single source of truth. The AI can see all valid routes in one place and stops inventing paths that don't exist. --- ### 2. Kill the Boolean Soup Components with state like isLoading, isError, and isSuccess create impossible states. What happens if isLoading and isError are both true? AI models struggle with this. They might write code that renders data while the loading spinner is still showing. **The solution: Discriminated Unions** Make impossible states actually impossible to write. type DataState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }; function UserList({ state }: { state: DataState }) { switch (state.status) { case 'loading': return ; case 'error': return ; case 'success': return ; } } **Why it works:** The status field forces the AI to handle every case. It can't access data before it's loaded because TypeScript won't allow it. --- ### 3. Treat Primitives Like Domain Objects To TypeScript (and AI), a string is just a string. It doesn't know that userId and email are different things. If you have sendEmail(to: string, from: string), the AI can swap them without noticing. **The solution: Branded Types** Give your primitives semantic meaning. type Brand = K & { __brand: T }; type Email = Brand; type UserId = Brand; function createEmail(value: string): Email { if (!value.includes('@')) throw new Error('Invalid email'); return value as Email; } function sendInvite(to: Email, from: UserId) { /* ... */ } const adminId = 'u-123' as UserId; const userEmail = createEmail('john@example.com'); sendInvite(userEmail, adminId); // // sendInvite(adminId, userEmail); // Type error! --- ### 4. Move Business Logic into Types Comments are invisible to the compiler. AI can read them, but often ignores them. If you write // Price must be positive, the AI might still generate price: -10. **The solution: Smart Constructors** Put the logic into the type creation. type Price = Brand; function createPrice(value: number): Price { if (value < 0) throw new Error('Price must be positive'); return value as Price; } interface Product { name: string; price: Price; // Not just any number } **Why it works:** It forces the AI (and you) to use createPrice to get a valid object. The validation always runs. --- ### 5. Stop Throwing Strings try/catch is opaque. When you call a function, you have no idea what it might throw. AI assistants are bad at guessing error types in catch blocks. They often just write console.log(error) and move on. **The solution: Result Types** Return errors as values. This makes error handling visible in the function signature. type Result = { ok: true; value: T } | { ok: false; error: E }; type FetchError = | { type: 'NetworkError' } | { type: 'NotFound'; id: string }; async function getUser(id: string): Promise> { // implementation returns objects, not throws } // Usage const result = await getUser('123'); if (!result.ok) { // The AI knows exactly which errors to handle switch(result.error.type) { case 'NotFound': /* ... */ case 'NetworkError': /* ... */ } } **Why it works:** No more "what could go wrong?" mystery. The AI can handle all error cases because they're listed in the type. --- ### 6. Bonus: Trust Nothing at the Edge Branded Types (Tip #4) are great for internal logic. But data from outside (APIs, forms) is unpredictable. If you write as User on an API response, you're lying to the compiler. And the AI will believe that lie. **The solution: Runtime Schema Validation** Use libraries like Zod to connect runtime data with static types. import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(['admin', 'user']), }); type User = z.infer; async function fetchUser(id: string) { const data = await fetch(`/api/users/${id}`).then(res => res.json()); return UserSchema.parse(data); // If this passes, the type is real } --- ### Wrapping Up Writing AI-friendly code isn't about simplifying things. It's about being **explicit**. TypeScript is a form of machine-readable documentation. When you use these patterns, you create a contract that both your compiler and your AI partner can understand. Next time your AI hallucinates something weird, ask yourself: did I give it enough context in the types? --- Did you like this content? Share it with your dev friends! See you around! [Ler em Português](/posts/ai-friendly-typescript) [Back to articles](/en/posts) --- # Animating elements when they enter and leave the screen with JavaScript — iwill.dev URL: https://iwill.dev/en/posts/animando-elementos-js > Animating elements when they enter and leave the screen with JavaScript Animating elements when they enter and leave the screen with JavaScript — iwill.dev | Senior Frontend Engineer - ** ## How to test if an element is in the viewport?** There are many ways to do this using JavaScript. This functionality can be useful for animating elements that become visible to the user when they enter the viewport, optimizing the experience and increasing immersion in your application. In this tutorial, I won't focus on animations themselves because I understand it's a topic that's very particular to both the developer and the project. The idea is to show a simple and easy-to-implement alternative, so you can capture an element's position and animate it, whether entering or leaving the window. --- We start with the basic structure (index.html). We'll use a set of 6 random images through an Unsplash API. These images will be animated in two situations: when they "leave" above or below the visible area of the window, the viewport. Document --- Next, we'll add styles in style.css that are just for demonstration, for the body and images: body { padding: 10rem 5rem; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 10rem; background: #121212; overflow-x: hidden; } img { width: 100%; max-width: 600px; height: 400px; object-fit: cover; transition: 0.5s; } --- Finally, still in the styles, we'll create two classes that will be applied for the two possible viewport exits: *.is-down*, which will be applied when the element is below the visible area - *.is-up*, which will be applied when the element is above the visible area Remember that the properties used here are just for demonstration purposes. Feel free to create your own transitions to achieve your expected result. .is-down { transform: translateX(25%); opacity: 0; } .is-up { transform: translateX(-25%); opacity: 0; } --- ## Capture and animate! In script.js, let's start by capturing our list of images using the querySelectorAll method, which will return a list of all images that have the image class: const images = document.querySelectorAll(".image"); --- Next, we capture the window height. Since we want to animate images leaving above and below the visible area, knowing the viewport height is essential to find out whether an element is within the user's visible area or not: let windowHeight = window.innerHeight; We'll create a function to animate the images. It will use the forEach method to loop through the image list and apply the necessary changes. For each image in the list, we'll create a variable called bounding which will be assigned the DOMRect object returned from the getBoundingClientRect() method. This object contains the element's dimensions as well as its coordinates relative to the viewport. The following code shows an example of this object's structure. It won't be part of our example. The property values are in pixels. { bottom: -413.316650390625, ​ height: 400, ​ left: 491.5, ​ right: 1091.5, ​ top: -813.316650390625, width: 600, ​ x: 491.5, ​ y: -813.316650390625 } --- From these coordinates, which will be assigned to the bounding variable, we can determine if an object is within the visible area using the following logic: Since the page's Y-axis starts at the top, this position equals 0. The bottom of the page will equal the height assigned to the windowHeight variable. If bounding.bottom, the image's bottom, is greater than windowHeight, the image is not within the viewport but below the visible area, totally or partially. If bounding.top, the image's top, is less than 0, the image is not within the viewport but above the visible area, totally or partially. From there, we apply the corresponding classes. And if none of the conditions are true, we remove the classes from the image so it has its default appearance, being visible. function animateImages() { images.forEach((image) => { let bounding = image.getBoundingClientRect(); if (bounding.bottom > windowHeight) { image.classList.add("is-down"); } else if (bounding.top < 0) { image.classList.add("is-up"); } else { image.classList.remove("is-up"); image.classList.remove("is-down"); } }); } --- And since we want this effect to be applied during page scrolling, we add a listener that will capture the scroll and execute the animateImages() function. document.addEventListener("scroll", function () { animateImages(); document.removeEventListener("scroll", this); }); --- Additionally, we include a listener that will capture window resizing, assigning the new height to the windowHeight variable. window.addEventListener("resize", function () { windowHeight = window.innerHeight; window.removeEventListener("resize", this); }); --- And to ensure the application already adds classes to images that aren't visible to the user, we execute animateImages() as soon as the application starts. animateImages(); --- You can see the demo [here](https://animate-on-scroll.vercel.app) --- And as I usually say, this is just the starting point. You can explore other possibilities with the DOMRect from getBoundingClientRect(). Just to leave you with another possible scenario in this example, if you want an element to only go through a transition when it's completely out of the viewport, you can change the conditionals to when bounding.bottom (element's bottom) is less than 0 (completely left, above), or bounding.top (element's top) is greater than windowHeight (completely left, below). You can also add safe areas so your element stays visible as long as needed. You can apply classes when it's, for example, 10% from the end of the screen, above or below. Infinite possibilities that will depend on what you intend to do with your elements. --- If you enjoyed this content, share it with others and help spread the word! --- See you next time! [Ler em Português](/posts/animando-elementos-js) [Back to articles](/en/posts) --- # TypeScript Fundamentals with Cars — iwill.dev URL: https://iwill.dev/en/posts/ensinando-ts-meu-filho-pt1 > Teaching TypeScript to my autistic son (pt 1) TypeScript Fundamentals with Cars — iwill.dev | Senior Frontend Engineer ### Pedro is an autistic boy, now 8 years old. He told me he wants to learn programming to work with daddy, and I decided to start this series in his honor. He has some hyperfocuses, but the main one is cars. So that will be the theme of this series, explaining TypeScript fundamentals and concepts applied to cars and their features. Starting with primitive and special types: ## number It's the type of information returned from dashboard instruments, like the speedometer. It will always be numbers and nothing else. If missing, we can't see how fast the car is going. function readSpeed(): number {} // 80 function readTotalMileage(): number {} // 230000 ## string It's information that contains letters and numbers. It can be the car's license plate, or even its make and model. If missing, nobody knows which car it is. function readLicensePlate(): string {} // 'ABC-1Z34' function readMakeAndModel(): string {} // 'Chevrolet Classic' ## boolean It's simple information, true or false. It serves, for example, to know if a part of the car is working or not. If missing, we don't know if the car works. function isEngineRunning(): boolean {} // true | false ## null It's like the luggage in the trunk before a trip. It's not there yet, but we'll put it there later. If missing, we might forget something important. let luggage: string | null = null; ## undefined It's information that nobody decided what it is. Like an empty space in the car console, which could be used to install something. It's missed if we decide to use it and there's nothing there. let consoleAccessory; /* We don't know what it is because nothing was assigned to the space */ ## symbol Stores things with the same name, without confusion. You want to put stickers on your car, but they should go in different places. Some even a bit hidden. But you know where you put them and can find them whenever you want. const sticker1 = Symbol("sticker"); const sticker2 = Symbol("sticker"); const car = { sticker: "sticker on hood", [sticker1]: "sticker under seat", [sticker2]: "sticker on wheel arch" } console.log(Object.values(car)) // sticker locations: ['sticker on hood'] console.log(car[sticker1]) // 'sticker under seat' console.log(car[sticker2]) // 'sticker on wheel arch' ## any It's the crazy junk drawer. It can store anything: lunchbox, tool, ball, tire, rock, paper, scissors... It's bad, because we never know what's in there. let junkDrawer: any = "broom"; junkDrawer = 22; junkDrawer = null; junkDrawer = false; ## unknown It's like the car's glove compartment. There's something in there, but you need to check what it is. Safer than any, because you just don't know what it is. After checking, you can use it freely. let gloveboxItem: unknown = "car manual"; function readManual(manual: string) { /* ... */ } if (typeof gloveboxItem === "string") { readManual(gloveboxItem) } ## never Appears when something breaks or gets out of control. Like when the car engine doesn't work. We normally don't use it, but we can apply it when we know something will go wrong. function startBrokenEngine(): never { throw new Error("Kaboom!"); } ## void Like opening the car door, the trunk, or even the glove compartment. It's used in actions that are necessary to use the car, but don't return any information. function openCarDoor(): void {} --- What else do you want to learn about TypeScript with cars? This series will continue soon... --- Like, share, and follow me on [social media](https://www.iwill.dev/links) for more content. [Ler em Português](/posts/ensinando-ts-meu-filho-pt1) [Back to articles](/en/posts) --- # Smart Generic Functions — iwill.dev URL: https://iwill.dev/en/posts/funcoes-genericas-inteligentes > Smart Generic Functions Smart Generic Functions — iwill.dev | Senior Frontend Engineer - What makes a lib feel "magical"? Type inference! And you can use this today! ## Reusable functions are the heart of any project or lib Imagine we need to return a user's name and age. The quickest solution would be this: type User = { id: string; name: string; age: number; email: string; } function getNameAndAge(user: User): { name: string; age: number } { return { name: user.name, age: user.age, }; } const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = getNameAndAge(user); // value: { "name": "William", "age": 36 } // type: { name: string, age: number } ## And what's the problem with this? The function only serves for this purpose - You probably won't use it again - It infers a type unrelated to the original - Or you'll declare a new type for the return ## The magic of generics + inference Let's create a pick function that accepts as parameters: (a) an object and (b) an array of keys corresponding to it. As the function's return, a "partial" of the original type. function pick( obj: T, keys: K[] ): Pick { const result = {} as Pick; for (const key of keys) { result[key] = obj[key]; } return result; } ## The result? A safe, precise, flexible, and reusable implementation, maintaining reference to the original type const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = pick(user, ["name", "age"]) // same value : { "name": "William", "age": 36 } // type related to original: Pick ## From there, you go to infinity and beyond - An omit function, to remove properties from an object type OmitKeys = { [P in keyof T as P extends K ? never : P]: T[P]; }; function omit( obj: T, keys: K[] ): OmitKeys { const result = { ...obj }; keys.forEach(key => delete result[key]); return result; } const userWithoutId = omit(user, ["id"]) /* value: { "name": "William", "age": 36, "email": "iwilldev@outlook.com.br" } */ // type: OmitKeys - A merge, to combine two different objects // const user: User = { ... } type Role = { role: 'admin' | 'editor' | 'viewer'; permissions: Array<'read' | 'write' | 'delete' | 'update'>; }; const role: Role = { role: 'viewer', permissions: ['read'] } const userWithRole = merge(user, role) /* value: { "id": "1234", "name": "William", "age": 36, "email": "iwilldev@outlook.com.br", "role": "viewer", "permissions": ["read"] } */ // type: User & Role ## The possibilities are endless With generics + inference, you write: - Safer code - Smarter helpers - More elegant implementations And of course: many libs bring functions like these. But do you need an entire lib to solve a specific problem? So master your code, embracing the power of TypeScript and everything it can do for your project! --- ## Which function did you like the most? Tell me in the comments! Save, share, and follow for more content [Ler em Português](/posts/funcoes-genericas-inteligentes) [Back to articles](/en/posts) --- # Posts — iwill.dev URL: https://iwill.dev/en/posts > Articles about web development, TypeScript, React and more. ## Posts Thoughts, tutorials and findings about web development, TypeScript, React and whatever else I'm learning. [TypeScript — January 27, 2026 — 13 minutes of reading 6 TypeScript Habits for AI-Friendly Code](/en/posts/ai-friendly-typescript) [React Router — June 26, 2025 — 25 minutes of reading React Router 7: 'Multiple Actions' in a Single Route](/en/posts/rr7-multiple-actions) [TypeScript — June 19, 2025 — 8 minutes of reading TypeScript Fundamentals with Cars Teaching TypeScript to my autistic s](/en/posts/ensinando-ts-meu-filho-pt1) [TypeScript — June 18, 2025 — 7 minutes of reading Smart Generic Functions](/en/posts/funcoes-genericas-inteligentes) [JavaScript — December 22, 2022 — 29 minutes of reading Intersection Observer - Lazy loading, animations, and infinite sc](/en/posts/intersection-observer) [JavaScript — January 14, 2021 — 13 minutes of reading Animating elements when they enter and leave the screen with JavaS](/en/posts/animando-elementos-js) [CSS — January 3, 2021 — 8 minutes of reading Changing screen theme with Pure CSS (Dark/Light Mode)](/en/posts/mudando-tema-com-css-puro) [JavaScript — November 26, 2020 — 8 minutes of reading 'Magic' text automatically typed with JavaScript](/en/posts/texto-magico-js) --- # Intersection Observer - Lazy loading, animations, and infinite scroll without libs — iwill.dev URL: https://iwill.dev/en/posts/intersection-observer > Intersection Observer - Lazy loading, animations, and infinite scroll without libs Intersection Observer - Lazy loading, animations, and infinite scroll without libs — iwill.dev | Senior Frontend Engineer - ** What's up, devs! This post starts a series aimed at exploring the [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API), discovering and presenting functionalities that can be achieved through them. And considering our habit of using abstractions that bring the same result, we want to empower native options in order to reduce dependencies in projects and deepen our knowledge about the resources available on the Web. --- As a *Front-Ender*, I've stumbled upon some challenges to increase page interactivity with *infinite scrollings* and element animations when they enter and leave the *viewport*, or even performance issues like *lazy-loading* images, based on user actions. In cases like this, everything would come down to checking the intersection between a target element and a parent element or even between it and the document's *viewport* (visible area to the user) and, based on the observed target's state and visibility, apply the necessary changes. Detecting the visibility of an element (or between two of them) involved not very reliable solutions that tended to cause performance problems on pages, since we needed *handlers* and *loops* applied to each affected element and calling methods like Element.getBoundingClientRect(), which created a burden on the application's *main thread*, making the page and the browser itself slower. --- ## Concepts and usage The Intersection Observer API** provides a way to asynchronously observe intersection changes. With its implementation, the site no longer needs to handle this responsibility on the *main thread* and the browser is free to manage observations as it sees fit. It's possible to declare a *callback* function that is executed in the following circumstances: A target element crosses (totally or partially, according to configuration) with the root element. - The first time the *Observer* is asked to observe a target element. This API has full compatibility with all modern browsers, with caveats for **Safari** (Desktop and iOS) and **Firefox for Android** where the root element cannot be a document. --- ## Creating an Intersection Observer To create an intersection observer you must call its constructor, sending a *callback* function as the first parameter and an options object as the (optional) next parameter: let options = { root: document.querySelector('#rootElement'), rootMargin: '0px', threshold: 1.0 } let observer = new IntersectionObserver(callback, options); ### Intersection observer options The options object passed to the IntersectionObserver() constructor allows you to control the circumstances in which the *callback* function will be executed: - root - A specified ancestor element or the *viewport* itself, in the absence of a declared element or if the value is null. - rootMargin - Defines the margin limits of the **root** element, increasing or decreasing the delimitation of this element, before computing an intersection. It can have values similar to CSS, like "10px 20px 30px 40px" (top, right, bottom, left). - threshold - The *intersection ratio*, which represents the percentage of visibility of the target element relative to the **root**: a value between 0.0 and 1.0. The *callback* will be executed whenever the target's visibility exceeds the declared value, up or down. It can be declared as: A number. Ex: 0.5. *Callback* executed when visibility exceeds 50%. - An Array of numbers: Ex: [0, 0.25, 0.5, 0.75, 1]. The *callback* will be executed at each percentage related to the declared values. In this case, every 25% of visibility. ### Declaring an element to be observed Now that you've created the observer, you need to declare an element to be observed by it: let target = document.querySelector('#targetElement'); observer.observe(target); At this moment, the *callback* is executed for the first time, even if the target element is not visible. Whenever the target's visibility exceeds the threshold value, the *callback* is invoked, receiving a list of IntersectionObserverEntry objects and the observer itself. Be aware that this *callback* itself will be executed on the *main thread*. So try not to complicate the logic executed in this scope: let callback = (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { /* We check the 'entry' state and make the necessary changes if it's visible */ } }); }; Most applications of this *Observer* can be done by just checking the isIntersecting property of the entry, which returns a *boolean* indicating whether the target element is, or is not, crossing with the root element, considering the parameters declared in the options object. To see more properties of the IntersectionObserverEntry interface, check the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). Assuming we have the necessary foundation to move forward, let's go to the use cases. --- ## Files used You can use the [repository](https://github.com/owillgoncalves/intersection-observer) for this article with the final files divided into folders for each case. --- ## Lazy-loading Imagine loading all the assets of an entire page and the user not even viewing them, because they decided to navigate to another page. It becomes a waste of resources for them who, in the case of being on a mobile network, consumed data for nothing, and for you who needed to serve files that weren't actually used. Based on this, let's create a page where images will only be loaded if they are visible. Starting with the index.html file: Lazy Loading
In the img tags, we declare a [*placeholder*](https://owillgoncalves.github.io/intersection-observer/01-lazy-loading/placeholder.png) in the src attribute, which will be rendered initially. In the data-src attribute, we put the desired image URL. Additionally, we declare the lazy class which will be used to select the images. We season with style.css: * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } section { height: 100%; width: 100%; align-items: center; display: flex; justify-content: center; } Now we need to observe the images and, when they are visible, swap the placeholder for the desired URL. In the script.js file: We start by selecting the images. const images = document.querySelectorAll('.lazy'); We create our *Observer*. const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const image = entry.target; image.src = image.dataset.src; image.classList.remove('lazy'); observer.unobserve(image); } }); }); Inside the *callback*, we use forEach on the entries and for each entry we check if it's crossing the visible area (entry.isIntersecting). If positive, we declare the entry.target as image, replace the src with data-src, remove the lazy class from the image and tell the observer to stop observing the image. Next, we use a forEach on the NodeList generated with our sel --- # Changing screen theme with Pure CSS (Dark/Light Mode) — iwill.dev URL: https://iwill.dev/en/posts/mudando-tema-com-css-puro > Changing screen theme with Pure CSS (Dark/Light Mode) Changing screen theme with Pure CSS (Dark/Light Mode) — iwill.dev | Senior Frontend Engineer ## *We know the JS way* ### *But what if we don't use scripts to switch the theme of our applications?* The path is a relationship between cascade and well-specified selectors. Let's start from the beginning: --- #### HTML The first element of the tree will be a checkbox input. Its sibling below will be the container of our application. It's the one that will have the styles changed for the theme switch. Inside it, we'll have a label related to our input above, inside a div that will be its transition area, serving as our button to change the theme.

Changing theme with Pure CSS

The text contrasts with the background

--- #### CSS In the styles, we apply the resets and declare the variables for the colors used in the theme: * { margin: 0; padding: 0; box-sizing: border-box; } :root { --light: #cccccc; --dark: #151515; } --- We make our input invisible, since we'll use its label as the trigger. #theme-switcher { display: none; } --- And we declare the properties of our app container. It will occupy the entire screen, have a light background and dark texts, as well as being a flex-container. The latter is optional and just to facilitate the demonstration of the result, centering the text on the screen. #app-container { height: 100vh; background: var(--light); color: var(--dark); font-family: monospace; font-size: 1.5rem; transition: 0.3s; display: flex; flex-direction: column; align-items: center; justify-content: center; } --- We declare the area where our button will slide, with absolute positioning at the top: .theme-switcher-area { border: 1px solid var(--light); background: var(--dark); border-radius: 2rem; width: 4.5rem; height: 2.5rem; padding: 0.2rem; position: absolute; top: 0.5rem; right: 0.5rem; } --- The button itself, which will use the dashed border style, creating an effect similar to sun rays, for the light theme. .theme-switcher-button { position: relative; display: block; background: #f1aa02; border-radius: 50%; width: 2rem; height: 2rem; border: 2px dashed var(--dark); transition: 0.3s; } --- And finally, an ::after pseudo-element over the button. It will have the shape of a smaller circle than the original element, becoming a shadow that will transform the trigger into a moon, in the dark theme. Therefore, its initial opacity will be 0. .theme-switcher-button::after { position: absolute; width: 80%; height: 80%; content: ""; background: var(--dark); border-radius: 50%; opacity: 0; transition: 0.3s; } --- ## And here comes the magic! Since our input is the first element of the tree, we can use the ':checked' pseudo-class, with the appropriate selectors, to change the style of any element below it. When it's selected, these properties will be applied. Starting with the trigger itself, transforming the sun into a moon. To do this, we remove the border that came to give the rays effect and move the button to the right. #theme-switcher:checked + #app-container .theme-switcher-button { transform: translateX(100%); border: none; } --- Next, we change the shadow's opacity, the ::after, to generate a crescent moon, in the button change. #theme-switcher:checked + #app-container .theme-switcher-button::after { opacity: 1; } --- Finally and for the desired effect, we invert the background color and text of our app container: #theme-switcher:checked + #app-container { background: var(--dark); color: var(--light); } --- ### And there it is, where the owl sleeps! --- This tutorial is just the beginning of the dive. So use your creativity from this base and change the styles as you see fit! --- If you enjoyed this content, share it with others and help spread the word! --- See you next time! [Ler em Português](/posts/mudando-tema-com-css-puro) [Back to articles](/en/posts) --- # React Router 7: 'Multiple Actions' in a Single Route — iwill.dev URL: https://iwill.dev/en/posts/rr7-multiple-actions > React Router 7: React Router 7: 'Multiple Actions' in a Single Route — iwill.dev | Senior Frontend Engineer This is perhaps the most common misconception for those unfamiliar with React Router as a framework (and the same goes for Remix): > I can only have one action per route. I believe this confusion arises mainly from comparisons with Next.js, its Server Actions, and even the way API routes are declared in it. Many developers see React Router's loader and action functions as "endpoints," functions with single responsibilities, when in reality, their true role goes far beyond that. loader and action, together, are like a complete Controller, where we can define different fetches and distinct mutations, each with its own purpose. And that's part of what we'll study here, by simulating a user CRUD: three different ways to handle multiple actions in a single React Router route. And if you use Remix up to version 2 (pre-React Router 7), the same approaches should work for you. --- ### Using the Form component and default behavior This is the most traditional approach. We use the
component and differentiate actions through the method attribute. In a route, create the loader: export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; Create your component with different forms, one for each different method/action: import { Form, /* ... */ } from "react-router"; /* ... */ export default function UI({ loaderData: { users } }: Route.ComponentProps) { return (

Users CRUD (Form - default behavior)

Add User

{/* Form with method="post" to create a user */}

Users

{users.map((user: User) => (
{/* Form with method="put" to edit a user */}
Role:
{/* Form with method="delete" to delete a user */}
))}

); } In the same route, define the action. We use await request.formData() to read the data and get request.method to differentiate between POST, PUT, and DELETE. export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const method = request.method.toLowerCase(); switch (method) { case "post": { const name = formData.get("name") as string; const email = formData.get("email") as string; return await createUser({ name, email }); } case "put": { const id = formData.get("id") as string; const name = formData.get("name") as string; const email = formData.get("email") as string; return await updateUser(id, { name, email }); } case "delete": { const id = formData.get("id") as string; return await deleteUser(id); } default: throw new Response("Method not allowed", { status: 405, statusText: "Method Not Allowed", }); } }; And that's it! This approach is robust, works without JavaScript, and follows the principle of Progressive Enhancement. Its disadvantage (compared to the other options) is that the number of mutations is limited to the standard HTTP methods. --- ### With useSubmit and JSON - actions defined in the submitted data In this method, we will use the useSubmit hook to send a JSON with a property called intent, which will define which action we want to perform. The first advantage here is that we gain the flexibility for custom mutations, outside the standard method patterns of the
component. We'll use the same loader as the previous example: export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; In the route's component, we define event handlers to deal with user actions and trigger the corresponding mutation. The main advantage here, for example, is being able to separate the user role editing from the other properties into a distinct action. export default function UI({ loaderData: { users }, }: Route.ComponentProps) { const submit = useSubmit(); // Event handler to create a user const handleCreate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "createUser", payload: { name, email } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); event.currentTarget.reset(); }; // Event handler to edit a user const handleUpdate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const userId = event.currentTarget.getAttribute("data-user-id"); if (userId) { const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "updateUser", payload: { id: userId, name, email }, }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler to change the user's role const handleChangeRole = (event: React.ChangeEvent) => { const userId = event.target.closest("form")?.getAttribute("data-user-id"); if (userId) { const role = event.target.value as "admin" | "user"; const data = { intent: "changeUserRole", payload: { id: userId, role } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler to delete a user const handleDelete = (userId: string) => { if (confirm("Are you sure you want to delete this user?")) { const data = { intent: "deleteUser", payload: { id: userId } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; return (

Users CRUD (JSON API)

Add User

{/* Form to create a user */}

Users

{users.map((user: User) => ( {/* Form to edit a user */}
Role:
{/* Event that triggers the role change handler */} {/* Click event to delete a user */}
))}

); } In the same route, define the action. We use await request.json() to read the data and get the intent property from the body to address the mutation: export const action = async ({ request }: Act --- # 'Magic' text automatically typed with JavaScript — iwill.dev URL: https://iwill.dev/en/posts/texto-magico-js 'Magic' text automatically typed with JavaScript — iwill.dev | Senior Frontend Engineer ** > "Blast, heretic! Are you telling me that the writings will appear by themselves? Isn't this sorcery, or witchcraft?" It's not magic. It's JavaScript. Let's break it down below: --- First of all, we need to create, in our HTML, an element to receive the spell, I mean, the created text. It can be a paragraph (p**) or even a heading (**h1**, **h2**...). It just needs to be a text element and have an **id**. Remember that the **id** needs to be exclusive to that element.

For our case, we'll use an **h1** with the id **magic-text**. --- Next, we create and import the JavaScript file which, for our example, will be **script.js**: --- Now in **script.js**, let's create a constant to interact with our **h1**, using the **querySelector** method, which allows us to select elements using the same selectors we see in CSS. In our case, we'll use the **id** preceded by **#**. const magicTextHeader = document.querySelector('#magic-text'); The **querySelector** method can be used both on the document and on other elements, after being declared, selecting their respective children. --- Next, we create a constant with the text to be used: const text = 'Text inserted automagically with JavaScript!'; --- Finally, we declare a variable that will help us "traverse" the text: let indexCharacter = 0; --- The function that will return the text is **writeText()**: function writeText() { magicTextHeader.innerText = text.slice(0, indexCharacter); indexCharacter++; if(indexCharacter > text.length - 1) { setTimeout(() => { indexCharacter = 0; }, 2000); } } In the first line, we include the text in the **innerText** property of the **h1**, using the **.slice()** method, which will traverse our **text** constant, letter by letter, as if it were an **array**. The **.slice()** syntax is .slice(a,b), where **a** is the initial key of the segment to be returned and **b** is the final key of that same segment. Since we want to return the text from the beginning, we start with key 0 and end with the value of **indexCharacter**, which is incremented in the following line, ensuring that the next execution of the function will return one more character and so on. Next, we use a conditional to check if **indexCharacter** is equal to the last position of the text (text.length - 1; since the first key is 0, the last will be the size (length) of the text minus 1). If the condition is true, **indexCharacter** will be reset to zero, after a **setTimeout** of 2000 milliseconds, making the text start being "typed" from the beginning again. --- And to execute this function continuously, ensuring the increment of **indexCharacter** and the desired effect for our text, we use a **setInterval** that will execute the **writeText** function every 100 milliseconds: setInterval(writeText, 100); --- And the magic is complete! --- You can see an example [here](https://g31-magic-text.vercel.app/). And check out my version of the code [here](https://github.com/williammago/goodbye.31/tree/main/28%20-%20Auto%20Write%20Text%20com%20JavaScript). --- And, optionally, use the styles I used there: * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; background: darkred; color: #FFF; } h1 { font-size: 2rem; max-width: 400px; text-align: center; } --- This article was inspired by a [video](https://www.youtube.com/watch?v=8GPPJpiLqHk) from [Florin Pop's channel](https://www.youtube.com/channel/UCeU-1X402kT-JlLdAitxSMA), which has amazing tutorials and challenges for those who are starting out. Content in English. --- See you next time! Big hug! [Ler em Português](/posts/texto-magico-js) [Back to articles](/en/posts) --- # Redirecting… URL: https://iwill.dev/feed/ai-friendly-typescript Redirecting… Redirecting to [/posts/ai-friendly-typescript](/posts/ai-friendly-typescript)… --- # Redirecting… URL: https://iwill.dev/feed/animando-elementos-js Redirecting… Redirecting to [/posts/animando-elementos-js](/posts/animando-elementos-js)… --- # Redirecting… URL: https://iwill.dev/feed/ensinando-ts-meu-filho-pt1 Redirecting… Redirecting to [/posts/ensinando-ts-meu-filho-pt1](/posts/ensinando-ts-meu-filho-pt1)… --- # Redirecting… URL: https://iwill.dev/feed/funcoes-genericas-inteligentes Redirecting… Redirecting to [/posts/funcoes-genericas-inteligentes](/posts/funcoes-genericas-inteligentes)… --- # Redirecting to: /posts URL: https://iwill.dev/feed Redirecting to: /posts [Redirecting from /feed/ to /posts](/posts) --- # Redirecting… URL: https://iwill.dev/feed/intersection-observer Redirecting… Redirecting to [/posts/intersection-observer](/posts/intersection-observer)… --- # Redirecting… URL: https://iwill.dev/feed/mudando-tema-com-css-puro Redirecting… Redirecting to [/posts/mudando-tema-com-css-puro](/posts/mudando-tema-com-css-puro)… --- # Redirecting… URL: https://iwill.dev/feed/porque-eu-amo-remix Redirecting… Redirecting to [/posts/porque-eu-amo-remix](/posts/porque-eu-amo-remix)… --- # Redirecting… URL: https://iwill.dev/feed/rr7-multiple-actions Redirecting… Redirecting to [/posts/rr7-multiple-actions](/posts/rr7-multiple-actions)… --- # Redirecting… URL: https://iwill.dev/feed/texto-magico-js Redirecting… Redirecting to [/posts/texto-magico-js](/posts/texto-magico-js)… --- # iwill.dev URL: https://iwill.dev > Senior Frontend Engineer com mais de 10 anos de experiência em desenvolvimento web. Especialista em arquitetura React (Remix, React Router, Next.js), TypeScript e Design Systems. FRONTEND ENGINEER REACT/TYPESCRIPT AI/ML ENTHUSIAST ## William**Gonçalves Construindo soluções que aproximam pessoas e tecnologias. Scroll to explore ## Sobre mim Oi! Eu sou o William Gonçalves** Senior Front-End Engineer com mais de 10 anos de experiência em desenvolvimento web, respaldado por uma carreira de 20 anos em ambientes de engenharia. Especialista em arquitetura React (Remix, React Router, Next.js), TypeScript e Design Systems. Atualmente liderando a arquitetura front-end na ConstruCode, conduzi migrações completas de plataforma (.NET Legacy → Next.js → Remix) e estabeleci o Design System da empresa. Meu foco está em decisões técnicas que impactam diretamente performance, manutenibilidade e developer experience. Em paralelo, tenho concentrado meus estudos em **AI Engineering** — desenvolvendo expertise em Generative AI, RAG e Agentic AI para agregar soluções inteligentes ao meu trabalho como engenheiro de software. Minha trajetória é diferenciada: construí uma base sólida em fundamentos de Ciência da Computação durante meus estudos acadêmicos (2006-2010), antes de passar 13 anos gerenciando infraestrutura crítica para clientes enterprise. Essa experiência me proporcionou uma mentalidade de tolerância zero a falhas. Um destaque na carreira criativa foi a criação do primeiro mapa de transporte público do Rio no Moovit, trabalho que foi matéria no "Jornal O Dia". ## Experiência ### ConstruCode Frontend Engineer • mar de 2023 — o momento Lidero o desenvolvimento front-end de uma plataforma SaaS de gestão de obras que atende clientes enterprise no mercado de construção civil brasileiro. - **Migração completa da plataforma:** Conduzi a migração para Next.js 13 e posteriormente para Remix/React-Router, implementando arquitetura MVVM no cliente e arquitetura orientada a domínio no servidor. - **Design System:** Construí e mantenho o Design System da empresa usando Storybook, Tailwind e Radix-UI. - **Features críticas:** Dashboards de insights, ferramentas de comparação de revisões, planejamento com Gantt e sistemas de gestão de tarefas. - **CI/CD:** Pipelines GitLab para deploys multi-ambiente na infraestrutura Fly.io. - **Expansão técnica:** Desenvolvimento backend com C#/.NET para manutenção de sistemas legados e novas APIs. ### Virgo Inc. Frontend Developer • jul de 2022 — fev de 2023 Contribuí para a reconstrução de uma aplicação de back-office para gestão de operações de capital. - **Re-arquitetura:** Migração de lógica de negócio para a camada servidor, melhorando segurança e escalabilidade. - **Acessibilidade:** Implementação de padrões WCAG. - **Performance:** Redução de overhead computacional no front-end. ### Petlove Frontend Developer • nov de 2021 — jul de 2022 Desenvolvedor front-end na unidade de planos de saúde da Petlove. - **Plataforma de vendas:** Plataforma online para emissão de propostas de planos de saúde para pets. - **Landing page:** Primeira landing page da Petlove Saúde. ### Taghos Tecnologia Frontend Developer • jun de 2021 — nov de 2021 Desenvolvedor front-end para uma plataforma de streaming OTT. - Features seguindo princípios Clean Code e padrões SOLID. - Arquitetura de componentes reutilizáveis. ### Independente Web Developer & Designer • jun de 2014 — jun de 2021 Consultor multidisciplinar, fazendo a ponte entre design visual e implementação técnica. - **Soluções end-to-end:** Websites responsivos, web apps e identidades visuais. - **Design-Engenharia:** Workflow integrando design gráfico com engenharia front-end. - **Gestão:** Operações de negócio, negociações e ciclos de projeto independentes. ### Infraestrutura Crítica (diversas empresas) Consultor Técnico / Coordenador • mar de 2006 — dez de 2019 13 anos em infraestrutura crítica e sistemas de energia, de estagiário a liderança gerenciando clientes enterprise (RIOgaleão, Santander, Petrobras). - **Gestão de crises:** Metodologia de resolução de problemas com zero tolerância a falhas. - **Liderança de projetos:** Coordenação de equipes técnicas para operações de missão crítica. ## Formação ### Bacharelado em Inteligência Artificial Faculdade UniBF • Jan 2026 - Dez 2027 Programa de Bacharelado Acelerado (Transferência Externa): Admitido via transferência acadêmica, aproveitando os créditos de uma graduação anterior em Sistemas de Informação (aprox. 80% concluída). Essa equivalência permitiu um foco imediato nas disciplinas centrais de Inteligência Artificial, reduzindo o tempo de conclusão para 2 anos. Principais Competências: • Core AI & Data Science: Modelagem Estatística, Complexidade Algorítmica, Machine Learning e Inferência Estatística Avançada. • Deep Tech: Redes Neurais Profundas, Arquiteturas Cognitivas, Visão Computacional e Processamento de Linguagem Natural (NLP). • Fronteiras da IA: IA Generativa, Sistemas Autônomos e Robótica Inteligente. • IA Responsável: Ética Algorítmica, IA Explicável (XAI) e Governança de Sistemas. ### Tecnólogo em Análise e Desenvolvimento de Sistemas (CST) Descomplica Faculdade Digital • Jan 2025 - Jul 2027 Formação em desenvolvimento de sistemas com foco em engenharia de software moderna e arquitetura de aplicações. ### Bacharelado em Sistemas de Informação FEUC - Fundação Educacional Unificada Campograndense • Ago 2006 - Jul 2010 • Status: Cerca de 80% da grade acadêmica concluída (Bacharelado). • Foco: Forte base nos fundamentos de TI, incluindo Engenharia de Software, Algoritmos, Estruturas de Dados e Análise de Sistemas. • Nota: Estudos pausados para iniciar a carreira em Engenharia de Infraestrutura Crítica. ### Técnico em Eletrônica FAETEC - ETE Ferreira Viana • Fev 2003 - Dez 2005 Base técnica em eletrônica, sistemas digitais e lógica — fundamentos que contribuem para minha abordagem analítica no desenvolvimento. ## Certificados Google #### Prompting Essentials Specialization Jan 2026 • ID: LC4NPH24OUW5 branas.io #### Clean Code and Clean Architecture Dez 2023 DIO - Digital Innovation One #### Node.js Development Ago 2020 • ID: F90C5B98 DIO - Digital Innovation One #### Front-end Developer ReactJS Jun 2020 • ID: 21810F13 ## Expertise ### Principais Skills React / TypeScript / JavaScript / React Router / Remix / Next.js / Vue.js / Nuxt.js / Tailwind CSS / Sass / Vite / Node.js / Express.js / Jest / Vitest / Git / Figma / Storybook / Pinia / Zustand / jQuery ### IA & Aprendizado Generative AIRAGAgentic AIPrompt EngineeringPythonLangChain ### Outras Skills PostgreSQLMySQLSQLiteRedisFirebaseSupabaseNestJSDockerKubernetesAmazon Web ServicesGoogle CloudAzureLinuxBashPostmanInsomniaSwaggerC#.NETKotlinElectron > "Construindo soluções que aproximam pessoas e tecnologias." — William Gonçalves ## Posts [TypeScript — 27 de janeiro de 2026 — 13 minutos de leitura 6 Hábitos de TypeScript para Código AI-Friendly](/posts/ai-friendly-typescript) [React Router — 26 de junho de 2025 — 25 minutos de leitura React Router 7: 'Múltiplas Actions' em uma única rota](/posts/rr7-multiple-actions) [TypeScript — 19 de junho de 2025 — 8 minutos de leitura Fundamentos TypeScript com carros Ensinando TypeScript para o me](/posts/ensinando-ts-meu-filho-pt1) [Ver todos os artigos](/posts) --- # 6 Hábitos de TypeScript para Código AI-Friendly — iwill.dev URL: https://iwill.dev/posts/ai-friendly-typescript > 6 Hábitos de TypeScript para Código AI-Friendly 6 Hábitos de TypeScript para Código AI-Friendly — iwill.dev | Senior Frontend Engineer ** Você pede um refactor simples pro seu assistente de IA. Ele retorna um código que parece correto, mas está sutilmente errado. Cria rotas que não existem, trata erros que não podem acontecer, ou inventa combinações de estado impossíveis. Se a IA não entende seu código, talvez seus tipos não estejam contando a história completa.** Não estamos mais escrevendo código só para compiladores. Estamos escrevendo contexto para nossos assistentes de IA também. Quanto mais explícitos seus tipos forem, menos espaço pra alucinações. Aqui estão 6 estratégias pra escrever TypeScript que ajuda sua IA (e seu time) a trabalhar melhor. --- ### 1. Pare de Usar Strings para Rotas Uma forma fácil de quebrar uma aplicação é um typo numa string de rota. Quando você escreve router.push('/users/' + id), está confiando em si mesmo (e na IA) pra lembrar o caminho exato toda vez. Modelos de IA adoram chutar aqui. Frequentemente sugerem /user/ ao invés de /users/ ou esquecem uma barra. **O problema:** // Ambíguo. A IA tem que adivinhar qual string vai aqui. function navigateTo(path: string) { ... } **A solução: Template Literal Types** Defina o formato das suas rotas. Isso cria uma "múltipla escolha" pra IA ao invés de uma pergunta aberta. const ROUTES = { HOME: '/', USERS: '/users', USER_DETAIL: '/users/:id', } as const; type AppRoute = typeof ROUTES[keyof typeof ROUTES]; function navigate(route: AppRoute) { /* ... */ } // Uso navigate(ROUTES.USER_DETAIL); **Por que funciona:** Você cria uma fonte única da verdade. A IA consegue ver todas as rotas válidas em um lugar só e para de inventar caminhos que não existem. --- ### 2. Mate a Sopa de Booleanos Componentes com estado tipo isLoading, isError e isSuccess criam estados impossíveis. O que acontece se isLoading e isError forem true ao mesmo tempo? Modelos de IA sofrem com isso. Podem escrever código que renderiza dados enquanto o spinner de loading ainda está aparecendo. **A solução: Discriminated Unions** Faça estados impossíveis serem realmente impossíveis de escrever. type DataState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }; function UserList({ state }: { state: DataState }) { switch (state.status) { case 'loading': return ; case 'error': return ; case 'success': return ; } } **Por que funciona:** O campo status força a IA a tratar cada caso. Ela não consegue acessar data antes de estar carregado porque o TypeScript não permite. --- ### 3. Trate Primitivos como Objetos de Domínio Pro TypeScript (e pra IA), uma string é só uma string. Não sabe que userId e email são coisas diferentes. Se você tem sendEmail(to: string, from: string), a IA pode trocar eles sem perceber. **A solução: Branded Types** Dê significado semântico aos seus primitivos. type Brand = K & { __brand: T }; type Email = Brand; type UserId = Brand; function createEmail(value: string): Email { if (!value.includes('@')) throw new Error('Email inválido'); return value as Email; } function sendInvite(to: Email, from: UserId) { /* ... */ } const adminId = 'u-123' as UserId; const userEmail = createEmail('john@example.com'); sendInvite(userEmail, adminId); // // sendInvite(adminId, userEmail); // Erro de tipo! --- ### 4. Mova Lógica de Negócio para os Tipos Comentários são invisíveis pro compilador. A IA consegue ler, mas frequentemente ignora. Se você escreve // Preço deve ser positivo, a IA ainda pode gerar price: -10. **A solução: Smart Constructors** Coloque a lógica na criação do tipo. type Price = Brand; function createPrice(value: number): Price { if (value < 0) throw new Error('Preço deve ser positivo'); return value as Price; } interface Product { name: string; price: Price; // Não é qualquer número } **Por que funciona:** Força a IA (e você) a usar createPrice pra obter um objeto válido. A validação sempre roda. --- ### 5. Pare de Jogar Strings try/catch é opaco. Quando você chama uma função, não tem ideia do que ela pode lançar. Assistentes de IA são ruins em adivinhar tipos de erro em blocos catch. Frequentemente só escrevem console.log(error) e seguem em frente. **A solução: Result Types** Retorne erros como valores. Isso torna o tratamento de erros visível na assinatura da função. type Result = { ok: true; value: T } | { ok: false; error: E }; type FetchError = | { type: 'NetworkError' } | { type: 'NotFound'; id: string }; async function getUser(id: string): Promise> { // implementação retorna objetos, não faz throw } // Uso const result = await getUser('123'); if (!result.ok) { // A IA sabe exatamente quais erros tratar switch(result.error.type) { case 'NotFound': /* ... */ case 'NetworkError': /* ... */ } } **Por que funciona:** Sem mais mistério de "o que pode dar errado?". A IA consegue tratar todos os casos de erro porque estão listados no tipo. --- ### 6. Bônus: Não Confie em Nada de Fora Branded Types (Dica #4) são ótimos pra lógica interna. Mas dados de fora (APIs, formulários) são imprevisíveis. Se você escreve as User numa resposta de API, está mentindo pro compilador. E a IA vai acreditar nessa mentira. **A solução: Validação de Schema em Runtime** Use bibliotecas como Zod pra conectar dados de runtime com tipos estáticos. import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(['admin', 'user']), }); type User = z.infer; async function fetchUser(id: string) { const data = await fetch(`/api/users/${id}`).then(res => res.json()); return UserSchema.parse(data); // Se passar, o tipo é real } --- ### Conclusão Escrever código AI-friendly não é sobre simplificar as coisas. É sobre ser **explícito**. TypeScript é uma forma de documentação legível por máquina. Quando você usa esses padrões, cria um contrato que tanto seu compilador quanto seu assistente de IA conseguem entender. Na próxima vez que sua IA alucinar algo estranho, pergunte-se: dei contexto suficiente nos tipos? --- Gostou desse conteúdo? Compartilha com seus amigos devs! Até a próxima! [Read in English](/en/posts/ai-friendly-typescript) [Voltar para artigos](/posts) --- # Animando elementos quando saem e entram na tela com JavaScript — iwill.dev URL: https://iwill.dev/posts/animando-elementos-js > Animando elementos quando saem e entram na tela com JavaScript Animando elementos quando saem e entram na tela com JavaScript — iwill.dev | Senior Frontend Engineer - ** ## Como testar se um elemento está no viewport?** Existem muitas formas de se fazer isso, utilizando JavaScript. Essa funcionalidade pode ser útil para animar elementos que se tornam visíveis para o usuário, quando entram no viewport, otimizando a experiência e aumentando a imersão da sua aplicação. Nesse tutorial, não vou focar na questão das animações, porque entendo que é um tópico muito particular, tanto do desenvolvedor, como do projeto. A ideia é mostrar uma alternativa simples e fácil de ser implementada, para que você consiga capturar a posição de um elemento e animá-lo, seja na entrada ou na saída da janela. --- Começamos pela estrutura básica (index.html). Utilizaremos um conjunto de 6 imagens aleatórias, através de uma API do Unsplash. Essas imagens serão animadas em duas situações: quando "saírem" para cima ou para baixo da área visível da janela, do viewport. Document --- Em seguida, adicionaremos estilos no style.css que são apenas demonstrativos, para o body e as imagens: body { padding: 10rem 5rem; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 10rem; background: #121212; overflow-x: hidden; } img { width: 100%; max-width: 600px; height: 400px; object-fit: cover; transition: 0.5s; } --- Por último, ainda nos estilos, criaremos duas classes que serão aplicadas nas duas saídas possíveis do viewport: *.is-down*, que será aplicada quando o elemento estiver abaixo da área visível - *.is-up*, que será aplicada quando o elemento estiver acima da área visível Lembrando que as propriedades utilizadas aqui são apenas para efeito de demonstração. Sinta-se a vontade para criar suas próprias transições, dentro do resultado esperado. .is-down { transform: translateX(25%); opacity: 0; } .is-up { transform: translateX(-25%); opacity: 0; } --- ## Capture e anime! Já no script.js, vamos começar capturando nossa lista de imagens, utilizando o método querySelectorAll, que vai retornar uma lista com todas as imagens que têm a classe image: const images = document.querySelectorAll(".image"); --- Em seguida, capturamos a altura da janela. Como queremos animar as imagens saindo acima e abaixo da área visível, saber a altura do viewport é fundamental para descobrir se um elemento está, ou não, na área visível para o usuário: let windowHeight = window.innerHeight; Criaremos uma função para animar as imagens. Ela vai utilizar o método forEach para percorrer a lista de imagens e aplicar as alterações necessárias. Para cada imagem da lista, vamos criar uma variável chamada bounding a qual será atribuída o objeto DOMRect, retornado do método getBoundingClientRect(). Esse objeto conta com as dimensões do elemento, bem como com suas coordenadas em relação ao viewport. O código a seguir mostra um exemplo da estrutura desse objeto. Ele não fará parte do nosso exemplo. Os valores das propriedades estão em pixels. { bottom: -413.316650390625, ​ height: 400, ​ left: 491.5, ​ right: 1091.5, ​ top: -813.316650390625, width: 600, ​ x: 491.5, ​ y: -813.316650390625 } --- A partir dessas coordenadas, que serão atribuídas a variável bounding, podemos definir se um objeto está dentro da área visível, partindo do seguinte raciocínio: Como o eixo Y da página começa no topo, essa posição é igual a 0. A base da página será igual a altura que foi atribuída a variável windowHeight. Se bounding.bottom, a base da imagem, for maior que windowHeight, a imagem não está dentro do viewport, mas abaixo da área visível, total ou parcialmente. Se bounding.top, o topo da imagem, for menor que 0, a imagem não está dentro do viewport, mas acima da área visível, total ou parcialmente. A partir daí, aplicamos as classes correspondentes. E caso nenhuma das lógicas seja verdadeira, removemos as classes da imagem, para que ela tenha sua aparência padrão, estando visível. function animateImages() { images.forEach((image) => { let bounding = image.getBoundingClientRect(); if (bounding.bottom > windowHeight) { image.classList.add("is-down"); } else if (bounding.top < 0) { image.classList.add("is-up"); } else { image.classList.remove("is-up"); image.classList.remove("is-down"); } }); } --- E como queremos que esse efeito seja aplicado durante a rolagem da página, adicionamos um listener que vai capturar o scroll e executar a função animateImages(). document.addEventListener("scroll", function () { animateImages(); document.removeEventListener("scroll", this); }); --- Além disso, incluímos um listener que vai capturar o redimensionamento da janela, atribuindo a nova altura a variável windowHeight. window.addEventListener("resize", function () { windowHeight = window.innerHeight; window.removeEventListener("resize", this); }); --- E para que a aplicação já adicione as classes as imagens que não estão visíveis para o usuário, executamos a animateImages(), assim que a aplicação é iniciada. animateImages(); --- Você pode ver a demonstração [aqui](https://animate-on-scroll.vercel.app) --- E como costumo dizer, aqui é só o ponto de partida. Você pode explorar outras possibilidades, com o DOMRect do getBoundingClientRect(). Só pra deixar um outro cenário possível nesse exemplo, se você quiser que um elemento só passe por uma transição quando ele estiver totalmente fora do viewport, você pode mudar as condicionais para quando o bounding.bottom (base do elemento) for menor que 0 (saiu totalmente, acima), ou o bounding.top (topo do elemento) for maior que windowHeight (saiu totalmente, abaixo). Você ainda pode adicionar áreas seguras para que seu elemento continue visível enquanto necessário. Pode aplicar as classes quando ele estiver a, por exemplo, 10% do fim da tela, acima ou abaixo. Possibilidades infinitas que vão depender do que você pretende fazer com seus elementos. --- Se você curtiu esse conteúdo, compartilhe com outras pessoas e ajude a espalhar a palavra! --- Nos vemos na próxima! [Read in English](/en/posts/animando-elementos-js) [Voltar para artigos](/posts) --- # Fundamentos TypeScript com carros — iwill.dev URL: https://iwill.dev/posts/ensinando-ts-meu-filho-pt1 > Ensinando TypeScript para o meu filho autista (pt 1) Fundamentos TypeScript com carros — iwill.dev | Senior Frontend Engineer ### Pedro é um menino autista, hoje com 8 anos. Ele me disse que quer aprender a programar pra trabalhar com o papai e eu decidi iniciar essa série em homenagem a ele. Ele tem alguns hiperfocos, mas o principal é com carros. Então esse será o tema dessa série, explicando fundamentos e conceitos TypeScript aplicados aos carros e suas funcionalidades. Começando pelos tipos primitivos e especiais: ## number É o tipo de informação que retorna dos instrumentos do painel, como o velocímetro. Sempre serão números e nada além disso. Se faltar, não vemos quão rápido o carro vai. function lervelocidade(): number {} // 80 function lerQuilometragemTotal(): number {} // 230000 ## string São informações que têm letras e números. Podem ser a placa do carro, ou até mesmo a marca e modelo dele Se faltar, ninguém sabe qual é o carro. function lerPlacaDoCarro(): string {} // ABC-1Z34' function lerMarcaEModelo(): string {} // 'Chevrolet Classic' ## boolean São informações simples, de verdadeiro ou falso. Servem, por exemplo, pra saber se uma parte do carro está, ou não, funcionando. Se faltar, não sabemos se o carro funciona. function motorEstaLigado(): boolean {} // true | false ## null É como a bagagem no porta-malas antes de uma viagem. Ela ainda não está lá, mas vamos colocar depois. Se faltar, podemos esquecer algo importante. let bagagem: string | null = null; ## undefined É uma informação que ninguém decidiu o que é. Como um espaço vazio no console do carro, que pode servir para instalar alguma coisa Faz falta se decidirmos usar e não houver nada lá. let acessorioDoConsole; /* Não sabemos o que é porque nada foi atrbuído ao espaço */ ## symbol Guarda coisas com mesmo nome, sem confusão. Você quer instalar adesivos no seu carro, mas eles devem ficar em lugares diferentes. Alguns até meio que escondidos. Mas você sabe onde colocou e pode achar quando quiser. const adesivo1 = Symbol("adesivo"); const adesivo2 = Symbol("adesivo"); const carro = { adesivo: "adesivo no capô", [adesivo1] "adesivo embaixo do banco", [adesivo2]: "adesivo na caixa de roda" } console.log(Object.values(carro)) // locais dos adesivos: ['adesivo no capô'] console.log(carro[adesivo1]) // 'adesivo embaixo do banco' console.lng(carro[adesivo2]) // 'adesivo na caixa de roda' ## any É o porta-trecos maluco. Pode guardar qualquer coisa: lancheira, ferramenta, bola, pneu, pedra, papel, tesoura... É ruim, porque a gente nunca sabe o que tem lá let portaTrecos: any = "vassoura"; portaTrecos = 22; portaTrecos = null; portaTrecos = false; ## unknown É como o porta-luvas do carro. Tem algo ali dentro, mas você precisa ver o que é. Mais seguro que o any, porque você só não sabe o que é. Depois de conferir, pode usar de boa. let objetoNoPortaluvas: unknown = "manual do carro"; function lerManual(manual: string) { /* ... */ } if (typeof objetoNoPortaluvas === "string") { lerManual(objetoNoPortaluvas) } ## never Aparece quando algo quebra, ou sai do controle Como quando o motor do carro não funciona. A gente normalmente não usa ele, mas dá pra aplicar quando sabemos que algo vai dar errado. function ligarMotorQuebrado(): never { throw new Error("Kaboom!"); } ## void Como abrir a porta do carro, o porta-malas ou até o porta-luvas. É usado em ações qeu são necessárias para usarmos o carro, mas que não devolvem nenhuma informação. function abrirPortaDoCarro(): void {} --- O que mais você quer aprender sobre TypeScript com carros? Essa série vai continuar em breve... --- Curta, compartilhe e me siga nas [redes](https://www.iwill.dev/links) para mais conteúdo. [Read in English](/en/posts/ensinando-ts-meu-filho-pt1) [Voltar para artigos](/posts) --- # Funções Genéricas Inteligentes — iwill.dev URL: https://iwill.dev/posts/funcoes-genericas-inteligentes > Funções Genéricas Inteligentes Funções Genéricas Inteligentes — iwill.dev | Senior Frontend Engineer - O que faz uma lib parecer "mágica"? Inferência de tipos! E você pode usar isso hoje! ## Funções reutilizáveis são o coração de qualquer projeto ou lib Imagine que precisamos retornar nome e idade de um usuário. A solução mais rápida seria essa: type User = { id: string; name: string; age: number; email: string; } function getNameAndAge(user: User): { name: string; age: number } { return { name: user.name, age: user.age, }; } const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = getNameAndAge(user); // valor: { "name": "William", "age": 36 } // tipo: { name: string, age: number } ## E qual é o problema nisso? A função só serve pra isso - Você provavelmente não vai usar de novo - Ela infere um tipo sem relação com o original - Ou você vai declarar um novo tipo pro retorno ## A mágica dos generics + inferência Vamos criar uma função pick que aceita como parâmetro: (a) um objeto e (b) um array de chaves correspondentes a ele. Como retorno da função, um "partial" do tipo original. function pick( obj: T, keys: K[] ): Pick { const result = {} as Pick; for (const key of keys) { result[key] = obj[key]; } return result; } ## O resultado? Uma implementação segura, precisa, flexível e reutilizável, mantendo referência ao tipo original const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = pick(user, ["name", "age"]) // valor igual : { "name": "William", "age": 36 } // tipo relacionado ao original: Pick ## A partir daí, você vai ao infinito e além - Uma função omit, para remover propriedades de um objeto type OmitKeys = { [P in keyof T as P extends K ? never : P]: T[P]; }; function omit( obj: T, keys: K[] ): OmitKeys { const result = { ...obj }; keys.forEach(key => delete result[key]); return result; } const userWithoutId = omit(user, ["id"]) /* valor: { "name": "William", "age": 36, "email": "iwilldev@outlook.com.br" } */ // tipo: OmitKeys - Uma merge, para combinar dois objetos diferentes // const user: User = { ... } type Role = { role: 'admin' | 'editor' | 'viewer'; permissions: Array<'read' | 'write' | 'delete' | 'update'>; }; const role: Role = { role: 'viewer', permissions: ['read'] } const userWithRole = merge(user, role) /* valor: { "id": "1234", "name": "William", "age": 36, "email": "iwilldev@outlook.com.br", "role": "viewer", "permissions": ["read"] } */ // tipo: User & Role ## As possibilidades são infinitas Com generics + inferência, você escreve: - Código mais seguro - Helpers mais inteligentes - Implementações mais elegantes E claro: muitas libs trazem funções como essas. Mas será que você precisa de uma lib inteira, pra resolver um problema pontual? Então domine seu código, abraçando o poder do TypeScript e tudo o que ele pode fazer pelo seu projeto! --- ## Qual função você mais curtiu? Me conta aí nos comentários! Salve, compartilhe e siga para mais conteúdo [Read in English](/en/posts/funcoes-genericas-inteligentes) [Voltar para artigos](/posts) --- # Posts — iwill.dev URL: https://iwill.dev/posts > Artigos sobre desenvolvimento web, TypeScript, React e mais. ## Posts Reflexões, tutoriais e descobertas sobre desenvolvimento web, TypeScript, React e o que mais eu estiver aprendendo. [TypeScript — 27 de janeiro de 2026 — 13 minutos de leitura 6 Hábitos de TypeScript para Código AI-Friendly](/posts/ai-friendly-typescript) [React Router — 26 de junho de 2025 — 25 minutos de leitura React Router 7: 'Múltiplas Actions' em uma única rota](/posts/rr7-multiple-actions) [TypeScript — 19 de junho de 2025 — 8 minutos de leitura Fundamentos TypeScript com carros Ensinando TypeScript para o me](/posts/ensinando-ts-meu-filho-pt1) [TypeScript — 18 de junho de 2025 — 7 minutos de leitura Funções Genéricas Inteligentes](/posts/funcoes-genericas-inteligentes) [Remix — 26 de janeiro de 2023 — 24 minutos de leitura Por que eu amo Remix? (por Kent C. Dodds)](/posts/porque-eu-amo-remix) [JavaScript — 22 de dezembro de 2022 — 29 minutos de leitura Intersection Observer - Lazy loading, animações e scroll inf](/posts/intersection-observer) [JavaScript — 14 de janeiro de 2021 — 13 minutos de leitura Animando elementos quando saem e entram na tela com JavaScrip](/posts/animando-elementos-js) [CSS — 3 de janeiro de 2021 — 8 minutos de leitura Mudando tema da tela com CSS Puro (Dark/Light Mode)](/posts/mudando-tema-com-css-puro) [JavaScript — 26 de novembro de 2020 — 8 minutos de leitura Texto 'mágico' escrito automaticamente com JavaScript](/posts/texto-magico-js) --- # Intersection Observer - Lazy loading, animações e scroll infinito sem libs — iwill.dev URL: https://iwill.dev/posts/intersection-observer > Intersection Observer - Lazy loading, animações e scroll infinito sem libs Intersection Observer - Lazy loading, animações e scroll infinito sem libs — iwill.dev | Senior Frontend Engineer - ** Salve, devs e divas! Esse post inicia uma série que visa explorar as [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API), descobrindo e apresentando funcionalidades que podem ser alcançadas a partir delas. E considerando o costume de utilizarmos abstrações que trazem o mesmo resultado, queremos empoderar as opções nativas a fim de reduzir dependências em projetos e aprofundar os conhecimentos sobre os recursos disponíveis na Web. --- Como *Front-Ender*, já esbarrei em alguns desafios para aumentar a interatividade da página com *infinite scrollings* e animações de elementos quando eles entram e saem do *viewport*, ou até mesmo questões que impactam performance como *lazy-loading* em imagens, a partir das ações do usuário. Em casos como esse, tudo se resumiria a verificar a intersecção entre um elemento alvo e um elemento pai ou até mesmo entre ele e o *viewport* (área visível para o usuário) do documento e, a partir do estado e da visibilidade do alvo observado, aplicar as mudanças necessárias. Detectar a visibilidade de um elemento (ou entre dois deles) envolvia soluções não muito confiáveis e que tendiam a gerar problemas de performance nas páginas, já que precisávamos de *handlers* e *loops* aplicados a cada elemento afetado e chamando métodos como o Element.getBoundingClientRect(), o que gerava um peso na *main thread* da aplicação, deixando a página e o próprio navegador mais lentos. --- ## Conceitos e uso A Intersection Observer API** fornece uma maneira de observar alterações de intersecção de forma assíncrona. Com sua implementação, o site não precisa mais lidar com essa responsabilidade na *main thread* e o navegador fica livre para gerenciar as observações como achar melhor. É possível declarar uma função de *callback* que é executada nas seguintes circunstâncias: Um elemento alvo cruza (total ou parcialmente, conforme configuração) com o elemento root. - A primeira vez que o *Observer* é solicitado a observar um elemento alvo. Essa API tem compatibilidade total com todos os navegadores modernos, com ressalvas para o **Safari** (Desktop e iOS) e o **Firefox for Android** onde o elemento root não pode ser um documento. --- ## Criando um Intersection Observer Para criar um intersection observer você deve chamar seu construtor, enviando uma função de *callback* como primeiro parâmetro e um objeto options como parâmetro (opcional) seguinte: let options = { root: document.querySelector('#rootElement'), rootMargin: '0px', threshold: 1.0 } let observer = new IntersectionObserver(callback, options); ### Intersection observer options O objeto options passado no construtor IntersectionObserver() te permite controlar as circunstâncias em que a função de *callback* será executada: - root - Um elemento ancestral especificado ou o próprio *viewport*, na ausência de elemento declarado ou se o valor for null. - rootMargin - Define os limites de margem do elemento **root**, aumentando ou diminuindo a delimitação desse elemento, antes de computar uma intersecção. Pode ter valores similares ao CSS, como "10px 20px 30px 40px" (top, right, bottom, left). - threshold - A taxa de interseção (*intersection ratio*), que representa o percentual de visibilidade do elemento alvo em relação ao **root**: um valor entre 0,0 e 1,0. O *callback* será executado sempre que a visibilidade do alvo ultrapassar o valor declarado, para cima ou para baixo. Pode ser declarado como: Um número. Ex: 0.5. *Callback* executado quando a visibilidade ultrapassar 50%. - Um Array de números: Ex: [0, 0.25, 0.5, 0.75, 1]. O *callback* será executado em cada percentual relacionado aos valores declarados. Nesse caso, a cada 25% de visibilidade. ### Declarando um elemento para ser observado Agora que criou o observer, você precisa declarar um elemento a ser observado por ele: let target = document.querySelector('#targetElement'); observer.observe(target); Nesse momento, o *callback* é executado pela primeira vez, mesmo que o elemento alvo não esteja visível. Sempre que a visibilidade do alvo ultrapassar o valor de threshold, o *callback* é invocado, recebendo uma lista de objetos IntersectionObserverEntry e o próprio observer. Esteja ciente de que esse *callback*, em si, será executado na *main thread*. Então tente não complicar a lógica executada nesse escopo: let callback = (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { /* Verificamos o estado da 'entry' e efetuamos as alterações necessárias caso ela esteja visível */ } }); }; Boa parte das aplicações desse *Observer* podem ser feitas verificando apenas a propriedade isIntersecting da entry, que retorna um *boolean* indicando se o elemento alvo está, ou não, cruzando com o elemento root, considerando os parâmetros declarados no objeto options. Para ver mais propriedades da interface IntersectionObserverEntry, confira a [documentação na MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). Partindo do princípio que temos a base necessária para avançar, vamos aos casos de uso. --- ## Arquivos utilizados Você pode usar o [repositório](https://github.com/owillgoncalves/intersection-observer) desse artigo com os arquivos finais divididos em pastas para cada caso. --- ## Lazy-loading Imagine carregar todos os assets de uma página inteira e o usuário nem visualizá-los, porque decidiu desviar a navegação para outra página. Vira um desperdício de recurso para ele que no caso de estar em uma rede móvel, consumiu dados à toa e para você que precisou servir arquivos que não foram utilizados de fato. Partindo disso, vamos criar uma página em que as imagens só serão carregadas se estiverem visíveis. Começando pelo arquivo index.html: Lazy Loading
Nas tags img, declaramos um [*placeholder*](https://owillgoncalves.github.io/intersection-observer/01-lazy-loading/placeholder.png) no atributo src, que será renderizado inicialmente. No atributo data-src, colocamos a URL da imagem desejada. Além disso, declaramos a classe lazy que será usada para selecionarmos as imagens. Temperamos com o style.css: * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } section { height: 100%; width: 100%; align-items: center; display: flex; justify-content: center; } Agora precisamos observar as imagens e, quando elas estiverem visíveis, trocar o placeholder para a URL desejada. No arquivo script.js: Começamos selecionando as imagens. const images = document.querySelectorAll('.lazy'); Criamos nosso *Observer*. const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const image = entry.target; image.src = image.dataset.src; image.classList.remove('lazy'); observer.unobserve(image); } }); }); Dentro do *callback*, usamos o forEach nas entries e para cada entry verificamos se ela está cruzando a área visível (entry.isIntersecting). Se positivo, declaramos o entry.target como image, substituímos o src pelo --- # Mudando tema da tela com CSS Puro (Dark/Light Mode) — iwill.dev URL: https://iwill.dev/posts/mudando-tema-com-css-puro > Mudando tema da tela com CSS Puro (Dark/Light Mode) Mudando tema da tela com CSS Puro (Dark/Light Mode) — iwill.dev | Senior Frontend Engineer ## *We know the JS way* ### *Mas e se não usarmos scripts para trocar o tema das nossas aplicações?* O caminho é uma relação entre cascata e seletores bem especificados. Vamos do início: --- #### HTML O primeiro elemento da árvore será um input do tipo checkbox. Seu irmão abaixo será o container da nossa aplicação. Ele é quem terá os estilos alterados para a mudança de tema. Dentro dele, teremos um label relacionado ao nosso input lá de cima, dentro de uma div que será sua área de transição, servindo como nosso botão para mudar o tema.

Mudando tema com CSS Puro

O texto fica em contraste com o fundo

--- #### CSS Nos estilos, aplicamos os resets e declaramos as variáveis para as cores usadas no tema: * { margin: 0; padding: 0; box-sizing: border-box; } :root { --light: #cccccc; --dark: #151515; } --- Tornamos nosso input invisível, já que usaremos o label dele como acionador. #theme-switcher { display: none; } --- E declaramos as propriedades do nosso container do app. Ele ocupará toda a tela, terá fundo claro e textos escuros, além de ser um flex-container. Esse último é opcional e só para facilitar a demonstração do resultado, centralizando o texto na tela. #app-container { height: 100vh; background: var(--light); color: var(--dark); font-family: monospace; font-size: 1.5rem; transition: 0.3s; display: flex; flex-direction: column; align-items: center; justify-content: center; } --- Declaramos a área por onde nosso botão vai deslizar, com posicionamento absoluto no topo: .theme-switcher-area { border: 1px solid var(--light); background: var(--dark); border-radius: 2rem; width: 4.5rem; height: 2.5rem; padding: 0.2rem; position: absolute; top: 0.5rem; right: 0.5rem; } --- O botão, em si, que usará o estilo dashed na borda, criando um efeito similar aos raios do sol, para o tema claro. .theme-switcher-button { position: relative; display: block; background: #f1aa02; border-radius: 50%; width: 2rem; height: 2rem; border: 2px dashed var(--dark); transition: 0.3s; } --- E por último, um pseudo-elemento ::after sobre o botão. Ele terá a forma de um círculo menor que o elemento original, tornando-se uma sombra que transformará o acionador em uma lua, no tema escuro. Por isso, sua opacidade inicial será 0. .theme-switcher-button::after { position: absolute; width: 80%; height: 80%; content: ""; background: var(--dark); border-radius: 50%; opacity: 0; transition: 0.3s; } --- ## E aí vem a mágica! Como nosso input é o primeiro elemento da árvore, podemos usar a pseudo-classe ':checked', com os seletores apropriados, para mudar o estilo de qualquer elemento abaixo dele. Quando ele for selecionado, essas propriedades serão aplicadas. Começando pelo próprio acionador, transformando o sol em lua. Para isso, removemos a borda que veio para dar o efeito dos raios e deslocamos o botão para a direita. #theme-switcher:checked + #app-container .theme-switcher-button { transform: translateX(100%); border: none; } --- Em seguida, mudamos a opacidade da sombra, o ::after, para gerar uma lua crescente, na mudança do botão. #theme-switcher:checked + #app-container .theme-switcher-button::after { opacity: 1; } --- Por último e pelo efeito desejado, invertemos a cor de fundo e de texto do nosso container da aplicação: #theme-switcher:checked + #app-container { background: var(--dark); color: var(--light); } --- ### E tá lá, onde a coruja dorme! --- Esse tutorial é só o início do mergulho. Por isso, use sua criatividade a partir dessa base e mude os estilos como você achar melhor! --- Se você curtiu esse conteúdo, compartilhe com outras pessoas e ajude a espalhar a palavra! --- Nos vemos na próxima! [Read in English](/en/posts/mudando-tema-com-css-puro) [Voltar para artigos](/posts) --- # Por que eu amo Remix? (por Kent C. Dodds) — iwill.dev URL: https://iwill.dev/posts/porque-eu-amo-remix > Por que eu amo Remix? (por Kent C. Dodds) Por que eu amo Remix? (por Kent C. Dodds) — iwill.dev | Senior Frontend Engineer - Esse artigo é uma tradução do post [Why I Love Remix](https://kentcdodds.com/blog/why-i-love-remix) do [Kent C. Dodds](https://kentcdodds.com). Obrigado, Kent, por permitir a tradução do conteúdo e por compartilhar tanto conhecimento com a comunidade. --- [kentcdodds.com](https://kentcdodds.com) é totalmente feito por mim (e pelo [time](https://kentcdodds.com/credits)) usando [Remix](https://remix.run). Depois de escrever dezenas de milhares de linhas de código usando esse framework, criei grande apreciação pelo que ele pode fazer por mim e pelos usuários do meu site. E quero te falar um pouco sobre isso. ## Em uma frase Essa é a principal razão pela qual eu amo usar o Remix para construir sites: > ### Remix me permite construir experiências de usuário incríveis e ainda ficar feliz com o código que eu tive que escrever para chegar lá. E o que isso significa? Vamos nos aprofundar... ## A Experiência do Usuário Há um monte de fatores que impactam a experiência do usuário quando eles usam nossos softwares. Na maior parte do tempo, acho que as pessoas estão focadas na performance/velocidade e, embora isso seja uma parte importante, é apenas um pedaço do todo. A experiência do usuário inclui uma série de outros aspectos do seu site. Aqui estão alguns: Acessibilidade - Performance - "Reflow" de conteúdo (quando um navegador processa e remonta parte ou toda uma página, após uma atualização ou interação). - Confiabilidade e disponibilidade - Tratamento de erros - Gerenciamento de pendências - Gerenciamento de estado - Melhoria progressiva - Resiliência (comportamento em condições de rede ruins) - Layout - Clareza do conteúdo Até mesmo a velocidade de desenvolvimento de features pode impactar a experiência do usuário. Então a UX é [indiretamente impactada](https://kentcdodds.com/blog/why-users-care-about-how-you-write-code) pela manutenibilidade do nosso código. Remix nos ajuda com muitos desses aspectos. Alguns sem que eu precise pensar nisso. Em particular, alguns dos maiores desafios envolvendo gerenciamento de estado (race conditions de mutações e carregamento de dados) são totalmente gerenciados dentro do framework. Por causa disso, os usuários não se deparam com a necessidade de atualizar a página porque estão vendo dados desatualizados. Isso acontece sem que eu precise pensar nisso. É como o Remix funciona. Remix faz muito para manter meu site rápido através do uso de tags para pré-carregar recursos e dados no momento certo. Às vezes, fico impressionado com o fato de meu site *parecer* composto por arquivos estáticos em um CDN, mas *na verdade* é renderizado/hidratado no servidor e cada página é completamente única para cada usuário (portanto, um cache HTTP compartilhado em um CDN não seria possível). Usar as APIs da plataforma é o que nos permite isso. É também o que faz o Remix ser tão resiliente e ótimo para melhorias progressivas. Em condições de rede ruins, onde o carregamento do JavaScript é lento ou falha, a API padrão do Remix para mutações (
) vai funcionar mesmo antes do app ser hidratado. Isso significa que o usuário pode começar a fazer o trabalho com o app, mesmo que esteja em uma conexão ruim. Muito melhor do que um botão em que o handler do onClick ainda não foi carregado (que era o meu padrão antes do Remix)! A forma declarativa do Remix tratar erros me permite lidar com eles no contexto em que eles acontecem. Combinado com o roteamento aninhado, o que você obtém é a capacidade de renderizar um erro contextual sem quebrar o resto do app. * E isso também funciona no servidor (que é sem igual no Remix), então os usuários terão a mesma experiência, independente do erro ter acontecido durante uma transição no client ou em um carregamento completo da página. > ### Remix traz a excelência como padrão da experiência do usuário. E é uma das principais razões pelas quais eu amo o framework. ## O Código Os apps que eu ajudei a construir são usados por milhões de pessoas pelo mundo. Construir sites com Remix me faz feliz com o código que coloco em produção. A maior razão para isso é que antes do Remix eu gastava muito tempo tentando resolver problemas de experiência do usuário. Pelo Remix ajudar tanto com a UX, lido com menos complexidades no código. Então tudo que me resta fazer é usar as APIs declarativas que Remix (e React) me dão, para construir meu app e a experiência do usuário é boa por padrão. > ### Quando uso Remix, posso deixar os truques* em casa. Francamente, esse é o maior ponto a se falar sobre o código: você percebe que códigos de demonstração são normalmente mais simples, ignorando nuances como estados pendentes, race conditions, tratamento de erros, acessibilidade, etc? Bem, meu código não é tão simples como um código de demonstração, mas o Remix deixa ele bem próximo, em simplicidade. Eu definitivamente ainda penso em acessibilidade (apesar das bibliotecas [@reach-ui](https://reach.tech) do Remix ajudarem bastante com isso) e estados pendentes/erros. Mas as APIs que o Remix me dá para essas coisas são simples e declarativas: // app/events/$eventId/attendees.tsx const loader: LoaderFunction = async ({request, params}) => { // isso é executado no servidor // erros inesperados de execução irão disparar o ErrorBoundary para ser renderizado // erros esperados (como 401s, 404s, etc) irão renderizar o CatchBoundary // caso contrário, retorno uma resposta e isso irá renderizar o componente padrão return json(data) } export default function AttendeesRoute() { const data = useLoaderData() return
{/* renderiza os dados em 'data' */}
} export function ErrorBoundary({error}) { return
{/* renderiza uma mensagem do erro inesperado */}
} export function CatchBoundary() { const caught = useCatch() return
{/* renderiza a mensagem de erro para respostas tipo 400 */}
} Ah! E para estados pendentes (seja mutações ou transições regulares) você pode colocar isso onde quiser que o efeito de Pending UI apareça (seja global ou local): const transition = useTransition() const text = transition.state === 'submitting' ? 'Saving...' : transition.state === 'loading' ? 'Saved!' : 'Ready' Isso simplifica drasticamente o código React que eu escrevo, ao ponto de eu não escrever nenhum código React ligado a HTTP. Essa comunicação *client-server* é totalmente gerenciada pelo Remix de uma forma que otimiza a UX. E a fronteira entre cliente e servidor pode ser totalmente tipada, então eu gasto menos tempo indo e voltando entre o navegador e o editor para corrigir erros bobos. Também amo o fato do Remix ser fundamentado nas APIs da Web. Essa função json que chamamos na loader é apenas uma função simples para criar um objeto [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). Isso mesmo! Se você quiser aprender a fazer algo com o Remix, gastará mais tempo na [mdn](https://developer.mozilla.org) do que na [documentação do Remix](https://docs.remix.run) e isso me leva a outra coisa que amo sobre o Remix: > ### Quanto melhor eu construo sites com Remix, melhor eu construo sites para a web. Isso acontece naturalmente graças ao fato do Remix usar muito da plataforma web e recorrer, o máximo possível, às APIs da Web. (Isso é semelhante ao fato de quanto melhor eu me sinto com React, melhor eu me sinto com JavaScript.) E pelo Remix ser fundamentado nas APIs da Web como a interface comum para o servidor, você pode implantar o mesmo aplicativo em qualquer plataforma (desde que o código que você traga junto funcione nessas plataformas) e tudo que você precisa fazer é mudar qual *adapter* você está usando. Se você quiser executar em serverless ou em um contêiner Docker, o Remix cuida de tudo. Remix é o jQuery das plataformas de hospedagem. Ele normaliza as diferenças para que você possa escrever uma vez e hospedar em qualq --- # React Router 7: 'Múltiplas Actions' em uma única rota — iwill.dev URL: https://iwill.dev/posts/rr7-multiple-actions > React Router 7: React Router 7: 'Múltiplas Actions' em uma única rota — iwill.dev | Senior Frontend Engineer Talvez esse seja a má interpretação mais comum de quem não conhece o React Router como framework (e o mesmo vale para o Remix): > Só consigo fazer uma action por rota Eu acredito que essa confusão se dê principalmente pelas comparações com o Next.js, suas Server Functions e até a forma como as rotas de API são declaradas nele. Muitos desenvolvedores enxergam as funções loader e action do React Router como "endpoints", funções com responsabilidades únicas, quando na verdade o verdadeiro papel delas vai muito além disso. loader e action, juntas, são como um Controller completo, onde podemos definir diferentes buscas e mutações distintas, cada uma com seu propósito. E é sobre parte disso que estudaremos aqui, simulando um CRUD de usuários: três maneiras diferentes de fazer múltiplas actions em uma única rota do React Router. E se você usa Remix até a versão 2 (pré-React Router 7), as mesmas abordagens devem servir para você. --- ### Usando o componente Form e comportamento padrão Esta é a abordagem mais tradicional. Usamos o componente e diferenciamos as ações através do atributo method. Em uma rota, crie a loader: export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; Crie seu componentes com diferentes formulários, um para cada método/ação diferente: import { Form, /* ... */ } from "react-router"; /* ... */ export default function UI({ loaderData: { users } }: Route.ComponentProps) { return (

Users CRUD (Form - default behavior)

Add User

{/* Formulário com method="post" para criar usuário */}

Users

{users.map((user: User) => (
{/* Formulário com method="put" para editar usuário */}
Role:
{/* Formulário com method="delete" para excluir usuário */}
))}

); } Na mesma rota, defina a action. Usamos await request.formData() para ler os dados e pegamos o request.method para diferenciar POST, PUT e DELETE. export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const method = request.method.toLowerCase(); switch (method) { case "post": { const name = formData.get("name") as string; const email = formData.get("email") as string; return await createUser({ name, email }); } case "put": { const id = formData.get("id") as string; const name = formData.get("name") as string; const email = formData.get("email") as string; return await updateUser(id, { name, email }); } case "delete": { const id = formData.get("id") as string; return await deleteUser(id); } default: throw new Response("Method not allowed", { status: 405, statusText: "Method Not Allowed", }); } }; E é isso! Esta abordagem é robusta, funciona sem JavaScript e segue o princípio do Progressive Enhancement. Sua desvantagem (em relações às outras opções) é que a quantidade de mutações fica limitada aos métodos padrões. --- ### Com useSubmit e JSON - actions definidas nos dados enviados Nesse método, utilizaremos o hook useSubmit, para enviar um JSON com propriedade chamada intent, que definirá qual action desejamos performar. A primeira vantagem daqui é que ganhamos a flexibilidade de mutações customizadas, fora dos padrões de method no componente
. Usaremos a mesma loader do exemplo anterior: export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; No componente da rota, definimos event handlers para lidar com as ações do usuário e disparar a mutação correspondente. A principal vantagem aqui, por exemplo, é poder separar a edição do role de usuário das demais propriedades, em uma action distinta. export default function UI({ loaderData: { users }, }: Route.ComponentProps) { const submit = useSubmit(); // Event handler para criar usuário const handleCreate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "createUser", payload: { name, email } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); event.currentTarget.reset(); }; // Event handler para editar usuário const handleUpdate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const userId = event.currentTarget.getAttribute("data-user-id"); if (userId) { const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "updateUser", payload: { id: userId, name, email }, }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler para alterar o cargo do usuário const handleChangeRole = (event: React.ChangeEvent) => { const userId = event.target.closest("form")?.getAttribute("data-user-id"); if (userId) { const role = event.target.value as "admin" | "user"; const data = { intent: "changeUserRole", payload: { id: userId, role } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler para excluir usuário const handleDelete = (userId: string) => { if (confirm("Tem certeza que deseja deletar este usuário?")) { const data = { intent: "deleteUser", payload: { id: userId } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; return (

Users CRUD (JSON API)

Add User

{/* Formulário para criar usuário */}

Users

{users.map((user: User) => ( {/* Formulário para editar usuário */}
Role:
{/* Evento que dispara a handler de edição de cargo */} {/* Evento de click para excluir usuário */}
))}

); } Na mesma rota, defina a action. Usamos await request.json() para ler os dados e pegamos a propriedade intent do body, para --- # Texto 'mágico' escrito automaticamente com JavaScript — iwill.dev URL: https://iwill.dev/posts/texto-magico-js > Texto Texto 'mágico' escrito automaticamente com JavaScript — iwill.dev | Senior Frontend Engineer ** > “Raios, herege! Estás a me dizer que os escritos surgirão sozinhos? Isso não é feitiço, ou bruxaria?” Não é mágica. É JavaScript. Vamos desenrolar isso abaixo: --- Antes de tudo, precisamos criar, no nosso HTML, um elemento para receber o feitiço, digo, o texto criado. Pode ser um parágrafo (p**) ou até um cabeçalho (**h1**, **h2**...). Basta ser de texto e ter um **id**. Lembrando que o **id** precisa ser exclusivo desse elemento.

Pro nosso caso, vamos usar um **h1** com a id **magic-text**. --- Em seguida, criamos e importamos o arquivo JavaScript que, pro nosso exemplo, será o **script.js**: --- Já no **script.js**, vamos criar uma constante para interagir com nosso **h1**, usando o método **querySelector**, que nos permite selecionar elementos usando os mesmos seletores que vemos no CSS. No nosso caso, vamos usar a **id** precedida pelo **#**. const magicTextHeader = document.querySelector('#magic-text'); O método **querySelector** pode ser usado, tanto no documento, como em outros elementos, após declarados, selecionando seus respectivos filhos. --- Em seguida, criamos uma constante com o texto a ser usado: const text = 'Texto inserido automagicamente com JavaScript!'; --- Por fim, declaramos uma variável que servirá para nos ajudar a "percorrer" o texto: let indexCharacter = 0; --- A função que retornará o texto é a **writeText()**: function writeText() { magicTextHeader.innerText = text.slice(0, indexCharacter); indexCharacter++; if(indexCharacter > text.length - 1) { setTimeout(() => { indexCharacter = 0; }, 2000); } } Na primeira linha, incluímos o texto na propriedade **innerText** do **h1**, utilizando o método **.slice()**, que percorrerá nossa constante **text**, letra a letra, como se ela fosse um **array**. A sintaxe do **.slice()** é .slice(a,b), onde **a** é a chave inicial do trecho a ser retornado e **b** é a chave final desse mesmo trecho. Como queremos retornar o texto desde o início, começamos com a chave 0 e finalizamos com o valor da **indexCharacter**, que é incrementada na linha seguinte, garantindo que a próxima execução da função retornará um caractere a mais e assim por diante. Em seguida, usamos uma condicional para verificar se a **indexCharacter** é igual a última posição do texto (text.length - 1; como a primeira chave é 0, a última será o tamanho (length) do texto menos 1). Se a condição for verdadeira, a **indexCharacter** será zerada, depois de um **setTimeout** de 2000 milissegundos, fazendo com que o texto volte a ser "digitado" do início. --- E para executar essa função de forma contínua, garantindo o incremento da **indexCharacter** e o efeito desejado para nosso texto, usamos um **setInterval** que executará a função **writeText** a cada 100 milissegundos: setInterval(writeText, 100); --- E a mágica está concluída! --- Você pode ver um exemplo [aqui](https://g31-magic-text.vercel.app/). E conferir minha versão do código [aqui](https://github.com/williammago/goodbye.31/tree/main/28%20-%20Auto%20Write%20Text%20com%20JavaScript). --- E, opcionalmente, usar os estilos que usei lá: * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; background: darkred; color: #FFF; } h1 { font-size: 2rem; max-width: 400px; text-align: center; } --- Esse artigo foi inspirado em um [vídeo](https://www.youtube.com/watch?v=8GPPJpiLqHk) do [canal](https://www.youtube.com/channel/UCeU-1X402kT-JlLdAitxSMA) do Florin Pop, que tem tutoriais e desafios incríveis pra quem está iniciando. Conteúdo em inglês. --- Nos vemos na próxima! Grande abraço! [Read in English](/en/posts/texto-magico-js) [Voltar para artigos](/posts) --- # 6 Hábitos de TypeScript para\nCódigo AI-Friendly Source: posts/ai-friendly-typescript.md Você pede um refactor simples pro seu assistente de IA. Ele retorna um código que parece correto, mas está sutilmente errado. Cria rotas que não existem, trata erros que não podem acontecer, ou inventa combinações de estado impossíveis. **Se a IA não entende seu código, talvez seus tipos não estejam contando a história completa.** Não estamos mais escrevendo código só para compiladores. Estamos escrevendo contexto para nossos assistentes de IA também. Quanto mais explícitos seus tipos forem, menos espaço pra alucinações. Aqui estão 6 estratégias pra escrever TypeScript que ajuda sua IA (e seu time) a trabalhar melhor. --- #### 1. Pare de Usar Strings para Rotas Uma forma fácil de quebrar uma aplicação é um typo numa string de rota. Quando você escreve `router.push('/users/' + id)`, está confiando em si mesmo (e na IA) pra lembrar o caminho exato toda vez. Modelos de IA adoram chutar aqui. Frequentemente sugerem `/user/` ao invés de `/users/` ou esquecem uma barra. **O problema:** ```typescript // ❌ Ambíguo. A IA tem que adivinhar qual string vai aqui. function navigateTo(path: string) { ... } ``` **A solução: Template Literal Types** Defina o formato das suas rotas. Isso cria uma "múltipla escolha" pra IA ao invés de uma pergunta aberta. ```typescript const ROUTES = { HOME: '/', USERS: '/users', USER_DETAIL: '/users/:id', } as const; type AppRoute = typeof ROUTES[keyof typeof ROUTES]; function navigate(route: AppRoute) { /* ... */ } // Uso navigate(ROUTES.USER_DETAIL); ``` **Por que funciona:** Você cria uma fonte única da verdade. A IA consegue ver todas as rotas válidas em um lugar só e para de inventar caminhos que não existem. --- #### 2. Mate a Sopa de Booleanos Componentes com estado tipo `isLoading`, `isError` e `isSuccess` criam estados impossíveis. O que acontece se `isLoading` e `isError` forem `true` ao mesmo tempo? Modelos de IA sofrem com isso. Podem escrever código que renderiza dados enquanto o spinner de loading ainda está aparecendo. **A solução: Discriminated Unions** Faça estados impossíveis serem realmente impossíveis de escrever. ```typescript type DataState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }; function UserList({ state }: { state: DataState }) { switch (state.status) { case 'loading': return ; case 'error': return ; case 'success': return ; } } ``` **Por que funciona:** O campo `status` força a IA a tratar cada caso. Ela não consegue acessar `data` antes de estar carregado porque o TypeScript não permite. --- #### 3. Trate Primitivos como Objetos de Domínio Pro TypeScript (e pra IA), uma `string` é só uma string. Não sabe que `userId` e `email` são coisas diferentes. Se você tem `sendEmail(to: string, from: string)`, a IA pode trocar eles sem perceber. **A solução: Branded Types** Dê significado semântico aos seus primitivos. ```typescript type Brand = K & { __brand: T }; type Email = Brand; type UserId = Brand; function createEmail(value: string): Email { if (!value.includes('@')) throw new Error('Email inválido'); return value as Email; } function sendInvite(to: Email, from: UserId) { /* ... */ } const adminId = 'u-123' as UserId; const userEmail = createEmail('john@example.com'); sendInvite(userEmail, adminId); // ✅ // sendInvite(adminId, userEmail); // ❌ Erro de tipo! ``` --- #### 4. Mova Lógica de Negócio para os Tipos Comentários são invisíveis pro compilador. A IA consegue ler, mas frequentemente ignora. Se você escreve `// Preço deve ser positivo`, a IA ainda pode gerar `price: -10`. **A solução: Smart Constructors** Coloque a lógica na criação do tipo. ```typescript type Price = Brand; function createPrice(value: number): Price { if (value < 0) throw new Error('Preço deve ser positivo'); return value as Price; } interface Product { name: string; price: Price; // Não é qualquer número } ``` **Por que funciona:** Força a IA (e você) a usar `createPrice` pra obter um objeto válido. A validação sempre roda. --- #### 5. Pare de Jogar Strings `try/catch` é opaco. Quando você chama uma função, não tem ideia do que ela pode lançar. Assistentes de IA são ruins em adivinhar tipos de erro em blocos catch. Frequentemente só escrevem `console.log(error)` e seguem em frente. **A solução: Result Types** Retorne erros como valores. Isso torna o tratamento de erros visível na assinatura da função. ```typescript type Result = { ok: true; value: T } | { ok: false; error: E }; type FetchError = | { type: 'NetworkError' } | { type: 'NotFound'; id: string }; async function getUser(id: string): Promise> { // implementação retorna objetos, não faz throw } // Uso const result = await getUser('123'); if (!result.ok) { // A IA sabe exatamente quais erros tratar switch(result.error.type) { case 'NotFound': /* ... */ case 'NetworkError': /* ... */ } } ``` **Por que funciona:** Sem mais mistério de "o que pode dar errado?". A IA consegue tratar todos os casos de erro porque estão listados no tipo. --- #### 6. Bônus: Não Confie em Nada de Fora Branded Types (Dica #4) são ótimos pra lógica interna. Mas dados de fora (APIs, formulários) são imprevisíveis. Se você escreve `as User` numa resposta de API, está mentindo pro compilador. E a IA vai acreditar nessa mentira. **A solução: Validação de Schema em Runtime** Use bibliotecas como Zod pra conectar dados de runtime com tipos estáticos. ```typescript import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(['admin', 'user']), }); type User = z.infer; async function fetchUser(id: string) { const data = await fetch(`/api/users/${id}`).then(res => res.json()); return UserSchema.parse(data); // Se passar, o tipo é real } ``` --- #### Conclusão Escrever código AI-friendly não é sobre simplificar as coisas. É sobre ser **explícito**. TypeScript é uma forma de documentação legível por máquina. Quando você usa esses padrões, cria um contrato que tanto seu compilador quanto seu assistente de IA conseguem entender. Na próxima vez que sua IA alucinar algo estranho, pergunte-se: dei contexto suficiente nos tipos? ----- Gostou desse conteúdo? Compartilha com seus amigos devs! Até a próxima! --- # Animando elementos quando saem e entram na tela com JavaScript Source: posts/animando-elementos-js.md ### **Como testar se um elemento está no viewport?** Existem muitas formas de se fazer isso, utilizando JavaScript. Essa funcionalidade pode ser útil para animar elementos que se tornam visíveis para o usuário, quando entram no viewport, otimizando a experiência e aumentando a imersão da sua aplicação. Nesse tutorial, não vou focar na questão das animações, porque entendo que é um tópico muito particular, tanto do desenvolvedor, como do projeto. A ideia é mostrar uma alternativa simples e fácil de ser implementada, para que você consiga capturar a posição de um elemento e animá-lo, seja na entrada ou na saída da janela. ---------- Começamos pela estrutura básica (`index.html`). Utilizaremos um conjunto de 6 imagens aleatórias, através de uma API do Unsplash. Essas imagens serão animadas em duas situações: quando "saírem" para cima ou para baixo da área visível da janela, do viewport. ```html Document ``` ---------- Em seguida, adicionaremos estilos no `style.css` que são apenas demonstrativos, para o `body` e as imagens: ```css body { padding: 10rem 5rem; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 10rem; background: #121212; overflow-x: hidden; } img { width: 100%; max-width: 600px; height: 400px; object-fit: cover; transition: 0.5s; } ``` ---------- Por último, ainda nos estilos, criaremos duas classes que serão aplicadas nas duas saídas possíveis do viewport: - *.is-down*, que será aplicada quando o elemento estiver abaixo da área visível - *.is-up*, que será aplicada quando o elemento estiver acima da área visível Lembrando que as propriedades utilizadas aqui são apenas para efeito de demonstração. Sinta-se a vontade para criar suas próprias transições, dentro do resultado esperado. ```css .is-down { transform: translateX(25%); opacity: 0; } .is-up { transform: translateX(-25%); opacity: 0; } ``` ---------- ### Capture e anime! Já no `script.js`, vamos começar capturando nossa lista de imagens, utilizando o método `querySelectorAll`, que vai retornar uma lista com todas as imagens que têm a classe `image`: ```javascript const images = document.querySelectorAll(".image"); ``` ---------- Em seguida, capturamos a altura da janela. Como queremos animar as imagens saindo acima e abaixo da área visível, saber a altura do viewport é fundamental para descobrir se um elemento está, ou não, na área visível para o usuário: ```javascript let windowHeight = window.innerHeight; ``` Criaremos uma função para animar as imagens. Ela vai utilizar o método `forEach` para percorrer a lista de imagens e aplicar as alterações necessárias. Para cada imagem da lista, vamos criar uma variável chamada `bounding` a qual será atribuída o objeto `DOMRect`, retornado do método `getBoundingClientRect()`. Esse objeto conta com as dimensões do elemento, bem como com suas coordenadas em relação ao viewport. O código a seguir mostra um exemplo da estrutura desse objeto. Ele não fará parte do nosso exemplo. Os valores das propriedades estão em pixels. ```javascript { bottom: -413.316650390625, ​ height: 400, ​ left: 491.5, ​ right: 1091.5, ​ top: -813.316650390625, width: 600, ​ x: 491.5, ​ y: -813.316650390625 } ``` ---------- A partir dessas coordenadas, que serão atribuídas a variável `bounding`, podemos definir se um objeto está dentro da área visível, partindo do seguinte raciocínio: Como o eixo Y da página começa no topo, essa posição é igual a 0. A base da página será igual a altura que foi atribuída a variável `windowHeight`. Se `bounding.bottom`, a base da imagem, for maior que `windowHeight`, a imagem não está dentro do viewport, mas abaixo da área visível, total ou parcialmente. Se `bounding.top`, o topo da imagem, for menor que 0, a imagem não está dentro do viewport, mas acima da área visível, total ou parcialmente. A partir daí, aplicamos as classes correspondentes. E caso nenhuma das lógicas seja verdadeira, removemos as classes da imagem, para que ela tenha sua aparência padrão, estando visível. ```javascript function animateImages() { images.forEach((image) => { let bounding = image.getBoundingClientRect(); if (bounding.bottom > windowHeight) { image.classList.add("is-down"); } else if (bounding.top < 0) { image.classList.add("is-up"); } else { image.classList.remove("is-up"); image.classList.remove("is-down"); } }); } ``` ---------- E como queremos que esse efeito seja aplicado durante a rolagem da página, adicionamos um `listener` que vai capturar o scroll e executar a função `animateImages()`. ```javascript document.addEventListener("scroll", function () { animateImages(); document.removeEventListener("scroll", this); }); ``` ---------- Além disso, incluímos um `listener` que vai capturar o redimensionamento da janela, atribuindo a nova altura a variável `windowHeight`. ```javascript window.addEventListener("resize", function () { windowHeight = window.innerHeight; window.removeEventListener("resize", this); }); ``` ---------- E para que a aplicação já adicione as classes as imagens que não estão visíveis para o usuário, executamos a `animateImages()`, assim que a aplicação é iniciada. ```javascript animateImages(); ``` ---------- Você pode ver a demonstração [aqui](https://animate-on-scroll.vercel.app) ---------- E como costumo dizer, aqui é só o ponto de partida. Você pode explorar outras possibilidades, com o `DOMRect` do `getBoundingClientRect()`. Só pra deixar um outro cenário possível nesse exemplo, se você quiser que um elemento só passe por uma transição quando ele estiver totalmente fora do viewport, você pode mudar as condicionais para quando o `bounding.bottom` (base do elemento) for menor que 0 (saiu totalmente, acima), ou o `bounding.top` (topo do elemento) for maior que `windowHeight` (saiu totalmente, abaixo). Você ainda pode adicionar áreas seguras para que seu elemento continue visível enquanto necessário. Pode aplicar as classes quando ele estiver a, por exemplo, 10% do fim da tela, acima ou abaixo. Possibilidades infinitas que vão depender do que você pretende fazer com seus elementos. ---------- Se você curtiu esse conteúdo, compartilhe com outras pessoas e ajude a espalhar a palavra! ---------- Nos vemos na próxima! --- # Fundamentos \nTypeScript com carros Source: posts/ensinando-ts-meu-filho-pt1.md > Ensinando TypeScript para o meu filho autista (pt 1) ![pedro-no-carrinho](https://github.com/user-attachments/assets/15c26a2b-0b27-4eea-a337-e1ed3e9436b8) #### Pedro é um menino autista, hoje com 8 anos. Ele me disse que quer aprender a programar pra trabalhar com o papai e eu decidi iniciar essa série em homenagem a ele. Ele tem alguns hiperfocos, mas o principal é com carros. Então esse será o tema dessa série, explicando fundamentos e conceitos TypeScript aplicados aos carros e suas funcionalidades. Começando pelos tipos primitivos e especiais: ### number É o tipo de informação que retorna dos instrumentos do painel, como o velocímetro. Sempre serão números e nada além disso. Se faltar, não vemos quão rápido o carro vai. ```ts function lervelocidade(): number {} // 80 function lerQuilometragemTotal(): number {} // 230000 ``` ### string São informações que têm letras e números. Podem ser a placa do carro, ou até mesmo a marca e modelo dele Se faltar, ninguém sabe qual é o carro. ```ts function lerPlacaDoCarro(): string {} // ABC-1Z34' function lerMarcaEModelo(): string {} // 'Chevrolet Classic' ``` ### boolean São informações simples, de verdadeiro ou falso. Servem, por exemplo, pra saber se uma parte do carro está, ou não, funcionando. Se faltar, não sabemos se o carro funciona. ```ts function motorEstaLigado(): boolean {} // true | false ``` ### null É como a bagagem no porta-malas antes de uma viagem. Ela ainda não está lá, mas vamos colocar depois. Se faltar, podemos esquecer algo importante. ```ts let bagagem: string | null = null; ``` ### undefined É uma informação que ninguém decidiu o que é. Como um espaço vazio no console do carro, que pode servir para instalar alguma coisa Faz falta se decidirmos usar e não houver nada lá. ```ts let acessorioDoConsole; /* Não sabemos o que é porque nada foi atrbuído ao espaço */ ``` ### symbol Guarda coisas com mesmo nome, sem confusão. Você quer instalar adesivos no seu carro, mas eles devem ficar em lugares diferentes. Alguns até meio que escondidos. Mas você sabe onde colocou e pode achar quando quiser. ```ts const adesivo1 = Symbol("adesivo"); const adesivo2 = Symbol("adesivo"); const carro = { adesivo: "adesivo no capô", [adesivo1] "adesivo embaixo do banco", [adesivo2]: "adesivo na caixa de roda" } console.log(Object.values(carro)) // locais dos adesivos: ['adesivo no capô'] console.log(carro[adesivo1]) // 'adesivo embaixo do banco' console.lng(carro[adesivo2]) // 'adesivo na caixa de roda' ``` ### any É o porta-trecos maluco. Pode guardar qualquer coisa: lancheira, ferramenta, bola, pneu, pedra, papel, tesoura... É ruim, porque a gente nunca sabe o que tem lá ```ts let portaTrecos: any = "vassoura"; portaTrecos = 22; portaTrecos = null; portaTrecos = false; ``` ### unknown É como o porta-luvas do carro. Tem algo ali dentro, mas você precisa ver o que é. Mais seguro que o `any`, porque você só não sabe o que é. Depois de conferir, pode usar de boa. ```ts let objetoNoPortaluvas: unknown = "manual do carro"; function lerManual(manual: string) { /* ... */ } if (typeof objetoNoPortaluvas === "string") { lerManual(objetoNoPortaluvas) } ``` ### never Aparece quando algo quebra, ou sai do controle Como quando o motor do carro não funciona. A gente normalmente não usa ele, mas dá pra aplicar quando sabemos que algo vai dar errado. ```ts function ligarMotorQuebrado(): never { throw new Error("Kaboom!"); } ``` ### void Como abrir a porta do carro, o porta-malas ou até o porta-luvas. É usado em ações qeu são necessárias para usarmos o carro, mas que não devolvem nenhuma informação. ```ts function abrirPortaDoCarro(): void {} ``` ----- O que mais você quer aprender sobre TypeScript com carros? Essa série vai continuar em breve... ----- Curta, compartilhe e me siga nas [redes](https://www.iwill.dev/links) para mais conteúdo. --- # Funções \nGenéricas \nInteligentes Source: posts/funcoes-genericas-inteligentes.md O que faz uma lib parecer "mágica"? Inferência de tipos! E você pode usar isso hoje! ### Funções reutilizáveis são o coração de qualquer projeto ou lib Imagine que precisamos retornar nome e idade de um usuário. A solução mais rápida seria essa: ```ts type User = { id: string; name: string; age: number; email: string; } function getNameAndAge(user: User): { name: string; age: number } { return { name: user.name, age: user.age, }; } const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = getNameAndAge(user); // valor: { "name": "William", "age": 36 } // tipo: { name: string, age: number } ``` ### E qual é o problema nisso? - ❌ A função só serve pra isso - ❌ Você provavelmente não vai usar de novo - ❌ Ela infere um tipo sem relação com o original - ❌ Ou você vai declarar um novo tipo pro retorno ### A mágica dos generics + inferência Vamos criar uma função `pick` que aceita como parâmetro: (a) um objeto e (b) um array de chaves correspondentes a ele. Como retorno da função, um "partial" do tipo original. ```ts function pick( obj: T, keys: K[] ): Pick { const result = {} as Pick; for (const key of keys) { result[key] = obj[key]; } return result; } ``` ### O resultado? Uma implementação segura, precisa, flexível e reutilizável, mantendo referência ao tipo original ```ts const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = pick(user, ["name", "age"]) // valor igual 🙃: { "name": "William", "age": 36 } // tipo relacionado ao original: Pick ``` ### A partir daí, você vai ao infinito e além 👉 - Uma função `omit`, para remover propriedades de um objeto ```ts type OmitKeys = { [P in keyof T as P extends K ? never : P]: T[P]; }; function omit( obj: T, keys: K[] ): OmitKeys { const result = { ...obj }; keys.forEach(key => delete result[key]); return result; } const userWithoutId = omit(user, ["id"]) /* valor: { "name": "William", "age": 36, "email": "iwilldev@outlook.com.br" } */ // tipo: OmitKeys ``` - Uma `merge`, para combinar dois objetos diferentes ```ts // const user: User = { ... } type Role = { role: 'admin' | 'editor' | 'viewer'; permissions: Array<'read' | 'write' | 'delete' | 'update'>; }; const role: Role = { role: 'viewer', permissions: ['read'] } const userWithRole = merge(user, role) /* valor: { "id": "1234", "name": "William", "age": 36, "email": "iwilldev@outlook.com.br", "role": "viewer", "permissions": [ "read" ] } */ // tipo: User & Role ``` ### As possibilidades são infinitas Com generics + inferência, você escreve: - ✅ Código mais seguro - ✅ Helpers mais inteligentes - ✅ Implementações mais elegantes 😎 E claro: muitas libs trazem funções como essas. Mas será que você precisa de uma lib inteira, pra resolver um problema pontual? Então domine seu código, abraçando o poder do TypeScript e tudo o que ele pode fazer pelo seu projeto! ----- ### Qual função você mais curtiu? Me conta aí nos comentários! 🔁 Salve, compartilhe e siga para mais conteúdo --- # Intersection Observer - Lazy loading, animações e scroll infinito sem libs Source: posts/intersection-observer.md Salve, devs e divas! Esse post inicia uma série que visa explorar as [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API), descobrindo e apresentando funcionalidades que podem ser alcançadas a partir delas. E considerando o costume de utilizarmos abstrações que trazem o mesmo resultado, queremos empoderar as opções nativas a fim de reduzir dependências em projetos e aprofundar os conhecimentos sobre os recursos disponíveis na Web. ----- Como _Front-Ender_, já esbarrei em alguns desafios para aumentar a interatividade da página com _infinite scrollings_ e animações de elementos quando eles entram e saem do _viewport_, ou até mesmo questões que impactam performance como _lazy-loading_ em imagens, a partir das ações do usuário. Em casos como esse, tudo se resumiria a verificar a intersecção entre um elemento alvo e um elemento pai ou até mesmo entre ele e o _viewport_ (área visível para o usuário) do documento e, a partir do estado e da visibilidade do alvo observado, aplicar as mudanças necessárias. Detectar a visibilidade de um elemento (ou entre dois deles) envolvia soluções não muito confiáveis e que tendiam a gerar problemas de performance nas páginas, já que precisávamos de _handlers_ e _loops_ aplicados a cada elemento afetado e chamando métodos como o `Element.getBoundingClientRect()`, o que gerava um peso na _main thread_ da aplicação, deixando a página e o próprio navegador mais lentos. ----- ### Conceitos e uso A **Intersection Observer API** fornece uma maneira de observar alterações de intersecção de forma assíncrona. Com sua implementação, o site não precisa mais lidar com essa responsabilidade na _main thread_ e o navegador fica livre para gerenciar as observações como achar melhor. É possível declarar uma função de _callback_ que é executada nas seguintes circunstâncias: - Um elemento alvo cruza (total ou parcialmente, conforme configuração) com o elemento `root`. - A primeira vez que o _Observer_ é solicitado a observar um elemento alvo. Essa API tem compatibilidade total com todos os navegadores modernos, com ressalvas para o **Safari** (Desktop e iOS) e o **Firefox for Android** onde o elemento `root` não pode ser um documento. ----- ### Criando um Intersection Observer Para criar um intersection observer você deve chamar seu construtor, enviando uma função de _callback_ como primeiro parâmetro e um objeto `options` como parâmetro (opcional) seguinte: ```js let options = { root: document.querySelector('#rootElement'), rootMargin: '0px', threshold: 1.0 } let observer = new IntersectionObserver(callback, options); ``` #### Intersection observer options O objeto `options` passado no construtor `IntersectionObserver()` te permite controlar as circunstâncias em que a função de _callback_ será executada: - `root` - Um elemento ancestral especificado ou o próprio _viewport_, na ausência de elemento declarado ou se o valor for `null`. - `rootMargin` - Define os limites de margem do elemento **root**, aumentando ou diminuindo a delimitação desse elemento, antes de computar uma intersecção. Pode ter valores similares ao CSS, como `"10px 20px 30px 40px"` (top, right, bottom, left). - `threshold` - A taxa de interseção (_intersection ratio_), que representa o percentual de visibilidade do elemento alvo em relação ao **root**: um valor entre 0,0 e 1,0. O _callback_ será executado sempre que a visibilidade do alvo ultrapassar o valor declarado, para cima ou para baixo. Pode ser declarado como: - Um número. Ex: `0.5`. _Callback_ executado quando a visibilidade ultrapassar 50%. - Um Array de números: Ex: `[0, 0.25, 0.5, 0.75, 1]`. O _callback_ será executado em cada percentual relacionado aos valores declarados. Nesse caso, a cada 25% de visibilidade. #### Declarando um elemento para ser observado Agora que criou o `observer`, você precisa declarar um elemento a ser observado por ele: ```js let target = document.querySelector('#targetElement'); observer.observe(target); ``` Nesse momento, o _callback_ é executado pela primeira vez, mesmo que o elemento alvo não esteja visível. Sempre que a visibilidade do alvo ultrapassar o valor de `threshold`, o _callback_ é invocado, recebendo uma lista de objetos `IntersectionObserverEntry` e o próprio `observer`. Esteja ciente de que esse _callback_, em si, será executado na _main thread_. Então tente não complicar a lógica executada nesse escopo: ```js let callback = (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { /* Verificamos o estado da 'entry' e efetuamos as alterações necessárias caso ela esteja visível */ } }); }; ``` Boa parte das aplicações desse _Observer_ podem ser feitas verificando apenas a propriedade `isIntersecting` da entry, que retorna um _boolean_ indicando se o elemento alvo está, ou não, cruzando com o elemento `root`, considerando os parâmetros declarados no objeto `options`. Para ver mais propriedades da interface `IntersectionObserverEntry`, confira a [documentação na MDN](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). Partindo do princípio que temos a base necessária para avançar, vamos aos casos de uso. ----- ### Arquivos utilizados Você pode usar o [repositório](https://github.com/owillgoncalves/intersection-observer) desse artigo com os arquivos finais divididos em pastas para cada caso. ----- ### Lazy-loading Imagine carregar todos os assets de uma página inteira e o usuário nem visualizá-los, porque decidiu desviar a navegação para outra página. Vira um desperdício de recurso para ele que no caso de estar em uma rede móvel, consumiu dados à toa e para você que precisou servir arquivos que não foram utilizados de fato. Partindo disso, vamos criar uma página em que as imagens só serão carregadas se estiverem visíveis. Começando pelo arquivo `index.html`: ```html Lazy Loading
``` Nas tags `img`, declaramos um [_placeholder_](https://owillgoncalves.github.io/intersection-observer/01-lazy-loading/placeholder.png) no atributo `src`, que será renderizado inicialmente. No atributo `data-src`, colocamos a URL da imagem desejada. Além disso, declaramos a classe `lazy` que será usada para selecionarmos as imagens. Temperamos com o `style.css`: ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } section { height: 100%; width: 100%; align-items: center; display: flex; justify-content: center; } ``` Agora precisamos observar as imagens e, quando elas estiverem visíveis, trocar o placeholder para a URL desejada. No arquivo `script.js`: Começamos selecionando as imagens. ```js const images = document.querySelectorAll('.lazy'); ``` Criamos nosso _Observer_. ```js const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const image = entry.target; image.src = image.dataset.src; image.classList.remove('lazy'); observer.unobserve(image); } }); }); ``` Dentro do _callback_, usamos o `forEach` nas `entries` e para cada `entry` verificamos se ela está cruzando a área visível (`entry.isIntersecting`). Se positivo, declaramos o `entry.target` como `image`, substituímos o `src` pelo `data-src`, removemos a classe `lazy` da imagem e mandamos o `observer` deixar de observar a imagem. Em seguida, utilizamos um `forEach` na `NodeList` gerada com nosso seletor do início, observando cada uma das imagens: ```js images.forEach(image => { observer.observe(image); }); ``` As imagens que já foram visualizadas têm o `src` com a URL desejada e as que ainda não apareceram na tela, seguem com o placeholder: ![Print-screen mostrando duas imagens no DOM, uma com a URL definitiva e outra com o placeholder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4olnbv195abbieih2zbv.png) Abrindo a aba Rede/Network no Dev Tools, você verá as imagens sendo carregadas conforme aparecem na tela. Você pode conferir o resultado [nesse link](https://owillgoncalves.github.io/intersection-observer/01-lazy-loading/). ----- ### Animações em scroll Esse caso é interessante para aumentar a interatividade e imersividade da página. Quando um elemento se torna visível, adicionamos uma classe CSS dando o efeito desejado. Podemos ainda removê-la, caso o elemento não esteja mais visível, repetindo o efeito a cada novo scroll. Começamos com o `index.html`: ```html Lazy Loading

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

``` As tags `p` serão capturadas pelo _observer_ através da classe `animate`. Adicionamos o `style.css`, incluindo as classes `animate` e `animate--active`. Essa segunda será responsável pelo efeito desejado. ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } section { height: 100%; width: 100%; padding: 20px; align-items: center; display: flex; justify-content: center; } .animate { width: 300px; opacity: 0; transform: translateX(-100px); transition: all 0.5s ease-in-out; } .animate--active { opacity: 1; transform: translateX(0); transition: all 0.5s ease-in-out; } ``` No `script.js`, começamos selecionando os textos através da classe `animate`. ```js const animatedTexts = document.querySelectorAll('.animate'); ``` Criamos o _observer_ e para cada `entry`, verificamos se ela está cruzando a tela. Se positivo, adicionamos a classe `animate--active`. Caso contrário, removemos essa classe. ```js const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate--active'); } else { entry.target.classList.remove('animate--active'); } }); }); ``` Por fim, usamos o `forEach` na lista de textos, para adicioná-los ao _observer_. ```js animatedTexts.forEach(text => { observer.observe(text); }); ``` O efeito será o texto deslizando a partir da esquerda, até o centro do `flex-container`. O resultado pode ser visto [nesse link](https://owillgoncalves.github.io/intersection-observer/02-animate-on-intersect/). A partir desse conceito, você tem a liberdade de fazer o que quiser com qualquer elemento, seja adicionando ou removendo classes, ou até usando animações CSS, para chegar ao efeito desejado. ----- ### Scroll Infinito Nesse caso, vamos criar uma página com rolagem infinita. Sempre que chegarmos ao último item da lista, novos itens serão adicionados, infinitamente. É uma aplicação boa para lista de produtos, por exemplo, em que o usuário pode simplesmente rolar a página e continuar visualizando os itens disponíveis, sem precisar navegar ou usar paginação. No `index.html` criamos uma div com a classe `container`, onde os itens serão adicionados. Abaixo dela, uma tag `p` com o texto _loading..._ vai indicar o fim da lista, trazendo um retorno para o usuário de que há mais para ser visto. ```html Document

loading...

``` No `style.css`, incluímos os estilos, incluindo os das imagens que serão carregadas. ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } .container { height: 100%; width: 100%; margin: 40px 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 40px; } img { width: 320px; height: 320px; object-fit: cover; } ``` No `script.js`, selecionamos o container: ```js const container = document.querySelector('.container'); ``` Vamos criar uma função chamada `getTenRandomImages`, que vai retornar 10 imagens, com URLs aleatórias. Essa função será responsável por popular o container. Em cenários reais, ela pode ser substituída por uma chamada a uma API que retorna dados a serem usados no aplicativo, por exemplo. ```js const getTenRandomImages = () => { const images = []; for (let i = 0; i < 10; i++) { const image = document.createElement('img'); image.src = `https://picsum.photos/300/300?random=${Math.random()}`; images.push(image); } return images; }; ``` Criamos o _observer_. No `callback`, se a entry observada (que será o último elemento filho do `container`) estiver cruzando a área visível, a função `getTenRandomImages` será usada para adicionar mais 10 imagens no `container`, a `entry` deixará de ser observada e o novo último filho (`lastElementChild`) do container passará a ser observado. ```js const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { container.append(...getTenRandomImages()); observer.unobserve(entry.target); observer.observe(container.lastElementChild); } }); }); ``` Por fim, adicionamos as 10 imagens iniciais no `container` e declaramos o último filho dele para ser observado, para que as novas imagens só sejam carregadas quando ele estiver visível. ```js container.append(...getTenRandomImages()); observer.observe(container.lastElementChild); ``` O resultado pode ser visto [aqui](https://owillgoncalves.github.io/intersection-observer/03-infinite-scroll/). ----- ### Conclusão Os casos apresentados aqui podem ser adaptados a contextos do mundo real, sem maiores dificuldades. Considerando que a **Intersection Observer API** tira da _main thread_ da aplicação essa responsabilidade de observar os elementos alvos, conseguimos escalar essa solução mesmo em aplicações com porte maior. Ela também é aplicável a frameworks como React e Vue, desde que você saiba como selecionar os elementos nos DOMs que são gerados por eles. É basicamente substituir o `querySelector` e o `querySelectorAll` pela abordagem da ferramenta que você utiliza. ----- Um grande abraço e até a próxima! ----- Referências: [Intersection Observer API - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) [Intersection Observer | W3C](https://w3c.github.io/IntersectionObserver/#intersection-observer-interface) --- # Mudando tema da tela com CSS Puro (Dark/Light Mode) Source: posts/mudando-tema-com-css-puro.md ### *We know the JS way* #### *Mas e se não usarmos scripts para trocar o tema das nossas aplicações?* O caminho é uma relação entre cascata e seletores bem especificados. Vamos do início: ---------- ##### HTML O primeiro elemento da árvore será um input do tipo checkbox. Seu irmão abaixo será o container da nossa aplicação. Ele é quem terá os estilos alterados para a mudança de tema. Dentro dele, teremos um label relacionado ao nosso input lá de cima, dentro de uma div que será sua área de transição, servindo como nosso botão para mudar o tema. ```html

Mudando tema com CSS Puro

O texto fica em contraste com o fundo

``` ---------- ##### CSS Nos estilos, aplicamos os resets e declaramos as variáveis para as cores usadas no tema: ```css * { margin: 0; padding: 0; box-sizing: border-box; } :root { --light: #cccccc; --dark: #151515; } ``` ---------- Tornamos nosso input invisível, já que usaremos o label dele como acionador. ```css #theme-switcher { display: none; } ``` ---------- E declaramos as propriedades do nosso container do app. Ele ocupará toda a tela, terá fundo claro e textos escuros, além de ser um flex-container. Esse último é opcional e só para facilitar a demonstração do resultado, centralizando o texto na tela. ```css #app-container { height: 100vh; background: var(--light); color: var(--dark); font-family: monospace; font-size: 1.5rem; transition: 0.3s; display: flex; flex-direction: column; align-items: center; justify-content: center; } ``` ---------- Declaramos a área por onde nosso botão vai deslizar, com posicionamento absoluto no topo: ```css .theme-switcher-area { border: 1px solid var(--light); background: var(--dark); border-radius: 2rem; width: 4.5rem; height: 2.5rem; padding: 0.2rem; position: absolute; top: 0.5rem; right: 0.5rem; } ``` ---------- O botão, em si, que usará o estilo `dashed` na borda, criando um efeito similar aos raios do sol, para o tema claro. ```css .theme-switcher-button { position: relative; display: block; background: #f1aa02; border-radius: 50%; width: 2rem; height: 2rem; border: 2px dashed var(--dark); transition: 0.3s; } ``` ---------- E por último, um pseudo-elemento `::after` sobre o botão. Ele terá a forma de um círculo menor que o elemento original, tornando-se uma sombra que transformará o acionador em uma lua, no tema escuro. Por isso, sua opacidade inicial será 0. ```css .theme-switcher-button::after { position: absolute; width: 80%; height: 80%; content: ""; background: var(--dark); border-radius: 50%; opacity: 0; transition: 0.3s; } ``` ---------- ### E aí vem a mágica! Como nosso input é o primeiro elemento da árvore, podemos usar a pseudo-classe ':checked', com os seletores apropriados, para mudar o estilo de qualquer elemento abaixo dele. Quando ele for selecionado, essas propriedades serão aplicadas. Começando pelo próprio acionador, transformando o sol em lua. Para isso, removemos a borda que veio para dar o efeito dos raios e deslocamos o botão para a direita. ```css #theme-switcher:checked + #app-container .theme-switcher-button { transform: translateX(100%); border: none; } ``` ---------- Em seguida, mudamos a opacidade da sombra, o `::after`, para gerar uma lua crescente, na mudança do botão. ```css #theme-switcher:checked + #app-container .theme-switcher-button::after { opacity: 1; } ``` ---------- Por último e pelo efeito desejado, invertemos a cor de fundo e de texto do nosso container da aplicação: ```css #theme-switcher:checked + #app-container { background: var(--dark); color: var(--light); } ``` ---------- #### E tá lá, onde a coruja dorme! ![Demonstração de mudança de tema usando só CSS](https://dev-to-uploads.s3.amazonaws.com/i/9q8ffcms8zgj2gqccihs.gif) ---------- Esse tutorial é só o início do mergulho. Por isso, use sua criatividade a partir dessa base e mude os estilos como você achar melhor! ---------- Se você curtiu esse conteúdo, compartilhe com outras pessoas e ajude a espalhar a palavra! ---------- Nos vemos na próxima! 🧙 --- # Por que eu amo Remix? \n(por Kent C. Dodds) Source: posts/porque-eu-amo-remix.md Esse artigo é uma tradução do post [Why I Love Remix](https://kentcdodds.com/blog/why-i-love-remix) do [Kent C. Dodds](https://kentcdodds.com). Obrigado, Kent, por permitir a tradução do conteúdo e por compartilhar tanto conhecimento com a comunidade. ----- [kentcdodds.com](https://kentcdodds.com) é totalmente feito por mim (e pelo [time](https://kentcdodds.com/credits)) usando [Remix](https://remix.run). Depois de escrever dezenas de milhares de linhas de código usando esse framework, criei grande apreciação pelo que ele pode fazer por mim e pelos usuários do meu site. E quero te falar um pouco sobre isso. ### Em uma frase Essa é a principal razão pela qual eu amo usar o Remix para construir sites: > ### Remix me permite construir experiências de usuário incríveis e ainda ficar feliz com o código que eu tive que escrever para chegar lá. E o que isso significa? Vamos nos aprofundar... ### A Experiência do Usuário Há um monte de fatores que impactam a experiência do usuário quando eles usam nossos softwares. Na maior parte do tempo, acho que as pessoas estão focadas na performance/velocidade e, embora isso seja uma parte importante, é apenas um pedaço do todo. A experiência do usuário inclui uma série de outros aspectos do seu site. Aqui estão alguns: - Acessibilidade - Performance - "Reflow" de conteúdo (quando um navegador processa e remonta parte ou toda uma página, após uma atualização ou interação). - Confiabilidade e disponibilidade - Tratamento de erros - Gerenciamento de pendências - Gerenciamento de estado - Melhoria progressiva - Resiliência (comportamento em condições de rede ruins) - Layout - Clareza do conteúdo Até mesmo a velocidade de desenvolvimento de features pode impactar a experiência do usuário. Então a UX é [indiretamente impactada](https://kentcdodds.com/blog/why-users-care-about-how-you-write-code) pela manutenibilidade do nosso código. Remix nos ajuda com muitos desses aspectos. Alguns sem que eu precise pensar nisso. Em particular, alguns dos maiores desafios envolvendo gerenciamento de estado (race conditions de mutações e carregamento de dados) são totalmente gerenciados dentro do framework. Por causa disso, os usuários não se deparam com a necessidade de atualizar a página porque estão vendo dados desatualizados. Isso acontece sem que eu precise pensar nisso. É como o Remix funciona. Remix faz muito para manter meu site rápido através do uso de tags `` para pré-carregar recursos e dados no momento certo. Às vezes, fico impressionado com o fato de meu site _parecer_ composto por arquivos estáticos em um CDN, mas _na verdade_ é renderizado/hidratado no servidor e cada página é completamente única para cada usuário (portanto, um cache HTTP compartilhado em um CDN não seria possível). Usar as APIs da plataforma é o que nos permite isso. É também o que faz o Remix ser tão resiliente e ótimo para melhorias progressivas. Em condições de rede ruins, onde o carregamento do JavaScript é lento ou falha, a API padrão do Remix para mutações (`
`) vai funcionar mesmo antes do app ser hidratado. Isso significa que o usuário pode começar a fazer o trabalho com o app, mesmo que esteja em uma conexão ruim. Muito melhor do que um botão em que o handler do `onClick` ainda não foi carregado (que era o meu padrão antes do Remix)! A forma declarativa do Remix tratar erros me permite lidar com eles no contexto em que eles acontecem. Combinado com o roteamento aninhado, o que você obtém é a capacidade de renderizar um erro contextual sem quebrar o resto do app. ![Exemplo de como os erros são exibidos dentro do contexto em que acontecem - imagem do post original](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dn27vfcko1q8d71s7ohp.png) E isso também funciona no servidor (que é sem igual no Remix), então os usuários terão a mesma experiência, independente do erro ter acontecido durante uma transição no client ou em um carregamento completo da página. > ### Remix traz a excelência como padrão da experiência do usuário. E é uma das principais razões pelas quais eu amo o framework. ### O Código Os apps que eu ajudei a construir são usados por milhões de pessoas pelo mundo. Construir sites com Remix me faz feliz com o código que coloco em produção. A maior razão para isso é que antes do Remix eu gastava muito tempo tentando resolver problemas de experiência do usuário. Pelo Remix ajudar tanto com a UX, lido com menos complexidades no código. Então tudo que me resta fazer é usar as APIs declarativas que Remix (e React) me dão, para construir meu app e a experiência do usuário é boa por padrão. > ### Quando uso Remix, posso deixar os _truques_ em casa. Francamente, esse é o maior ponto a se falar sobre o código: você percebe que códigos de demonstração são normalmente mais simples, ignorando nuances como estados pendentes, race conditions, tratamento de erros, acessibilidade, etc? Bem, meu código não é tão simples como um código de demonstração, mas o Remix deixa ele bem próximo, em simplicidade. Eu definitivamente ainda penso em acessibilidade (apesar das bibliotecas [@reach-ui](https://reach.tech) do Remix ajudarem bastante com isso) e estados pendentes/erros. Mas as APIs que o Remix me dá para essas coisas são simples e declarativas: ```tsx // app/events/$eventId/attendees.tsx const loader: LoaderFunction = async ({request, params}) => { // isso é executado no servidor // erros inesperados de execução irão disparar o ErrorBoundary para ser renderizado // erros esperados (como 401s, 404s, etc) irão renderizar o CatchBoundary // caso contrário, retorno uma resposta e isso irá renderizar o componente padrão return json(data) } export default function AttendeesRoute() { const data = useLoaderData() return
{/* renderiza os dados em 'data' */}
} export function ErrorBoundary({error}) { return
{/* renderiza uma mensagem do erro inesperado */}
} export function CatchBoundary() { const caught = useCatch() return
{/* renderiza a mensagem de erro para respostas tipo 400 */}
} ``` Ah! E para estados pendentes (seja mutações ou transições regulares) você pode colocar isso onde quiser que o efeito de Pending UI apareça (seja global ou local): ```tsx const transition = useTransition() const text = transition.state === 'submitting' ? 'Saving...' : transition.state === 'loading' ? 'Saved!' : 'Ready' ``` Isso simplifica drasticamente o código React que eu escrevo, ao ponto de eu não escrever nenhum código React ligado a HTTP. Essa comunicação _client-server_ é totalmente gerenciada pelo Remix de uma forma que otimiza a UX. E a fronteira entre cliente e servidor pode ser totalmente tipada, então eu gasto menos tempo indo e voltando entre o navegador e o editor para corrigir erros bobos. Também amo o fato do Remix ser fundamentado nas APIs da Web. Essa função `json` que chamamos na loader é apenas uma função simples para criar um objeto [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response). Isso mesmo! Se você quiser aprender a fazer algo com o Remix, gastará mais tempo na [mdn](https://developer.mozilla.org) do que na [documentação do Remix](https://docs.remix.run) e isso me leva a outra coisa que amo sobre o Remix: > ### Quanto melhor eu construo sites com Remix, melhor eu construo sites para a web. Isso acontece naturalmente graças ao fato do Remix usar muito da plataforma web e recorrer, o máximo possível, às APIs da Web. (Isso é semelhante ao fato de quanto melhor eu me sinto com React, melhor eu me sinto com JavaScript.) E pelo Remix ser fundamentado nas APIs da Web como a interface comum para o servidor, você pode implantar o mesmo aplicativo em qualquer plataforma (desde que o código que você traga junto funcione nessas plataformas) e tudo que você precisa fazer é mudar qual _adapter_ você está usando. Se você quiser executar em serverless ou em um contêiner Docker, o Remix cuida de tudo. Remix é o jQuery das plataformas de hospedagem. Ele normaliza as diferenças para que você possa escrever uma vez e hospedar em qualquer lugar. Outro lance incrível da `loader` é que, como ela é executada no servidor, eu posso integrar com APIs que me dão muito mais dados do que eu preciso e filtrar apenas o que é necessário. Isso significa que eu posso eliminar o problema de overfetching (sobrecarga de dados) que nos leva a usar a complexidade do `graphql`, por exemplo. Quero dizer, você ainda pode utilizar o `graphql` com o Remix, mas já que ele gerencia a comunicação cliente-servidor pra você, não há a necessidade de enviar uma biblioteca `graphql` enorme e complexa para o navegador: é só confiar no Remix para fazer a coisa certa no momento certo (o que ele faz). E se eu precisar de dados extras, basta voltar na loader e incluí-los na resposta. Tudo tipado e pronto para o código do lado do cliente. É fabuloso. Eu mencionei o componente `` anteriormente e como ele ainda funcionará mesmo antes do JavaScript ser carregado. E isso é ótimo para a experiência do usuário. E também é ótimo para a DX (Developer Experience - Experiência do Desenvolvedor), porque não preciso gerenciar um monte de `fetch` e estados para minhas mutações. Normalmente, tenho um `onSubmit` que adiciona um `event.preventDefault()`, `fetch`s, gerenciamento de race condition e invalidação de cache. Bem, com o Remix, tudo isso some e eu fico com uma API declarativa para as mutações: ```tsx // app/events/$eventId/attendees.tsx const action: ActionFunction = async ({request}) => { /* isso é executado no servidor e eu posso lidar com os dados do formulário (formData) aqui, seja em uma interação direta com o banco de dados ou chamando um serviço downstream para realizar a mutação. É simplesmente brilhante. */ return redirect(/* envie o usuário pra onde quiser, depois disso */) } export default function AttendeesRoute() { // Olha só! Nenhum event handler ou useEffect necessário! // Race conditions resolvidas. return (
) } ``` Ah! E você quer validação, certo? Bem, se você quiser fazer isso no servidor, então pode colocá-la na `action`. E se você quiser validar no cliente também? Bem, você literalmente move a lógica de validação da `action` para uma função e chama-a tanto na `action` quanto no seu componente. É isso! ### Conclusão Eu poderia seguir em frente, falando mais. Há tantos posts e workshops dentro da minha cabeça. Eu nem falei sobre como é simples implementar o Optimistic UI com o Remix, autenticação segura, abstração (reuso de código), paginação, não precisar de componentes `` graças ao roteamento aninhado, e muito mais. Eu vou falar sobre tudo isso eventualmente, prometo. E no fim do dia, tudo volta a isso: > ### Eu amo o Remix porque ele me permite construir experiências incríveis para o usuário e ainda ficar feliz com o código que escrevi para chegar lá. E isso é algo que eu posso apoiar e impulsionar. Quer se juntar a mim? --- # React Router 7: \n'Múltiplas Actions' em uma única rota Source: posts/rr7-multiple-actions.md Talvez esse seja a má interpretação mais comum de quem não conhece o React Router como framework (e o mesmo vale para o Remix): > Só consigo fazer uma action por rota Eu acredito que essa confusão se dê principalmente pelas comparações com o Next.js, suas Server Functions e até a forma como as rotas de API são declaradas nele. Muitos desenvolvedores enxergam as funções `loader` e `action` do React Router como "endpoints", funções com responsabilidades únicas, quando na verdade o verdadeiro papel delas vai muito além disso. `loader` e `action`, juntas, são como um Controller completo, onde podemos definir diferentes buscas e mutações distintas, cada uma com seu propósito. E é sobre parte disso que estudaremos aqui, simulando um CRUD de usuários: três maneiras diferentes de fazer múltiplas actions em uma única rota do React Router. E se você usa Remix até a versão 2 (pré-React Router 7), as mesmas abordagens devem servir para você. --- #### Usando o componente Form e comportamento padrão Esta é a abordagem mais tradicional. Usamos o componente `
` e diferenciamos as ações através do atributo `method`. Em uma rota, crie a `loader`: ```tsx export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; ``` Crie seu componentes com diferentes formulários, um para cada método/ação diferente: ```tsx import { Form, /* ... */ } from "react-router"; /* ... */ export default function UI({ loaderData: { users } }: Route.ComponentProps) { return (

Users CRUD (Form - default behavior)

Add User

{/* Formulário com method="post" para criar usuário */}

Users

{users.map((user: User) => (
{/* Formulário com method="put" para editar usuário */}
Role:
{/* Formulário com method="delete" para excluir usuário */}
))}

); } ``` Na mesma rota, defina a `action`. Usamos `await request.formData()` para ler os dados e pegamos o `request.method` para diferenciar POST, PUT e DELETE. ```tsx export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const method = request.method.toLowerCase(); switch (method) { case "post": { const name = formData.get("name") as string; const email = formData.get("email") as string; return await createUser({ name, email }); } case "put": { const id = formData.get("id") as string; const name = formData.get("name") as string; const email = formData.get("email") as string; return await updateUser(id, { name, email }); } case "delete": { const id = formData.get("id") as string; return await deleteUser(id); } default: throw new Response("Method not allowed", { status: 405, statusText: "Method Not Allowed", }); } }; ``` E é isso! Esta abordagem é robusta, funciona sem JavaScript e segue o princípio do Progressive Enhancement. Sua desvantagem (em relações às outras opções) é que a quantidade de mutações fica limitada aos métodos padrões. --- #### Com useSubmit e JSON - actions definidas nos dados enviados Nesse método, utilizaremos o hook `useSubmit`, para enviar um JSON com propriedade chamada `intent`, que definirá qual action desejamos performar. A primeira vantagem daqui é que ganhamos a flexibilidade de mutações customizadas, fora dos padrões de `method` no componente `
`. Usaremos a mesma loader do exemplo anterior: ```tsx export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; ``` No componente da rota, definimos event handlers para lidar com as ações do usuário e disparar a mutação correspondente. A principal vantagem aqui, por exemplo, é poder separar a edição do `role` de usuário das demais propriedades, em uma action distinta. ```tsx export default function UI({ loaderData: { users }, }: Route.ComponentProps) { const submit = useSubmit(); // Event handler para criar usuário const handleCreate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "createUser", payload: { name, email } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); event.currentTarget.reset(); }; // Event handler para editar usuário const handleUpdate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const userId = event.currentTarget.getAttribute("data-user-id"); if (userId) { const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "updateUser", payload: { id: userId, name, email }, }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler para alterar o cargo do usuário const handleChangeRole = (event: React.ChangeEvent) => { const userId = event.target.closest("form")?.getAttribute("data-user-id"); if (userId) { const role = event.target.value as "admin" | "user"; const data = { intent: "changeUserRole", payload: { id: userId, role } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler para excluir usuário const handleDelete = (userId: string) => { if (confirm("Tem certeza que deseja deletar este usuário?")) { const data = { intent: "deleteUser", payload: { id: userId } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; return (

Users CRUD (JSON API)

Add User

{/* Formulário para criar usuário */}

Users

{users.map((user: User) => ( {/* Formulário para editar usuário */}
Role:
{/* Evento que dispara a handler de edição de cargo */} {/* Evento de click para excluir usuário */}
))}

); } ``` Na mesma rota, defina a `action`. Usamos `await request.json()` para ler os dados e pegamos a propriedade `intent` do body, para endereçar a mutação: ```tsx export const action = async ({ request }: ActionFunctionArgs) => { const body = await request.json(); const { intent, payload } = body; switch (intent) { case "createUser": return await createUser(payload); case "updateUser": return await updateUser(payload.id, payload); case "deleteUser": return await deleteUser(payload.id); case "changeUserRole": return await changeUserRole(payload.id, payload.role); default: throw new Error("Method not allowed"); } }; ``` Essa versão é ideal para estruturas de dados mais complexas, como objetos aninhados, pela flexibilidade que o JSON dá. Você pode usar `Discriminated Unions`, com o TypeScript, para garantir uma tipagem forte dos payloads. A desvantagem é que depende de JavaScript e fica muito mais verbosa. Principalmente nos exemplos que trago aqui. Outro ponto é que, para envio de arquivos, usar FormData ainda é a melhor opção. Deus nos livre de base64. --- #### Actions como parâmetros de rota - a mais delícia das três opções Aqui, nós voltamos para o componente `Form`. Mas ao invés de usarmos o atributo `method`, para diferenciar as actions, usaremos o atributo `action`. Usaremos a mesma loader dos exemplos anteriores: ```tsx export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; ``` Na UI usamos o componente `Form`, associando a mutação desejada ao parâmetro `action`, além de definirmos `navigate={false}` para cada um dos formulários, para evitar navegações e alterações no histórico de navegação (você vai entender o motivo em seguida): ```tsx export default function UI({ loaderData: { users } }: Route.ComponentProps) { return (

Users CRUD (Actions as params)

Add User

{/* Formulário para criar usuário */}

Users

{users.map((user: User) => (
{/* Formulário para editar usuário */}
{/* Formulário para mudar o cargo */}
Role:
{/* Formulário para excluir usuário */}
))}

); } ``` Antes de avançar para a `action`, vamos entender o que acontecerá aqui: Imagine que essa rota sejá `/users`. Quando eu excluir um usuário, por exemplo, o formulário buscará `deleteUser` como uma rota filha de `/users`: `/users/deleteUser`. Com isso, para esse método, precisaremos criar uma rota adicional, dedicada às actions. > Ah, William! Mas você disse que seria tudo na mesma rota! Sim, pequeno gafanhoto! As actions continuarão sendo concentradas em uma única rota. Mas para ganhar flexibilidade, comportar uma UI mais complexa com múltiplas actions (inclusive customizadas), manter o comportamento padrão dos formulários e alcançar isso com um código simples e elegante não pode ser de graça. E o preço é separar as actions em uma rota com segmento dinâmico. No exemplo `/users`, essa rota seria `/users/:action`, declarando manualmente, ou `users.$action.tsx` usando as File Route Conventions (que funcionam exatamente como no Remix). Aqui nós voltamos a usar o `request.formData()` e passamos a buscar a action no parâmetro da rota. ```tsx export const action = async ({ request, params }: ActionFunctionArgs) => { const formData = await request.formData(); const action = params.action; switch (action) { case "createUser": const name = formData.get("name") as string; const email = formData.get("email") as string; return await createUser({ name, email }); case "updateUser": const id = formData.get("id") as string; const userName = formData.get("name") as string; const userEmail = formData.get("email") as string; return await updateUser(id, { name: userName, email: userEmail }); case "deleteUser": const deleteId = formData.get("id") as string; return await deleteUser(deleteId); case "changeUserRole": const roleId = formData.get("id") as string; const role = formData.get("role") as "admin" | "user"; return await changeUserRole(roleId, role); default: throw new Error("Method not allowed"); } }; ``` E porque eu acho que ela é a melhor abordagem? Porque mantem os comportamentos padrão do React Router (e também do Remix) ao mesmo tempo em que dá flexibilidade para diferentes cenários. A `action` deixa de ser um "caminho estreito" dando espaço para inúmeras possibilidades, além de servir para concentrar e organizar as diferentes mutações que uma tela e seus componentes fazem. --- Você pode conferir o exemplo completo no repositório, onde defini diferentes rotas para cada abordagem. E também testar ao vivo, para ter uma melhor visualização dos diferentes cenários, inspecionar o app e tudo mais. --- Espero que você tenha curtido! E compartilhe com seus amigos desenvolvedores que precisam conhecer mais sobre o React Router 7! A gente se vê! --- # Texto 'mágico' escrito automaticamente com JavaScript Source: posts/texto-magico-js.md > “Raios, herege! Estás a me dizer que os escritos surgirão sozinhos? Isso não é feitiço, ou bruxaria?” ![Confia](https://dev-to-uploads.s3.amazonaws.com/i/lppf5cp0b2sejw0rabzr.png) Não é mágica. É JavaScript. Vamos desenrolar isso abaixo: ----- Antes de tudo, precisamos criar, no nosso HTML, um elemento para receber o feitiço, digo, o texto criado. Pode ser um parágrafo (**p**) ou até um cabeçalho (**h1**, **h2**...). Basta ser de texto e ter um **id**. Lembrando que o **id** precisa ser exclusivo desse elemento. ```js

``` Pro nosso caso, vamos usar um **h1** com a id **magic-text**. ----- Em seguida, criamos e importamos o arquivo JavaScript que, pro nosso exemplo, será o **script.js**: ```js ``` ----- Já no **script.js**, vamos criar uma constante para interagir com nosso **h1**, usando o método **querySelector**, que nos permite selecionar elementos usando os mesmos seletores que vemos no CSS. No nosso caso, vamos usar a **id** precedida pelo **#**. ```js const magicTextHeader = document.querySelector('#magic-text'); ``` O método **querySelector** pode ser usado, tanto no documento, como em outros elementos, após declarados, selecionando seus respectivos filhos. ----- Em seguida, criamos uma constante com o texto a ser usado: ```js const text = 'Texto inserido automagicamente com JavaScript!'; ``` ----- Por fim, declaramos uma variável que servirá para nos ajudar a "percorrer" o texto: ```js let indexCharacter = 0; ``` ----- A função que retornará o texto é a **writeText()**: ```js function writeText() { magicTextHeader.innerText = text.slice(0, indexCharacter); indexCharacter++; if(indexCharacter > text.length - 1) { setTimeout(() => { indexCharacter = 0; }, 2000); } } ``` Na primeira linha, incluímos o texto na propriedade **innerText** do **h1**, utilizando o método **.slice()**, que percorrerá nossa constante **text**, letra a letra, como se ela fosse um **array**. A sintaxe do **.slice()** é `.slice(a,b)`, onde **a** é a chave inicial do trecho a ser retornado e **b** é a chave final desse mesmo trecho. Como queremos retornar o texto desde o início, começamos com a chave 0 e finalizamos com o valor da **indexCharacter**, que é incrementada na linha seguinte, garantindo que a próxima execução da função retornará um caractere a mais e assim por diante. Em seguida, usamos uma condicional para verificar se a **indexCharacter** é igual a última posição do texto (`text.length - 1`; como a primeira chave é 0, a última será o tamanho (length) do texto menos 1). Se a condição for verdadeira, a **indexCharacter** será zerada, depois de um **setTimeout** de 2000 milissegundos, fazendo com que o texto volte a ser "digitado" do início. ----- E para executar essa função de forma contínua, garantindo o incremento da **indexCharacter** e o efeito desejado para nosso texto, usamos um **setInterval** que executará a função **writeText** a cada 100 milissegundos: ```js setInterval(writeText, 100); ``` ----- E a mágica está concluída! ----- Você pode ver um exemplo [aqui](https://g31-magic-text.vercel.app/). E conferir minha versão do código [aqui](https://github.com/williammago/goodbye.31/tree/main/28%20-%20Auto%20Write%20Text%20com%20JavaScript). ----- E, opcionalmente, usar os estilos que usei lá: ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; background: darkred; color: #FFF; } h1 { font-size: 2rem; max-width: 400px; text-align: center; } ``` ----- Esse artigo foi inspirado em um [vídeo](https://www.youtube.com/watch?v=8GPPJpiLqHk) do [canal](https://www.youtube.com/channel/UCeU-1X402kT-JlLdAitxSMA) do Florin Pop, que tem tutoriais e desafios incríveis pra quem está iniciando. Conteúdo em inglês. ----- Nos vemos na próxima! Grande abraço! --- # 6 TypeScript Habits for\nAI-Friendly Code Source: posts-en/ai-friendly-typescript.md You ask your AI assistant for a simple refactor. It returns code that looks correct but is subtly wrong. It creates routes that don't exist, handles errors that can't happen, or invents impossible state combinations. **If your AI doesn't understand your code, maybe your types aren't telling the full story.** We're not just writing code for compilers anymore. We're also writing context for our AI teammates. The more explicit your types are, the less room for hallucinations. Here are 6 strategies to write TypeScript that helps your AI (and your team) work better. --- #### 1. Stop Using Strings for Routes One easy way to break an app is a typo in a route string. When you write `router.push('/users/' + id)`, you're trusting yourself (and the AI) to remember the exact path every time. AI models love to guess here. They often suggest `/user/` instead of `/users/` or miss a slash. **The problem:** ```typescript // ❌ Ambiguous. The AI has to guess what string goes here. function navigateTo(path: string) { ... } ``` **The solution: Template Literal Types** Define the shape of your routes. This creates a "multiple choice" for the AI instead of an open question. ```typescript const ROUTES = { HOME: '/', USERS: '/users', USER_DETAIL: '/users/:id', } as const; type AppRoute = typeof ROUTES[keyof typeof ROUTES]; function navigate(route: AppRoute) { /* ... */ } // Usage navigate(ROUTES.USER_DETAIL); ``` **Why it works:** You create a single source of truth. The AI can see all valid routes in one place and stops inventing paths that don't exist. --- #### 2. Kill the Boolean Soup Components with state like `isLoading`, `isError`, and `isSuccess` create impossible states. What happens if `isLoading` and `isError` are both `true`? AI models struggle with this. They might write code that renders data while the loading spinner is still showing. **The solution: Discriminated Unions** Make impossible states actually impossible to write. ```typescript type DataState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }; function UserList({ state }: { state: DataState }) { switch (state.status) { case 'loading': return ; case 'error': return ; case 'success': return ; } } ``` **Why it works:** The `status` field forces the AI to handle every case. It can't access `data` before it's loaded because TypeScript won't allow it. --- #### 3. Treat Primitives Like Domain Objects To TypeScript (and AI), a `string` is just a string. It doesn't know that `userId` and `email` are different things. If you have `sendEmail(to: string, from: string)`, the AI can swap them without noticing. **The solution: Branded Types** Give your primitives semantic meaning. ```typescript type Brand = K & { __brand: T }; type Email = Brand; type UserId = Brand; function createEmail(value: string): Email { if (!value.includes('@')) throw new Error('Invalid email'); return value as Email; } function sendInvite(to: Email, from: UserId) { /* ... */ } const adminId = 'u-123' as UserId; const userEmail = createEmail('john@example.com'); sendInvite(userEmail, adminId); // ✅ // sendInvite(adminId, userEmail); // ❌ Type error! ``` --- #### 4. Move Business Logic into Types Comments are invisible to the compiler. AI can read them, but often ignores them. If you write `// Price must be positive`, the AI might still generate `price: -10`. **The solution: Smart Constructors** Put the logic into the type creation. ```typescript type Price = Brand; function createPrice(value: number): Price { if (value < 0) throw new Error('Price must be positive'); return value as Price; } interface Product { name: string; price: Price; // Not just any number } ``` **Why it works:** It forces the AI (and you) to use `createPrice` to get a valid object. The validation always runs. --- #### 5. Stop Throwing Strings `try/catch` is opaque. When you call a function, you have no idea what it might throw. AI assistants are bad at guessing error types in catch blocks. They often just write `console.log(error)` and move on. **The solution: Result Types** Return errors as values. This makes error handling visible in the function signature. ```typescript type Result = { ok: true; value: T } | { ok: false; error: E }; type FetchError = | { type: 'NetworkError' } | { type: 'NotFound'; id: string }; async function getUser(id: string): Promise> { // implementation returns objects, not throws } // Usage const result = await getUser('123'); if (!result.ok) { // The AI knows exactly which errors to handle switch(result.error.type) { case 'NotFound': /* ... */ case 'NetworkError': /* ... */ } } ``` **Why it works:** No more "what could go wrong?" mystery. The AI can handle all error cases because they're listed in the type. --- #### 6. Bonus: Trust Nothing at the Edge Branded Types (Tip #4) are great for internal logic. But data from outside (APIs, forms) is unpredictable. If you write `as User` on an API response, you're lying to the compiler. And the AI will believe that lie. **The solution: Runtime Schema Validation** Use libraries like Zod to connect runtime data with static types. ```typescript import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), role: z.enum(['admin', 'user']), }); type User = z.infer; async function fetchUser(id: string) { const data = await fetch(`/api/users/${id}`).then(res => res.json()); return UserSchema.parse(data); // If this passes, the type is real } ``` --- #### Wrapping Up Writing AI-friendly code isn't about simplifying things. It's about being **explicit**. TypeScript is a form of machine-readable documentation. When you use these patterns, you create a contract that both your compiler and your AI partner can understand. Next time your AI hallucinates something weird, ask yourself: did I give it enough context in the types? ----- Did you like this content? Share it with your dev friends! See you around! --- # Animating elements when they enter and leave the screen with JavaScript Source: posts-en/animando-elementos-js.md ### **How to test if an element is in the viewport?** There are many ways to do this using JavaScript. This functionality can be useful for animating elements that become visible to the user when they enter the viewport, optimizing the experience and increasing immersion in your application. In this tutorial, I won't focus on animations themselves because I understand it's a topic that's very particular to both the developer and the project. The idea is to show a simple and easy-to-implement alternative, so you can capture an element's position and animate it, whether entering or leaving the window. ---------- We start with the basic structure (`index.html`). We'll use a set of 6 random images through an Unsplash API. These images will be animated in two situations: when they "leave" above or below the visible area of the window, the viewport. ```html Document ``` ---------- Next, we'll add styles in `style.css` that are just for demonstration, for the `body` and images: ```css body { padding: 10rem 5rem; display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: 10rem; background: #121212; overflow-x: hidden; } img { width: 100%; max-width: 600px; height: 400px; object-fit: cover; transition: 0.5s; } ``` ---------- Finally, still in the styles, we'll create two classes that will be applied for the two possible viewport exits: - *.is-down*, which will be applied when the element is below the visible area - *.is-up*, which will be applied when the element is above the visible area Remember that the properties used here are just for demonstration purposes. Feel free to create your own transitions to achieve your expected result. ```css .is-down { transform: translateX(25%); opacity: 0; } .is-up { transform: translateX(-25%); opacity: 0; } ``` ---------- ### Capture and animate! In `script.js`, let's start by capturing our list of images using the `querySelectorAll` method, which will return a list of all images that have the `image` class: ```javascript const images = document.querySelectorAll(".image"); ``` ---------- Next, we capture the window height. Since we want to animate images leaving above and below the visible area, knowing the viewport height is essential to find out whether an element is within the user's visible area or not: ```javascript let windowHeight = window.innerHeight; ``` We'll create a function to animate the images. It will use the `forEach` method to loop through the image list and apply the necessary changes. For each image in the list, we'll create a variable called `bounding` which will be assigned the `DOMRect` object returned from the `getBoundingClientRect()` method. This object contains the element's dimensions as well as its coordinates relative to the viewport. The following code shows an example of this object's structure. It won't be part of our example. The property values are in pixels. ```javascript { bottom: -413.316650390625, ​ height: 400, ​ left: 491.5, ​ right: 1091.5, ​ top: -813.316650390625, width: 600, ​ x: 491.5, ​ y: -813.316650390625 } ``` ---------- From these coordinates, which will be assigned to the `bounding` variable, we can determine if an object is within the visible area using the following logic: Since the page's Y-axis starts at the top, this position equals 0. The bottom of the page will equal the height assigned to the `windowHeight` variable. If `bounding.bottom`, the image's bottom, is greater than `windowHeight`, the image is not within the viewport but below the visible area, totally or partially. If `bounding.top`, the image's top, is less than 0, the image is not within the viewport but above the visible area, totally or partially. From there, we apply the corresponding classes. And if none of the conditions are true, we remove the classes from the image so it has its default appearance, being visible. ```javascript function animateImages() { images.forEach((image) => { let bounding = image.getBoundingClientRect(); if (bounding.bottom > windowHeight) { image.classList.add("is-down"); } else if (bounding.top < 0) { image.classList.add("is-up"); } else { image.classList.remove("is-up"); image.classList.remove("is-down"); } }); } ``` ---------- And since we want this effect to be applied during page scrolling, we add a `listener` that will capture the scroll and execute the `animateImages()` function. ```javascript document.addEventListener("scroll", function () { animateImages(); document.removeEventListener("scroll", this); }); ``` ---------- Additionally, we include a `listener` that will capture window resizing, assigning the new height to the `windowHeight` variable. ```javascript window.addEventListener("resize", function () { windowHeight = window.innerHeight; window.removeEventListener("resize", this); }); ``` ---------- And to ensure the application already adds classes to images that aren't visible to the user, we execute `animateImages()` as soon as the application starts. ```javascript animateImages(); ``` ---------- You can see the demo [here](https://animate-on-scroll.vercel.app) ---------- And as I usually say, this is just the starting point. You can explore other possibilities with the `DOMRect` from `getBoundingClientRect()`. Just to leave you with another possible scenario in this example, if you want an element to only go through a transition when it's completely out of the viewport, you can change the conditionals to when `bounding.bottom` (element's bottom) is less than 0 (completely left, above), or `bounding.top` (element's top) is greater than `windowHeight` (completely left, below). You can also add safe areas so your element stays visible as long as needed. You can apply classes when it's, for example, 10% from the end of the screen, above or below. Infinite possibilities that will depend on what you intend to do with your elements. ---------- If you enjoyed this content, share it with others and help spread the word! ---------- See you next time! --- # TypeScript Fundamentals\nwith Cars Source: posts-en/ensinando-ts-meu-filho-pt1.md > Teaching TypeScript to my autistic son (pt 1) ![pedro-no-carrinho](https://github.com/user-attachments/assets/15c26a2b-0b27-4eea-a337-e1ed3e9436b8) #### Pedro is an autistic boy, now 8 years old. He told me he wants to learn programming to work with daddy, and I decided to start this series in his honor. He has some hyperfocuses, but the main one is cars. So that will be the theme of this series, explaining TypeScript fundamentals and concepts applied to cars and their features. Starting with primitive and special types: ### number It's the type of information returned from dashboard instruments, like the speedometer. It will always be numbers and nothing else. If missing, we can't see how fast the car is going. ```ts function readSpeed(): number {} // 80 function readTotalMileage(): number {} // 230000 ``` ### string It's information that contains letters and numbers. It can be the car's license plate, or even its make and model. If missing, nobody knows which car it is. ```ts function readLicensePlate(): string {} // 'ABC-1Z34' function readMakeAndModel(): string {} // 'Chevrolet Classic' ``` ### boolean It's simple information, true or false. It serves, for example, to know if a part of the car is working or not. If missing, we don't know if the car works. ```ts function isEngineRunning(): boolean {} // true | false ``` ### null It's like the luggage in the trunk before a trip. It's not there yet, but we'll put it there later. If missing, we might forget something important. ```ts let luggage: string | null = null; ``` ### undefined It's information that nobody decided what it is. Like an empty space in the car console, which could be used to install something. It's missed if we decide to use it and there's nothing there. ```ts let consoleAccessory; /* We don't know what it is because nothing was assigned to the space */ ``` ### symbol Stores things with the same name, without confusion. You want to put stickers on your car, but they should go in different places. Some even a bit hidden. But you know where you put them and can find them whenever you want. ```ts const sticker1 = Symbol("sticker"); const sticker2 = Symbol("sticker"); const car = { sticker: "sticker on hood", [sticker1]: "sticker under seat", [sticker2]: "sticker on wheel arch" } console.log(Object.values(car)) // sticker locations: ['sticker on hood'] console.log(car[sticker1]) // 'sticker under seat' console.log(car[sticker2]) // 'sticker on wheel arch' ``` ### any It's the crazy junk drawer. It can store anything: lunchbox, tool, ball, tire, rock, paper, scissors... It's bad, because we never know what's in there. ```ts let junkDrawer: any = "broom"; junkDrawer = 22; junkDrawer = null; junkDrawer = false; ``` ### unknown It's like the car's glove compartment. There's something in there, but you need to check what it is. Safer than `any`, because you just don't know what it is. After checking, you can use it freely. ```ts let gloveboxItem: unknown = "car manual"; function readManual(manual: string) { /* ... */ } if (typeof gloveboxItem === "string") { readManual(gloveboxItem) } ``` ### never Appears when something breaks or gets out of control. Like when the car engine doesn't work. We normally don't use it, but we can apply it when we know something will go wrong. ```ts function startBrokenEngine(): never { throw new Error("Kaboom!"); } ``` ### void Like opening the car door, the trunk, or even the glove compartment. It's used in actions that are necessary to use the car, but don't return any information. ```ts function openCarDoor(): void {} ``` ----- What else do you want to learn about TypeScript with cars? This series will continue soon... ----- Like, share, and follow me on [social media](https://www.iwill.dev/links) for more content. --- # Smart Generic\nFunctions Source: posts-en/funcoes-genericas-inteligentes.md What makes a lib feel "magical"? Type inference! And you can use this today! ### Reusable functions are the heart of any project or lib Imagine we need to return a user's name and age. The quickest solution would be this: ```ts type User = { id: string; name: string; age: number; email: string; } function getNameAndAge(user: User): { name: string; age: number } { return { name: user.name, age: user.age, }; } const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = getNameAndAge(user); // value: { "name": "William", "age": 36 } // type: { name: string, age: number } ``` ### And what's the problem with this? - ❌ The function only serves for this purpose - ❌ You probably won't use it again - ❌ It infers a type unrelated to the original - ❌ Or you'll declare a new type for the return ### The magic of generics + inference Let's create a `pick` function that accepts as parameters: (a) an object and (b) an array of keys corresponding to it. As the function's return, a "partial" of the original type. ```ts function pick( obj: T, keys: K[] ): Pick { const result = {} as Pick; for (const key of keys) { result[key] = obj[key]; } return result; } ``` ### The result? A safe, precise, flexible, and reusable implementation, maintaining reference to the original type ```ts const user: User = { id: '1234', name: 'William', age: 36, email: 'iwilldev@outlook.com.br' } const userNameAndAge = pick(user, ["name", "age"]) // same value 🙃: { "name": "William", "age": 36 } // type related to original: Pick ``` ### From there, you go to infinity and beyond 👉 - An `omit` function, to remove properties from an object ```ts type OmitKeys = { [P in keyof T as P extends K ? never : P]: T[P]; }; function omit( obj: T, keys: K[] ): OmitKeys { const result = { ...obj }; keys.forEach(key => delete result[key]); return result; } const userWithoutId = omit(user, ["id"]) /* value: { "name": "William", "age": 36, "email": "iwilldev@outlook.com.br" } */ // type: OmitKeys ``` - A `merge`, to combine two different objects ```ts // const user: User = { ... } type Role = { role: 'admin' | 'editor' | 'viewer'; permissions: Array<'read' | 'write' | 'delete' | 'update'>; }; const role: Role = { role: 'viewer', permissions: ['read'] } const userWithRole = merge(user, role) /* value: { "id": "1234", "name": "William", "age": 36, "email": "iwilldev@outlook.com.br", "role": "viewer", "permissions": [ "read" ] } */ // type: User & Role ``` ### The possibilities are endless With generics + inference, you write: - ✅ Safer code - ✅ Smarter helpers - ✅ More elegant implementations 😎 And of course: many libs bring functions like these. But do you need an entire lib to solve a specific problem? So master your code, embracing the power of TypeScript and everything it can do for your project! ----- ### Which function did you like the most? Tell me in the comments! 🔁 Save, share, and follow for more content --- # Intersection Observer - Lazy loading, animations, and infinite scroll without libs Source: posts-en/intersection-observer.md What's up, devs! This post starts a series aimed at exploring the [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API), discovering and presenting functionalities that can be achieved through them. And considering our habit of using abstractions that bring the same result, we want to empower native options in order to reduce dependencies in projects and deepen our knowledge about the resources available on the Web. ----- As a _Front-Ender_, I've stumbled upon some challenges to increase page interactivity with _infinite scrollings_ and element animations when they enter and leave the _viewport_, or even performance issues like _lazy-loading_ images, based on user actions. In cases like this, everything would come down to checking the intersection between a target element and a parent element or even between it and the document's _viewport_ (visible area to the user) and, based on the observed target's state and visibility, apply the necessary changes. Detecting the visibility of an element (or between two of them) involved not very reliable solutions that tended to cause performance problems on pages, since we needed _handlers_ and _loops_ applied to each affected element and calling methods like `Element.getBoundingClientRect()`, which created a burden on the application's _main thread_, making the page and the browser itself slower. ----- ### Concepts and usage The **Intersection Observer API** provides a way to asynchronously observe intersection changes. With its implementation, the site no longer needs to handle this responsibility on the _main thread_ and the browser is free to manage observations as it sees fit. It's possible to declare a _callback_ function that is executed in the following circumstances: - A target element crosses (totally or partially, according to configuration) with the `root` element. - The first time the _Observer_ is asked to observe a target element. This API has full compatibility with all modern browsers, with caveats for **Safari** (Desktop and iOS) and **Firefox for Android** where the `root` element cannot be a document. ----- ### Creating an Intersection Observer To create an intersection observer you must call its constructor, sending a _callback_ function as the first parameter and an `options` object as the (optional) next parameter: ```js let options = { root: document.querySelector('#rootElement'), rootMargin: '0px', threshold: 1.0 } let observer = new IntersectionObserver(callback, options); ``` #### Intersection observer options The `options` object passed to the `IntersectionObserver()` constructor allows you to control the circumstances in which the _callback_ function will be executed: - `root` - A specified ancestor element or the _viewport_ itself, in the absence of a declared element or if the value is `null`. - `rootMargin` - Defines the margin limits of the **root** element, increasing or decreasing the delimitation of this element, before computing an intersection. It can have values similar to CSS, like `"10px 20px 30px 40px"` (top, right, bottom, left). - `threshold` - The _intersection ratio_, which represents the percentage of visibility of the target element relative to the **root**: a value between 0.0 and 1.0. The _callback_ will be executed whenever the target's visibility exceeds the declared value, up or down. It can be declared as: - A number. Ex: `0.5`. _Callback_ executed when visibility exceeds 50%. - An Array of numbers: Ex: `[0, 0.25, 0.5, 0.75, 1]`. The _callback_ will be executed at each percentage related to the declared values. In this case, every 25% of visibility. #### Declaring an element to be observed Now that you've created the `observer`, you need to declare an element to be observed by it: ```js let target = document.querySelector('#targetElement'); observer.observe(target); ``` At this moment, the _callback_ is executed for the first time, even if the target element is not visible. Whenever the target's visibility exceeds the `threshold` value, the _callback_ is invoked, receiving a list of `IntersectionObserverEntry` objects and the `observer` itself. Be aware that this _callback_ itself will be executed on the _main thread_. So try not to complicate the logic executed in this scope: ```js let callback = (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { /* We check the 'entry' state and make the necessary changes if it's visible */ } }); }; ``` Most applications of this _Observer_ can be done by just checking the `isIntersecting` property of the entry, which returns a _boolean_ indicating whether the target element is, or is not, crossing with the `root` element, considering the parameters declared in the `options` object. To see more properties of the `IntersectionObserverEntry` interface, check the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). Assuming we have the necessary foundation to move forward, let's go to the use cases. ----- ### Files used You can use the [repository](https://github.com/owillgoncalves/intersection-observer) for this article with the final files divided into folders for each case. ----- ### Lazy-loading Imagine loading all the assets of an entire page and the user not even viewing them, because they decided to navigate to another page. It becomes a waste of resources for them who, in the case of being on a mobile network, consumed data for nothing, and for you who needed to serve files that weren't actually used. Based on this, let's create a page where images will only be loaded if they are visible. Starting with the `index.html` file: ```html Lazy Loading
``` In the `img` tags, we declare a [_placeholder_](https://owillgoncalves.github.io/intersection-observer/01-lazy-loading/placeholder.png) in the `src` attribute, which will be rendered initially. In the `data-src` attribute, we put the desired image URL. Additionally, we declare the `lazy` class which will be used to select the images. We season with `style.css`: ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } section { height: 100%; width: 100%; align-items: center; display: flex; justify-content: center; } ``` Now we need to observe the images and, when they are visible, swap the placeholder for the desired URL. In the `script.js` file: We start by selecting the images. ```js const images = document.querySelectorAll('.lazy'); ``` We create our _Observer_. ```js const observer = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const image = entry.target; image.src = image.dataset.src; image.classList.remove('lazy'); observer.unobserve(image); } }); }); ``` Inside the _callback_, we use `forEach` on the `entries` and for each `entry` we check if it's crossing the visible area (`entry.isIntersecting`). If positive, we declare the `entry.target` as `image`, replace the `src` with `data-src`, remove the `lazy` class from the image and tell the `observer` to stop observing the image. Next, we use a `forEach` on the `NodeList` generated with our selector from the beginning, observing each of the images: ```js images.forEach(image => { observer.observe(image); }); ``` Images that have already been viewed have the `src` with the desired URL and those that haven't appeared on screen yet still have the placeholder: ![Print-screen showing two images in the DOM, one with the definitive URL and another with the placeholder](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4olnbv195abbieih2zbv.png) Opening the Network tab in Dev Tools, you'll see the images being loaded as they appear on screen. You can check the result [at this link](https://owillgoncalves.github.io/intersection-observer/01-lazy-loading/). ----- ### Scroll animations This case is interesting for increasing interactivity and immersiveness of the page. When an element becomes visible, we add a CSS class giving the desired effect. We can also remove it if the element is no longer visible, repeating the effect with each new scroll. We start with `index.html`: ```html Lazy Loading

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Minima, impedit explicabo sunt omnis veritatis quia soluta alias sed animi earum error recusandae maxime, at reiciendis amet magnam perspiciatis iure dolorem.

``` The `p` tags will be captured by the _observer_ through the `animate` class. We add the `style.css`, including the `animate` and `animate--active` classes. The second one will be responsible for the desired effect. ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } section { height: 100%; width: 100%; padding: 20px; align-items: center; display: flex; justify-content: center; } .animate { width: 300px; opacity: 0; transform: translateX(-100px); transition: all 0.5s ease-in-out; } .animate--active { opacity: 1; transform: translateX(0); transition: all 0.5s ease-in-out; } ``` In `script.js`, we start by selecting the texts through the `animate` class. ```js const animatedTexts = document.querySelectorAll('.animate'); ``` We create the _observer_ and for each `entry`, we check if it's crossing the screen. If positive, we add the `animate--active` class. Otherwise, we remove this class. ```js const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate--active'); } else { entry.target.classList.remove('animate--active'); } }); }); ``` Finally, we use `forEach` on the list of texts to add them to the _observer_. ```js animatedTexts.forEach(text => { observer.observe(text); }); ``` The effect will be the text sliding from the left to the center of the `flex-container`. The result can be seen [at this link](https://owillgoncalves.github.io/intersection-observer/02-animate-on-intersect/). From this concept, you have the freedom to do whatever you want with any element, whether adding or removing classes, or even using CSS animations, to achieve the desired effect. ----- ### Infinite Scroll In this case, we'll create a page with infinite scrolling. Whenever we reach the last item in the list, new items will be added, infinitely. It's a good application for product lists, for example, where the user can simply scroll the page and continue viewing the available items, without needing to navigate or use pagination. In `index.html` we create a div with the `container` class, where items will be added. Below it, a `p` tag with the text _loading..._ will indicate the end of the list, providing feedback to the user that there's more to be seen. ```html Document

loading...

``` In `style.css`, we include the styles, including those for the images that will be loaded. ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { font-family: "Roboto", sans-serif; background-color: #f5f5f5; } .container { height: 100%; width: 100%; margin: 40px 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 40px; } img { width: 320px; height: 320px; object-fit: cover; } ``` In `script.js`, we select the container: ```js const container = document.querySelector('.container'); ``` We'll create a function called `getTenRandomImages`, which will return 10 images with random URLs. This function will be responsible for populating the container. In real scenarios, it can be replaced by an API call that returns data to be used in the application, for example. ```js const getTenRandomImages = () => { const images = []; for (let i = 0; i < 10; i++) { const image = document.createElement('img'); image.src = `https://picsum.photos/300/300?random=${Math.random()}`; images.push(image); } return images; }; ``` We create the _observer_. In the `callback`, if the observed entry (which will be the last child element of the `container`) is crossing the visible area, the `getTenRandomImages` function will be used to add 10 more images to the `container`, the `entry` will stop being observed and the new last child (`lastElementChild`) of the container will be observed. ```js const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { container.append(...getTenRandomImages()); observer.unobserve(entry.target); observer.observe(container.lastElementChild); } }); }); ``` Finally, we add the initial 10 images to the `container` and declare its last child to be observed, so that new images are only loaded when it's visible. ```js container.append(...getTenRandomImages()); observer.observe(container.lastElementChild); ``` The result can be seen [here](https://owillgoncalves.github.io/intersection-observer/03-infinite-scroll/). ----- ### Conclusion The cases presented here can be adapted to real-world contexts without major difficulties. Considering that the **Intersection Observer API** takes this responsibility of observing target elements off the application's _main thread_, we can scale this solution even in larger applications. It's also applicable to frameworks like React and Vue, as long as you know how to select elements in the DOMs that are generated by them. It's basically replacing `querySelector` and `querySelectorAll` with the approach of the tool you're using. ----- Take care and see you next time! ----- References: [Intersection Observer API - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) [Intersection Observer | W3C](https://w3c.github.io/IntersectionObserver/#intersection-observer-interface) --- # Changing screen theme with Pure CSS (Dark/Light Mode) Source: posts-en/mudando-tema-com-css-puro.md ### *We know the JS way* #### *But what if we don't use scripts to switch the theme of our applications?* The path is a relationship between cascade and well-specified selectors. Let's start from the beginning: ---------- ##### HTML The first element of the tree will be a checkbox input. Its sibling below will be the container of our application. It's the one that will have the styles changed for the theme switch. Inside it, we'll have a label related to our input above, inside a div that will be its transition area, serving as our button to change the theme. ```html

Changing theme with Pure CSS

The text contrasts with the background

``` ---------- ##### CSS In the styles, we apply the resets and declare the variables for the colors used in the theme: ```css * { margin: 0; padding: 0; box-sizing: border-box; } :root { --light: #cccccc; --dark: #151515; } ``` ---------- We make our input invisible, since we'll use its label as the trigger. ```css #theme-switcher { display: none; } ``` ---------- And we declare the properties of our app container. It will occupy the entire screen, have a light background and dark texts, as well as being a flex-container. The latter is optional and just to facilitate the demonstration of the result, centering the text on the screen. ```css #app-container { height: 100vh; background: var(--light); color: var(--dark); font-family: monospace; font-size: 1.5rem; transition: 0.3s; display: flex; flex-direction: column; align-items: center; justify-content: center; } ``` ---------- We declare the area where our button will slide, with absolute positioning at the top: ```css .theme-switcher-area { border: 1px solid var(--light); background: var(--dark); border-radius: 2rem; width: 4.5rem; height: 2.5rem; padding: 0.2rem; position: absolute; top: 0.5rem; right: 0.5rem; } ``` ---------- The button itself, which will use the `dashed` border style, creating an effect similar to sun rays, for the light theme. ```css .theme-switcher-button { position: relative; display: block; background: #f1aa02; border-radius: 50%; width: 2rem; height: 2rem; border: 2px dashed var(--dark); transition: 0.3s; } ``` ---------- And finally, an `::after` pseudo-element over the button. It will have the shape of a smaller circle than the original element, becoming a shadow that will transform the trigger into a moon, in the dark theme. Therefore, its initial opacity will be 0. ```css .theme-switcher-button::after { position: absolute; width: 80%; height: 80%; content: ""; background: var(--dark); border-radius: 50%; opacity: 0; transition: 0.3s; } ``` ---------- ### And here comes the magic! Since our input is the first element of the tree, we can use the ':checked' pseudo-class, with the appropriate selectors, to change the style of any element below it. When it's selected, these properties will be applied. Starting with the trigger itself, transforming the sun into a moon. To do this, we remove the border that came to give the rays effect and move the button to the right. ```css #theme-switcher:checked + #app-container .theme-switcher-button { transform: translateX(100%); border: none; } ``` ---------- Next, we change the shadow's opacity, the `::after`, to generate a crescent moon, in the button change. ```css #theme-switcher:checked + #app-container .theme-switcher-button::after { opacity: 1; } ``` ---------- Finally and for the desired effect, we invert the background color and text of our app container: ```css #theme-switcher:checked + #app-container { background: var(--dark); color: var(--light); } ``` ---------- #### And there it is, where the owl sleeps! ![Theme change demonstration using only CSS](https://dev-to-uploads.s3.amazonaws.com/i/9q8ffcms8zgj2gqccihs.gif) ---------- This tutorial is just the beginning of the dive. So use your creativity from this base and change the styles as you see fit! ---------- If you enjoyed this content, share it with others and help spread the word! ---------- See you next time! 🧙 --- # React Router 7: \n'Multiple Actions' in a Single Route Source: posts-en/rr7-multiple-actions.md This is perhaps the most common misconception for those unfamiliar with React Router as a framework (and the same goes for Remix): > I can only have one action per route. I believe this confusion arises mainly from comparisons with Next.js, its Server Actions, and even the way API routes are declared in it. Many developers see React Router's `loader` and `action` functions as "endpoints," functions with single responsibilities, when in reality, their true role goes far beyond that. `loader` and `action`, together, are like a complete Controller, where we can define different fetches and distinct mutations, each with its own purpose. And that's part of what we'll study here, by simulating a user CRUD: three different ways to handle multiple actions in a single React Router route. And if you use Remix up to version 2 (pre-React Router 7), the same approaches should work for you. --- #### Using the Form component and default behavior This is the most traditional approach. We use the `
` component and differentiate actions through the `method` attribute. In a route, create the `loader`: ```tsx export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; ``` Create your component with different forms, one for each different method/action: ```tsx import { Form, /* ... */ } from "react-router"; /* ... */ export default function UI({ loaderData: { users } }: Route.ComponentProps) { return (

Users CRUD (Form - default behavior)

Add User

{/* Form with method="post" to create a user */}

Users

{users.map((user: User) => (
{/* Form with method="put" to edit a user */}
Role:
{/* Form with method="delete" to delete a user */}
))}

); } ``` In the same route, define the `action`. We use `await request.formData()` to read the data and get `request.method` to differentiate between POST, PUT, and DELETE. ```tsx export const action = async ({ request }: ActionFunctionArgs) => { const formData = await request.formData(); const method = request.method.toLowerCase(); switch (method) { case "post": { const name = formData.get("name") as string; const email = formData.get("email") as string; return await createUser({ name, email }); } case "put": { const id = formData.get("id") as string; const name = formData.get("name") as string; const email = formData.get("email") as string; return await updateUser(id, { name, email }); } case "delete": { const id = formData.get("id") as string; return await deleteUser(id); } default: throw new Response("Method not allowed", { status: 405, statusText: "Method Not Allowed", }); } }; ``` And that's it! This approach is robust, works without JavaScript, and follows the principle of Progressive Enhancement. Its disadvantage (compared to the other options) is that the number of mutations is limited to the standard HTTP methods. --- #### With useSubmit and JSON - actions defined in the submitted data In this method, we will use the `useSubmit` hook to send a JSON with a property called `intent`, which will define which action we want to perform. The first advantage here is that we gain the flexibility for custom mutations, outside the standard `method` patterns of the `
` component. We'll use the same loader as the previous example: ```tsx export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; ``` In the route's component, we define event handlers to deal with user actions and trigger the corresponding mutation. The main advantage here, for example, is being able to separate the user `role` editing from the other properties into a distinct action. ```tsx export default function UI({ loaderData: { users }, }: Route.ComponentProps) { const submit = useSubmit(); // Event handler to create a user const handleCreate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "createUser", payload: { name, email } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); event.currentTarget.reset(); }; // Event handler to edit a user const handleUpdate = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); const userId = event.currentTarget.getAttribute("data-user-id"); if (userId) { const name = String(formData.get("name")); const email = String(formData.get("email")); const data = { intent: "updateUser", payload: { id: userId, name, email }, }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler to change the user's role const handleChangeRole = (event: React.ChangeEvent) => { const userId = event.target.closest("form")?.getAttribute("data-user-id"); if (userId) { const role = event.target.value as "admin" | "user"; const data = { intent: "changeUserRole", payload: { id: userId, role } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; // Event handler to delete a user const handleDelete = (userId: string) => { if (confirm("Are you sure you want to delete this user?")) { const data = { intent: "deleteUser", payload: { id: userId } }; submit(JSON.stringify(data), { method: "post", encType: "application/json", }); } }; return (

Users CRUD (JSON API)

Add User

{/* Form to create a user */}

Users

{users.map((user: User) => ( {/* Form to edit a user */}
Role:
{/* Event that triggers the role change handler */} {/* Click event to delete a user */}
))}

); } ``` In the same route, define the `action`. We use `await request.json()` to read the data and get the `intent` property from the body to address the mutation: ```tsx export const action = async ({ request }: ActionFunctionArgs) => { const body = await request.json(); const { intent, payload } = body; switch (intent) { case "createUser": return await createUser(payload); case "updateUser": return await updateUser(payload.id, payload); case "deleteUser": return await deleteUser(payload.id); case "changeUserRole": return await changeUserRole(payload.id, payload.role); default: throw new Error("Method not allowed"); } }; ``` This version is ideal for more complex data structures, like nested objects, due to the flexibility that JSON provides. You can use Discriminated Unions with TypeScript to ensure strong typing of the payloads. The disadvantage is that it depends on JavaScript and becomes much more verbose, especially in the examples I'm showing here. Another point is that for file uploads, using FormData is still the best option. We want to avoid base64 encoding at all costs. --- #### Actions as route parameters - the most delightful of the three options Here, we return to the `Form` component. But instead of using the `method` attribute to differentiate actions, we will use the `action` attribute. We'll use the same loader as in the previous examples: ```tsx export const loader = async ({ request }: LoaderFunctionArgs) => { const users = await getUsers(); return data({ users }); }; ``` In the UI, we use the `Form` component, associating the desired mutation with the `action` parameter, and we also set `navigate={false}` for each form to prevent navigations and changes to the browser history (you'll understand why in a moment): ```tsx export default function UI({ loaderData: { users } }: Route.ComponentProps) { return (

Users CRUD (Actions as params)

Add User

{/* Form to create a user */}

Users

{users.map((user: User) => (
{/* Form to update a user */}
{/* Form to change user's role */}
Role:
{/* Form to delete a user */}
))}

); } ``` Before moving on to the `action`, let's understand what will happen here: Imagine this route is `/users`. When I delete a user, for example, the form will look for `deleteUser` as a child route of `/users`: `/users/deleteUser`. Therefore, for this method, we will need to create an additional route dedicated to the actions. > Oh, William! But you said everything would be in the same route! Yes, young grasshopper! The actions will still be concentrated in a single route. But to gain flexibility, accommodate a more complex UI with multiple (including custom) actions, maintain the default behavior of forms, and achieve this with simple and elegant code, it can't come for free. And the price is to separate the actions into a route with a dynamic segment. In the `/users` example, this route would be `/users/:action`, declared manually, or `users.$action.tsx` using the File Route Conventions (which work exactly like in Remix). Here we go back to using `request.formData()` and now get the action from the route parameter. ```tsx export const action = async ({ request, params }: ActionFunctionArgs) => { const formData = await request.formData(); const action = params.action; switch (action) { case "createUser": const name = formData.get("name") as string; const email = formData.get("email") as string; return await createUser({ name, email }); case "updateUser": const id = formData.get("id") as string; const userName = formData.get("name") as string; const userEmail = formData.get("email") as string; return await updateUser(id, { name: userName, email: userEmail }); case "deleteUser": const deleteId = formData.get("id") as string; return await deleteUser(deleteId); case "changeUserRole": const roleId = formData.get("id") as string; const role = formData.get("role") as "admin" | "user"; return await changeUserRole(roleId, role); default: throw new Error("Method not allowed"); } }; ``` And why do I think this is the best approach? Because it maintains the default behaviors of React Router (and also Remix) while providing flexibility for different scenarios. The `action` ceases to be a "narrow path," giving way to countless possibilities, in addition to serving to centralize and organize the different mutations that a screen and its components perform. --- You can check out the complete example in the repository, where I have defined different routes for each approach. And also test it live, to get a better view of the different scenarios, inspect the app, and everything else. --- I hope you enjoyed it! And share it with your developer friends who need to learn more about React Router 7! See you around! --- # 'Magic' text automatically typed with JavaScript Source: posts-en/texto-magico-js.md > "Blast, heretic! Are you telling me that the writings will appear by themselves? Isn't this sorcery, or witchcraft?" ![Trust me](https://dev-to-uploads.s3.amazonaws.com/i/lppf5cp0b2sejw0rabzr.png) It's not magic. It's JavaScript. Let's break it down below: ----- First of all, we need to create, in our HTML, an element to receive the spell, I mean, the created text. It can be a paragraph (**p**) or even a heading (**h1**, **h2**...). It just needs to be a text element and have an **id**. Remember that the **id** needs to be exclusive to that element. ```js

``` For our case, we'll use an **h1** with the id **magic-text**. ----- Next, we create and import the JavaScript file which, for our example, will be **script.js**: ```js ``` ----- Now in **script.js**, let's create a constant to interact with our **h1**, using the **querySelector** method, which allows us to select elements using the same selectors we see in CSS. In our case, we'll use the **id** preceded by **#**. ```js const magicTextHeader = document.querySelector('#magic-text'); ``` The **querySelector** method can be used both on the document and on other elements, after being declared, selecting their respective children. ----- Next, we create a constant with the text to be used: ```js const text = 'Text inserted automagically with JavaScript!'; ``` ----- Finally, we declare a variable that will help us "traverse" the text: ```js let indexCharacter = 0; ``` ----- The function that will return the text is **writeText()**: ```js function writeText() { magicTextHeader.innerText = text.slice(0, indexCharacter); indexCharacter++; if(indexCharacter > text.length - 1) { setTimeout(() => { indexCharacter = 0; }, 2000); } } ``` In the first line, we include the text in the **innerText** property of the **h1**, using the **.slice()** method, which will traverse our **text** constant, letter by letter, as if it were an **array**. The **.slice()** syntax is `.slice(a,b)`, where **a** is the initial key of the segment to be returned and **b** is the final key of that same segment. Since we want to return the text from the beginning, we start with key 0 and end with the value of **indexCharacter**, which is incremented in the following line, ensuring that the next execution of the function will return one more character and so on. Next, we use a conditional to check if **indexCharacter** is equal to the last position of the text (`text.length - 1`; since the first key is 0, the last will be the size (length) of the text minus 1). If the condition is true, **indexCharacter** will be reset to zero, after a **setTimeout** of 2000 milliseconds, making the text start being "typed" from the beginning again. ----- And to execute this function continuously, ensuring the increment of **indexCharacter** and the desired effect for our text, we use a **setInterval** that will execute the **writeText** function every 100 milliseconds: ```js setInterval(writeText, 100); ``` ----- And the magic is complete! ----- You can see an example [here](https://g31-magic-text.vercel.app/). And check out my version of the code [here](https://github.com/williammago/goodbye.31/tree/main/28%20-%20Auto%20Write%20Text%20com%20JavaScript). ----- And, optionally, use the styles I used there: ```css * { margin: 0; padding: 0; box-sizing: border-box; } html, body { height: 100%; } body { display: flex; flex-direction: column; align-items: center; justify-content: center; font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif; background: darkred; color: #FFF; } h1 { font-size: 2rem; max-width: 400px; text-align: center; } ``` ----- This article was inspired by a [video](https://www.youtube.com/watch?v=8GPPJpiLqHk) from [Florin Pop's channel](https://www.youtube.com/channel/UCeU-1X402kT-JlLdAitxSMA), which has amazing tutorials and challenges for those who are starting out. Content in English. ----- See you next time! Big hug! --- ## About This Document This concatenated documentation file is generated automatically by aeo.js to make it easier for AI systems to understand the complete context of this project. For a structured index, see: https://iwill.dev/llms.txt For individual files, see: https://iwill.dev/docs.json Generated by aeo.js - https://aeojs.org